mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Count Repeats inside symbol art toward the Repeat instance budget
Squashed from #3788 onto main.
This commit is contained in:
@@ -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| {
|
||||
|
||||
@@ -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()],
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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`]).
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 { .. });
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user