Merge pull request #3788 from Silvertree2010/fix/3577-audit-repeat-the-250-000-instanc

Count Repeats inside symbol art toward the Repeat instance budget
This commit is contained in:
BFlat
2026-10-24 02:44:58 +07:00
committed by GitHub
22 changed files with 231 additions and 135 deletions
+2 -2
View File
@@ -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<NodeId>, 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| {
+4 -1
View File
@@ -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<PatternTiles<'a>>,
/// 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<Outliner<'a>> for Hooks<'a> {
@@ -1792,7 +1795,7 @@ pub fn expand_live_hooks(n: &Node, hooks: Hooks) -> Vec<Node> {
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()],
+1 -1
View File
@@ -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),
+80 -39
View File
@@ -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<Node>]) -> 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<HashMap<String, Option<usize>>>,
}
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<Node>]) -> 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<Rect> {
crate::live::nodes_bounds(&self.source)
}
pub fn transforms(&self) -> Vec<Affine> {
/// 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<Affine> {
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<Node> {
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<Node> {
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<Rect> {
self.copies_bounds(self.source_bounds()?)
/// Geometric bounds of all instances ([`Self::transforms`] for `doc`).
pub fn bounds(&self, doc: Option<&Document>) -> Option<Rect> {
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<Rect> {
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<Rect> {
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<crate::node::Scaling>) {
@@ -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<Point> = r.transforms().iter().map(|m| *m * Point::new(0.0, -100.0)).collect();
let cs: Vec<Point> = 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<Node>| {
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);
+1 -1
View File
@@ -415,7 +415,7 @@ pub fn adjust_in_document(d: &mut Document, n: &Node) -> Option<Node> {
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,
+9 -10
View File
@@ -104,7 +104,7 @@ fn bake_node(d: &mut Document, n: &Node) -> Option<Node> {
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<Node> {
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<Node> {
/// 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<Node> {
fn container_keeping_type(d: &Document, n: &Node) -> Option<Node> {
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<Node> {
}
}
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<Node> {
if !text {
return None;
}
let hook: &dyn Fn(&Node) -> Option<Node> = &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<Node> {
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.
+19 -17
View File
@@ -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<Node> {
/// 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<Node> {
if !is_container(n) {
return None;
}
@@ -434,12 +436,12 @@ pub fn evaluate_container(n: &Node) -> Option<Node> {
}
}
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<Arc<Node>> { 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() {
+1 -1
View File
@@ -37,7 +37,7 @@ fn outline(doc: Option<&Document>, n: &Node) -> Option<Node> {
fn with_hooks<R>(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`]).
+4 -3
View File
@@ -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))
}
+25 -17
View File
@@ -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<Node> {
let hook: &dyn Fn(&Node) -> Option<Node> = &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<Node> {
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<Node> {
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<Node> {
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<Node> = &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<Node> {
pub fn reshape(doc: Option<&Document>, n: &Node, symbol_art: Option<&Node>) -> Option<Node> {
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<Node> {
pub fn expand_outlined(doc: Option<&Document>, n: &Node, symbol_art: Option<&Node>) -> Option<Node> {
// 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<Node> {
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<Node> {
pub(crate) fn as_art(doc: Option<&Document>, n: &Node, symbol_art: Option<&Node>) -> Option<Node> {
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> {
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<Node> = &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
}
+46 -6
View File
@@ -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<Node>, 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);
}
+6 -6
View File
@@ -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<f64> = 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<f64> = 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}");
+3 -3
View File
@@ -101,10 +101,10 @@ type ImageRow = (String, u32, u32, Option<String>);
/// 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<Value> {
}
}
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);
}
+3 -3
View File
@@ -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::<f64>();
let area = |n: &vectorcraft_doc::Node| effects::member_shapes(None, n).iter().map(|(p, r)| vectorcraft_pathops::area(p, *r)).sum::<f64>();
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());
+3 -2
View File
@@ -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<Node> = &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,
+2 -1
View File
@@ -737,7 +737,8 @@ fn repeat_expand(s: &mut Session, _: &Value) -> Result<Value> {
/// 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));
+3 -3
View File
@@ -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);
}
+5 -5
View File
@@ -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();
+2 -2
View File
@@ -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() {
+1 -1
View File
@@ -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 {
+8 -8
View File
@@ -316,7 +316,7 @@ fn shape_bounds(symbols: &SymbolBoxes, n: &Node) -> Option<Rect> {
};
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<Rect> {
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 { .. });
+3 -3
View File
@@ -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]