Files
vectorcraft/crates/render/src/tests_placed_document.rs
T
dotCooCoo 1c4ec81ec2 Placed documents: a ready bitmap redraws only its objects, and bitmaps on screen are not made again
On screen, placed documents (PDF, .ai and .vectorcraft) draw from
bitmaps made in the background. Each bitmap that became ready changed
a generation in the canvas key, so the canvas rendered the whole frame
again. The bitmaps share a 512 MiB budget, and making one dropped the
least recently drawn others, even those on screen: with more than that
on screen, every frame dropped a bitmap it drew and asked for it again,
without end. A bitmap that could not be made (the file can't be read
and there is no preview) also changed the generation and was asked for
again in every frame.

When a bitmap is ready, placed_document::made_between names its file,
and the canvas renders again only where the objects showing it are
(placed_area), as it does after an edit, when the frame shown was
rendered on the UI thread and that part is at most half the frame.
Art of a symbol, a graph design, a pattern or a brush that shows the
file still renders the whole frame.

Bitmaps drawn or made since a frame began are not dropped to make room
for the ones it asks for. They stay past the budget up to 2 GiB. A
bitmap that would take them past 2 GiB is not kept, and its file draws
exactly at that zoom. A bitmap that can't be made shows the
placeholder, does not change the generation, and is not asked for
again.

Docs: docs/target-app-parity.md.
2026-10-26 21:41:44 +07:00

322 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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};
use vectorcraft_geom::shapes;
use super::*;
/// Every file reads as a 40×20 artboard filled blue; a file whose bytes start with `loop` also
/// places itself (the same key) in its middle, and one whose bytes start with `bad` can't be read.
fn blue_page(p: &PlacedDocument, bytes: &[u8]) -> Option<(Document, Node, Rect)> {
if bytes.starts_with(b"bad") {
return None;
}
let mut d = Document::new(40.0, 20.0);
d.layers.clear();
let blue = Appearance::basic(Paint::solid(Color::rgb(0.0, 0.0, 1.0)), Paint::None, 0.0);
let mut children = vec![Arc::new(Node::path(NodeId(1), shapes::rectangle(Rect::new(0.0, 0.0, 40.0, 20.0)), blue))];
if bytes.starts_with(b"loop") {
d.images.insert(p.key.clone(), ImageBlob::new("application/json", bytes.to_vec()));
let me = PlacedDocument { xf: Affine::translate((10.0, 5.0)) * Affine::scale(0.5), ..p.clone() };
children.push(Arc::new(Node::new(NodeId(2), NodeKind::PlacedDocument(Box::new(me)))));
}
Some((d, Node::new(NodeId(0), NodeKind::Group { children, clip: false }), Rect::new(0.0, 0.0, 40.0, 20.0)))
}
/// A 100×100 document showing one placed file (`bytes` under `key`) in the box (10, 10)–(90, 50).
fn doc_with(key: &str, bytes: &[u8]) -> Document {
vectorcraft_doc::placed_document::set_loader(blue_page);
let mut d = Document::new(100.0, 100.0);
d.images.insert(key.into(), ImageBlob::new("application/json", bytes.to_vec()));
let l = d.layers[0].id;
d.insert(Some(l), 0, placed_node(50, key, 40.0, 20.0, Affine::translate((10.0, 10.0)) * Affine::scale(2.0))).unwrap();
d
}
/// Object `id` showing file `key` in a `width`×`height` box placed by `xf`.
fn placed_node(id: u64, key: &str, width: f64, height: f64, xf: Affine) -> Node {
let p = PlacedDocument {
link: LinkInfo::new("/art/card.vectorcraft"),
key: key.into(),
bounding: false,
page_box: Default::default(),
width,
height,
xf,
placement: Default::default(),
};
Node::new(NodeId(id), NodeKind::PlacedDocument(Box::new(p)))
}
/// Held by the tests that draw placed documents from bitmaps, which share the bitmaps, their
/// generation and the jobs being made: one test's bitmaps don't push out another's, and
/// [`settle`] waits for its own.
fn serial() -> std::sync::MutexGuard<'static, ()> {
static SERIAL: std::sync::Mutex<()> = std::sync::Mutex::new(());
SERIAL.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn doc(key: &str) -> Document {
doc_with(key, format!("{{\"doc\": \"{key}\"}}").as_bytes())
}
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, 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 an_enveloped_placed_document_shows_its_file_once_read() {
let mut d = doc("enveloped-1");
let placed = d.remove(NodeId(50)).unwrap();
let NodeKind::PlacedDocument(p) = &placed.kind else { panic!("a placed document") };
let p = (**p).clone();
let kind = vectorcraft_doc::EnvelopeKind::Warp { style: "arc".into(), bend: 30.0, h: 0.0, v: 0.0, horizontal: true };
let envelope = NodeKind::Envelope {
content: vec![placed],
kind: Box::new(kind),
fidelity: 50.0,
editing: false,
options: Default::default(),
frame: Box::new(Affine::IDENTITY),
};
let l = d.layers[0].id;
d.insert(Some(l), 0, Node::new(NodeId(60), envelope)).unwrap();
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
let mut r = Renderer::new();
let before = r.render(&d, 100, 100, Affine::IDENTITY, &opts).pixels;
// Exporting the document as PDF, SVG or EPS reads the file.
d.with_placed_art();
assert!(vectorcraft_doc::placed_document::read_cached(&p).is_some(), "the file is read");
let after = r.render(&d, 100, 100, Affine::IDENTITY, &opts).pixels;
assert!(after != before, "the file's art shows");
assert!(after == Renderer::new().render(&d, 100, 100, Affine::IDENTITY, &opts).pixels, "as a new renderer draws it");
}
#[test]
fn a_placed_document_with_a_drop_shadow_shows_its_file_once_read() {
let mut d = doc("shadowed-1");
let n = d.node_mut(NodeId(50)).unwrap();
let params = serde_json::json!({"x": 4, "y": 4, "blur": 1, "opacity": 100});
n.appearance.effects.push(vectorcraft_doc::Effect { id: "stylize.dropShadow".into(), params, visible: true });
let NodeKind::PlacedDocument(p) = &n.kind else { panic!("a placed document") };
let p = (**p).clone();
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
let mut r = Renderer::new();
let before = r.render(&d, 100, 100, Affine::IDENTITY, &opts).pixels;
// Exporting the document as PDF, SVG or EPS reads the file.
d.with_placed_art();
assert!(vectorcraft_doc::placed_document::read_cached(&p).is_some(), "the file is read");
let after = r.render(&d, 100, 100, Affine::IDENTITY, &opts).pixels;
assert!(after != before, "the file's art shows");
assert!(after == Renderer::new().render(&d, 100, 100, Affine::IDENTITY, &opts).pixels, "as a new renderer draws it");
}
#[test]
fn on_screen_a_placed_document_shows_a_placeholder_then_its_bitmap() {
let _serial = serial();
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 _serial = serial();
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]);
}
/// Nine files on screen, each drawn from a 4096 × 4096 bitmap (576 MiB together, past the 512 MiB
/// the bitmaps keep): once made, the next frames draw all nine and ask for none again.
#[test]
fn nine_large_bitmaps_on_screen_are_not_made_again() {
let _serial = serial();
vectorcraft_doc::placed_document::set_loader(blue_page);
let mut d = Document::new(100.0, 100.0);
let l = d.layers[0].id;
let keys: Vec<String> = (0..9).map(|i| format!("nine-{i}")).collect();
for (i, key) in keys.iter().enumerate() {
d.images.insert(key.clone(), ImageBlob::new("application/json", format!("{{\"doc\": \"{key}\"}}").into_bytes()));
// A 1 × 1 point box at 4096 pixels a point: a 4096-pixel bitmap.
d.insert(Some(l), i, placed_node(100 + i as u64, key, 1.0, 1.0, Affine::translate((i as f64 * 0.001, 0.0)))).unwrap();
}
let (view, opts) = (Affine::scale(4096.0), RenderOptions { progressive_placed: true, ..Default::default() });
let mut r = Renderer::new();
r.render(&d, 32, 32, view, &opts);
settle();
let asked = || keys.iter().map(|k| placed_document::scheduled(k)).collect::<Vec<_>>();
assert_eq!(asked(), vec![1; 9], "each asked for once");
for frame in 1..=2 {
let shown = r.render(&d, 32, 32, view, &opts);
assert_eq!(asked(), vec![1; 9], "frame {frame} asks for none again");
assert_eq!(&shown.pixel(16, 16)[..3], &[0, 0, 255], "frame {frame} draws the art");
}
}
/// A bitmap that was not kept (it would have taken the bitmaps past the most they may hold) is
/// not asked for again: its file draws exactly at that zoom step, at once.
#[test]
fn a_bitmap_not_kept_draws_exactly_without_being_asked_for() {
let _serial = serial();
let d = doc("unkept-1");
// The box is drawn at 2 pixels a point: zoom step 2.
placed_document::mark_unkept("unkept-1", 2.0);
let shown = render(&d, true);
assert_eq!(&shown.pixel(50, 30)[..3], &[0, 0, 255], "the art, drawn exactly");
assert_eq!(placed_document::scheduled("unkept-1"), 0, "not asked for");
}
/// A file that can't be read, without a preview: its bitmap is asked for once, the screen is
/// not told to draw again, and later frames draw the placeholder without asking again.
#[test]
fn a_bitmap_that_cant_be_made_is_not_asked_for_again() {
let _serial = serial();
let d = doc_with("unreadable-1", b"bad");
let generation = placed_document::generation();
render(&d, true);
settle();
assert_eq!(placed_document::generation(), generation, "nothing new to draw");
let later = render(&d, true);
assert_eq!(placed_document::scheduled("unreadable-1"), 1, "asked for once");
let shown = &later.pixel(50, 30)[..3];
assert!(shown != [255, 255, 255] && shown != [0, 0, 255], "the placeholder: {shown:?}");
}
/// Once bitmaps are ready, drawing again where [`placed_area`] says gives what drawing the whole
/// frame again gives. The area of a file covers every object showing it (a group's member and
/// an object with a drop shadow too) and no object showing another file; art of a brush or a
/// symbol showing it makes it everything.
#[test]
fn a_ready_bitmap_draws_again_where_its_file_shows() {
let _serial = serial();
vectorcraft_doc::placed_document::set_loader(blue_page);
let mut d = Document::new(100.0, 100.0);
for key in ["area-a", "area-b"] {
d.images.insert(key.into(), ImageBlob::new("application/json", format!("{{\"doc\": \"{key}\"}}").into_bytes()));
}
let l = d.layers[0].id;
// 20 × 10 point boxes: A loose at (5, 5), with a shadow at (30, 40) and in a group at (5, 80);
// B at (75, 5). Only the shadow reaches past x = 50, and only the group's member past y = 50.
let at = |x: f64, y: f64| Affine::translate((x, y)) * Affine::scale(0.5);
d.insert(Some(l), 0, placed_node(60, "area-a", 40.0, 20.0, at(5.0, 5.0))).unwrap();
let group = NodeKind::Group { children: vec![Arc::new(placed_node(61, "area-a", 40.0, 20.0, at(5.0, 80.0)))], clip: false };
d.insert(Some(l), 1, Node::new(NodeId(62), group)).unwrap();
let mut shadowed = placed_node(63, "area-a", 40.0, 20.0, at(30.0, 40.0));
let params = serde_json::json!({"x": 4, "y": 4, "blur": 1, "opacity": 100});
shadowed.appearance.effects.push(vectorcraft_doc::Effect { id: "stylize.dropShadow".into(), params, visible: true });
d.insert(Some(l), 2, shadowed).unwrap();
d.insert(Some(l), 3, placed_node(64, "area-b", 40.0, 20.0, at(75.0, 5.0))).unwrap();
let opts = RenderOptions { background: Some([255, 255, 255, 255]), progressive_placed: true, ..Default::default() };
let mut r = Renderer::new();
let generation = placed_document::generation();
let before = r.render(&d, 100, 100, Affine::IDENTITY, &opts);
settle();
let made = placed_document::made_between(generation, placed_document::generation()).expect("known");
let files = |keys: &[&str]| keys.iter().map(|k| k.to_string()).collect::<std::collections::HashSet<_>>();
assert_eq!(made, files(&["area-a", "area-b"]), "both files' bitmaps");
let only_a = files(&["area-a"]);
let Changed::Area(a) = placed_area(&d, &only_a) else { panic!("an area") };
for (x, y) in [(6.0, 6.0), (24.0, 14.0), (31.0, 41.0), (53.0, 53.0), (6.0, 81.0), (24.0, 89.0)] {
assert!(a.contains(vectorcraft_geom::Point::new(x, y)), "({x}, {y}) in {a:?}");
}
assert!(a.x1 < 75.0, "not B: {a:?}");
// Drawn again only there, it is what the whole frame drawn again shows.
let Changed::Area(area) = placed_area(&d, &made) else { panic!("an area") };
let after = Renderer::new().render(&d, 100, 100, Affine::IDENTITY, &opts);
let [x0, y0, x1, y1] = view_pixels(area, Affine::IDENTITY, 100, 100).expect("on screen");
let part = r.render(&d, x1 - x0, y1 - y0, Affine::translate((-f64::from(x0), -f64::from(y0))), &opts);
let mut patched = before.pixels.clone();
for y in 0..y1 - y0 {
let (from, to, n) = ((y * (x1 - x0) * 4) as usize, (((y0 + y) * 100 + x0) * 4) as usize, ((x1 - x0) * 4) as usize);
patched[to..to + n].copy_from_slice(&part.pixels[from..from + n]);
}
assert!(patched == after.pixels, "drawn again in {area:?}, it differs from drawing it all");
assert!(before.pixels != after.pixels, "the bitmaps show");
// Other files change nothing; art of a brush or a symbol showing the file changes everything.
assert_eq!(placed_area(&d, &files(&["area-z"])), Changed::Nothing);
let mut brushed = d.clone();
let scatter = vectorcraft_brush::Scatter { art: placed_node(66, "area-a", 40.0, 20.0, Affine::IDENTITY), ..Default::default() };
let brush = vectorcraft_brush::Brush { name: "Cards".into(), kind: vectorcraft_brush::BrushKind::Scatter(scatter) };
vectorcraft_brush::store(&mut brushed, &[brush]);
assert_eq!(placed_area(&brushed, &only_a), Changed::Everything);
d.symbols.push(vectorcraft_doc::Symbol { name: "Card".into(), art: Arc::new(placed_node(65, "area-a", 40.0, 20.0, Affine::IDENTITY)) });
assert_eq!(placed_area(&d, &only_a), Changed::Everything);
}
#[test]
fn a_placed_document_kept_as_its_preview_draws_the_preview() {
let _serial = serial();
let mut d = doc("preview-1");
// Only the preview (a 20×10 green PNG) is there, and the file can't be read here.
let img = image::RgbaImage::from_pixel(20, 10, image::Rgba([0, 200, 0, 255]));
let mut png = vec![];
img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
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, 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 _serial = serial();
let d = doc_with("loop-1", b"loop");
let t = std::time::Instant::now();
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]);
}