vectorcraft/crates/render/src/tests_memory.rs

505 lines
26 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.

//! What renderers keep between frames: decoded images shared by all of them, image mips, symbol
//! art placed for instances and inline graphics, and caches swept by every kind of render.
use vectorcraft_color::{Color, Paint};
use vectorcraft_doc::{Document, ImageBlob, ImageObject, Node, NodeKind};
use super::*;
/// A `w`×`h` PNG whose pixels depend on `seed` (each test its own image: the store is shared by
/// the tests running alongside).
fn png(w: u32, h: u32, seed: u8) -> Vec<u8> {
let img = image::RgbaImage::from_fn(w, h, |x, y| {
let v = (x * 7 + y * 13) as u8;
image::Rgba([v ^ seed, v.wrapping_mul(3), seed, 255])
});
let mut out = vec![];
img.write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png).unwrap();
out
}
/// A document showing `bytes` as image `key` of `w`×`h` pixels, one point a pixel.
fn doc_with_image(key: &str, bytes: Vec<u8>, w: u32, h: u32) -> Document {
let mut d = Document::new(w as f64, h as f64);
d.images.insert(key.into(), ImageBlob::new("image/png", bytes));
let (id, layer) = (d.alloc_id(), d.layers[0].id);
let im = ImageObject { key: key.into(), width: w, height: h, xf: Affine::IDENTITY, link: None, placement: Default::default() };
d.insert(Some(layer), 0, Node::new(id, NodeKind::Image(Box::new(im)))).unwrap();
d
}
#[test]
fn renderers_share_one_decode_of_an_image() {
let bytes = png(16, 16, 101);
let a = doc_with_image("img", bytes.clone(), 16, 16);
// Another document holding the same image under another key (its own copy of the bytes).
let b = doc_with_image("photo", bytes, 16, 16);
let (mut r1, mut r2) = (Renderer::new(), Renderer::new());
let (k1, p1) = r1.image_pixmap(&a, "img", false, true).unwrap();
let (k2, p2) = r2.image_pixmap(&b, "photo", false, true).unwrap();
assert_eq!(k1, k2);
assert!(Arc::ptr_eq(&p1, &p2), "decoded once for both renderers");
// Drawing goes through the store too: the pixels stay the ones decoded.
let _ = r2.render(&b, 16, 16, Affine::IDENTITY, &RenderOptions::default());
assert!(Arc::ptr_eq(&r1.image_pixmap(&b, "photo", false, true).unwrap().1, &p1));
// A document using the same key for other pixels gets its own.
let c = doc_with_image("img", png(16, 16, 102), 16, 16);
let (k3, p3) = r1.image_pixmap(&c, "img", false, true).unwrap();
assert!(k3 != k1 && !Arc::ptr_eq(&p3, &p1));
assert!(
r1.render(&c, 16, 16, Affine::IDENTITY, &RenderOptions::default()).pixels
!= r1.render(&a, 16, 16, Affine::IDENTITY, &RenderOptions::default()).pixels
);
}
/// Thumbnails and single-node renders end their frame as full renders do: entries of nodes no
/// longer drawn are dropped, with the nodes they kept alive.
#[test]
fn node_renders_sweep_stale_cache_entries() {
let mut d = Document::new(10.0, 10.0);
let id = d.alloc_id();
let square = |x: f64| {
let p = vectorcraft_geom::shapes::rectangle(Rect::new(x, 0.0, x + 5.0, 5.0));
Arc::new(Node::path(id, p, vectorcraft_doc::Appearance::basic(Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0)))
};
let first = square(0.0);
let mut r = Renderer::new();
r.render_node(&d, &first, 10, 10, Affine::IDENTITY).unwrap();
assert!(Arc::strong_count(&first) > 1, "cached while drawn");
for i in 0..1100 {
let n = square(i as f64);
if i % 2 == 0 {
r.render_node(&d, &n, 10, 10, Affine::IDENTITY).unwrap();
} else {
// A thumbnail of a group caches its member's geometry.
r.render_node_thumbnail(&d, &Node::group(id, vec![n]), 16, None).unwrap();
}
}
assert_eq!(Arc::strong_count(&first), 1, "swept: the renderer no longer keeps the node");
assert!(r.geom.len() <= 1025, "{} geometry entries", r.geom.len());
}
/// With `image_mips` off (the default; exports) an image draws from its full pixels whatever the
/// store holds; on, a reduced image draws from a box-filtered half (quarter…) instead, and at
/// more than half size from the full pixels as before.
#[test]
fn image_mips_are_opt_in_and_change_nothing_off() {
let d = doc_with_image("img", png(64, 64, 103), 64, 64);
let (off, on) = (RenderOptions::default(), RenderOptions { image_mips: true, ..Default::default() });
let quarter = Affine::scale(0.25);
let cold = Renderer::new().render(&d, 16, 16, quarter, &off);
// Mips made by another renderer don't change what this one draws with them off.
let mipped = Renderer::new().render(&d, 16, 16, quarter, &on);
let mut warm = Renderer::new();
assert!(warm.render(&d, 16, 16, quarter, &off).pixels == cold.pixels);
assert!(warm.render(&d, 16, 16, quarter, &off).pixels == cold.pixels, "warm");
// On, the quarter-size image samples the quarter-size mip: close to the full pixels' average
// but not the same pixels.
assert!(mipped.pixels != cold.pixels);
let (key, _) = warm.image_pixmap(&d, "img", false, true).unwrap();
let mip = images::store().peek(&images::ImageKey { mip: 2, ..key }).expect("quarter mip kept");
assert_eq!((mip.width(), mip.height()), (16, 16));
// Above half size, mips on draw the full pixels.
let near = Affine::scale(0.75);
assert!(Renderer::new().render(&d, 48, 48, near, &on).pixels == Renderer::new().render(&d, 48, 48, near, &off).pixels);
}
/// A document of `n` point type objects reading `word`, in rows.
fn text_doc(n: usize, word: &str) -> Document {
use vectorcraft_doc::text::{CharStyle, TextObject};
let mut d = Document::new(400.0, 400.0);
let l = d.layers[0].id;
for i in 0..n {
let at = vectorcraft_geom::Point::new(10.0 + (i % 8) as f64 * 48.0, 30.0 + (i / 8) as f64 * 30.0);
let style = CharStyle { size: 18.0, ..Default::default() };
let mut t = Node::new(NodeId(0), NodeKind::Text(Box::new(TextObject::point(at, word, style))));
t.id = d.alloc_id();
d.insert(Some(l), 0, t).unwrap();
}
d
}
/// The cache keys of `d`'s type objects (their nodes' addresses).
fn text_keys(d: &Document) -> Vec<usize> {
let mut keys = vec![];
d.walk(|n| {
if matches!(n.kind, NodeKind::Text(_)) {
keys.push(n as *const Node as usize);
}
});
keys
}
/// More type than the glyph cache's budget holds: the entries kept stay from frame to frame (the
/// rest is laid out again), where emptying the cache when full laid out every object every frame.
#[test]
fn type_over_the_text_budget_keeps_its_entries_between_frames() {
let d = text_doc(40, "Type");
let opts = RenderOptions::default();
let mut r = Renderer::new();
let want = r.render(&d, 400, 400, Affine::IDENTITY, &opts).pixels;
assert_eq!(r.texts.len(), 40);
let all = r.texts.bytes();
let mut r = Renderer::new();
r.texts = FrameCache::new(all / 2, 1024);
let keys = text_keys(&d);
let kept = |r: &Renderer| keys.iter().filter(|k| r.texts.contains_key(*k)).copied().collect::<Vec<_>>();
assert_eq!(r.render(&d, 400, 400, Affine::IDENTITY, &opts).pixels, want);
let first = kept(&r);
assert!(!first.is_empty() && first.len() < 40 && r.texts.bytes() <= all / 2, "{} entries, {} bytes", first.len(), r.texts.bytes());
for _ in 0..3 {
assert_eq!(r.render(&d, 400, 400, Affine::IDENTITY, &opts).pixels, want);
assert_eq!(kept(&r), first, "the same entries stay");
}
// Other type: its entries replace the earlier frames' ones, within the budget.
let e = text_doc(40, "Other");
r.render(&e, 400, 400, Affine::IDENTITY, &opts);
assert!(kept(&r).is_empty(), "entries of earlier frames went first");
assert!(!r.texts.is_empty() && r.texts.bytes() <= all / 2);
}
/// Type no longer drawn leaves the glyph cache after a few frames, and its bytes with it.
#[test]
fn swept_type_entries_give_back_their_bytes() {
let d = text_doc(12, "Swept");
let mut r = Renderer::new();
r.texts = FrameCache::new(lru::TEXT_BUDGET, 0);
r.render(&d, 400, 400, Affine::IDENTITY, &RenderOptions::default());
let full = r.texts.bytes();
assert!(full > 0 && r.texts.len() == 12);
let empty = Document::new(10.0, 10.0);
for _ in 0..5 {
r.render(&empty, 10, 10, Affine::IDENTITY, &RenderOptions::default());
}
assert_eq!((r.texts.len(), r.texts.bytes()), (0, 0));
}
/// A symbol's art inside the 20 × 20 square around the origin: a group of a square filled blue
/// with a 2 pt black stroke and a black line.
fn symbol_art(d: &mut Document) -> Node {
use vectorcraft_doc::Appearance;
let square = vectorcraft_geom::shapes::rectangle(Rect::new(-8.0, -8.0, 8.0, 8.0));
let square = Node::path(d.alloc_id(), square, Appearance::basic(Paint::solid(Color::rgb(0.0, 0.0, 1.0)), Paint::solid(Color::BLACK), 2.0));
let line = vectorcraft_geom::shapes::line((-8.0, 4.0).into(), (8.0, 4.0).into());
let line = Node::path(d.alloc_id(), line, Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 1.5));
Node::group(d.alloc_id(), vec![Arc::new(square), Arc::new(line)])
}
/// Three instances of symbol `S` ([`symbol_art`]): two in a row, the second stained red, and a
/// third turned and scaled by 1.5.
fn symbol_doc() -> Document {
let mut d = Document::new(200.0, 100.0);
let art = symbol_art(&mut d);
d.symbols.push(vectorcraft_doc::Symbol { name: "S".into(), art: Arc::new(art) });
let l = d.layers[0].id;
let xfs = [
Affine::translate((30.0, 30.0)),
Affine::translate((80.0, 30.0)),
Affine::translate((140.0, 60.0)) * Affine::rotate(0.5) * Affine::scale(1.5),
];
for (i, xf) in xfs.into_iter().enumerate() {
let mut inst = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "S".into(), xf });
if i == 1 {
inst.appearance.set_fill(Paint::solid(Color::rgb(1.0, 0.0, 0.0)));
}
d.insert(Some(l), i, inst).unwrap();
}
d
}
/// The addresses of the nodes `r` keeps geometry for that are not the own nodes of any version
/// of the document drawn (`docs`): the symbol art it placed for instances and inline graphics.
fn placed_copies(r: &Renderer, docs: &[&Document]) -> std::collections::BTreeSet<usize> {
let mut own = std::collections::HashSet::new();
for d in docs {
d.walk(|n| {
own.insert(n as *const Node as usize);
});
}
r.geom.values().map(|e| Arc::as_ptr(&e.node) as usize).filter(|p| !own.contains(p)).collect()
}
/// A second frame of an unchanged document draws the symbol art placed for its instances in the
/// first: it copies no art, and the geometry kept for the art's paths is used again. The renderer
/// holds the symbol's art itself only weakly.
#[test]
fn symbol_instances_keep_their_placed_art_between_frames() {
let d = symbol_doc();
let opts = RenderOptions::default();
let mut r = Renderer::new();
let first = r.render(&d, 200, 100, Affine::IDENTITY, &opts).pixels;
let placed = placed_copies(&r, &[&d]);
assert_eq!(placed.len(), 6, "the square and the line of three instances");
assert!(r.render(&d, 200, 100, Affine::IDENTITY, &opts).pixels == first);
assert_eq!(placed_copies(&r, &[&d]), placed, "the second frame draws the art the first placed");
assert_eq!(Arc::strong_count(&d.symbols[0].art), 1, "only the document holds the symbol's art");
}
/// Moving an instance places its art again, and editing a symbol's art places the art of its
/// instances again: the other instances keep theirs, and each frame draws what a new renderer
/// draws.
#[test]
fn editing_an_instance_or_a_symbol_places_only_its_art_again() {
let mut d = symbol_doc();
// Symbol `T` is a green square, and one instance of it stands below the others.
let green = vectorcraft_doc::Appearance::basic(Paint::solid(Color::rgb(0.0, 1.0, 0.0)), Paint::None, 0.0);
let square = Node::path(d.alloc_id(), vectorcraft_geom::shapes::rectangle(Rect::new(-5.0, -5.0, 5.0, 5.0)), green);
let t = Node::group(d.alloc_id(), vec![Arc::new(square)]);
d.symbols.push(vectorcraft_doc::Symbol { name: "T".into(), art: Arc::new(t) });
let (l, id) = (d.layers[0].id, d.alloc_id());
d.insert(Some(l), 0, Node::new(id, NodeKind::SymbolInstance { symbol: "T".into(), xf: Affine::translate((30.0, 80.0)) })).unwrap();
let opts = RenderOptions::default();
let frame = |r: &mut Renderer, d: &Document| r.render(d, 200, 100, Affine::IDENTITY, &opts).pixels;
let mut r = Renderer::new();
frame(&mut r, &d);
let before = placed_copies(&r, &[&d]);
assert_eq!(before.len(), 7);
// The first instance of `S` moves 5 pt right.
let unmoved = d.clone();
let first = d.layers[0].children().unwrap()[1].id;
if let Some(NodeKind::SymbolInstance { xf, .. }) = d.node_mut(first).map(|n| &mut n.kind) {
*xf = Affine::translate((5.0, 0.0)) * *xf;
}
assert!(frame(&mut r, &d) == frame(&mut Renderer::new(), &d));
let moved = placed_copies(&r, &[&unmoved, &d]);
assert!(before.is_subset(&moved) && moved.len() == before.len() + 2, "{} copies, then {}", before.len(), moved.len());
// `S`'s square is repainted red: its instances show it, and `T`'s instance keeps its art.
let mut art = (*d.symbols[0].art).clone();
if let Some(square) = art.children_mut().and_then(|c| c.first_mut()) {
Arc::make_mut(square).appearance.set_fill(Paint::solid(Color::rgb(1.0, 0.0, 0.0)));
}
d.symbols[0].art = Arc::new(art);
let red = frame(&mut r, &d);
assert!(red == frame(&mut Renderer::new(), &d));
let img = Rendered { width: 200, height: 100, pixels: red };
assert_eq!(img.pixel(140, 60), [255, 0, 0, 255], "the turned instance's square");
let edited = placed_copies(&r, &[&unmoved, &d]);
assert!(moved.is_subset(&edited) && edited.len() == moved.len() + 6, "{} copies, then {}", moved.len(), edited.len());
}
/// Draws `d` (200 × 100 points, on white), applies `edit` and draws it again with the same
/// renderer: the frame changed, a new renderer draws the same, and only the document holds the
/// symbols' art.
fn edit_draws_as_new(mut d: Document, edit: impl FnOnce(&mut Document)) {
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
let mut r = Renderer::new();
let before = r.render(&d, 200, 100, Affine::IDENTITY, &opts).pixels;
edit(&mut d);
let after = r.render(&d, 200, 100, Affine::IDENTITY, &opts).pixels;
assert!(after != before, "the edit changes the frame");
assert!(after == Renderer::new().render(&d, 200, 100, Affine::IDENTITY, &opts).pixels, "a new renderer draws the same");
assert!(d.symbols.iter().all(|s| Arc::strong_count(&s.art) == 1), "only the document holds the symbols' art");
}
/// `d` with symbol `Holder` (a group of `members`) shown by one instance at (100, 50).
fn holder_doc(mut d: Document, members: Vec<Node>) -> Document {
let holder = Node::group(d.alloc_id(), members.into_iter().map(Arc::new).collect());
d.symbols.push(vectorcraft_doc::Symbol { name: "Holder".into(), art: Arc::new(holder) });
let (l, id) = (d.layers[0].id, d.alloc_id());
d.insert(Some(l), 0, Node::new(id, NodeKind::SymbolInstance { symbol: "Holder".into(), xf: Affine::translate((100.0, 50.0)) })).unwrap();
d
}
/// Symbol art that shows another symbol (type with an inline graphic of it, an instance of it with
/// a Transform effect) is placed again in every frame: when that symbol's art changes, the next
/// frame draws what a new renderer draws.
#[test]
fn art_showing_another_symbol_draws_its_edits() {
use vectorcraft_doc::text::{CharStyle, InlineArt, TextObject, TextRun};
let mut d = Document::new(200.0, 100.0);
let art = symbol_art(&mut d);
d.symbols.push(vectorcraft_doc::Symbol { name: "S".into(), art: Arc::new(art) });
let style = CharStyle { size: 24.0, ..Default::default() };
let mut t = TextObject::point(vectorcraft_geom::Point::new(-60.0, 10.0), "A", style.clone());
t.runs.push(TextRun::inline(InlineArt::new("S"), style.clone()));
t.runs.push(TextRun::new("B", style));
let text = Node::new(d.alloc_id(), NodeKind::Text(Box::new(t)));
let mut moved = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "S".into(), xf: Affine::translate((40.0, 0.0)) });
moved.appearance.effects.push(vectorcraft_doc::Effect {
id: "distort.transform".into(),
params: serde_json::json!({"moveH": 4.0}),
visible: true,
});
let d = holder_doc(d, vec![text, moved]);
// `S` becomes a wide red bar: the inline graphic grows, and the type after it moves.
edit_draws_as_new(d, |d| {
let bar = vectorcraft_geom::shapes::rectangle(Rect::new(-20.0, -4.0, 20.0, 4.0));
let red = vectorcraft_doc::Appearance::basic(Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0);
d.symbols[0].art = Arc::new(Node::path(d.alloc_id(), bar, red));
});
}
/// Symbol art with raster effects is placed again in every frame: when a pattern under a blur or
/// the document's raster resolution under De-Interlace changes, the next frame draws what a new
/// renderer draws.
#[test]
fn art_with_raster_effects_draws_pattern_and_resolution_edits() {
use vectorcraft_doc::pattern::{PatternDef, pattern_paint};
let effect = |id: &str, params: serde_json::Value| vectorcraft_doc::Effect { id: id.into(), params, visible: true };
let mut d = Document::new(200.0, 100.0);
d.raster_effects_ppi = 72.0;
// Pattern `Checks` is a red 10 × 10 square in a 20 × 20 tile.
let tile = |c: Color| {
let square = vectorcraft_geom::shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0));
vec![Arc::new(Node::path(NodeId(900), square, vectorcraft_doc::Appearance::basic(Paint::solid(c), Paint::None, 0.0)))]
};
let mut def = PatternDef::new("Checks", tile(Color::rgb(1.0, 0.0, 0.0)));
def.tile = Rect::new(0.0, 0.0, 20.0, 20.0);
d.patterns.push(def);
let band = vectorcraft_geom::shapes::rectangle(Rect::new(-90.0, -45.0, -20.0, -25.0));
let mut blurred = Node::path(d.alloc_id(), band, vectorcraft_doc::Appearance::basic(pattern_paint("Checks"), Paint::None, 0.0));
blurred.appearance.effects.push(effect("blur.gaussian", serde_json::json!({"radius": 2.0})));
// The path holds ten bars, 1 pt high and 2 pt apart.
let mut bars = vectorcraft_geom::shapes::rectangle(Rect::new(20.0, -40.0, 90.0, -39.0));
for k in 1..10 {
let y = -40.0 + 2.0 * k as f64;
bars.subpaths.extend(vectorcraft_geom::shapes::rectangle(Rect::new(20.0, y, 90.0, y + 1.0)).subpaths);
}
let mut striped = Node::path(d.alloc_id(), bars, vectorcraft_doc::Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0));
striped.appearance.effects.push(effect("video.deinterlace", serde_json::json!({})));
let d = holder_doc(d, vec![blurred, striped]);
edit_draws_as_new(d, |d| {
if let Some(def) = d.pattern_mut("Checks") {
def.art = tile(Color::rgb(0.0, 0.0, 1.0));
}
d.raster_effects_ppi = 24.0;
});
}
/// Symbol art with a live object (an envelope that distorts a pattern fill) is placed again in
/// every frame: when the pattern's tile type changes, the next frame draws what a new renderer
/// draws.
#[test]
fn art_with_live_objects_draws_pattern_layout_edits() {
use vectorcraft_doc::live::EnvelopeOptions;
use vectorcraft_doc::pattern::{PatternDef, TileType, pattern_paint};
let mut d = Document::new(200.0, 100.0);
// Pattern `Checks` is a red 10 × 10 square in a 20 × 20 tile.
let square = vectorcraft_geom::shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0));
let red = vectorcraft_doc::Appearance::basic(Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0);
let mut def = PatternDef::new("Checks", vec![Arc::new(Node::path(NodeId(900), square, red))]);
def.tile = Rect::new(0.0, 0.0, 20.0, 20.0);
d.patterns.push(def);
let band = vectorcraft_geom::shapes::rectangle(Rect::new(-80.0, -40.0, 80.0, 40.0));
let filled = Node::path(d.alloc_id(), band, vectorcraft_doc::Appearance::basic(pattern_paint("Checks"), Paint::None, 0.0));
let kind = vectorcraft_doc::EnvelopeKind::Warp { style: "arc".into(), bend: 30.0, h: 0.0, v: 0.0, horizontal: true };
let options = EnvelopeOptions { distort_appearance: true, distort_pattern_fills: true, ..Default::default() };
let envelope = Node::new(
d.alloc_id(),
NodeKind::Envelope {
content: vec![Arc::new(filled)],
kind: Box::new(kind),
fidelity: 50.0,
editing: false,
options,
frame: Box::new(Affine::IDENTITY),
},
);
let d = holder_doc(d, vec![envelope]);
edit_draws_as_new(d, |d| {
if let Some(def) = d.pattern_mut("Checks") {
def.tile_type = TileType::BrickByRow { offset: 0.5 };
}
});
}
/// Symbol instances draw what groups of their art, stained and placed, draw: in every frame, when
/// two instances placed alike are stained differently, when the art is a single path, and with a
/// cache too small for all of the art. Art no longer drawn leaves the cache after a few frames.
#[test]
fn symbol_instances_draw_as_groups_of_their_placed_art() {
let mut d = symbol_doc();
let l = d.layers[0].id;
// An instance placed like the first one lies over it, stained green.
let mut over = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "S".into(), xf: Affine::translate((30.0, 30.0)) });
over.appearance.set_fill(Paint::solid(Color::rgb(0.0, 1.0, 0.0)));
d.insert(Some(l), 1, over).unwrap();
// Symbol `U` is a single path, a circle with a black stroke.
let circle = vectorcraft_geom::shapes::ellipse(Rect::new(-6.0, -6.0, 6.0, 6.0));
let ap = vectorcraft_doc::Appearance::basic(Paint::solid(Color::rgb(1.0, 0.8, 0.0)), Paint::solid(Color::BLACK), 1.0);
let circle = Node::path(d.alloc_id(), circle, ap);
d.symbols.push(vectorcraft_doc::Symbol { name: "U".into(), art: Arc::new(circle) });
let id = d.alloc_id();
d.insert(Some(l), usize::MAX, Node::new(id, NodeKind::SymbolInstance { symbol: "U".into(), xf: Affine::translate((175.0, 25.0)) })).unwrap();
// `groups` holds the same art in place of the instances: groups, and the circle as a path.
let mut groups = d.clone();
let placed = d.layers[0].children().unwrap().iter().map(|c| match &c.kind {
NodeKind::SymbolInstance { symbol, xf } => {
let sym = d.symbols.iter().find(|s| s.name == *symbol).unwrap();
let mut art = instance_art(&sym.art, c);
art.transform(*xf, false);
Arc::new(art)
}
_ => c.clone(),
});
*groups.children_mut(Some(l)).unwrap() = placed.collect();
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
let want = Renderer::new().render(&groups, 200, 100, Affine::IDENTITY, &opts).pixels;
let mut r = Renderer::new();
r.placed_arts = FrameCache::new(lru::PLACED_ART_BUDGET, 0);
for frame in 0..3 {
assert!(r.render(&d, 200, 100, Affine::IDENTITY, &opts).pixels == want, "frame {frame}");
}
// A cache that holds half the bytes of that art keeps what fits and places the rest every frame.
let all = r.placed_arts.bytes();
let mut small = Renderer::new();
small.placed_arts = FrameCache::new(all / 2, 0);
for frame in 0..3 {
assert!(small.render(&d, 200, 100, Affine::IDENTITY, &opts).pixels == want, "frame {frame} over the budget");
assert!(!small.placed_arts.is_empty() && small.placed_arts.bytes() <= all / 2, "{} of {all} bytes", small.placed_arts.bytes());
}
let empty = Document::new(10.0, 10.0);
for _ in 0..5 {
r.render(&empty, 10, 10, Affine::IDENTITY, &opts);
}
assert_eq!((r.placed_arts.len(), r.placed_arts.bytes()), (0, 0));
}
/// Inline graphics in type draw the art placed for them in an earlier frame, as instances do.
#[test]
fn inline_graphics_keep_their_placed_art_between_frames() {
use vectorcraft_doc::text::{CharStyle, InlineArt, TextObject, TextRun};
let mut d = Document::new(200.0, 100.0);
let art = symbol_art(&mut d);
d.symbols.push(vectorcraft_doc::Symbol { name: "S".into(), art: Arc::new(art) });
let style = CharStyle { size: 24.0, ..Default::default() };
let mut t = TextObject::point(vectorcraft_geom::Point::new(10.0, 50.0), "A", style.clone());
t.runs.push(TextRun::inline(InlineArt::new("S"), style.clone()));
t.runs.push(TextRun::new("B", style));
let (l, id) = (d.layers[0].id, d.alloc_id());
d.insert(Some(l), 0, Node::new(id, NodeKind::Text(Box::new(t)))).unwrap();
let opts = RenderOptions::default();
let mut r = Renderer::new();
let first = r.render(&d, 200, 100, Affine::IDENTITY, &opts).pixels;
let placed = placed_copies(&r, &[&d]);
assert_eq!(placed.len(), 2, "the square and the line of the inline graphic");
assert!(r.render(&d, 200, 100, Affine::IDENTITY, &opts).pixels == first);
assert_eq!(placed_copies(&r, &[&d]), placed, "the second frame draws the art the first placed");
}
/// Each run draws exactly the outlines it drew when kept as a path of its own: its glyphs and
/// decorations in layout order (the whole text's path is kept once, the runs as spans of it).
#[test]
fn text_runs_draw_their_own_glyphs_from_the_shared_outlines() {
use vectorcraft_doc::text::{CharStyle, TextObject, TextRun};
let plain = CharStyle { size: 24.0, ..Default::default() };
let under = CharStyle { underline: true, ..plain.clone() };
let mut t = TextObject::point(vectorcraft_geom::Point::new(10.0, 40.0), "Ab", plain.clone());
t.runs.push(TextRun::new("cd", under));
t.runs.push(TextRun::new("ef", plain));
let g = text_geom(&t);
let db = vectorcraft_text::FontDb::global();
let layout = vectorcraft_text::layout(db, &t);
let mut want = vec![BezPath::new(); t.runs.len()];
for gl in &layout.glyphs {
want[gl.run].extend(gl.outline.iter());
}
for (run, bar) in vectorcraft_text::decorations(&layout, db, &t) {
want[run].extend(bar.iter());
}
for (i, w) in want.iter().enumerate() {
assert_eq!(g.run(i).elements(), w.elements(), "run {i}");
}
assert!(g.run(7).elements().is_empty());
}