diff --git a/crates/doc/src/hit.rs b/crates/doc/src/hit.rs index 18deed56b..799bcd6f6 100644 --- a/crates/doc/src/hit.rs +++ b/crates/doc/src/hit.rs @@ -547,7 +547,7 @@ fn hit_leaf(n: &Node, p: Point, opt: HitOptions, symbols: &SymbolBoxes) -> Optio NodeKind::Text(_) | NodeKind::PlacedDocument(_) | NodeKind::SymbolInstance { .. } | NodeKind::Blend { .. } | NodeKind::Envelope { .. } => { n.geometric_bounds_with(symbols).filter(|b| b.inflate(opt.tol, opt.tol).contains(p)).map(|_| HitKind::Bounds) } - NodeKind::Repeat(r) => r.expand().iter().find_map(|g| hit_any(g, p, opt, symbols)), + NodeKind::Repeat(r) => r.expand(Some(symbols.document())).iter().find_map(|g| hit_any(g, p, opt, symbols)), NodeKind::Mesh(m) => { let bp = m.outline().to_bezpath(); if stroke_contains(&bp, 0.0, opt.tol, p) { @@ -659,7 +659,7 @@ pub fn marquee(doc: &Document, r: Rect, scope: Option, leaves: bool) -> NodeKind::Layer { children, .. } | NodeKind::Group { children, .. } | NodeKind::Intertwine { children } => marquee_in(n, r) .is_some_and(|r| children.iter().enumerate().any(|(i, c)| c.visible && !shaper_hides(n, i, scope) && touches(c, s, r, scope))), NodeKind::Compound { children, .. } => children.iter().any(|c| c.visible && touches(c, s, r, scope)), - NodeKind::Repeat(spec) => spec.expand().iter().any(|copy| touches(copy, s, r, scope)), + NodeKind::Repeat(spec) => spec.expand(Some(s.document())).iter().any(|copy| touches(copy, s, r, scope)), // A box with no width or no height (an instance of a symbol that draws a straight // line) is touched where the marquee crosses it. _ => n.geometric_bounds_with(s).is_some_and(|b| { diff --git a/crates/doc/src/live.rs b/crates/doc/src/live.rs index fafa2fc98..b434245cc 100644 --- a/crates/doc/src/live.rs +++ b/crates/doc/src/live.rs @@ -1476,6 +1476,9 @@ pub struct Hooks<'a> { pub appearance: Outliner<'a>, /// Distort Pattern Fills: the tiles a pattern fill lays (see [`PatternTiles`]). pub pattern: Option>, + /// The document whose symbols' art counts toward the instance budget of a Repeat of their + /// instances ([`crate::RepeatSpec::expand`]). + pub symbols: Option<&'a crate::Document>, } impl<'a> From> for Hooks<'a> { @@ -1792,7 +1795,7 @@ pub fn expand_live_hooks(n: &Node, hooks: Hooks) -> Vec { NodeKind::Blend { children, spec } => blend_expand(children, spec), NodeKind::Envelope { content, kind, fidelity, options, frame, .. } => expand_envelope(content, kind, *fidelity, *options, **frame, hooks), NodeKind::Mesh(m) => mesh_quad_nodes(m, 8), - NodeKind::Repeat(r) => r.expand(), + NodeKind::Repeat(r) => r.expand(hooks.symbols), NodeKind::PlacedDocument(p) => p.art(n), NodeKind::Intertwine { children } => crate::intertwine::expand(children), _ => vec![n.clone()], diff --git a/crates/doc/src/node.rs b/crates/doc/src/node.rs index 7f7f34022..c81383cd7 100644 --- a/crates/doc/src/node.rs +++ b/crates/doc/src/node.rs @@ -1227,7 +1227,7 @@ impl Node { NodeKind::Mesh(m) => m.bounds(), // Each copy is measured where the repeat draws it: `RepeatSpec::transforms` lays the // copies out by the source's bounds measured without the document. - NodeKind::Repeat(r) => r.copies_bounds(union(&r.source)?), + NodeKind::Repeat(r) => r.copies_bounds(union(&r.source)?, symbols.map(|s| s.document())), NodeKind::PlacedDocument(p) => Some(p.bounds()), // The members drawn over the others are drawn where they are. NodeKind::Intertwine { children } => union(children), diff --git a/crates/doc/src/pattern.rs b/crates/doc/src/pattern.rs index fdab92861..da310eb65 100644 --- a/crates/doc/src/pattern.rs +++ b/crates/doc/src/pattern.rs @@ -11,12 +11,15 @@ //! **Repeats.** A [`RepeatSpec`] holds source art and a radial / grid / mirror arrangement; the //! instances are the source under [`repeat_transforms`] (evaluated on demand like blends). +use std::cell::RefCell; +use std::collections::HashMap; use std::sync::Arc; use serde::{Deserialize, Serialize}; use vectorcraft_color::Paint; use vectorcraft_geom::{Affine, Point, Rect, Vec2}; +use crate::Document; use crate::appearance::AppearanceItem; use crate::node::{Node, NodeId, NodeKind}; @@ -519,17 +522,51 @@ pub struct RepeatSpec { /// many as one 500 × 500 grid, the budget [`PatternDef::offsets_covering`] also keeps. const MAX_REPEAT_INSTANCES: usize = 250_000; -/// Instances the Repeats in `nodes` make in all (0 when there are none). -fn nested_instances(nodes: &[Arc]) -> usize { - let mut sum = 0usize; - for n in nodes { - sum = sum.saturating_add(match (&n.kind, n.children()) { - (NodeKind::Repeat(r), _) => r.instances_in_all(), - (_, Some(c)) => nested_instances(c), - _ => 0, - }); +/// Counts the instances Repeats make in all, following symbol instances into their symbol's art +/// in `doc`: each symbol's art is counted once, and a missing symbol, or an instance of a symbol +/// inside its own art, counts as none. +/// +/// Limitation: symbols that show each other (A's art holds an instance of B and B's holds one of +/// A) are counted along the first path that reaches them. While A is counted, the instance of A +/// in B's art counts as none, and B keeps that count; had B been counted first, A's count would +/// include B's Repeats and B's would not. So within one count, what such symbols add can depend on +/// which of them came first. A symbol whose art reaches no such loop always counts the same. +struct NestedCount<'a> { + doc: Option<&'a Document>, + /// The counts made so far, by symbol name (`None` while that symbol's art is being counted). + symbols: RefCell>>, +} + +impl<'a> NestedCount<'a> { + fn new(doc: Option<&'a Document>) -> Self { + Self { doc, symbols: RefCell::default() } + } + + /// Instances the Repeats in the art of symbol `name` make in all. + fn symbol(&self, name: &str) -> usize { + if let Some(known) = self.symbols.borrow().get(name) { + return known.unwrap_or(0); + } + let Some(sym) = self.doc.and_then(|d| d.symbols.iter().find(|s| s.name == name)) else { return 0 }; + self.symbols.borrow_mut().insert(name.to_owned(), None); + let n = self.nodes(std::slice::from_ref(&sym.art)); + self.symbols.borrow_mut().insert(name.to_owned(), Some(n)); + n + } + + /// Instances the Repeats in `nodes` make in all (0 when there are none). + fn nodes(&self, nodes: &[Arc]) -> usize { + let mut sum = 0usize; + for n in nodes { + sum = sum.saturating_add(match (&n.kind, n.children()) { + (NodeKind::Repeat(r), _) => r.instances_in_all(self), + (NodeKind::SymbolInstance { symbol, .. }, _) => self.symbol(symbol), + (_, Some(c)) => self.nodes(c), + _ => 0, + }); + } + sum } - sum } /// Reflection across the line through `c` with direction angle `deg`. @@ -592,11 +629,13 @@ impl RepeatSpec { pub fn source_bounds(&self) -> Option { crate::live::nodes_bounds(&self.source) } - pub fn transforms(&self) -> Vec { + /// Where the instances go, counting toward the budget the Repeats in the art of the symbols + /// of `doc` that instances in the source show (none without a document). + pub fn transforms(&self, doc: Option<&Document>) -> Vec { let mut v = repeat_transforms(&self.kind, self.source_bounds()); // Each instance draws the Repeats in the source again, so nested ones multiply (a 500 × 500 // grid of one is 62.5 billion instances): keep the first that fit the budget. - v.truncate(self.kept(nested_instances(&self.source))); + v.truncate(self.kept(NestedCount::new(doc).nodes(&self.source))); v } /// How many of its instances this Repeat keeps when each makes `nested` instances of the @@ -604,19 +643,21 @@ impl RepeatSpec { fn kept(&self, nested: usize) -> usize { self.kind.count().min(MAX_REPEAT_INSTANCES / nested.max(1)).max(1) } - /// How many instances this Repeat draws ([`Self::transforms`]): as many as its options make, - /// fewer when the Repeats in its source would take it past [`MAX_REPEAT_INSTANCES`]. - pub fn instances(&self) -> usize { - self.kept(nested_instances(&self.source)) + /// How many instances this Repeat draws ([`Self::transforms`] for `doc`): as many as its + /// options make, fewer when the Repeats in its source would take it past + /// [`MAX_REPEAT_INSTANCES`]. + pub fn instances(&self, doc: Option<&Document>) -> usize { + self.kept(NestedCount::new(doc).nodes(&self.source)) } /// Instances made in all, those of the Repeats nested in the source included. - fn instances_in_all(&self) -> usize { - let nested = nested_instances(&self.source); + fn instances_in_all(&self, count: &NestedCount) -> usize { + let nested = count.nodes(&self.source); self.kept(nested).saturating_mul(nested.max(1)) } - /// Evaluated instances: one group of the (transformed) source per instance (ids 0). - pub fn expand(&self) -> Vec { - self.transforms() + /// Evaluated instances: one group of the (transformed) source per instance (ids 0), as many + /// as [`Self::transforms`] keeps for `doc`. + pub fn expand(&self, doc: Option<&Document>) -> Vec { + self.transforms(doc) .into_iter() .map(|m| { let children = self @@ -633,14 +674,14 @@ impl RepeatSpec { }) .collect() } - /// Geometric bounds of all instances. - pub fn bounds(&self) -> Option { - self.copies_bounds(self.source_bounds()?) + /// Geometric bounds of all instances ([`Self::transforms`] for `doc`). + pub fn bounds(&self, doc: Option<&Document>) -> Option { + self.copies_bounds(self.source_bounds()?, doc) } /// The bounds of all instances of a source measuring `source`, each placed by its transform - /// ([`Self::transforms`]). - pub(crate) fn copies_bounds(&self, source: Rect) -> Option { - self.transforms().into_iter().fold(None, |acc, m| vectorcraft_geom::union_opt(acc, Some(m.transform_rect_bbox(source)))) + /// ([`Self::transforms`] for `doc`). + pub(crate) fn copies_bounds(&self, source: Rect, doc: Option<&Document>) -> Option { + self.transforms(doc).into_iter().fold(None, |acc, m| vectorcraft_geom::union_opt(acc, Some(m.transform_rect_bbox(source)))) } /// Apply a document transform (source + arrangement parameters). pub fn transform(&mut self, a: Affine, scaling: impl Into) { @@ -864,11 +905,11 @@ mod tests { let r = RepeatSpec::radial(vec![sq(-5.0, -105.0, 10.0)], 4, Some(100.0)); let RepeatKind::Radial { center, .. } = r.kind else { panic!() }; assert!((center - Point::new(0.0, -0.0)).hypot() < 1e-9, "{center:?}"); - let cs: Vec = r.transforms().iter().map(|m| *m * Point::new(0.0, -100.0)).collect(); + let cs: Vec = r.transforms(None).iter().map(|m| *m * Point::new(0.0, -100.0)).collect(); assert!((cs[0] - Point::new(0.0, -100.0)).hypot() < 1e-9); assert!((cs[1] - Point::new(100.0, 0.0)).hypot() < 1e-9, "{cs:?}"); assert!((cs[2] - Point::new(0.0, 100.0)).hypot() < 1e-9); - assert_eq!(r.expand().len(), 4); + assert_eq!(r.expand(None).len(), 4); } #[test] @@ -877,18 +918,18 @@ mod tests { if let RepeatKind::Radial { radius, .. } = &mut r.kind { *radius = 50.0; } - let m = r.transforms()[0]; + let m = r.transforms(None)[0]; assert!((m * Point::new(0.0, -100.0) - Point::new(0.0, -50.0)).hypot() < 1e-9); } #[test] fn grid_repeat_transforms() { let g = RepeatSpec::grid(vec![sq(0.0, 0.0, 10.0)], 5.0, 2.0); - let t = g.transforms(); + let t = g.transforms(None); assert_eq!(t.len(), 9); assert!((t[1] * Point::ZERO - Point::new(15.0, 0.0)).hypot() < 1e-9); assert!((t[3] * Point::ZERO - Point::new(0.0, 12.0)).hypot() < 1e-9); - assert_eq!(g.bounds(), Some(Rect::new(0.0, 0.0, 40.0, 34.0))); + assert_eq!(g.bounds(None), Some(Rect::new(0.0, 0.0, 40.0, 34.0))); } #[test] @@ -897,7 +938,7 @@ mod tests { if let RepeatKind::Grid { flip_cols, .. } = &mut g.kind { *flip_cols = true; } - let t = g.transforms(); + let t = g.transforms(None); // Column 1 is mirrored about the source centre, then moved one tile right. assert!((t[1] * Point::new(0.0, 0.0) - Point::new(20.0, 0.0)).hypot() < 1e-9); } @@ -905,10 +946,10 @@ mod tests { #[test] fn mirror_transforms() { let m = RepeatSpec::mirror(vec![sq(0.0, 0.0, 10.0)], 90.0, 5.0); - let t = m.transforms(); + let t = m.transforms(None); assert_eq!(t.len(), 2); assert!((t[1] * Point::new(0.0, 3.0) - Point::new(30.0, 3.0)).hypot() < 1e-9); - let b = m.bounds().unwrap(); + let b = m.bounds(None).unwrap(); assert!((b.x1 - 30.0).abs() < 1e-9 && b.x0.abs() < 1e-9 && (b.y1 - 10.0).abs() < 1e-9, "{b:?}"); // Horizontal axis. let h = reflect_about(Point::new(0.0, 10.0), 0.0); @@ -918,9 +959,9 @@ mod tests { #[test] fn repeat_transform_moves_arrangement() { let mut r = RepeatSpec::radial(vec![sq(-5.0, -105.0, 10.0)], 4, Some(100.0)); - let before = r.bounds().unwrap(); + let before = r.bounds(None).unwrap(); r.transform(Affine::translate((50.0, 0.0)), false); - let after = r.bounds().unwrap(); + let after = r.bounds(None).unwrap(); assert!((after.x0 - before.x0 - 50.0).abs() < 1e-9); let mut m = RepeatSpec::mirror(vec![sq(0.0, 0.0, 10.0)], 90.0, 5.0); m.transform(Affine::rotate(std::f64::consts::FRAC_PI_2), false); @@ -933,7 +974,7 @@ mod tests { // Instances made in all, counted from the transforms without drawing any. fn made(r: &RepeatSpec) -> usize { let each: usize = r.source.iter().map(|s| if let NodeKind::Repeat(n) = &s.kind { made(n) } else { 1 }).sum(); - r.transforms().len().saturating_mul(each) + r.transforms(None).len().saturating_mul(each) } let grid = |source: Arc| { let mut g = RepeatSpec::grid(vec![source], 0.0, 0.0); @@ -950,7 +991,7 @@ mod tests { // A 500 × 500 grid of it made 62.5 billion. let outer = grid(repeat(inner.clone())); assert!(made(&outer) <= 250_000, "{} instances", made(&outer)); - assert_eq!(outer.expand().len(), outer.transforms().len()); + assert_eq!(outer.expand(None).len(), outer.transforms(None).len()); assert!(made(&RepeatSpec::mirror(vec![repeat(inner)], 90.0, 5.0)) <= 250_000); // Small nests are left as they are. let small = RepeatSpec::mirror(vec![repeat(RepeatSpec::grid(vec![sq(0.0, 0.0, 10.0)], 1.0, 1.0))], 90.0, 5.0); diff --git a/crates/effects/src/adjust.rs b/crates/effects/src/adjust.rs index d4f156d39..faa7a5a7c 100644 --- a/crates/effects/src/adjust.rs +++ b/crates/effects/src/adjust.rs @@ -415,7 +415,7 @@ pub fn adjust_in_document(d: &mut Document, n: &Node) -> Option { let src = match &n.kind { NodeKind::SymbolInstance { symbol, .. } if has_adjustment(n) => { let symbol = d.symbols.iter().find(|s| s.name == *symbol).map(|s| s.art.clone()); - art = crate::reshape::as_art(n, symbol.as_deref())?; + art = crate::reshape::as_art(Some(d), n, symbol.as_deref())?; &art } _ => n, diff --git a/crates/effects/src/bake.rs b/crates/effects/src/bake.rs index 9cb35674a..496946e30 100644 --- a/crates/effects/src/bake.rs +++ b/crates/effects/src/bake.rs @@ -104,7 +104,7 @@ fn bake_node(d: &mut Document, n: &Node) -> Option { return Some(bake_pieces(d, m)); } if let Some(m) = crate::revolve_art(n).or_else(|| crate::solid_art(n)) { - let m = evaluate_container(&m).unwrap_or(m); + let m = evaluate_container(Some(d), &m).unwrap_or(m); return Some(bake_pieces(d, m)); } // Type, images, symbol instances and live objects: reshaped through their outlines, and so are @@ -119,21 +119,21 @@ fn bake_node(d: &mut Document, n: &Node) -> Option { NodeKind::SymbolInstance { symbol, .. } => d.symbols.iter().find(|s| s.name == *symbol).map(|s| s.art.clone()), _ => None, }; - let m = reshape(n, symbol.as_deref())?; + let m = reshape(Some(d), n, symbol.as_deref())?; return Some(bake_node(d, &m).unwrap_or(m)); } // A path whose Transform effect copies it with a gradient, or scales its strokes or moves its // patterns: each copy an object of its own, so its gradient fits it rather than spanning // every copy and its strokes and patterns can differ from the others'. if crate::has_transform_art(n) - && let Some(m) = reshape(n, None) + && let Some(m) = reshape(Some(d), n, None) { return Some(bake_pieces(d, m)); } // Groups and layers: Pathfinder, geometry effects and their own fills and strokes (type that // Transform effects only move stays type). The new pieces (results, outlines, copies, the // fills' and strokes' art) get ids of their own. - if let Some(m) = container_keeping_type(n).or_else(|| evaluate_container(n)) { + if let Some(m) = container_keeping_type(d, n).or_else(|| evaluate_container(Some(d), n)) { return Some(bake_pieces(d, m)); } if let Some(ch) = n.children() { @@ -324,7 +324,7 @@ fn moved_type_group(n: &Node) -> Option { /// objects or inside groups with Pathfinder or geometry effects or fills and strokes that paint, is /// outlined as [`evaluate_container`] outlines it, and the other members become what it makes of /// them. `None` when no type stays type. -fn container_keeping_type(n: &Node) -> Option { +fn container_keeping_type(d: &Document, n: &Node) -> Option { fn keep(n: &mut Node, moves: &[serde_json::Value], clipping: bool, kept: &mut bool) { let clips = n.clips(); let Some(ch) = n.children_mut() else { return }; @@ -349,7 +349,7 @@ fn container_keeping_type(n: &Node) -> Option { } } None if clipping => outline_text(c).map(|o| Node { visible: c.visible, ..o }), - None => as_art(c, None), + None => as_art(None, c, None), }; if let Some(o) = art { *c = Arc::new(o); @@ -366,8 +366,7 @@ fn container_keeping_type(n: &Node) -> Option { if !text { return None; } - let hook: &dyn Fn(&Node) -> Option = &outline_text; - let mut m = vectorcraft_doc::live::expand_deep(n, Some(hook)); + let mut m = crate::reshape::evaluated(Some(d), n); let mut kept = false; keep(&mut m, &moves, false, &mut kept); if !kept { @@ -455,7 +454,7 @@ pub type StrokeArt<'a> = &'a mut dyn FnMut(&mut Document, &PathData, FillRule, & /// effects are dropped. `None` for other kinds of objects. pub fn expand_leaf(d: &mut Document, n: &Node, stroke_art: StrokeArt) -> Option { if let Some(m) = crate::revolve::expand_revolve_art(n).or_else(|| crate::solid_art(n)) { - let m = evaluate_container(&m).unwrap_or(m); + let m = evaluate_container(Some(d), &m).unwrap_or(m); return Some(bake_pieces(d, m)); } let (g, rule, ctx, mut m) = leaf_base(n)?; @@ -755,7 +754,7 @@ mod tests { baked.walk(&mut |n| assert!(ids.insert(n.id), "{params}: duplicate id {}", n.id)); // The renderer and Expand Appearance still draw it as outlines. let mut text = 0; - evaluate_container(&g).unwrap().walk(&mut |n| text += usize::from(matches!(n.kind, NodeKind::Text(_)))); + evaluate_container(None, &g).unwrap().walk(&mut |n| text += usize::from(matches!(n.kind, NodeKind::Text(_)))); assert_eq!(text, 0, "{params}"); } // Stacked effects that only move: every copy of a copy. diff --git a/crates/effects/src/group.rs b/crates/effects/src/group.rs index 5196b4fcc..fa82303a7 100644 --- a/crates/effects/src/group.rs +++ b/crates/effects/src/group.rs @@ -15,7 +15,7 @@ use std::sync::Arc; use serde_json::Value; use vectorcraft_doc::color::{Color, Paint}; -use vectorcraft_doc::{Appearance, AppearanceItem, Node, NodeId, NodeKind, StrokeLayer}; +use vectorcraft_doc::{Appearance, AppearanceItem, Document, Node, NodeId, NodeKind, StrokeLayer}; use vectorcraft_geom::{FillRule, PathData, Rect}; use vectorcraft_pathops::{self as po, PathfinderOp}; @@ -346,9 +346,10 @@ pub fn has_container_appearance(n: &Node) -> bool { /// The regions a group's or layer's own fills and strokes paint, in document space, each with its /// fill rule: every visible member path and compound path, type as its glyph outlines and live /// objects evaluated; the members of a clip group or clipping layer without its clipping path. -/// Images, symbol instances and guides add nothing. -pub fn member_shapes(n: &Node) -> Vec<(PathData, FillRule)> { - fn push(n: &Node, out: &mut Vec<(PathData, FillRule)>) { +/// Images, symbol instances and guides add nothing. A Repeat counts the Repeats in the art of +/// `doc`'s symbols that its instances show toward its instance budget. +pub fn member_shapes(doc: Option<&Document>, n: &Node) -> Vec<(PathData, FillRule)> { + fn push(doc: Option<&Document>, n: &Node, out: &mut Vec<(PathData, FillRule)>) { if !n.visible { return; } @@ -360,19 +361,19 @@ pub fn member_shapes(n: &Node) -> Vec<(PathData, FillRule)> { out.push((PathData::from_bezpath(&all).transformed(t.xf), FillRule::NonZero)); } } - NodeKind::Group { .. } | NodeKind::Layer { .. } => members(n, out), - _ if vectorcraft_doc::live::is_live(n) => members(&crate::reshape::outlined_members(n), out), + NodeKind::Group { .. } | NodeKind::Layer { .. } => members(doc, n, out), + _ if vectorcraft_doc::live::is_live(n) => members(doc, &crate::reshape::outlined_members(doc, n), out), _ => {} } } - fn members(n: &Node, out: &mut Vec<(PathData, FillRule)>) { + fn members(doc: Option<&Document>, n: &Node, out: &mut Vec<(PathData, FillRule)>) { let clip = usize::from(n.clips()); for c in n.children().into_iter().flatten().skip(clip) { - push(c, out); + push(doc, c, out); } } let mut out = vec![]; - members(n, &mut out); + members(doc, n, &mut out); out } @@ -414,8 +415,9 @@ fn item_art(id: NodeId, item: &AppearanceItem, shapes: &[(PathData, FillRule)], /// reshaping all its members as one piece (the whole art's bounds as their reference box), and its /// own fills and strokes turned into art painting the members' geometry ([`member_shapes`]) below /// the members (after a clipping path) or above them, as its contents slot says. Its -/// transparency, opacity mask and raster effects stay. `None` when it has none of these. -pub fn evaluate_container(n: &Node) -> Option { +/// transparency, opacity mask and raster effects stay. `None` when it has none of these. Live +/// objects among the members are evaluated with the symbols of `doc` (see [`member_shapes`]). +pub fn evaluate_container(doc: Option<&Document>, n: &Node) -> Option { if !is_container(n) { return None; } @@ -434,12 +436,12 @@ pub fn evaluate_container(n: &Node) -> Option { } } if !fx.is_empty() { - m = crate::reshape::outlined_members(&m); + m = crate::reshape::outlined_members(doc, &m); crate::reshape::reshape_leaves(&mut m, &fx); } m.appearance.effects.retain(|e| !is_pathfinder(&e.id) && !crate::is_geometry(&e.id)); if paint { - let shapes = member_shapes(&m); + let shapes = member_shapes(doc, &m); let bounds = m.geometric_bounds(); let art = |items: &[AppearanceItem]| -> Vec> { items.iter().filter_map(|i| item_art(n.id, i, &shapes, bounds)).map(Arc::new).collect() }; @@ -624,7 +626,7 @@ mod tests { fn a_random_transform_on_a_group_moves_its_members_together() { let mut g = group_with("distort.transform"); g.appearance.effects[0].params = serde_json::json!({"moveH": 100, "moveV": 100, "random": true}); - let art = evaluate_container(&g).unwrap(); + let art = evaluate_container(None, &g).unwrap(); let corner = |i: usize| art.children().and_then(|c| c.get(i)).and_then(|n| n.geometric_bounds()).map(|b| (b.x0, b.y0)).unwrap(); let (a, b) = (corner(0), corner(1)); assert!(a.0 > 0.0 && a.0 < 100.0, "moved by a share of the value: {a:?}"); @@ -640,7 +642,7 @@ mod tests { style(&mut t); let mut g = Node::group(NodeId(10), vec![sq(1, 0.0, Color::BLACK), Arc::new(t)]); g.appearance.effects.push(Effect { id: "path.offsetPath".into(), params: serde_json::json!({"offset": 0}), visible: true }); - let art = evaluate_container(&g).unwrap(); + let art = evaluate_container(None, &g).unwrap(); (*art.children().unwrap()[1]).clone() } @@ -698,7 +700,7 @@ mod tests { let before = start(&crate::outline_text(&t).unwrap().children().unwrap()[0]); let mut g = Node::group(NodeId(10), vec![square, t]); g.appearance.effects.push(Effect { id: "distort.transform".into(), params: serde_json::json!({"moveH": 30}), visible: true }); - let art = evaluate_container(&g).unwrap(); + let art = evaluate_container(None, &g).unwrap(); let members = art.children().unwrap(); let after = start(&members[1].children().unwrap()[0]); assert!((after - before - 30.0).abs() < 1e-6, "the type's gradient starts at {before}, then {after}"); @@ -718,7 +720,7 @@ mod tests { let mut g = Node::group(NodeId(10), vec![square]); let params = serde_json::json!({"moveH": 30, "copies": 2}); g.appearance.effects.push(Effect { id: "distort.transform".into(), params, visible: true }); - let art = evaluate_container(&g).unwrap(); + let art = evaluate_container(None, &g).unwrap(); let copies = art.children().unwrap()[0].children().expect("the object and its copies are objects of their own"); assert_eq!(copies.len(), 3); for (i, c) in copies.iter().enumerate() { diff --git a/crates/effects/src/live.rs b/crates/effects/src/live.rs index 7d03068c0..aad8d8edd 100644 --- a/crates/effects/src/live.rs +++ b/crates/effects/src/live.rs @@ -37,7 +37,7 @@ fn outline(doc: Option<&Document>, n: &Node) -> Option { fn with_hooks(doc: Option<&Document>, f: impl FnOnce(Hooks) -> R) -> R { let outline = |n: &Node| outline(doc, n); let tiles = |name: &str, xf: Affine, region: Rect| Some(doc?.pattern(name)?.instances_in(xf, region)); - f(Hooks { outline: Some(&outline), appearance: Some(&bake_appearance), pattern: Some(&tiles) }) + f(Hooks { outline: Some(&outline), appearance: Some(&bake_appearance), pattern: Some(&tiles), symbols: doc }) } /// One level of evaluation of live object `n` ([`live::expand_live_hooks`]). diff --git a/crates/effects/src/preview.rs b/crates/effects/src/preview.rs index a2f0daef6..a1b8eed98 100644 --- a/crates/effects/src/preview.rs +++ b/crates/effects/src/preview.rs @@ -186,7 +186,7 @@ impl<'s, 'd> Drawn<'s, 'd> { let inner = || n.children().is_some_and(|ch| ch.iter().any(|c| self.draws_geometry(c))); let painted = || has_own_paint(n) && inner(); match &n.kind { - NodeKind::Group { .. } | NodeKind::Layer { .. } if reshapes(n) || painted() => evaluate_container(n), + NodeKind::Group { .. } | NodeKind::Layer { .. } if reshapes(n) || painted() => evaluate_container(Some(self.symbols.document()), n), NodeKind::Path { .. } | NodeKind::Compound { .. } if reshapes(n) => path_art(n), NodeKind::SymbolInstance { symbol, .. } => { if geometry_effects(n).is_empty() && !self.symbol_draws(symbol) { @@ -196,9 +196,10 @@ impl<'s, 'd> Drawn<'s, 'd> { self.left.set(left); let art = self.symbols.measure_art(symbol, Arc::clone)?; // An instance in perspective is projected by `reshape`. - reshape(n, Some(&*art)).or_else(|| outline_art(n, Some(&*art))) + let doc = Some(self.symbols.document()); + reshape(doc, n, Some(&*art)).or_else(|| outline_art(doc, n, Some(&*art))) } - _ if reshapes(n) => reshape(n, None), + _ if reshapes(n) => reshape(Some(self.symbols.document()), n, None), NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Repeat(_) if inner() => { Some(expanded_live_group(Some(self.symbols.document()), n)) } diff --git a/crates/effects/src/reshape.rs b/crates/effects/src/reshape.rs index f7bf07c19..d11a5ae26 100644 --- a/crates/effects/src/reshape.rs +++ b/crates/effects/src/reshape.rs @@ -13,7 +13,8 @@ use std::sync::Arc; -use vectorcraft_doc::{Appearance, AppearanceItem, Effect, Node, NodeId, NodeKind}; +use vectorcraft_doc::live::Hooks; +use vectorcraft_doc::{Appearance, AppearanceItem, Document, Effect, Node, NodeId, NodeKind}; use vectorcraft_geom::{Affine, BezPath, FillRule, PathData, Rect, Shape, shapes}; use crate::{Budget, GeomContext, apply_one, is_geometry, merged_params}; @@ -118,7 +119,7 @@ pub fn image_paint(n: &Node) -> Vec<(BezPath, FillRule, AppearanceItem)> { if !matches!(n.kind, NodeKind::Image(_)) || !has_item_geometry(n) { return vec![]; } - let Some(art) = reshape(n, None) else { return vec![] }; + let Some(art) = reshape(None, n, None) else { return vec![] }; let mut out = vec![]; art.walk(&mut |c| { let (NodeKind::Path { rule, .. } | NodeKind::Compound { rule, .. }) = &c.kind else { return }; @@ -228,9 +229,9 @@ fn with_id(art: &Node, id: NodeId) -> Node { /// `n` as plain art that geometry effects can reshape, without its transparency and object /// effects: type outlined ([`outline_text`]), live objects evaluated (type inside them outlined), /// a symbol instance as `symbol_art` (the symbol's art, stained as the caller draws it) placed by -/// the instance's transform. `None` for other kinds (images, paths, containers). -pub fn outline_art(n: &Node, symbol_art: Option<&Node>) -> Option { - let hook: &dyn Fn(&Node) -> Option = &outline_text; +/// the instance's transform. `None` for other kinds (images, paths, containers). A Repeat counts +/// the Repeats in the art of `doc`'s symbols that its instances show toward its instance budget. +pub fn outline_art(doc: Option<&Document>, n: &Node, symbol_art: Option<&Node>) -> Option { let mut art = match &n.kind { NodeKind::Text(_) => outline_text(n)?, NodeKind::SymbolInstance { xf, .. } => { @@ -248,7 +249,7 @@ pub fn outline_art(n: &Node, symbol_art: Option<&Node>) -> Option { one.appearance.effects.clear(); Node::group(n.id, vec![Arc::new(one)]) } - _ if vectorcraft_doc::live::is_live(n) => vectorcraft_doc::live::expand_deep(n, Some(hook)), + _ if vectorcraft_doc::live::is_live(n) => evaluated(doc, n), _ => return None, }; outline_nested_text(&mut art, false); @@ -262,6 +263,14 @@ pub fn outline_art(n: &Node, symbol_art: Option<&Node>) -> Option { Some(art) } +/// `n` with every live object in it evaluated, type inside them outlined ([`outline_text`]). The +/// Repeats among them count the Repeats in the art of `doc`'s symbols that their instances show +/// toward their instance budget ([`vectorcraft_doc::RepeatSpec::expand`]). +pub(crate) fn evaluated(doc: Option<&Document>, n: &Node) -> Node { + let hook: &dyn Fn(&Node) -> Option = &outline_text; + vectorcraft_doc::live::expand_deep_hooks(n, Hooks { outline: Some(hook), symbols: doc, ..Default::default() }) +} + /// Replace type anywhere inside `n` by its outlines, as art that keeps the type's look /// ([`as_art`]). Type in a clip group's clipping path (all of `n` when `clipping` is set) becomes /// its plain outlines ([`outline_text`]) with its visibility, which is what @@ -273,7 +282,7 @@ pub(crate) fn outline_nested_text(n: &mut Node, clipping: bool) { let clipping = clipping || (clips && i == 0); let art = match &c.kind { NodeKind::Text(_) if clipping => outline_text(c).map(|o| Node { visible: c.visible, ..o }), - NodeKind::Text(_) => as_art(c, None), + NodeKind::Text(_) => as_art(None, c, None), _ => None, }; if let Some(o) = art { @@ -293,22 +302,22 @@ pub(crate) fn outline_nested_text(n: &mut Node, clipping: bool) { /// and remaining (raster) effects; `None` when `n` has no visible geometry effect, no perspective /// and is not type or an image with a fill or stroke of its own that [`has_item_geometry`] counts, /// or has no art (a symbol instance without `symbol_art`). -pub fn reshape(n: &Node, symbol_art: Option<&Node>) -> Option { +pub fn reshape(doc: Option<&Document>, n: &Node, symbol_art: Option<&Node>) -> Option { if (geometry_effects(n).is_empty() && n.projection().is_none() && !has_item_geometry(n)) || !(needs_outline(n) || has_transform_art(n)) { return None; } - as_art(n, symbol_art) + as_art(doc, n, symbol_art) } /// `n` (see [`needs_outline`], or a path with [`has_transform_art`]) as plain art for Expand /// Appearance: [`reshape`]d when it has /// geometry effects, else type with fills or strokes of its own outlined, or an image with fills /// or strokes of its own that paint a shape, in the same form. `None` when it has none of these. -pub fn expand_outlined(n: &Node, symbol_art: Option<&Node>) -> Option { +pub fn expand_outlined(doc: Option<&Document>, n: &Node, symbol_art: Option<&Node>) -> Option { // A path whose Transform effect copies it with a gradient, or scales its strokes or moves its // patterns: each copy an object of its own. if has_transform_art(n) { - return as_art(n, symbol_art); + return as_art(doc, n, symbol_art); } let own_paint = match n.kind { NodeKind::Text(_) => !n.appearance.items.is_empty(), @@ -317,11 +326,11 @@ pub fn expand_outlined(n: &Node, symbol_art: Option<&Node>) -> Option { if !needs_outline(n) || (geometry_effects(n).is_empty() && !own_paint) { return None; } - as_art(n, symbol_art) + as_art(doc, n, symbol_art) } /// [`reshape`] without its checks (no geometry effects: just the art). -pub(crate) fn as_art(n: &Node, symbol_art: Option<&Node>) -> Option { +pub(crate) fn as_art(doc: Option<&Document>, n: &Node, symbol_art: Option<&Node>) -> Option { let fx = geometry_effects(n); let mut art = match &n.kind { NodeKind::Image(im) => { @@ -349,7 +358,7 @@ pub(crate) fn as_art(n: &Node, symbol_art: Option<&Node>) -> Option { Node::group(n.id, rows(below).into_iter().chain([Arc::new(image)]).chain(rows(above)).collect()) } } - _ => outline_art(n, symbol_art)?, + _ => outline_art(doc, n, symbol_art)?, }; reshape_leaves(&mut art, &fx); // Type and symbols in perspective: the art projected onto their plane. @@ -693,9 +702,8 @@ fn placed_count(n: &Node) -> usize { /// `n` with type anywhere inside it outlined and live objects evaluated, so geometry effects on /// it reach every member. -pub(crate) fn outlined_members(n: &Node) -> Node { - let hook: &dyn Fn(&Node) -> Option = &outline_text; - let mut art = vectorcraft_doc::live::expand_deep(n, Some(hook)); +pub(crate) fn outlined_members(doc: Option<&Document>, n: &Node) -> Node { + let mut art = evaluated(doc, n); outline_nested_text(&mut art, false); art } diff --git a/crates/effects/src/tests.rs b/crates/effects/src/tests.rs index 80d67eef6..b0da45373 100644 --- a/crates/effects/src/tests.rs +++ b/crates/effects/src/tests.rs @@ -284,7 +284,7 @@ fn stacked_transform_copies_stay_within_the_image_budget() { assert_eq!(count(&art), 25); let mut g = Node::group(id, vec![Arc::new(image())]); g.appearance.effects.push(fx("distort.transform", json!({"moveH": 70, "copies": 1000}))); - assert_eq!(count(&evaluate_container(&g).unwrap()), 1001); + assert_eq!(count(&evaluate_container(None, &g).unwrap()), 1001); } /// What `effects` make of `path` within `budget`. @@ -462,7 +462,7 @@ fn transform_on_a_group_moves_and_copies_its_images() { ); let mut g = Node::group(id, vec![Arc::new(sq), Arc::new(image)]); g.appearance.effects.push(fx("distort.transform", json!({"moveH": 70, "copies": 2}))); - let art = evaluate_container(&g).unwrap(); + let art = evaluate_container(None, &g).unwrap(); let mut xs = vec![]; image_xs(&art, &mut xs); xs.sort_by(f64::total_cmp); @@ -483,7 +483,7 @@ fn transform_on_a_group_moves_and_copies_its_images() { ))], ); g.appearance.effects.push(fx("distort.transform", json!({"scaleH": 50, "scaleV": 50, "reference": 8}))); - let art = evaluate_container(&g).unwrap(); + let art = evaluate_container(None, &g).unwrap(); let mut xs = vec![]; image_xs(&art, &mut xs); // The bounds are the image itself, so the bottom-right reference is its own: it halves in place. @@ -508,11 +508,11 @@ fn transform_on_a_group_moves_and_copies_its_symbol_instances() { }; let mut g = Node::group(id, vec![Arc::new(Node::path(id, square(), Appearance::default())), Arc::new(instance())]); g.appearance.effects.push(fx("distort.transform", json!({"moveH": 70, "copies": 2}))); - assert_eq!(xs(&evaluate_container(&g).unwrap()), [5.0, 75.0, 145.0], "the instance and its two copies, 70 pt apart"); + assert_eq!(xs(&evaluate_container(None, &g).unwrap()), [5.0, 75.0, 145.0], "the instance and its two copies, 70 pt apart"); // Without copies the instance itself takes the transform, as a path does. let mut g = Node::group(id, vec![Arc::new(instance())]); g.appearance.effects.push(fx("distort.transform", json!({"moveH": 70}))); - assert_eq!(xs(&evaluate_container(&g).unwrap()), [75.0]); + assert_eq!(xs(&evaluate_container(None, &g).unwrap()), [75.0]); } /// Images and symbol instances share the copy budget: copies of one leave less room for the other. @@ -566,7 +566,7 @@ fn transform_copies_of_images_stop_at_the_reach_limit() { // The largest move: every copy is 1e7 pt further right, so the 100th would end past 1e9. g.appearance.effects.push(fx("distort.transform", json!({"moveH": 1.0e7, "copies": 1000}))); let mut xs = vec![]; - image_xs(&evaluate_container(&g).unwrap(), &mut xs); + image_xs(&evaluate_container(None, &g).unwrap(), &mut xs); assert_eq!(xs.len(), 100, "the image and the 99 copies that stay within reach"); assert!(xs.iter().all(|x| x + 40.0 <= 1.0e9)); } @@ -1061,3 +1061,43 @@ fn bench_catalog_lookup() { } eprintln!("10k lookups: {:?} ({n})", t.elapsed()); } + +#[test] +fn a_repeat_of_a_symbol_whose_art_is_a_repeat_shares_the_instance_budget() { + use std::sync::Arc; + use vectorcraft_doc::{Document, NodeId, RepeatKind, RepeatSpec, Symbol}; + let unit = || { + Arc::new(Node::path( + NodeId(0), + PathData::single(SubPath::polyline(&[Point::new(0.0, 0.0), Point::new(1.0, 0.0), Point::new(1.0, 1.0)], true)), + Appearance::default(), + )) + }; + let grid = |source: Arc, n: u32| { + let mut g = RepeatSpec::grid(vec![source], 0.0, 0.0); + if let RepeatKind::Grid { rows, cols, .. } = &mut g.kind { + *rows = n; + *cols = n; + } + g + }; + let repeat = |r: RepeatSpec| Arc::new(Node::new(NodeId(0), NodeKind::Repeat(r))); + let instance = |name: &str| Arc::new(Node::new(NodeId(0), NodeKind::SymbolInstance { symbol: name.into(), xf: Affine::IDENTITY })); + let mut d = Document::new(100.0, 100.0); + // A 500 × 500 grid as a symbol's art, directly and inside a group; a symbol holding itself. + d.symbols.push(Symbol { name: "Grid".into(), art: repeat(grid(unit(), 500)) }); + d.symbols.push(Symbol { name: "Grouped".into(), art: Arc::new(Node::group(NodeId(0), vec![repeat(grid(unit(), 500))])) }); + d.symbols.push(Symbol { name: "Loop".into(), art: Arc::new(Node::group(NodeId(0), vec![unit(), instance("Loop")])) }); + d.symbols.push(Symbol { name: "Small".into(), art: repeat(grid(unit(), 3)) }); + // A 3 × 3 grid of an instance of each: each copy draws 250,000 instances, so one copy fits. + for name in ["Grid", "Grouped"] { + let outer = Node::new(NodeId(0), NodeKind::Repeat(grid(instance(name), 3))); + let kept = expand_live(Some(&d), &outer).len(); + assert!(kept <= 1, "{name}: {kept} copies of 250,000 instances"); + } + // A symbol holding itself counts no Repeats, and small nests keep every instance. + let looped = Node::new(NodeId(0), NodeKind::Repeat(grid(instance("Loop"), 3))); + assert_eq!(expand_live(Some(&d), &looped).len(), 9); + let small = Node::new(NodeId(0), NodeKind::Repeat(RepeatSpec::mirror(vec![instance("Small")], 90.0, 5.0))); + assert_eq!(expand_live(Some(&d), &small).len(), 2); +} diff --git a/crates/effects/src/tests_transformfx.rs b/crates/effects/src/tests_transformfx.rs index 39a76256c..a5fdd674c 100644 --- a/crates/effects/src/tests_transformfx.rs +++ b/crates/effects/src/tests_transformfx.rs @@ -90,7 +90,7 @@ fn transform_objects_off_maps_nothing_and_makes_no_copies() { fn scale_strokes_and_effects_scales_the_stroke_and_dash_with_the_object() { let n = stroked(json!({"scaleH": 50, "scaleV": 50, "strokes": true})); assert!(has_transform_art(&n)); - let art = reshape(&n, None).unwrap(); + let art = reshape(None, &n, None).unwrap(); let paths = leaves(&art); assert_eq!(paths.len(), 1); let (w, dash) = stroke_of(paths[0]); @@ -103,7 +103,7 @@ fn scale_strokes_and_effects_scales_the_stroke_and_dash_with_the_object() { #[test] fn each_copy_scales_its_stroke_once_more() { let n = stroked(json!({"scaleH": 200, "scaleV": 200, "copies": 2, "strokes": true})); - let art = reshape(&n, None).unwrap(); + let art = reshape(None, &n, None).unwrap(); let widths: Vec = leaves(&art).iter().map(|p| stroke_of(p).0).collect(); assert_eq!(widths, vec![2.0, 4.0, 8.0]); // Without the option the copies keep the original's weight. @@ -114,7 +114,7 @@ fn each_copy_scales_its_stroke_once_more() { #[test] fn transform_objects_off_keeps_the_geometry_and_scales_the_stroke_once() { let n = stroked(json!({"scaleH": 50, "scaleV": 50, "moveH": 40, "copies": 3, "objects": false, "strokes": true})); - let art = reshape(&n, None).unwrap(); + let art = reshape(None, &n, None).unwrap(); let paths = leaves(&art); assert_eq!(paths.len(), 1, "no copies"); assert_eq!(paths[0].geometric_bounds().unwrap(), Rect::new(0.0, 0.0, 100.0, 100.0)); @@ -125,7 +125,7 @@ fn transform_objects_off_keeps_the_geometry_and_scales_the_stroke_once() { fn transform_patterns_moves_each_copys_pattern() { let n = patterned(json!({"moveH": 30, "copies": 2, "patterns": true})); assert!(has_transform_options(&n)); - let art = reshape(&n, None).unwrap(); + let art = reshape(None, &n, None).unwrap(); let xs: Vec = leaves(&art).iter().map(|p| pattern_xf(p).as_coeffs()[4]).collect(); assert_eq!(xs, vec![0.0, 30.0, 60.0]); // Without it, every copy's pattern stays put (the tiles don't follow the copies). @@ -136,7 +136,7 @@ fn transform_patterns_moves_each_copys_pattern() { #[test] fn transform_patterns_with_objects_off_moves_only_the_pattern() { let n = patterned(json!({"rotate": 45, "objects": false, "patterns": true})); - let art = reshape(&n, None).unwrap(); + let art = reshape(None, &n, None).unwrap(); let paths = leaves(&art); assert_eq!(paths.len(), 1); assert_eq!(paths[0].geometric_bounds().unwrap(), Rect::new(0.0, 0.0, 100.0, 100.0)); @@ -150,7 +150,7 @@ fn stroke_scaling_across_copies_stays_finite() { let mut n = Node::path(NodeId(9), shapes::line((0.0, 0.0).into(), (100.0, 0.0).into()), Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 1.0)); n.appearance.effects.push(transform(json!({"scaleH": 4000, "scaleV": 4000, "copies": 1000, "strokes": true}))); - if let Some(art) = reshape(&n, None) { + if let Some(art) = reshape(None, &n, None) { for p in leaves(&art) { let w = stroke_of(p).0; assert!(w.is_finite() && w <= 1.0e6, "{w}"); diff --git a/crates/engine/src/cmd/docinfo.rs b/crates/engine/src/cmd/docinfo.rs index 0a2a43e7b..c3407b6b9 100644 --- a/crates/engine/src/cmd/docinfo.rs +++ b/crates/engine/src/cmd/docinfo.rs @@ -101,10 +101,10 @@ type ImageRow = (String, u32, u32, Option); /// A Repeat object as Document Info lists it: its `repeatObjects` entry, and its row, which gives /// its name and then its kind with the number of instances it draws (for a grid, its rows and /// columns, followed by the number it draws when that is fewer). -fn repeat_entry(n: &Node, spec: &RepeatSpec) -> (Value, (String, String)) { +fn repeat_entry(d: &Document, n: &Node, spec: &RepeatSpec) -> (Value, (String, String)) { let name = n.display_name(); let count = |n: usize, one: &str, many: &str| if n == 1 { format!("1 {one}") } else { format!("{n} {many}") }; - let instances = spec.instances(); + let instances = spec.instances(Some(d)); let mut entry = json!({ "id": n.id.0, "name": name, "instances": instances }); let kind = &spec.kind; let detail = match kind { @@ -377,7 +377,7 @@ fn info(s: &mut Session, p: &Value) -> Result { } } if let NodeKind::Repeat(spec) = &n.kind { - let (entry, row) = repeat_entry(n, spec); + let (entry, row) = repeat_entry(d, n, spec); repeats.push(entry); repeat_rows.push(row); } diff --git a/crates/engine/src/cmd/effectcmd.rs b/crates/engine/src/cmd/effectcmd.rs index 4a6eb0d04..fc81b88f8 100644 --- a/crates/engine/src/cmd/effectcmd.rs +++ b/crates/engine/src/cmd/effectcmd.rs @@ -626,7 +626,7 @@ fn expanded_node(d: &mut Document, mut n: Node, brushes: &[vectorcraft_brush::Br image => { let m = if container { // A group's or layer's own fills and strokes become art among its members. - effects::evaluate_container(&v).map(|mut m| { + effects::evaluate_container(Some(&*d), &v).map(|mut m| { members = expand_pieces(d, &mut m, false, brushes, &mut stroke_art); m }) @@ -639,7 +639,7 @@ fn expanded_node(d: &mut Document, mut n: Node, brushes: &[vectorcraft_brush::Br NodeKind::SymbolInstance { symbol, .. } => d.symbols.iter().find(|s| s.name == *symbol).map(|s| s.art.clone()), _ => None, }; - effects::expand_outlined(&v, symbol.as_deref()).map(|mut m| { + effects::expand_outlined(Some(&*d), &v, symbol.as_deref()).map(|mut m| { expand_pieces(d, &mut m, true, brushes, &mut stroke_art); m }) @@ -1111,7 +1111,7 @@ mod tests { let glyphs = effects::outline_text(&node(&s, t)).unwrap(); let b = glyphs.geometric_bounds().unwrap(); assert!(b.x0 > 105.0 && b.y0 > 105.0 && b.x1 < 195.0 && b.y1 < 195.0, "the type lies inside the square: {b:?}"); - let area = |n: &vectorcraft_doc::Node| effects::member_shapes(n).iter().map(|(p, r)| vectorcraft_pathops::area(p, *r)).sum::(); + let area = |n: &vectorcraft_doc::Node| effects::member_shapes(None, n).iter().map(|(p, r)| vectorcraft_pathops::area(p, *r)).sum::(); let glyph_area = area(&glyphs); s.execute("select.set", &json!({"ids": [a.0, t.0]})).unwrap(); let g = NodeId(s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap()); diff --git a/crates/engine/src/cmd/flatten.rs b/crates/engine/src/cmd/flatten.rs index 52b0f144c..cccd6f7bc 100644 --- a/crates/engine/src/cmd/flatten.rs +++ b/crates/engine/src/cmd/flatten.rs @@ -1226,13 +1226,14 @@ impl Scratch { NodeKind::Text(_) => outlined_text(n), NodeKind::SymbolInstance { symbol, .. } => { let art = self.doc.symbols.iter().find(|s| &s.name == symbol).map(|s| vectorcraft_render::instance_art(&s.art, n)); - art.and_then(|a| effects::outline_art(n, Some(&a))).map(|g| carry(n, g)) + art.and_then(|a| effects::outline_art(Some(&self.doc), n, Some(&a))).map(|g| carry(n, g)) } // A placed document stays one: its art's resources aren't the document's. NodeKind::PlacedDocument(_) => None, NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Repeat(_) => { let hook: &dyn Fn(&Node) -> Option = &effects::outline_text; - Some(carry(n, vectorcraft_doc::live::expanded_group(n, Some(hook)))) + let hooks = vectorcraft_doc::live::Hooks { outline: Some(hook), symbols: Some(&self.doc), ..Default::default() }; + Some(carry(n, vectorcraft_doc::live::expanded_group_hooks(n, hooks))) } NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. } => outline_strokes(&mut self.doc, &self.brushes, n), _ => None, diff --git a/crates/engine/src/cmd/patterncmds.rs b/crates/engine/src/cmd/patterncmds.rs index 0b713341a..b25b427d3 100644 --- a/crates/engine/src/cmd/patterncmds.rs +++ b/crates/engine/src/cmd/patterncmds.rs @@ -737,7 +737,8 @@ fn repeat_expand(s: &mut Session, _: &Value) -> Result { /// instance node gets a fresh id. fn expand_repeat(d: &mut Document, r: NodeId) -> Result<()> { let Some(n) = d.node(r).cloned() else { return Ok(()) }; - let mut g = vectorcraft_doc::live::expanded_group(&n, None); + let hooks = vectorcraft_doc::live::Hooks { symbols: Some(d), ..Default::default() }; + let mut g = vectorcraft_doc::live::expanded_group_hooks(&n, hooks); if let Some(ch) = g.children_mut() { for c in ch.iter_mut() { fix_ids(d, Arc::make_mut(c)); diff --git a/crates/engine/src/tests_outlineobject.rs b/crates/engine/src/tests_outlineobject.rs index f11cb88cf..c1ff06b53 100644 --- a/crates/engine/src/tests_outlineobject.rs +++ b/crates/engine/src/tests_outlineobject.rs @@ -56,7 +56,7 @@ fn type_with_outline_object_stays_editable_type_and_keeps_its_geometric_bounds() assert_eq!(s.transform_bounds(&[id]), before); // With Use Preview Bounds on, the type is measured by its glyph outlines. s.prefs.use_preview_bounds = true; - let glyphs = vectorcraft_render::effects::reshape(&node(&s, id), None).unwrap().geometric_bounds(); + let glyphs = vectorcraft_render::effects::reshape(None, &node(&s, id), None).unwrap().geometric_bounds(); assert_eq!(s.transform_bounds(&[id]), glyphs); s.prefs.use_preview_bounds = false; // A native save keeps the effect. @@ -66,10 +66,10 @@ fn type_with_outline_object_stays_editable_type_and_keeps_its_geometric_bounds() // The type is still type, drawn as its outlines, and a text edit changes them. let n = node(&s, id); assert!(matches!(n.kind, NodeKind::Text(_))); - let drawn = vectorcraft_render::effects::reshape(&n, None).unwrap(); + let drawn = vectorcraft_render::effects::reshape(None, &n, None).unwrap(); assert_eq!(count_text(&drawn), 0); s.execute("text.setText", &json!({"id": id.0, "text": "OUTLINED TYPE"})).unwrap(); - let longer = vectorcraft_render::effects::reshape(&node(&s, id), None).unwrap(); + let longer = vectorcraft_render::effects::reshape(None, &node(&s, id), None).unwrap(); assert!(longer.geometric_bounds().unwrap().width() > drawn.geometric_bounds().unwrap().width() + 50.0); } diff --git a/crates/engine/src/tests_pattern.rs b/crates/engine/src/tests_pattern.rs index 1d33e9d56..437f5e288 100644 --- a/crates/engine/src/tests_pattern.rs +++ b/crates/engine/src/tests_pattern.rs @@ -504,7 +504,7 @@ fn radial_repeat_wraps_selection() { let RepeatKind::Radial { instances, radius, center, .. } = spec.kind else { panic!() }; assert_eq!((instances, radius), (8, 50.0)); assert!(close(center, Point::new(200.0, 150.0))); - assert_eq!(spec.transforms().len(), 8); + assert_eq!(spec.transforms(None).len(), 8); assert_eq!(n.kind_label(), "Radial Repeat"); } @@ -544,7 +544,7 @@ fn radial_instance_transforms_rotate_around_center() { sel(&mut s, &[r]); let v = s.execute("object.repeat.radial", &json!({"instances": 4, "radius": 50})).unwrap(); let NodeKind::Repeat(spec) = node(&s, id_of(&v)).kind else { panic!() }; - let t = spec.transforms(); + let t = spec.transforms(None); let c = Point::new(200.0, 100.0); assert!(close(t[0] * c, Point::new(200.0, 100.0))); assert!(close(t[1] * c, Point::new(250.0, 150.0))); @@ -583,7 +583,7 @@ fn object_expand_expands_repeats() { sel(&mut s, &[r]); let id = id_of(&s.execute(cmd, &opts).unwrap()); let NodeKind::Repeat(spec) = node(&s, id).kind else { panic!("{cmd}") }; - let n = spec.expand().len(); + let n = spec.expand(None).len(); assert_eq!(s.execute("object.expand.info", &json!({})).unwrap()["object"], json!(true), "{cmd}"); let undo = s.doc().unwrap().history.undo.len(); s.execute("object.expand", &json!({"fill": false, "stroke": false})).unwrap(); @@ -632,7 +632,7 @@ fn grid_repeat_and_options() { let RepeatKind::Grid { rows, cols, flip_cols, grid_type, .. } = spec.kind else { panic!() }; assert_eq!((rows, cols, flip_cols), (2, 4, true)); assert_eq!(grid_type, TileType::BrickByRow { offset: 0.5 }); - assert_eq!(spec.expand().len(), 8); + assert_eq!(spec.expand(None).len(), 8); } #[test] @@ -788,7 +788,7 @@ fn mirror_axis_drag_previews_once_and_undo_redo_preserve_source() { let after = node(&s, id); let (NodeKind::Repeat(a), NodeKind::Repeat(b)) = (&before.kind, &after.kind) else { panic!() }; assert_eq!(a.source, b.source, "moving the mirror axis leaves the source artwork in place"); - assert_ne!(a.transforms(), b.transforms(), "the reflected copy updates live"); + assert_ne!(a.transforms(None), b.transforms(None), "the reflected copy updates live"); s.execute("edit.undo", &json!({})).unwrap(); assert_eq!(node(&s, id), before); s.execute("edit.redo", &json!({})).unwrap(); diff --git a/crates/engine/src/tests_persp_text.rs b/crates/engine/src/tests_persp_text.rs index 88c9ac1fe..846306fc2 100644 --- a/crates/engine/src/tests_persp_text.rs +++ b/crates/engine/src/tests_persp_text.rs @@ -26,7 +26,7 @@ fn node(s: &Session, id: NodeId) -> vectorcraft_doc::Node { /// The art the renderer and exporters draw for `id` (its outlines, projected). fn drawn_bounds(s: &Session, id: NodeId) -> Rect { let n = node(s, id); - vectorcraft_render::effects::reshape(&n, None).unwrap().geometric_bounds().unwrap() + vectorcraft_render::effects::reshape(None, &n, None).unwrap().geometric_bounds().unwrap() } #[test] @@ -39,7 +39,7 @@ fn type_in_perspective_is_projected_and_stays_type() { let h = n.projection().expect("drawn through a projection"); // Really foreshortened: the right plane recedes to the right, so the letters' far end is // shorter than their near end. - let out = vectorcraft_render::effects::reshape(&n, None).unwrap(); + let out = vectorcraft_render::effects::reshape(None, &n, None).unwrap(); let mut pts = vec![]; out.walk(&mut |c| { if let Some(p) = c.path_data() { diff --git a/crates/engine/src/tests_previewbounds.rs b/crates/engine/src/tests_previewbounds.rs index 88b87394d..95440f2ae 100644 --- a/crates/engine/src/tests_previewbounds.rs +++ b/crates/engine/src/tests_previewbounds.rs @@ -96,7 +96,7 @@ fn outlined_type(s: &mut Session, x: f64, text: &str) -> NodeId { /// The box of the glyph outlines that type `id` with Outline Object is drawn as. fn glyphs(s: &Session, id: NodeId) -> Rect { let n = s.doc().unwrap().doc.node(id).unwrap().clone(); - vectorcraft_render::effects::reshape(&n, None).unwrap().geometric_bounds().unwrap() + vectorcraft_render::effects::reshape(None, &n, None).unwrap().geometric_bounds().unwrap() } fn near(a: Rect, b: Rect, tolerance: f64) -> bool { diff --git a/crates/render/src/fx.rs b/crates/render/src/fx.rs index 50c19a35c..374466481 100644 --- a/crates/render/src/fx.rs +++ b/crates/render/src/fx.rs @@ -316,7 +316,7 @@ fn shape_bounds(symbols: &SymbolBoxes, n: &Node) -> Option { }; match &n.kind { _ if effects::has_container_appearance(n) => { - let art = effects::evaluate_container(n); + let art = effects::evaluate_container(Some(symbols.document()), n); let art = art.as_ref().unwrap_or(n); if art.clips() { art.visual_bounds_with(symbols) } else { union(art) } } @@ -350,7 +350,7 @@ fn shape_bounds(symbols: &SymbolBoxes, n: &Node) -> Option { NodeKind::SymbolInstance { symbol, .. } => symbols.document().symbols.iter().find(|s| s.name == *symbol).map(|s| s.art.clone()), _ => None, }; - let reshaped = effects::reshape(n, symbol.as_deref()); + let reshaped = effects::reshape(Some(symbols.document()), n, symbol.as_deref()); let b = match &reshaped { Some(r) if matches!(n.kind, NodeKind::Text(_)) || (matches!(n.kind, NodeKind::Image(_)) && !r.clips()) => { r.children().into_iter().flatten().fold(None, |acc, c| vectorcraft_geom::union_opt(acc, painted_reach(symbols, c))) @@ -430,7 +430,7 @@ fn cull_bounds_in(symbols: &SymbolBoxes, n: &Node, after_reshape: bool) -> Optio _ if effects::has_crop_marks(n) => effects::crop_marks_art(n).and_then(|art| inner(&art)), // The evaluated art (its fills, strokes and geometry effects) and the raster effects' reach. _ if effects::has_container_appearance(n) => { - let art = effects::evaluate_container(n); + let art = effects::evaluate_container(Some(symbols.document()), n); let art = art.as_ref().unwrap_or(n); let b = if art.clips() { art.visual_bounds_with(symbols) @@ -453,7 +453,7 @@ fn cull_bounds_in(symbols: &SymbolBoxes, n: &Node, after_reshape: bool) -> Optio NodeKind::SymbolInstance { symbol, .. } => symbols.document().symbols.iter().find(|s| s.name == *symbol).map(|s| s.art.clone()), _ => None, }; - let reshaped = effects::reshape(n, symbol.as_deref()); + let reshaped = effects::reshape(Some(symbols.document()), n, symbol.as_deref()); let b = match &reshaped { Some(r) if matches!(n.kind, NodeKind::Text(_)) || (matches!(n.kind, NodeKind::Image(_)) && !r.clips()) => { r.children().into_iter().flatten().fold(None, |acc, c| vectorcraft_geom::union_opt(acc, inner(c))) @@ -1483,14 +1483,14 @@ impl Renderer { let art = match effects::crop_marks_art(a) .or_else(|| effects::revolve_art(a)) .or_else(|| effects::solid_art(a)) - .or_else(|| effects::reshape(a, symbol.as_ref())) + .or_else(|| effects::reshape(Some(doc), a, symbol.as_ref())) { Some(r) => r, - None if container => effects::evaluate_container(a).unwrap_or_else(|| (**a).clone()), + None if container => effects::evaluate_container(Some(doc), a).unwrap_or_else(|| (**a).clone()), None if matches!(a.kind, NodeKind::Text(_) | NodeKind::Image(_) | NodeKind::Path { .. } | NodeKind::Compound { .. }) => (**a).clone(), - None => effects::outline_art(a, symbol.as_ref()).unwrap_or_else(|| Node::group(a.id, vec![])), + None => effects::outline_art(Some(doc), a, symbol.as_ref()).unwrap_or_else(|| Node::group(a.id, vec![])), }; - let art = if effects::has_revolve(a) || effects::has_solid(a) { effects::evaluate_container(&art).unwrap_or(art) } else { art }; + let art = if effects::has_revolve(a) || effects::has_solid(a) { effects::evaluate_container(Some(doc), &art).unwrap_or(art) } else { art }; let mut art = self.adjusted_art(art, effects::color_map(&a.appearance.effects).as_ref()); // A path's geometry effects still apply when it is drawn. let path = matches!(art.kind, NodeKind::Path { .. } | NodeKind::Compound { .. }); diff --git a/crates/render/src/tests_objectfx.rs b/crates/render/src/tests_objectfx.rs index 95ecb085e..2bd35dd86 100644 --- a/crates/render/src/tests_objectfx.rs +++ b/crates/render/src/tests_objectfx.rs @@ -593,7 +593,7 @@ fn geometry_effects_reshape_type_through_its_outlines() { assert!(changed > 100, "{changed} pixels changed"); // The reshaped art is plain paths painted with the text's fill. let n = &d.layers[0].children().unwrap()[0]; - let art = vectorcraft_effects::reshape(n, None).unwrap(); + let art = vectorcraft_effects::reshape(None, n, None).unwrap(); let (mut text, mut black) = (0, 0); art.walk(&mut |m| { text += usize::from(matches!(m.kind, NodeKind::Text(_))); @@ -616,7 +616,7 @@ fn outline_object_draws_type_as_its_outlines_and_leaves_its_look() { // The type is drawn through its outlines: plain paths painted with the text's fill. let d = text_doc(vec![fx("path.outlineObject", json!({}))]); let n = &d.layers[0].children().unwrap()[0]; - let art = vectorcraft_effects::reshape(n, None).unwrap(); + let art = vectorcraft_effects::reshape(None, n, None).unwrap(); let (mut text, mut black) = (0, 0); art.walk(&mut |m| { text += usize::from(matches!(m.kind, NodeKind::Text(_))); @@ -649,7 +649,7 @@ fn outline_object_leaves_paths_and_images_as_they_are() { assert!(changed.iter().all(|&n| n < 10), "pixels changed: {changed:?}"); // The image is drawn through its frame, as under any geometry effect. let d = image_doc(vec![fx("path.outlineObject", json!({}))]); - assert!(vectorcraft_effects::reshape(&d.layers[0].children().unwrap()[0], None).is_some()); + assert!(vectorcraft_effects::reshape(None, &d.layers[0].children().unwrap()[0], None).is_some()); } #[test]