Merge pull request #1667 from dotCooCoo/feat/stroke-outlines-per-object
Render: objects that share an appearance keep their own stroke outlines between frames
This commit is contained in:
commit
1b54bc335d
|
|
@ -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`):
|
||||
|
|
|
|||
|
|
@ -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,191 @@ 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() {
|
||||
let _fonts = fonts_steady();
|
||||
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 _fonts = fonts_steady();
|
||||
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() {
|
||||
let _fonts = fonts_steady();
|
||||
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 _fonts = fonts_steady();
|
||||
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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue