mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
Render: objects that share an appearance keep their own stroke outlines between frames
The renderer kept the outline of a stroke under the address of the stroke in the object's appearance, the bits of the width and the tolerance level, and used an entry only when the entry held the object being drawn. The slices of a gradient along or across a stroke were kept the same way. Objects can share one appearance until one of them changes it. Copies made by Duplicate Selection share the original's appearance, gradients included. For objects without a gradient fill or stroke, so do the art the renderer places for unstained symbol instances, the copies a live repeat draws and copies made by edit.duplicate. In each frame, the objects that share an appearance replaced each other's entries. The renderer therefore outlined their strokes again in every frame. The two caches now key each entry by the address of the object as well, and each object keeps its own outlines from frame to frame. Art that the renderer makes again in every frame keeps the old key: symbol art that placed_art does not keep, and the evaluations of live objects and the art of objects with effects that their caches do not keep. The copies in such art replace each other's entries as before. vectorcraft-cli bench of 2,000 unstained instances of a symbol of eight paths took 45.7 ms a frame on main and 31.1 ms with this change, at 2880x1800 on 8 threads, and a grid repeat of the same paths, which draws 2,000 groups, took 45.0 ms on main and 27.2 ms with this change. The peak working set of a bench run on the instances went from 88 MiB to 122 MiB. In the tests, each frame draws what a new renderer draws.
This commit is contained in:
1 parent
cd89392ac9
commit
9f0a9a362b
4 files changed
+244
-29
No files matched your search
@@ -754,6 +754,10 @@ impl Renderer {
|
||||
/// What [`Self::draw_object_fx`] draws inside the object's transparency group.
|
||||
fn draw_object_fx_art(&mut self, ctx: &mut RenderContext, f: &Frame, a: &Arc<Node>, cache: bool) {
|
||||
let art = self.fx_art(f.doc, a, cache);
|
||||
// Art the cache did not keep is made again in the next frame.
|
||||
let kept = cache && self.fx_arts.get(&(Arc::as_ptr(a) as usize), self.stamp).is_some_and(|e| Arc::ptr_eq(&e.1, &art));
|
||||
let outer = self.made_again;
|
||||
self.made_again = outer || !kept;
|
||||
// With Crop Marks, the object inside the art paints its own raster effects.
|
||||
let rfx = if effects::has_crop_marks(a) { vec![] } else { effects::raster_effects(&a.appearance.effects) };
|
||||
match cull_bounds(&art) {
|
||||
@@ -769,6 +773,7 @@ impl Renderer {
|
||||
}
|
||||
_ => self.draw_node(ctx, f, &art, true),
|
||||
}
|
||||
self.made_again = outer;
|
||||
}
|
||||
|
||||
/// The art [`Self::draw_object_fx`] draws for `a` (cached by `Arc` identity when `cache`):
|
||||
|
||||
+44
-26
@@ -395,11 +395,15 @@ pub struct Renderer {
|
||||
/// Worker threads for the multithreaded rasterizer (0 = single-threaded).
|
||||
pub threads: u16,
|
||||
geom: PtrMap<usize, GeomEntry>,
|
||||
/// Expanded stroke outlines keyed by (stroke layer address, width bits, tolerance level):
|
||||
/// panning re-renders reuse them instead of re-expanding every stroke.
|
||||
strokes: PtrMap<(usize, u64, i32), StrokeEntry>,
|
||||
/// Expanded stroke outlines keyed by ([`Self::stroke_owner`], stroke layer address, width bits,
|
||||
/// tolerance level): panning re-renders reuse them instead of re-expanding every stroke.
|
||||
strokes: PtrMap<(usize, usize, u64, i32), StrokeEntry>,
|
||||
/// The node being drawn on the plain-path fast path (owner of cached stroke outlines).
|
||||
cur: Option<Arc<Node>>,
|
||||
/// Whether the art being drawn is made again in every frame: symbol art that
|
||||
/// [`Self::placed_art`] did not keep, and evaluations of live objects and effect art that their
|
||||
/// caches did not keep.
|
||||
made_again: bool,
|
||||
/// The node `draw_shape_fx` may cache the live-effect geometry of (the one `draw_arc` is drawing).
|
||||
fx_owner: Option<Arc<Node>>,
|
||||
/// Live-effect geometry per node ([`fx::FxGeom`]), keyed by `Arc` identity like `geom`.
|
||||
@@ -445,8 +449,9 @@ pub struct Renderer {
|
||||
clip_paths: Vec<(BezPath, FillRule, Affine)>,
|
||||
/// What clipping paths paint, per clipping path (see [`Self::clip_paint_of`]).
|
||||
clip_paints: FrameCache<usize, ClipPaintEntry>,
|
||||
/// Gradients along or across strokes, keyed like `strokes` (see [`Self::fill_path_gradient`]).
|
||||
stroke_slices: PtrMap<(usize, i32), SliceEntry>,
|
||||
/// Gradients along or across strokes (see [`Self::fill_path_gradient`]), keyed by
|
||||
/// ([`Self::stroke_owner`], stroke layer address, tolerance level).
|
||||
stroke_slices: PtrMap<(usize, usize, i32), SliceEntry>,
|
||||
/// What the image keys colour adjustments give their recoloured images name: the image
|
||||
/// recoloured and the adjustment, with its hash (see [`Self::adjusted_image`]).
|
||||
adjusts: HashMap<String, (String, u64, vectorcraft_effects::ColorMap)>,
|
||||
@@ -529,6 +534,7 @@ impl Renderer {
|
||||
geom: PtrMap::default(),
|
||||
strokes: PtrMap::default(),
|
||||
cur: None,
|
||||
made_again: false,
|
||||
fx_owner: None,
|
||||
fx_geom: PtrMap::default(),
|
||||
stamp: 0,
|
||||
@@ -1190,8 +1196,10 @@ impl Renderer {
|
||||
NodeKind::Image(im) => self.draw_image(ctx, f, im),
|
||||
NodeKind::SymbolInstance { symbol, xf } => {
|
||||
if let Some(sym) = f.doc.symbols.iter().find(|s| &s.name == symbol) {
|
||||
let art = self.placed_art(&sym.art, *xf, false, Some(n));
|
||||
let (art, kept) = self.placed_art(&sym.art, *xf, false, Some(n));
|
||||
let outer = std::mem::replace(&mut self.made_again, !kept);
|
||||
self.draw_node(ctx, f, &art, true);
|
||||
self.made_again = outer;
|
||||
}
|
||||
}
|
||||
NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Mesh(_) | NodeKind::Repeat(_) | NodeKind::PlacedDocument(_) => {
|
||||
@@ -1443,7 +1451,7 @@ impl Renderer {
|
||||
};
|
||||
let slices = match self.cur.clone() {
|
||||
Some(node) => {
|
||||
let key = (st as *const StrokeLayer as usize, level);
|
||||
let key = (self.stroke_owner(&node), st as *const StrokeLayer as usize, level);
|
||||
match self.stroke_slices.get_mut(&key) {
|
||||
Some(e) if Arc::ptr_eq(&e.node, &node) => {
|
||||
e.stamp = self.stamp;
|
||||
@@ -1481,13 +1489,13 @@ impl Renderer {
|
||||
}
|
||||
|
||||
/// The line part of a stroke of the fast-path node `self.cur` (see
|
||||
/// [`effects::stroke::line_outline`]), cached across frames. The tolerance (vello's 0.25
|
||||
/// device px) is bucketed to powers of two, never coarser than needed, so zooming reuses
|
||||
/// outlines until the level changes.
|
||||
/// [`effects::stroke::line_outline`]), cached across frames under [`Self::stroke_owner`] and
|
||||
/// the stroke layer. The tolerance (vello's 0.25 device px) is bucketed to powers of two, never
|
||||
/// coarser than needed, so zooming reuses outlines until the level changes.
|
||||
fn cached_stroke(&mut self, f: &Frame, line: &BezPath, st: &StrokeLayer, width: f64) -> Option<Arc<BezPath>> {
|
||||
let node = self.cur.clone()?;
|
||||
let level = (f.px * 0.25).log2().floor() as i32;
|
||||
let key = (st as *const StrokeLayer as usize, width.to_bits(), level);
|
||||
let key = (self.stroke_owner(&node), st as *const StrokeLayer as usize, width.to_bits(), level);
|
||||
if let Some(e) = self.strokes.get_mut(&key)
|
||||
&& Arc::ptr_eq(&e.node, &node)
|
||||
{
|
||||
@@ -1499,6 +1507,12 @@ impl Renderer {
|
||||
Some(outline)
|
||||
}
|
||||
|
||||
/// What the `strokes` and `stroke_slices` keys hold for the fast-path node `node`: its address,
|
||||
/// or 0 while art made again in every frame is drawn ([`Self::made_again`]).
|
||||
fn stroke_owner(&self, node: &Arc<Node>) -> usize {
|
||||
if self.made_again { 0 } else { Arc::as_ptr(node) as usize }
|
||||
}
|
||||
|
||||
fn draw_text(&mut self, ctx: &mut RenderContext, f: &Frame, n: &Node, t: &TextObject) {
|
||||
let t = &*f.doc.inline_resolved(t);
|
||||
let g = text_geom_snapped(t, f.text_snap(t));
|
||||
@@ -1525,23 +1539,26 @@ impl Renderer {
|
||||
let Some(art) = t.runs.get(ig.run).and_then(|r| r.inline.as_ref()) else { continue };
|
||||
let Some(sym) = f.doc.symbols.iter().find(|s| s.name == art.symbol) else { continue };
|
||||
// Strokes scale with the art (it is sized to the type, as in the SVG `<use>`).
|
||||
let node = self.placed_art(&sym.art, t.xf * ig.xf * f.doc.symbol_natural_xf(&art.symbol), true, None);
|
||||
let (node, kept) = self.placed_art(&sym.art, t.xf * ig.xf * f.doc.symbol_natural_xf(&art.symbol), true, None);
|
||||
let outer = std::mem::replace(&mut self.made_again, !kept);
|
||||
self.draw_node(ctx, f, &node, true);
|
||||
self.made_again = outer;
|
||||
}
|
||||
self.inline_depth -= 1;
|
||||
}
|
||||
|
||||
/// Symbol art `art` as an instance or an inline graphic draws it ([`place`]). Art that is a
|
||||
/// group or a layer without an appearance of its own is kept between frames under the art's
|
||||
/// `Arc`, `xf`, `scale_strokes` and the stain of instance `inst`: its members keep their `Arc`s
|
||||
/// from frame to frame, and the caches keyed by a node's address (geometry, glyphs, live-effect
|
||||
/// geometry) hit for them. Other art is placed again in every frame, and so is art that holds
|
||||
/// symbol instances, type with inline graphics, images, placed documents, live blends,
|
||||
/// envelopes or repeats, or raster effects ([`depends_on_document`]). Art that holds type is
|
||||
/// Symbol art `art` as an instance or an inline graphic draws it ([`place`]), and whether it is
|
||||
/// kept. Art that is a group or a layer without an appearance of its own is kept between frames
|
||||
/// under the art's `Arc`, `xf`, `scale_strokes` and the stain of instance `inst`: its members
|
||||
/// keep their `Arc`s from frame to frame, and the caches keyed by a node's address (geometry,
|
||||
/// stroke outlines, glyphs, live-effect geometry) hit for them. Other art is placed again in
|
||||
/// every frame, and so is art that holds symbol instances, type with inline graphics, images,
|
||||
/// placed documents, live blends, envelopes or repeats, or raster effects
|
||||
/// ([`depends_on_document`]), and art that the cache has no room for. Art that holds type is
|
||||
/// placed again when the fonts change.
|
||||
fn placed_art(&mut self, art: &Arc<Node>, xf: Affine, scale_strokes: bool, inst: Option<&Node>) -> Arc<Node> {
|
||||
fn placed_art(&mut self, art: &Arc<Node>, xf: Affine, scale_strokes: bool, inst: Option<&Node>) -> (Arc<Node>, bool) {
|
||||
if !matches!(art.kind, NodeKind::Group { .. } | NodeKind::Layer { .. }) || fx::has_object_fx(art) {
|
||||
return place(art, xf, scale_strokes, inst);
|
||||
return (place(art, xf, scale_strokes, inst), false);
|
||||
}
|
||||
let stain = stain_bits(inst.and_then(vectorcraft_brush::stain));
|
||||
let key = (Arc::as_ptr(art) as usize, xf.as_coeffs().map(f64::to_bits), scale_strokes, stain);
|
||||
@@ -1550,18 +1567,19 @@ impl Renderer {
|
||||
&& e.fonts.is_none_or(|g| g == vectorcraft_text::FontDb::global().generation())
|
||||
{
|
||||
return match &e.placed {
|
||||
Some(placed) => placed.clone(),
|
||||
None => place(art, xf, scale_strokes, inst),
|
||||
Some(placed) => (placed.clone(), true),
|
||||
None => (place(art, xf, scale_strokes, inst), false),
|
||||
};
|
||||
}
|
||||
let placed = place(art, xf, scale_strokes, inst);
|
||||
let kept = (!depends_on_document(art)).then(|| placed.clone());
|
||||
let fonts = (kept.is_some() && holds_type(art)).then(|| vectorcraft_text::FontDb::global().generation());
|
||||
let keeps = kept.is_some();
|
||||
let fonts = (keeps && holds_type(art)).then(|| vectorcraft_text::FontDb::global().generation());
|
||||
// The bytes count the key and the entry, the art's allocation that the weak reference
|
||||
// keeps, and the kept art.
|
||||
let bytes = size_of::<(PlacedKey, PlacedEntry, Node)>() + kept.as_deref().map_or(0, lru::node_bytes);
|
||||
self.placed_arts.insert(key, PlacedEntry { art: Arc::downgrade(art), placed: kept, fonts }, bytes, self.stamp);
|
||||
placed
|
||||
let inserted = self.placed_arts.insert(key, PlacedEntry { art: Arc::downgrade(art), placed: kept, fonts }, bytes, self.stamp);
|
||||
(placed, keeps && inserted)
|
||||
}
|
||||
|
||||
/// Cached glyph geometry for a text node (keyed by Arc identity like paths).
|
||||
|
||||
@@ -42,6 +42,12 @@ impl LiveCache {
|
||||
self.expanded.sweep(stamp);
|
||||
self.meshes.sweep(stamp);
|
||||
}
|
||||
|
||||
/// A cache whose evaluations take at most `budget` bytes.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn with_budget(budget: usize) -> Self {
|
||||
Self { expanded: FrameCache::new(budget, 256), ..Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
/// One level of evaluation of a live node (blend steps, envelope result with type outlined run by
|
||||
@@ -146,6 +152,10 @@ impl Renderer {
|
||||
NodeKind::Mesh(m) => self.draw_mesh(ctx, f, a, m, cache),
|
||||
NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Repeat(_) | NodeKind::Group { .. } | NodeKind::Layer { .. } => {
|
||||
let items = self.live_expanded(f.doc, a, cache);
|
||||
// An evaluation the cache did not keep is made again in the next frame.
|
||||
let kept = cache && self.live.expanded.get(&(Arc::as_ptr(a) as usize), self.stamp).is_some_and(|e| Arc::ptr_eq(&e.1, &items));
|
||||
let outer = self.made_again;
|
||||
self.made_again = outer || !kept;
|
||||
// The flag holds while this blend is drawn (see `draw_live`).
|
||||
if self.knockout && matches!(a.kind, NodeKind::Blend { .. }) {
|
||||
self.draw_knockout(ctx, f, &items);
|
||||
@@ -154,6 +164,7 @@ impl Renderer {
|
||||
self.draw_arc(ctx, f, c);
|
||||
}
|
||||
}
|
||||
self.made_again = outer;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! What renderers keep between frames: decoded images shared by all of them, image mips, symbol
|
||||
//! art placed for instances and inline graphics, raster effects made again when what they read
|
||||
//! changes, and caches swept by every kind of render. Each test holds [`fonts_steady`]: fonts added
|
||||
//! while it runs would make its renderers lay type out again.
|
||||
//! art placed for instances and inline graphics, stroke outlines, raster effects made again when
|
||||
//! what they read changes, and caches swept by every kind of render. Each test holds [`fonts_steady`]:
|
||||
//! fonts added while it runs would make its renderers lay type out again.
|
||||
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::{Document, ImageBlob, ImageObject, Node, NodeKind};
|
||||
@@ -244,6 +244,187 @@ fn symbol_instances_keep_their_placed_art_between_frames() {
|
||||
assert_eq!(Arc::strong_count(&d.symbols[0].art), 1, "only the document holds the symbol's art");
|
||||
}
|
||||
|
||||
/// The stroke outlines and the gradient slices of strokes that `r` keeps and that its last frame made
|
||||
/// or used.
|
||||
fn stroke_outlines(r: &Renderer) -> (Vec<Arc<BezPath>>, Vec<Slices>) {
|
||||
let outlines = r.strokes.values().filter(|e| e.stamp == r.stamp).map(|e| e.outline.clone()).collect();
|
||||
(outlines, r.stroke_slices.values().filter(|e| e.stamp == r.stamp).map(|e| e.slices.clone()).collect())
|
||||
}
|
||||
|
||||
/// How many of the outlines and of the slices in `before` (see [`stroke_outlines`]) the last frame of
|
||||
/// `r` used again.
|
||||
fn used_again(r: &Renderer, before: &(Vec<Arc<BezPath>>, Vec<Slices>)) -> (usize, usize) {
|
||||
fn again<T>(was: &[Arc<T>], now: &[Arc<T>]) -> usize {
|
||||
was.iter().filter(|a| now.iter().any(|b| Arc::ptr_eq(a, b))).count()
|
||||
}
|
||||
let (outlines, slices) = stroke_outlines(r);
|
||||
(again(&before.0, &outlines), again(&before.1, &slices))
|
||||
}
|
||||
|
||||
/// Copies can share the original's appearance: three copies of [`symbol_art`] made as
|
||||
/// `edit.duplicate` makes them, and a wave whose stroke has a gradient along it with a copy made as
|
||||
/// the Layers panel's Duplicate Selection makes it. Each object keeps its own stroke outlines and
|
||||
/// gradient slices from frame to frame, a copy moved gets new ones while the others keep theirs,
|
||||
/// and each frame draws what a new renderer draws.
|
||||
#[test]
|
||||
fn copies_sharing_an_appearance_keep_their_own_stroke_outlines() {
|
||||
use vectorcraft_doc::{Appearance, StrokeGradientMode};
|
||||
let mut d = Document::new(200.0, 100.0);
|
||||
let art = symbol_art(&mut d);
|
||||
let l = d.layers[0].id;
|
||||
let mut copies = vec![];
|
||||
for x in [30.0, 80.0, 130.0] {
|
||||
let mut copy = d.reid(&art);
|
||||
copy.transform(Affine::translate((x, 30.0)), true);
|
||||
copies.push(d.insert(Some(l), usize::MAX, copy).unwrap());
|
||||
}
|
||||
let mut st = StrokeLayer::new(Paint::Gradient(Box::new(vectorcraft_color::GradientPaint::new(Default::default()))), 6.0);
|
||||
st.gradient_mode = StrokeGradientMode::Along;
|
||||
let mut wave = BezPath::new();
|
||||
wave.move_to((20.0, 75.0));
|
||||
wave.curve_to((60.0, 55.0), (100.0, 95.0), (180.0, 75.0));
|
||||
let ap = Appearance { items: vec![AppearanceItem::Stroke(st)], ..Default::default() };
|
||||
let wave = Node::path(d.alloc_id(), vectorcraft_geom::PathData::from_bezpath(&wave), ap);
|
||||
let duplicate = d.reid(&wave);
|
||||
d.insert(Some(l), usize::MAX, wave).unwrap();
|
||||
d.insert(Some(l), usize::MAX, duplicate).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 = stroke_outlines(&r);
|
||||
assert_eq!((before.0.len(), before.1.len()), (8, 2), "the strokes of three copies and two waves, and the waves' slices");
|
||||
assert!(frame(&mut r, &d) == frame(&mut Renderer::new(), &d));
|
||||
assert_eq!(used_again(&r, &before), (8, 2), "the second frame draws the outlines the first made");
|
||||
// The first copy moves 5 pt right.
|
||||
if let Some(n) = d.node_mut(copies[0]) {
|
||||
n.transform(Affine::translate((5.0, 0.0)), true);
|
||||
}
|
||||
assert!(frame(&mut r, &d) == frame(&mut Renderer::new(), &d));
|
||||
assert_eq!(used_again(&r, &before), (6, 2), "the other copies and the waves keep theirs");
|
||||
}
|
||||
|
||||
/// The art placed for unstained symbol instances shares its appearance with the symbol's art (the
|
||||
/// first and the third instance of [`symbol_doc`]). Each instance keeps its own stroke outlines from
|
||||
/// frame to frame, and the second frame draws what a new renderer draws.
|
||||
#[test]
|
||||
fn symbol_instances_keep_their_own_stroke_outlines_between_frames() {
|
||||
let d = symbol_doc();
|
||||
let opts = RenderOptions::default();
|
||||
let frame = |r: &mut Renderer| r.render(&d, 200, 100, Affine::IDENTITY, &opts).pixels;
|
||||
let mut r = Renderer::new();
|
||||
frame(&mut r);
|
||||
let before = stroke_outlines(&r);
|
||||
assert_eq!(before.0.len(), 6, "the square's and the line's strokes of three instances");
|
||||
assert!(frame(&mut r) == frame(&mut Renderer::new()));
|
||||
assert_eq!(used_again(&r, &before), (6, 0), "the second frame draws the outlines the first made");
|
||||
}
|
||||
|
||||
/// The art of symbol `I` ([`symbol_art`] with an image over it) is placed again in every frame.
|
||||
/// The copies placed for its three unstained instances and for an inline graphic at the art's own
|
||||
/// size replace each other's stroke outlines: the renderer keeps one outline for each of the art's
|
||||
/// two strokes. Two copies of [`symbol_art`] made as `edit.duplicate` makes them and two instances
|
||||
/// of symbol `S` ([`symbol_art`]), drawn after those, keep their own outlines. With no room in the
|
||||
/// placed-art cache, `S`'s art is placed again in every frame as well, and its copies keep one
|
||||
/// outline for each stroke. Each frame draws what a new renderer draws.
|
||||
#[test]
|
||||
fn art_placed_again_in_every_frame_keeps_one_outline_per_stroke() {
|
||||
use vectorcraft_doc::text::{CharStyle, InlineArt, TextObject, TextRun};
|
||||
let mut d = Document::new(200.0, 100.0);
|
||||
d.images.insert("dot".into(), ImageBlob::new("image/png", png(4, 4, 108)));
|
||||
let mut art = symbol_art(&mut d);
|
||||
let im = ImageObject { key: "dot".into(), width: 4, height: 4, xf: Affine::translate((-2.0, -2.0)), link: None, placement: Default::default() };
|
||||
let image = Node::new(d.alloc_id(), NodeKind::Image(Box::new(im)));
|
||||
if let Some(c) = art.children_mut() {
|
||||
c.push(Arc::new(image));
|
||||
}
|
||||
d.symbols.push(vectorcraft_doc::Symbol { name: "I".into(), art: Arc::new(art) });
|
||||
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 instance = |d: &mut Document, symbol: &str, x: f64, y: f64| {
|
||||
let id = d.alloc_id();
|
||||
d.insert(Some(l), usize::MAX, Node::new(id, NodeKind::SymbolInstance { symbol: symbol.into(), xf: Affine::translate((x, y)) })).unwrap();
|
||||
};
|
||||
for x in [25.0, 65.0, 105.0] {
|
||||
instance(&mut d, "I", x, 25.0);
|
||||
}
|
||||
// Type as tall as `I`'s art shows the inline graphic at scale 1.
|
||||
let size = d.symbol_art_bounds("I").map_or(0.0, |b| b.height());
|
||||
let style = CharStyle { size, ..Default::default() };
|
||||
let mut t = TextObject::point(vectorcraft_geom::Point::new(140.0, 32.0), "A", style.clone());
|
||||
t.runs.push(TextRun::inline(InlineArt::new("I"), style));
|
||||
let id = d.alloc_id();
|
||||
d.insert(Some(l), usize::MAX, Node::new(id, NodeKind::Text(Box::new(t)))).unwrap();
|
||||
let art = symbol_art(&mut d);
|
||||
for x in [105.0, 145.0] {
|
||||
let mut copy = d.reid(&art);
|
||||
copy.transform(Affine::translate((x, 72.0)), true);
|
||||
d.insert(Some(l), usize::MAX, copy).unwrap();
|
||||
}
|
||||
for x in [25.0, 65.0] {
|
||||
instance(&mut d, "S", x, 72.0);
|
||||
}
|
||||
let opts = RenderOptions::default();
|
||||
let frame = |r: &mut Renderer| r.render(&d, 200, 100, Affine::IDENTITY, &opts).pixels;
|
||||
let counts = |r: &Renderer| (r.strokes.len(), r.stroke_slices.len());
|
||||
let mut r = Renderer::new();
|
||||
let mut full = Renderer::new();
|
||||
full.placed_arts = FrameCache::new(0, 0);
|
||||
let mut before = (vec![], vec![]);
|
||||
for n in 1..=4 {
|
||||
let want = frame(&mut Renderer::new());
|
||||
assert!(frame(&mut r) == want && frame(&mut full) == want, "frame {n}");
|
||||
// Two outlines for the art placed for `I`, two for each copy and two for each instance of
|
||||
// `S`; with no room in the cache, two for the art placed for `S`.
|
||||
assert_eq!((counts(&r), counts(&full)), ((10, 0), (8, 0)), "frame {n}");
|
||||
if n == 1 {
|
||||
before = stroke_outlines(&r);
|
||||
}
|
||||
}
|
||||
assert_eq!(used_again(&r, &before), (8, 0), "the instances of `S` and the copies keep theirs");
|
||||
}
|
||||
|
||||
/// The document holds a Grid Repeat of [`symbol_art`] (nine groups) and a group with a Transform
|
||||
/// effect around two copies of [`symbol_art`] made as `edit.duplicate` makes them. With no room in
|
||||
/// the caches of evaluated live objects and of effect art, the repeat is evaluated again and the
|
||||
/// group's art is made again in every frame. Their copies then replace each other's stroke
|
||||
/// outlines: the renderer keeps two outlines for the repeat and two for the copies. With room, each
|
||||
/// copy keeps its own. Each frame draws what a new renderer draws.
|
||||
#[test]
|
||||
fn art_evaluated_again_in_every_frame_keeps_one_outline_per_stroke() {
|
||||
let mut d = Document::new(200.0, 100.0);
|
||||
let l = d.layers[0].id;
|
||||
let mut source = symbol_art(&mut d);
|
||||
source.transform(Affine::translate((20.0, 20.0)), true);
|
||||
let repeat = vectorcraft_doc::pattern::RepeatSpec::grid(vec![Arc::new(source)], 4.0, 4.0);
|
||||
let id = d.alloc_id();
|
||||
d.insert(Some(l), usize::MAX, Node::new(id, NodeKind::Repeat(repeat))).unwrap();
|
||||
let art = symbol_art(&mut d);
|
||||
let mut copies = vec![];
|
||||
for x in [130.0, 170.0] {
|
||||
let mut copy = d.reid(&art);
|
||||
copy.transform(Affine::translate((x, 30.0)), true);
|
||||
copies.push(Arc::new(copy));
|
||||
}
|
||||
let mut moved = Node::group(d.alloc_id(), copies);
|
||||
moved.appearance.effects.push(effect("distort.transform", serde_json::json!({"moveV": 30.0})));
|
||||
d.insert(Some(l), usize::MAX, moved).unwrap();
|
||||
let opts = RenderOptions::default();
|
||||
let frame = |r: &mut Renderer| r.render(&d, 200, 100, Affine::IDENTITY, &opts).pixels;
|
||||
let mut r = Renderer::new();
|
||||
let mut full = Renderer::new();
|
||||
full.live = live::LiveCache::with_budget(0);
|
||||
full.fx_arts = FrameCache::new(0, 0);
|
||||
for n in 1..=4 {
|
||||
let want = frame(&mut Renderer::new());
|
||||
assert!(frame(&mut r) == want && frame(&mut full) == want, "frame {n}");
|
||||
// Two outlines for each of the nine repeated groups and each copy; with no room in the
|
||||
// caches, two for the repeat and two for the copies.
|
||||
assert_eq!((r.strokes.len(), full.strokes.len()), (22, 4), "frame {n}");
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
||||
Reference in new issue
Block a user