mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 05:07:51 -06:00
Placed documents: on screen, cached bitmaps made in the background
This commit is contained in:
@@ -97,6 +97,9 @@ pub struct RenderOptions {
|
||||
pub highlight_substitutions: bool,
|
||||
/// Edge smoothing (raster export option).
|
||||
pub anti_alias: AntiAlias,
|
||||
/// Screen view: placed documents draw from cached bitmaps made in the background (see
|
||||
/// [`placed_document`]); off, they draw exactly, read when needed.
|
||||
pub progressive_placed: bool,
|
||||
}
|
||||
|
||||
/// How edges are rasterized (raster export option).
|
||||
@@ -161,6 +164,7 @@ impl Default for RenderOptions {
|
||||
mask_view: None,
|
||||
highlight_substitutions: false,
|
||||
anti_alias: AntiAlias::Art,
|
||||
progressive_placed: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,22 @@ pub(crate) fn set_paint(ctx: &mut RenderContext, p: &Paint, bounds: Rect, f: &Fr
|
||||
|
||||
/// Decode encoded image bytes into a premultiplied pixmap.
|
||||
pub fn decode_pixmap(bytes: &[u8]) -> Option<vello_cpu::Pixmap> {
|
||||
let img = image::load_from_memory(bytes).ok()?.to_rgba8();
|
||||
pixmap_of(image::load_from_memory(bytes).ok()?.to_rgba8())
|
||||
}
|
||||
|
||||
/// [`decode_pixmap`] shrunk to `w`×`h` pixels when the image is larger (never enlarged).
|
||||
pub fn decode_pixmap_fit(bytes: &[u8], w: u16, h: u16) -> Option<vello_cpu::Pixmap> {
|
||||
let img = image::load_from_memory(bytes).ok()?;
|
||||
let img = if img.width() > w as u32 || img.height() > h as u32 {
|
||||
img.resize_exact(w as u32, h as u32, image::imageops::FilterType::Triangle)
|
||||
} else {
|
||||
img
|
||||
};
|
||||
pixmap_of(img.to_rgba8())
|
||||
}
|
||||
|
||||
/// An RGBA image as a premultiplied pixmap.
|
||||
fn pixmap_of(img: image::RgbaImage) -> Option<vello_cpu::Pixmap> {
|
||||
let (w, h) = img.dimensions();
|
||||
if w == 0 || h == 0 || w > u16::MAX as u32 || h > u16::MAX as u32 {
|
||||
return None;
|
||||
|
||||
@@ -1,22 +1,138 @@
|
||||
//! Drawing placed documents ([`PlacedDocument`]): the art read from the file
|
||||
//! ([`vectorcraft_doc::placed_document::read`]) drawn with its own resources, through a view that
|
||||
//! maps its artboard into the object's box (nothing is copied into the document).
|
||||
//! Drawing placed documents ([`PlacedDocument`]).
|
||||
//!
|
||||
//! A document opened with only its placed documents' previews
|
||||
//! ([`vectorcraft_doc::ImageBlob::is_proxy`]) shows them until the file is at hand
|
||||
//! ([`vectorcraft_doc::placed_document::full_bytes_cached`]), and for good when it can't be found. Placed documents inside a placed document are read from their
|
||||
//! files, up to [`MAX_DEPTH`] deep; deeper ones show their previews.
|
||||
//! Exactly: the art read from the file ([`vectorcraft_doc::placed_document::read`]) drawn with its
|
||||
//! own resources, through a view that maps its artboard into the object's box (nothing is copied
|
||||
//! into the document).
|
||||
//!
|
||||
//! On screen ([`RenderOptions::progressive_placed`]): a placed document can't be edited, so what
|
||||
//! it shows is cached as a bitmap per file and zoom step (a power of two, in device pixels per
|
||||
//! point), shared by every object showing that file. A bitmap not made yet is made on a
|
||||
//! background worker while the frame shows another step's bitmap, or a light placeholder;
|
||||
//! [`generation`] changes when one is ready and [`busy`] says whether any is coming, so the screen
|
||||
//! can draw again. Past [`MAX_SIDE`] pixels, and in outline, proofing and ink frames, it is drawn
|
||||
//! exactly.
|
||||
//!
|
||||
//! A document opened with only its placed documents' previews ([`ImageBlob::is_proxy`]) shows them
|
||||
//! up to their resolution; past it the file is read again in the background
|
||||
//! ([`vectorcraft_doc::placed_document::full_bytes`]), the preview standing in until then (and for
|
||||
//! good when the file can't be found). Placed documents inside a placed document are drawn the
|
||||
//! same way, up to [`MAX_DEPTH`] deep; deeper ones show their previews.
|
||||
|
||||
use std::cell::Cell;
|
||||
use std::sync::Arc;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::{Arc, LazyLock, Mutex, MutexGuard, PoisonError};
|
||||
|
||||
use vectorcraft_doc::placed_document::MAX_DEPTH;
|
||||
use vectorcraft_doc::{Node, PlacedDocument};
|
||||
use vectorcraft_geom::Affine;
|
||||
use vello_cpu::RenderContext;
|
||||
use vectorcraft_doc::{ImageBlob, Node, PlacedDocument};
|
||||
use vectorcraft_geom::{Affine, Rect};
|
||||
use vello_cpu::{Pixmap, RenderContext, kurbo, peniko};
|
||||
|
||||
use crate::ink::Ink;
|
||||
use crate::{Frame, Renderer};
|
||||
|
||||
/// The longest side of a cached bitmap (pixels); larger steps draw exactly.
|
||||
pub const MAX_SIDE: f64 = 4096.0;
|
||||
/// The most bytes of bitmaps kept.
|
||||
const BUDGET: usize = 512 << 20;
|
||||
/// The smallest zoom step (device pixels per point).
|
||||
const MIN_STEP: f64 = 1.0 / 64.0;
|
||||
|
||||
/// A bitmap's identity: the file shown (its key) and the zoom step's bits.
|
||||
type BitmapKey = (String, u64);
|
||||
|
||||
struct Bitmap {
|
||||
pm: Arc<Pixmap>,
|
||||
step: f64,
|
||||
used: u64,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Bitmaps {
|
||||
map: HashMap<BitmapKey, Bitmap>,
|
||||
clock: u64,
|
||||
bytes: usize,
|
||||
}
|
||||
|
||||
static BITMAPS: LazyLock<Mutex<Bitmaps>> = LazyLock::new(Default::default);
|
||||
/// Bitmaps being made.
|
||||
static PENDING: LazyLock<Mutex<HashSet<BitmapKey>>> = LazyLock::new(Default::default);
|
||||
static GENERATION: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
fn lock<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {
|
||||
// The caches stay whole if a holder panicked (entries are only inserted and removed).
|
||||
m.lock().unwrap_or_else(PoisonError::into_inner)
|
||||
}
|
||||
|
||||
/// Changes whenever a bitmap made in the background is ready: a screen showing placed documents
|
||||
/// draws again when it does.
|
||||
pub fn generation() -> u64 {
|
||||
GENERATION.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Are bitmaps being made in the background?
|
||||
pub fn busy() -> bool {
|
||||
!lock(&PENDING).is_empty()
|
||||
}
|
||||
|
||||
/// Is a bitmap of file `key` being made?
|
||||
#[cfg(test)]
|
||||
pub(crate) fn pending_for(key: &str) -> bool {
|
||||
lock(&PENDING).iter().any(|k| k.0 == key)
|
||||
}
|
||||
|
||||
/// The zoom step for `scale` device pixels per point: the power of two at or above it.
|
||||
pub(crate) fn step(scale: f64) -> Option<f64> {
|
||||
(scale.is_finite() && scale > 0.0).then(|| 2f64.powf(scale.log2().ceil()).max(MIN_STEP))
|
||||
}
|
||||
|
||||
impl Bitmaps {
|
||||
fn get(&mut self, k: &BitmapKey) -> Option<Arc<Pixmap>> {
|
||||
self.clock += 1;
|
||||
let clock = self.clock;
|
||||
let b = self.map.get_mut(k)?;
|
||||
b.used = clock;
|
||||
Some(b.pm.clone())
|
||||
}
|
||||
|
||||
/// The bitmap of another step of the same file, the nearest to `step`.
|
||||
fn nearest(&self, key: &str, step: f64) -> Option<(Arc<Pixmap>, f64)> {
|
||||
self.map
|
||||
.iter()
|
||||
.filter(|((k, _), _)| k == key)
|
||||
.min_by(|a, b| (a.1.step / step).log2().abs().total_cmp(&(b.1.step / step).log2().abs()))
|
||||
.map(|(_, b)| (b.pm.clone(), b.step))
|
||||
}
|
||||
|
||||
fn insert(&mut self, k: BitmapKey, pm: Pixmap, step: f64) {
|
||||
self.clock += 1;
|
||||
let size = pm.width() as usize * pm.height() as usize * 4;
|
||||
self.bytes += size;
|
||||
if let Some(old) = self.map.insert(k, Bitmap { pm: Arc::new(pm), step, used: self.clock }) {
|
||||
self.bytes = self.bytes.saturating_sub(old.pm.width() as usize * old.pm.height() as usize * 4);
|
||||
}
|
||||
while self.bytes > BUDGET && self.map.len() > 1 {
|
||||
let Some(k) = self.map.iter().min_by_key(|(_, b)| b.used).map(|(k, _)| k.clone()) else { break };
|
||||
if let Some(b) = self.map.remove(&k) {
|
||||
self.bytes = self.bytes.saturating_sub(b.pm.width() as usize * b.pm.height() as usize * 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What a bitmap is made from.
|
||||
struct Job {
|
||||
key: BitmapKey,
|
||||
object: PlacedDocument,
|
||||
blob: ImageBlob,
|
||||
step: f64,
|
||||
}
|
||||
|
||||
/// Device pixels per point of `object`'s box its preview `bytes` have.
|
||||
fn preview_density(object: &PlacedDocument, bytes: &[u8]) -> f64 {
|
||||
vectorcraft_doc::placed_document::image_size(bytes).map_or(0.0, |(w, _)| w as f64 / object.width.max(1e-9))
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
/// How deep inside placed documents this thread is drawing.
|
||||
static DEPTH: Cell<usize> = const { Cell::new(0) };
|
||||
@@ -49,14 +165,108 @@ pub fn render_inside(r: &mut Renderer, doc: &vectorcraft_doc::Document, art: &Ar
|
||||
r.render_node(doc, art, w, h, view)
|
||||
}
|
||||
|
||||
/// `object`'s file (`blob`) as a bitmap of `step` device pixels per point of its box: from its
|
||||
/// preview when that is all there is and is fine enough, else from the file (read again when
|
||||
/// needed; the preview when it can't be).
|
||||
fn make(r: &mut Renderer, object: &PlacedDocument, blob: &ImageBlob, step: f64) -> Option<Pixmap> {
|
||||
let (w, h) = (pixels(object.width * step), pixels(object.height * step));
|
||||
let preview = if blob.is_proxy() { Some(blob.bytes.clone()) } else { blob.proxy.clone() };
|
||||
let full = match blob.is_proxy() {
|
||||
true if step <= preview_density(object, &blob.bytes) * 1.01 => None,
|
||||
true => vectorcraft_doc::placed_document::full_bytes(object, blob),
|
||||
false => Some(blob.bytes.clone()),
|
||||
};
|
||||
let read = full.and_then(|bytes| vectorcraft_doc::placed_document::read(object, &bytes));
|
||||
let Some(e) = read else { return crate::paint::decode_pixmap_fit(&preview?, w, h) };
|
||||
let f = e.frame;
|
||||
let (sx, sy) = (w as f64 / f.width(), h as f64 / f.height());
|
||||
let view = Affine::scale_non_uniform(sx, sy) * Affine::translate((-f.x0, -f.y0));
|
||||
let img = render_inside(r, &e.doc, &e.art, w, h, view)?;
|
||||
let data = img.pixels.as_chunks::<4>().0.iter().map(|&[r, g, b, a]| vello_cpu::color::PremulRgba8 { r, g, b, a }).collect();
|
||||
Some(Pixmap::from_parts(data, w, h))
|
||||
}
|
||||
|
||||
/// A bitmap side for `v` pixels (at least 1).
|
||||
fn pixels(v: f64) -> u16 {
|
||||
v.round().clamp(1.0, MAX_SIDE) as u16
|
||||
}
|
||||
|
||||
/// Make `job`'s bitmap and keep it.
|
||||
fn run(r: &mut Renderer, job: Job) {
|
||||
if let Some(pm) = make(r, &job.object, &job.blob, job.step) {
|
||||
lock(&BITMAPS).insert(job.key.clone(), pm, job.step);
|
||||
}
|
||||
lock(&PENDING).remove(&job.key);
|
||||
GENERATION.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod workers {
|
||||
use std::sync::mpsc::{Sender, channel};
|
||||
use std::sync::{Arc, LazyLock, Mutex};
|
||||
|
||||
use super::Job;
|
||||
|
||||
/// The queue the workers take jobs from; `None` when no worker could start.
|
||||
static QUEUE: LazyLock<Option<Mutex<Sender<Box<Job>>>>> = LazyLock::new(|| {
|
||||
let (tx, rx) = channel::<Box<Job>>();
|
||||
let rx = Arc::new(Mutex::new(rx));
|
||||
let n = std::thread::available_parallelism().map_or(2, |n| (n.get() / 2).clamp(1, 4));
|
||||
let mut started = 0;
|
||||
for i in 0..n {
|
||||
let rx = rx.clone();
|
||||
let spawned = std::thread::Builder::new().name(format!("vectorcraft-placed-{i}")).spawn(move || {
|
||||
let mut r = crate::Renderer::new();
|
||||
r.threads = 0;
|
||||
loop {
|
||||
let job = super::lock(&rx).recv();
|
||||
let Ok(job) = job else { break };
|
||||
// A panic loses this bitmap, not the worker.
|
||||
let key = job.key.clone();
|
||||
if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| super::run(&mut r, *job))).is_err() {
|
||||
super::lock(&super::PENDING).remove(&key);
|
||||
}
|
||||
}
|
||||
});
|
||||
started += usize::from(spawned.is_ok());
|
||||
}
|
||||
(started > 0).then(|| Mutex::new(tx))
|
||||
});
|
||||
|
||||
/// Hand `job` to a worker; back when there is none.
|
||||
pub(super) fn submit(job: Box<Job>) -> Result<(), Box<Job>> {
|
||||
match QUEUE.as_ref() {
|
||||
Some(q) => super::lock(q).send(job).map_err(|e| e.0),
|
||||
None => Err(job),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Make `job`'s bitmap in the background (here and now where there are no threads).
|
||||
fn schedule(job: Job) {
|
||||
if !lock(&PENDING).insert(job.key.clone()) {
|
||||
return;
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let job = match workers::submit(Box::new(job)) {
|
||||
Ok(()) => return,
|
||||
Err(job) => *job,
|
||||
};
|
||||
run(&mut Renderer::new(), job);
|
||||
}
|
||||
|
||||
/// The mean scale of `a`.
|
||||
fn mean_scale(a: Affine) -> f64 {
|
||||
a.determinant().abs().sqrt()
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
/// Draw placed document `p` in frame `f`.
|
||||
/// Draw placed document `p` in frame `f`: from a cached bitmap on screen, else exactly.
|
||||
pub(crate) fn draw_placed(&mut self, ctx: &mut RenderContext, f: &Frame, p: &PlacedDocument) {
|
||||
let plain = f.ink == Ink::Display && !f.opts.outline && f.opts.proof.is_none() && !f.opts.overprint_preview;
|
||||
if f.opts.progressive_placed && plain && depth() == 0 && self.draw_placed_bitmap(ctx, f, p) {
|
||||
return;
|
||||
}
|
||||
let Some(blob) = f.doc.images.get(&p.key) else { return };
|
||||
// The file's bytes: stored, read again already, or (inside another placed document, while
|
||||
// not too deep) read again now.
|
||||
@@ -85,4 +295,65 @@ impl Renderer {
|
||||
let _inside = Inside::enter();
|
||||
self.draw_arc(ctx, &inner, &e.art);
|
||||
}
|
||||
|
||||
/// Draw `p` from its bitmap at this zoom step, or (while that is made) another step's, or a
|
||||
/// placeholder. False when bitmaps don't suit (too large).
|
||||
fn draw_placed_bitmap(&mut self, ctx: &mut RenderContext, f: &Frame, p: &PlacedDocument) -> bool {
|
||||
let m = f.view * p.xf;
|
||||
let Some(step) = step(mean_scale(m)) else { return false };
|
||||
if p.width * step > MAX_SIDE || p.height * step > MAX_SIDE {
|
||||
return false;
|
||||
}
|
||||
let Some(blob) = f.doc.images.get(&p.key).cloned() else { return true };
|
||||
let key = (p.key.clone(), step.to_bits());
|
||||
let (found, nearest) = {
|
||||
let mut b = lock(&BITMAPS);
|
||||
let found = b.get(&key);
|
||||
let nearest = if found.is_none() { b.nearest(&p.key, step) } else { None };
|
||||
(found, nearest)
|
||||
};
|
||||
let shown = match found {
|
||||
Some(pm) => Some(pm),
|
||||
None => {
|
||||
schedule(Job { key, object: p.clone(), blob, step });
|
||||
// Made here and now (no threads) or meanwhile another step's.
|
||||
let mut b = lock(&BITMAPS);
|
||||
b.get(&(p.key.clone(), step.to_bits())).or(nearest.map(|n| n.0))
|
||||
}
|
||||
};
|
||||
let rect = Rect::new(0.0, 0.0, p.width, p.height);
|
||||
match shown {
|
||||
Some(pm) => {
|
||||
let (sx, sy) = (p.width / pm.width().max(1) as f64, p.height / pm.height().max(1) as f64);
|
||||
ctx.set_paint(vello_cpu::Image { image: vello_cpu::ImageSource::Pixmap(pm), sampler: peniko::ImageSampler::default() });
|
||||
ctx.set_transform(m);
|
||||
ctx.set_paint_transform(Affine::scale_non_uniform(sx, sy));
|
||||
ctx.fill_rect(&kurbo::Rect::new(rect.x0, rect.y0, rect.x1, rect.y1));
|
||||
ctx.reset_paint_transform();
|
||||
}
|
||||
None => {
|
||||
// Coming: a light box where it will be.
|
||||
ctx.set_transform(m);
|
||||
ctx.set_paint(peniko::Color::from_rgba8(128, 128, 128, 40));
|
||||
ctx.fill_rect(&kurbo::Rect::new(rect.x0, rect.y0, rect.x1, rect.y1));
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn zoom_steps_are_powers_of_two_at_or_above_the_scale() {
|
||||
assert_eq!(step(1.0), Some(1.0));
|
||||
assert_eq!(step(1.2), Some(2.0));
|
||||
assert_eq!(step(0.3), Some(0.5));
|
||||
assert_eq!(step(5.0), Some(8.0));
|
||||
assert_eq!(step(1e-9), Some(MIN_STEP));
|
||||
assert_eq!(step(f64::NAN), None);
|
||||
assert_eq!(step(0.0), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Placed documents drawn exactly: with their own art, as their previews when that is all there
|
||||
//! is, and a few levels deep when they place themselves.
|
||||
//! Placed documents on screen: drawn from bitmaps made in the background (a placeholder until
|
||||
//! then), the same as drawn exactly; exports draw them exactly at once.
|
||||
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::{Appearance, ImageBlob, LinkInfo, Node, NodeId, NodeKind, PlacedDocument};
|
||||
@@ -45,19 +45,63 @@ fn doc(key: &str) -> Document {
|
||||
doc_with(key, format!("{{\"doc\": \"{key}\"}}").as_bytes())
|
||||
}
|
||||
|
||||
fn render(d: &Document) -> Rendered {
|
||||
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
|
||||
fn render(d: &Document, progressive: bool) -> Rendered {
|
||||
let opts = RenderOptions { background: Some([255, 255, 255, 255]), progressive_placed: progressive, ..Default::default() };
|
||||
Renderer::new().render(d, 100, 100, Affine::IDENTITY, &opts)
|
||||
}
|
||||
|
||||
/// Wait for the background bitmaps (at most 10 s).
|
||||
fn settle() {
|
||||
let t = std::time::Instant::now();
|
||||
while placed_document::busy() && t.elapsed().as_secs() < 10 {
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
assert!(!placed_document::busy(), "bitmaps still coming");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_placed_document_draws_exactly_with_its_own_art() {
|
||||
let d = doc("exact-1");
|
||||
let r = render(&d);
|
||||
let r = render(&d, false);
|
||||
assert_eq!(&r.pixel(50, 30)[..3], &[0, 0, 255], "blue inside the box");
|
||||
assert_eq!(&r.pixel(95, 95)[..3], &[255, 255, 255], "nothing outside");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_screen_a_placed_document_shows_a_placeholder_then_its_bitmap() {
|
||||
let d = doc("progressive-1");
|
||||
let generation = placed_document::generation();
|
||||
let first = render(&d, true);
|
||||
let shown = &first.pixel(50, 30)[..3];
|
||||
assert!(shown == [0, 0, 255] || shown != [255, 255, 255], "a placeholder or the art: {shown:?}");
|
||||
settle();
|
||||
assert!(placed_document::generation() > generation || shown == [0, 0, 255], "a bitmap was made");
|
||||
let later = render(&d, true);
|
||||
let exact = render(&d, false);
|
||||
assert_eq!(&later.pixel(50, 30)[..3], &[0, 0, 255]);
|
||||
for (x, y) in [(12, 12), (50, 30), (88, 48), (95, 95), (5, 5)] {
|
||||
let (a, b) = (later.pixel(x, y), exact.pixel(x, y));
|
||||
assert!(a.iter().zip(b).all(|(a, b)| a.abs_diff(b) <= 24), "at ({x}, {y}): {a:?} vs {b:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copies_of_one_placed_document_share_its_bitmap() {
|
||||
let mut d = doc("shared-1");
|
||||
let n = d.node(NodeId(50)).unwrap().clone();
|
||||
let l = d.layers[0].id;
|
||||
let mut copy = n.clone();
|
||||
copy.id = NodeId(51);
|
||||
copy.transform(Affine::translate((0.0, 45.0)), false);
|
||||
d.insert(Some(l), 1, copy).unwrap();
|
||||
render(&d, true);
|
||||
settle();
|
||||
let r = render(&d, true);
|
||||
assert!(!placed_document::pending_for("shared-1"), "nothing more to make");
|
||||
assert_eq!(&r.pixel(50, 30)[..3], &[0, 0, 255]);
|
||||
assert_eq!(&r.pixel(50, 75)[..3], &[0, 0, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_placed_document_kept_as_its_preview_draws_the_preview() {
|
||||
let mut d = doc("preview-1");
|
||||
@@ -68,16 +112,25 @@ fn a_placed_document_kept_as_its_preview_draws_the_preview() {
|
||||
let mut blob = ImageBlob::new("image/png", png);
|
||||
blob.proxy = Some(blob.bytes.clone());
|
||||
d.images.insert("preview-1".into(), blob);
|
||||
let exact = render(&d);
|
||||
let exact = render(&d, false);
|
||||
assert_eq!(&exact.pixel(50, 30)[..3], &[0, 200, 0], "the preview, in the box");
|
||||
assert_eq!(&exact.pixel(95, 95)[..3], &[255, 255, 255]);
|
||||
// On screen past the preview's resolution (it has 0.5 px a point; this is 2): still the preview.
|
||||
render(&d, true);
|
||||
settle();
|
||||
let shown = render(&d, true);
|
||||
assert_eq!(&shown.pixel(50, 30)[..3], &[0, 200, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_document_that_places_itself_draws_a_few_levels_deep_then_stops() {
|
||||
let d = doc_with("loop-1", b"loop");
|
||||
let t = std::time::Instant::now();
|
||||
let r = render(&d);
|
||||
let r = render(&d, false);
|
||||
assert!(t.elapsed().as_secs() < 10);
|
||||
assert_eq!(&r.pixel(50, 30)[..3], &[0, 0, 255]);
|
||||
// On screen too.
|
||||
render(&d, true);
|
||||
settle();
|
||||
assert_eq!(&render(&d, true).pixel(50, 30)[..3], &[0, 0, 255]);
|
||||
}
|
||||
|
||||
@@ -230,7 +230,12 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
hidden: vec![],
|
||||
rot: v.rotation,
|
||||
anti_alias: app.session.prefs.anti_aliased_artwork,
|
||||
placed: vectorcraft_render::placed_document::generation(),
|
||||
};
|
||||
// Placed documents' bitmaps are being made: draw again when they are ready.
|
||||
if vectorcraft_render::placed_document::busy() {
|
||||
ui.ctx().request_repaint_after(std::time::Duration::from_millis(30));
|
||||
}
|
||||
if !app.canvas.worker_started {
|
||||
app.canvas.worker_started = true;
|
||||
if std::env::var_os("VECTORCRAFT_SYNC_RENDER").is_none() {
|
||||
@@ -265,6 +270,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
// General › Anti-aliased Artwork: off, edges are hard on screen (raster effects and
|
||||
// pattern tiles stay smooth), as in Illustrator.
|
||||
anti_alias: if app.session.prefs.anti_aliased_artwork { vectorcraft_render::AntiAlias::Art } else { vectorcraft_render::AntiAlias::None },
|
||||
progressive_placed: true,
|
||||
..opts
|
||||
};
|
||||
// Light documents render synchronously (no lag vs overlays); heavy ones go to the worker.
|
||||
@@ -1030,6 +1036,13 @@ fn node_outline(n: &Node) -> BezPath {
|
||||
bp.extend(vectorcraft_geom::shapes::rectangle(b).to_bezpath());
|
||||
}
|
||||
}
|
||||
// A placed document shows its box, turned with it.
|
||||
NodeKind::PlacedDocument(p) => {
|
||||
let [a, b, c, d] = p.corners();
|
||||
bp.move_to(a);
|
||||
[b, c, d].into_iter().for_each(|q| bp.line_to(q));
|
||||
bp.close_path();
|
||||
}
|
||||
// An envelope shows its mesh (or top object), not its content.
|
||||
NodeKind::Envelope { .. } => {
|
||||
if let Some((lines, _)) = vectorcraft_doc::live::envelope_overlay(c) {
|
||||
|
||||
@@ -277,6 +277,8 @@ pub struct CacheKey {
|
||||
pub rot: f64,
|
||||
/// General › Anti-aliased Artwork.
|
||||
pub anti_alias: bool,
|
||||
/// [`vectorcraft_render::placed_document::generation`]: placed documents' bitmaps made since.
|
||||
pub placed: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
|
||||
Reference in New Issue
Block a user