diff --git a/crates/doc/src/hit.rs b/crates/doc/src/hit.rs index a04ee7c38..f099b5f95 100644 --- a/crates/doc/src/hit.rs +++ b/crates/doc/src/hit.rs @@ -84,8 +84,9 @@ impl Default for HitOptions { /// The shapes that an image's own fills and strokes paint, each with its fill rule and the fill or /// stroke that paints it (none for fills and strokes that paint nothing). Effects are evaluated -/// above this crate, so the engine registers one with [`set_image_paint`]. -pub type ImagePaint = fn(&Node) -> Vec<(BezPath, FillRule, AppearanceItem)>; +/// above this crate, so the engine registers one with [`set_image_paint`]. It looks up swatches and +/// patterns in the document it is given. +pub type ImagePaint = fn(Option<&Document>, &Node) -> Vec<(BezPath, FillRule, AppearanceItem)>; static IMAGE_PAINT: OnceLock = OnceLock::new(); @@ -98,8 +99,8 @@ pub fn set_image_paint(f: ImagePaint) { /// The shapes that the image `n`'s own fills and strokes paint ([`set_image_paint`]), none while /// nothing is registered. -pub(crate) fn image_paint(n: &Node) -> Vec<(BezPath, FillRule, AppearanceItem)> { - IMAGE_PAINT.get().map_or_else(Vec::new, |f| f(n)) +pub(crate) fn image_paint(doc: Option<&Document>, n: &Node) -> Vec<(BezPath, FillRule, AppearanceItem)> { + IMAGE_PAINT.get().map_or_else(Vec::new, |f| f(doc, n)) } /// One level of evaluation of a live object in a document, as the canvas draws it: type in an @@ -871,7 +872,7 @@ fn hit_leaf(n: &Node, p: Point, opt: HitOptions, symbols: &SymbolBoxes) -> Optio if opt.outline { return None; } - image_paint(n).iter().rev().find_map(|(bp, rule, item)| match item { + image_paint(Some(symbols.document()), n).iter().rev().find_map(|(bp, rule, item)| match item { AppearanceItem::Stroke(s) => s.hits(bp, *rule, p, opt.tol).then_some(HitKind::Stroke), AppearanceItem::Fill(_) => (!opt.path_only && fill_contains(bp, *rule, p)).then_some(HitKind::Fill), }) diff --git a/crates/doc/src/node/ops.rs b/crates/doc/src/node/ops.rs index f908e50a1..f76957258 100644 --- a/crates/doc/src/node/ops.rs +++ b/crates/doc/src/node/ops.rs @@ -239,10 +239,12 @@ impl Node { NodeKind::Intertwine { children } => children.iter().fold(None, |acc, c| vectorcraft_geom::union_opt(acc, c.reach(symbols))), // The frame and each shape the image's own fills and strokes paint, a stroke's grown by // the farthest it can reach from its shape. - NodeKind::Image(_) => crate::hit::image_paint(self).iter().fold(self.geometric(symbols), |acc, (bp, _, item)| { - let o = if let crate::AppearanceItem::Stroke(s) = item { s.reach() } else { 0.0 }; - vectorcraft_geom::union_opt(acc, (!bp.elements().is_empty()).then(|| bp.bounding_box().inflate(o, o))) - }), + NodeKind::Image(_) => { + crate::hit::image_paint(symbols.map(SymbolBoxes::document), self).iter().fold(self.geometric(symbols), |acc, (bp, _, item)| { + let o = if let crate::AppearanceItem::Stroke(s) = item { s.reach() } else { 0.0 }; + vectorcraft_geom::union_opt(acc, (!bp.elements().is_empty()).then(|| bp.bounding_box().inflate(o, o))) + }) + } // Keys and steps placed on a spine, turned to it with Align to Path, can reach past the // keys and the spine. NodeKind::Blend { children, spec } => { diff --git a/crates/effects/src/bake.rs b/crates/effects/src/bake.rs index 3eaf685f0..f66937446 100644 --- a/crates/effects/src/bake.rs +++ b/crates/effects/src/bake.rs @@ -348,16 +348,16 @@ fn moved_type_group(n: &Node) -> Option { /// 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(d: &Document, n: &Node) -> Option { - fn keep(n: &mut Node, moves: &[serde_json::Value], clipping: bool, kept: &mut bool) { + fn keep(d: &Document, n: &mut Node, moves: &[serde_json::Value], clipping: bool, kept: &mut bool) { let clips = n.clips(); let Some(ch) = n.children_mut() else { return }; for (i, c) in ch.iter_mut().enumerate() { let clipping = clipping || (clips && i == 0); if !matches!(c.kind, NodeKind::Text(_)) { if has_pathfinder(c) || has_own_paint(c) || !geometry_effects(c).is_empty() { - outline_nested_text(Arc::make_mut(c), clipping); + outline_nested_text(Some(d), Arc::make_mut(c), clipping); } else if c.children().is_some() { - keep(Arc::make_mut(c), moves, clipping, kept); + keep(d, Arc::make_mut(c), moves, clipping, kept); } continue; } @@ -372,7 +372,7 @@ fn container_keeping_type(d: &Document, n: &Node) -> Option { } } None if clipping => outline_text(c).map(|o| Node { visible: c.visible, ..o }), - None => as_art(None, c, None), + None => as_art(Some(d), c, None), }; if let Some(o) = art { *c = Arc::new(o); @@ -391,11 +391,11 @@ fn container_keeping_type(d: &Document, n: &Node) -> Option { } let mut m = crate::reshape::evaluated(Some(d), n); let mut kept = false; - keep(&mut m, &moves, false, &mut kept); + keep(d, &mut m, &moves, false, &mut kept); if !kept { return None; } - reshape_leaves(&mut m, &fx); + reshape_leaves(&mut m, &fx, Some(d)); m.appearance.effects.retain(|e| !is_pathfinder(&e.id) && !is_geometry(&e.id)); m.appearance.items.clear(); m.appearance.contents_index = None; @@ -547,9 +547,10 @@ pub fn expand_art(d: &mut Document, n: &mut Node, stroke_art: StrokeArt) { /// its gradient meshes clipped to the outline; a brushed stroke stays a stroke), in paint order, as /// the members of a group keeping `n`'s id, transparency, opacity mask and raster effects. A path /// with [`has_transform_art`](crate::has_transform_art) is first made into the art [`reshape`] -/// makes of it, and each copy is baked this way. `None` when it has neither a stroke to outline nor -/// a geometry effect, so bending first changes nothing. -pub fn bake_appearance(n: &Node) -> Option { +/// makes of it, and each copy is baked this way; the swatches and patterns that live-effect plug-ins +/// painting their pieces name are looked up in `doc`. `None` when it has neither a stroke to +/// outline nor a geometry effect, so bending first changes nothing. +pub fn bake_appearance(doc: Option<&Document>, n: &Node) -> Option { if let Some(m) = crate::revolve_art(n).or_else(|| crate::solid_art(n)) { return Some(m); } @@ -560,9 +561,9 @@ pub fn bake_appearance(n: &Node) -> Option { // patterns: the art Expand Appearance makes of it (each copy an object with its own strokes, // patterns and gradients), each piece baked in turn. if crate::has_transform_art(n) - && let Some(mut m) = reshape(None, n, None) + && let Some(mut m) = reshape(doc, n, None) { - bake_appearance_pieces(&mut m); + bake_appearance_pieces(doc, &mut m); return Some(m); } let outlined = |i: &AppearanceItem| paints(i) && matches!(i, AppearanceItem::Stroke(s) if s.brush.is_none()); @@ -617,15 +618,15 @@ pub fn bake_appearance(n: &Node) -> Option { } /// [`bake_appearance`] every path and compound path in `n` that it changes, in place. -fn bake_appearance_pieces(n: &mut Node) { +fn bake_appearance_pieces(doc: Option<&Document>, n: &mut Node) { if matches!(n.kind, NodeKind::Path { .. } | NodeKind::Compound { .. }) { - if let Some(m) = bake_appearance(n) { + if let Some(m) = bake_appearance(doc, n) { *n = m; } return; } for c in n.children_mut().into_iter().flatten() { - bake_appearance_pieces(Arc::make_mut(c)); + bake_appearance_pieces(doc, Arc::make_mut(c)); } } diff --git a/crates/effects/src/bake/tests.rs b/crates/effects/src/bake/tests.rs index 7ecb869bb..a2959a843 100644 --- a/crates/effects/src/bake/tests.rs +++ b/crates/effects/src/bake/tests.rs @@ -536,7 +536,7 @@ fn a_baked_stroke_keeps_its_raster_effects() { s.effects.push(fx("path.offsetPath", json!({"offset": 5.0}))); s.effects.push(fx("stylize.dropShadow", json!({}))); } - let m = bake_appearance(&n).unwrap(); + let m = bake_appearance(None, &n).unwrap(); let ch = m.children().unwrap(); let Some(AppearanceItem::Fill(f)) = ch[1].appearance.items.first() else { panic!("the outline is filled") }; assert_eq!(f.effects.iter().map(|e| e.id.as_str()).collect::>(), ["stylize.dropShadow"]); @@ -551,7 +551,7 @@ fn a_baked_stroke_leaves_dither_behind() { g.gradient.dither = true; s.paint = Paint::Gradient(Box::new(g)); } - let m = bake_appearance(&n).unwrap(); + let m = bake_appearance(None, &n).unwrap(); let ch = m.children().unwrap(); let Some(AppearanceItem::Fill(f)) = ch[1].appearance.items.first() else { panic!("the outline is filled") }; let Paint::Gradient(g) = &f.paint else { panic!("a gradient fill") }; @@ -567,7 +567,7 @@ fn a_baked_stroke_with_a_gradient_along_it_becomes_meshes_clipped_to_its_outline s.gradient_mode = vectorcraft_doc::StrokeGradientMode::Along; s.opacity = 0.5; } - let m = bake_appearance(&n).unwrap(); + let m = bake_appearance(None, &n).unwrap(); let ch = m.children().unwrap(); let NodeKind::Group { children, clip: true } = &ch[1].kind else { panic!("a clip group: {:?}", ch[1].kind) }; assert!(matches!(children[0].kind, NodeKind::Path { clipping: true, .. })); @@ -600,7 +600,7 @@ fn a_baked_transform_copy_keeps_its_scaled_stroke() { // Distort Appearance: the 200% copy's stroke is 4 pt, as outside the envelope, so its outline // is 204 pt wide. let n = transformed(Paint::None, Paint::solid(Color::BLACK), 2.0, json!({"scaleH": 200, "scaleV": 200, "copies": 1, "strokes": true})); - let m = bake_appearance(&n).unwrap(); + let m = bake_appearance(None, &n).unwrap(); let w = widths(&m); assert_eq!(w.len(), 2, "{w:?}"); assert!((w[0] - 102.0).abs() < 0.5 && (w[1] - 204.0).abs() < 0.5, "{w:?}"); @@ -611,7 +611,7 @@ fn a_baked_transform_copy_keeps_its_scaled_stroke() { fn a_baked_transform_copy_keeps_its_moved_pattern() { let paint = Paint::Pattern { pattern: "p".into(), xf: vectorcraft_geom::Affine::IDENTITY }; let n = transformed(paint, Paint::None, 0.0, json!({"moveH": 30, "copies": 1, "patterns": true})); - let m = bake_appearance(&n).unwrap(); + let m = bake_appearance(None, &n).unwrap(); let xs: Vec = baked_paths(&m) .iter() .map(|p| match p.appearance.fill_paint() { @@ -626,7 +626,7 @@ fn a_baked_transform_copy_keeps_its_moved_pattern() { fn a_baked_transform_without_its_options_outlines_the_copies_together() { // Without Scale Strokes & Effects the copy keeps the 2 pt stroke: one outline of both squares. let n = transformed(Paint::None, Paint::solid(Color::BLACK), 2.0, json!({"scaleH": 200, "scaleV": 200, "copies": 1})); - let m = bake_appearance(&n).unwrap(); + let m = bake_appearance(None, &n).unwrap(); let w = widths(&m); assert_eq!(w.len(), 1, "{w:?}"); assert!((w[0] - 202.0).abs() < 0.5, "{w:?}"); diff --git a/crates/effects/src/geometry.rs b/crates/effects/src/geometry.rs index eea263d48..85248e08c 100644 --- a/crates/effects/src/geometry.rs +++ b/crates/effects/src/geometry.rs @@ -3,6 +3,7 @@ use super::{ Appearance, AppearanceItem, BezPath, Effect, FillRule, Node, NodeKind, PathData, Rect, StrokeLayer, Value, WarpStyle, distort, stroke, stylize, util, warp, }; +use vectorcraft_doc::Document; /// Context the geometry effects may need from the object's appearance. #[derive(Clone, Copy, Debug, Default, PartialEq)] @@ -22,6 +23,9 @@ pub struct GeomContext<'a> { /// The index in `appearance.items` of the fill or stroke whose own effects these are; `None`: /// the object's effects. pub item: Option, + /// The document that the swatches and patterns named by a live-effect plug-in that paints its + /// pieces are looked up in. Without one, a result that names any is refused. + pub doc: Option<&'a Document>, } impl<'a> GeomContext<'a> { @@ -32,7 +36,7 @@ impl<'a> GeomContext<'a> { _ => FillRule::NonZero, }; let stroke = n.appearance.stroke().filter(|s| !s.paint.is_none() && s.width > 0.0); - Self { stroke, rule, seed: n.id.0, appearance: Some(&n.appearance), item: None } + Self { stroke, rule, seed: n.id.0, appearance: Some(&n.appearance), item: None, doc: None } } /// The context of the effects on `item`, one of the same object's appearance items: a @@ -374,7 +378,7 @@ fn apply_unchecked(id: &str, p: &Value, path: &PathData, b: Rect, ctx: &GeomCont use util::{join, num}; if let Some(plugin) = vectorcraft_plugins::effect::plugin_id(id) { let paints = vectorcraft_plugins::effect::Paints { appearance: ctx.appearance, item: ctx.item }; - return vectorcraft_plugins::effect::apply(plugin, p, path, b, ctx.rule, paints).unwrap_or_else(|| path.clone()); + return vectorcraft_plugins::effect::apply(plugin, p, path, b, ctx.rule, paints, ctx.doc).unwrap_or_else(|| path.clone()); } match id { "distort.freeDistort" => distort::free_distort(path, b, p, max), diff --git a/crates/effects/src/group.rs b/crates/effects/src/group.rs index a1abaac7d..d04982d98 100644 --- a/crates/effects/src/group.rs +++ b/crates/effects/src/group.rs @@ -67,25 +67,28 @@ pub type OutlineHook<'a> = &'a dyn Fn(&Node) -> Option; /// back to front: a compound shape as its area, a blend, an envelope or an object with a warp as /// the art it draws, and type and symbol instances as their art through `hook`. Past /// [`SYMBOL_ART_MAX_NODES`](crate::SYMBOL_ART_MAX_NODES) objects of symbol art, further -/// instances add nothing. -pub fn member_leaves(n: &Node, hook: Option) -> Vec { +/// instances add nothing. The swatches and patterns that live-effect plug-ins painting their +/// pieces name are looked up in `doc` ([`shape_art`]). +pub fn member_leaves(doc: Option<&Document>, n: &Node, hook: Option) -> Vec { let mut out = vec![]; - leaves(n, hook, &SymbolNodes::default(), &mut out); + leaves(doc, n, hook, &SymbolNodes::default(), &mut out); out } /// [`member_leaves`], with the symbol art given so far counted in `nodes`. -fn leaves(n: &Node, hook: Option, nodes: &SymbolNodes, out: &mut Vec) { +fn leaves(doc: Option<&Document>, n: &Node, hook: Option, nodes: &SymbolNodes, out: &mut Vec) { if !n.visible { return; } // A blend, an envelope or an object with a warp takes part as the art it draws. - if let Some(art) = shape_art_in(n, hook, nodes) { + if let Some(art) = shape_art_in(doc, n, hook, nodes) { let first = out.len(); - leaves(&art, hook, nodes, out); - // Every path of a live object's, type's or symbol instance's art is drawn inside the - // object's opacity and blend mode. - if crate::needs_outline(n) { + leaves(doc, &art, hook, nodes, out); + // Every path of a live object's, type's or symbol instance's art, and every piece of a path + // whose live-effect plug-in paints its pieces, is drawn inside the object's opacity and + // blend mode. + let pieces = matches!(n.kind, NodeKind::Path { .. } | NodeKind::Compound { .. }) && matches!(art.kind, NodeKind::Group { .. }); + if crate::needs_outline(n) || pieces { for l in out.iter_mut().skip(first) { within(l, n); } @@ -95,11 +98,11 @@ fn leaves(n: &Node, hook: Option, nodes: &SymbolNodes, out: &mut Ve match &n.kind { NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. } => out.push(n.clone()), // A compound shape takes part as its area, styled like it. - NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } => match compound_shape_art_in(n, hook, nodes) { + NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } => match compound_shape_art_in(doc, n, hook, nodes) { Some(art) => out.push(art), None => { for c in children { - leaves(c, hook, nodes, out); + leaves(doc, c, hook, nodes, out); } } }, @@ -114,7 +117,7 @@ fn leaves(n: &Node, hook: Option, nodes: &SymbolNodes, out: &mut Ve let art = match &n.kind { NodeKind::SymbolInstance { symbol, .. } if nodes.charge(&art) => { let mut art = crate::reshape::symbol_art_with(&art, h, nodes, symbol); - crate::reshape::outline_nested_text(&mut art, false); + crate::reshape::outline_nested_text(doc, &mut art, false); transparency_into_members(&mut art); art } @@ -122,7 +125,7 @@ fn leaves(n: &Node, hook: Option, nodes: &SymbolNodes, out: &mut Ve _ => art, }; let first = out.len(); - leaves(&art, None, nodes, out); + leaves(doc, &art, None, nodes, out); for l in out.iter_mut().skip(first) { within(l, n); } @@ -176,29 +179,37 @@ fn transparency_into_members(n: &mut Node) { /// [`outline_object`](crate::outline_object) gives a symbol instance its symbol's art. The art /// keeps the effects of `n` other than its geometry effects, and the groups inside it pass their /// opacity and blend mode to the objects they hold. `None` when `n` is none of these, for images, -/// for symbol instances without geometry effects, and for one without art from `hook`. -pub fn shape_art(n: &Node, hook: Option) -> Option { - shape_art_in(n, hook, &SymbolNodes::default()) +/// for symbol instances without geometry effects, and for one without art from `hook`. A +/// live-effect plug-in that paints its pieces makes a path, and each path of a group's or symbol's +/// art, a group of its pieces, whose swatches and patterns must be in `doc` (without one, pieces +/// that name one are refused and the path stays as it is). +pub fn shape_art(doc: Option<&Document>, n: &Node, hook: Option) -> Option { + shape_art_in(doc, n, hook, &SymbolNodes::default()) } /// [`shape_art`], with the symbol art given so far counted in `nodes`. -fn shape_art_in(n: &Node, hook: Option, nodes: &SymbolNodes) -> Option { - let mut art = reshaped_art(n, hook, nodes)?; +fn shape_art_in(doc: Option<&Document>, n: &Node, hook: Option, nodes: &SymbolNodes) -> Option { + let mut art = reshaped_art(doc, n, hook, nodes)?; transparency_into_members(&mut art); Some(art) } /// [`shape_art_in`] before the groups inside the art pass on their transparency. -fn reshaped_art(n: &Node, hook: Option, nodes: &SymbolNodes) -> Option { +fn reshaped_art(doc: Option<&Document>, n: &Node, hook: Option, nodes: &SymbolNodes) -> Option { let fx = crate::reshape::geometry_effects(n); let outline = hook.unwrap_or(&crate::outline_text); match &n.kind { - NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. } if !fx.is_empty() => Some(reshaped(n, n)), - NodeKind::Group { .. } | NodeKind::Layer { .. } if !fx.is_empty() => match compound_shape_art_in(n, hook, nodes) { - Some(area) => Some(reshaped(&area, n)), + // A path with a live-effect plug-in that paints its pieces: the group of its pieces, each + // with its own paints, as the renderer draws it. + NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. } if !fx.is_empty() => { + let painted = crate::reshape::has_painted_effects(n).then(|| crate::reshape::reshape(doc, n, None)).flatten(); + Some(painted.unwrap_or_else(|| reshaped(n, n, doc))) + } + NodeKind::Group { .. } | NodeKind::Layer { .. } if !fx.is_empty() => match compound_shape_art_in(doc, n, hook, nodes) { + Some(area) => Some(reshaped(&area, n, doc)), None => { - let mut m = crate::reshape::outlined_members_with(None, n, outline, nodes); - let carry = crate::reshape::reshape_leaves_carrying(&mut m, &fx); + let mut m = crate::reshape::outlined_members_with(None, doc, n, outline, nodes); + let carry = crate::reshape::reshape_leaves_carrying(&mut m, &fx, doc); // The group's own fills and strokes follow the effects as its members do. if carry.moves(&m.appearance) { carry.apply(&mut m.appearance); @@ -213,8 +224,8 @@ fn reshaped_art(n: &Node, hook: Option, nodes: &SymbolNodes) -> Opt let h = hook?; let art = h(n).filter(|art| nodes.charge(art))?; let mut art = crate::reshape::symbol_art_with(&art, h, nodes, symbol); - crate::reshape::outline_nested_text(&mut art, false); - let carry = crate::reshape::reshape_leaves_carrying(&mut art, &fx); + crate::reshape::outline_nested_text(doc, &mut art, false); + let carry = crate::reshape::reshape_leaves_carrying(&mut art, &fx, doc); // The art's own fills and strokes follow the effects as its members do. if carry.moves(&art.appearance) { carry.apply(&mut art.appearance); @@ -226,19 +237,21 @@ fn reshaped_art(n: &Node, hook: Option, nodes: &SymbolNodes) -> Opt } NodeKind::Image(_) | NodeKind::SymbolInstance { .. } => None, _ if crate::needs_outline(n) && (vectorcraft_doc::live::is_live(n) || !fx.is_empty()) => { - crate::reshape::as_art_with(None, n, None, outline, nodes) + crate::reshape::as_art_with(None, doc, n, None, outline, nodes) } _ => None, } } /// `src`, a path, compound path or compound shape's area, as one path reshaped by the visible -/// geometry effects of `n`, without those effects. -fn reshaped(src: &Node, n: &Node) -> Node { +/// geometry effects of `n`, without those effects. The swatches and patterns a live-effect plug-in +/// that paints its pieces names are looked up in `doc`. +fn reshaped(src: &Node, n: &Node, doc: Option<&Document>) -> Node { let mut m = src.clone(); if let Some((path, rule)) = geometry(src) { let bounds = vectorcraft_geom::Shape::bounding_box(&path.to_bezpath()); - let (g, carry) = crate::apply_geometry_carrying(&n.appearance.effects, &path, bounds, &crate::GeomContext::of(n)); + let (g, carry) = + crate::apply_geometry_carrying(&n.appearance.effects, &path, bounds, &crate::GeomContext { doc, ..crate::GeomContext::of(n) }); m.kind = NodeKind::Path { path: g, rule, live: None, clipping: false, guide: false }; // Its gradients move with the outline, as the canvas paints them. if carry.moves(&m.appearance) { @@ -282,12 +295,12 @@ fn shape_op(mode: ShapeMode) -> po::BoolOp { /// it by the member's shape mode; the backmost member starts the area. `None` when `n` is not a /// compound shape, or its members are past a live Pathfinder's budget ([`PATHFINDER_MAX_SEGMENTS`], /// [`PATHFINDER_MAX_OVERLAPS`], [`SYMBOL_ART_MAX_NODES`](crate::SYMBOL_ART_MAX_NODES)). -pub fn compound_shape_area(n: &Node, hook: Option) -> Option { - compound_shape_area_in(n, hook, &SymbolNodes::default()) +pub fn compound_shape_area(doc: Option<&Document>, n: &Node, hook: Option) -> Option { + compound_shape_area_in(doc, n, hook, &SymbolNodes::default()) } /// [`compound_shape_area`], with the symbol art given so far counted in `nodes`. -fn compound_shape_area_in(n: &Node, hook: Option, nodes: &SymbolNodes) -> Option { +fn compound_shape_area_in(doc: Option<&Document>, n: &Node, hook: Option, nodes: &SymbolNodes) -> Option { if !n.is_compound_shape() { return None; } @@ -296,7 +309,7 @@ fn compound_shape_area_in(n: &Node, hook: Option, nodes: &SymbolNod let mut members: Vec<(po::BoolOp, usize)> = vec![]; for c in n.children()? { let before = all.len(); - leaves(c, hook, nodes, &mut all); + leaves(doc, c, hook, nodes, &mut all); if all.len() > before { members.push((shape_op(c.extras().shape_mode), all.len() - before)); } @@ -323,13 +336,13 @@ fn compound_shape_area_in(n: &Node, hook: Option, nodes: &SymbolNod /// Compound shape `n` as one path of its area ([`compound_shape_area`]) with its appearance, /// opacity and blend mode: how it takes part in a Pathfinder operation. -pub fn compound_shape_art(n: &Node, hook: Option) -> Option { - compound_shape_art_in(n, hook, &SymbolNodes::default()) +pub fn compound_shape_art(doc: Option<&Document>, n: &Node, hook: Option) -> Option { + compound_shape_art_in(doc, n, hook, &SymbolNodes::default()) } /// [`compound_shape_art`], with the symbol art given so far counted in `nodes`. -fn compound_shape_art_in(n: &Node, hook: Option, nodes: &SymbolNodes) -> Option { - let area = compound_shape_area_in(n, hook, nodes)?; +fn compound_shape_art_in(doc: Option<&Document>, n: &Node, hook: Option, nodes: &SymbolNodes) -> Option { + let area = compound_shape_area_in(doc, n, hook, nodes)?; let mut art = Node::path(n.id, area, Appearance { items: n.appearance.items.clone(), ..Default::default() }); (art.opacity, art.blend) = (n.opacity, n.blend); Some(art) @@ -435,15 +448,16 @@ fn within_budget<'a>(leaves: impl IntoIterator) -> bool { /// Evaluate the visible Pathfinder effects of `group` in order: the resulting child objects. /// `None` when the group has no Pathfinder effect. Past the budget ([`PATHFINDER_MAX_SEGMENTS`], /// [`PATHFINDER_MAX_OVERLAPS`], [`SYMBOL_ART_MAX_NODES`](crate::SYMBOL_ART_MAX_NODES) for -/// each effect) it returns the children as they are. -pub fn pathfinder_children(group: &Node, hook: Option) -> Option>> { +/// each effect) it returns the children as they are. The swatches and patterns that live-effect +/// plug-ins painting their pieces name are looked up in `doc` ([`shape_art`]). +pub fn pathfinder_children(doc: Option<&Document>, group: &Node, hook: Option) -> Option>> { if !has_pathfinder(group) { return None; } let source: &[Arc] = group.children()?; let mut stack: Vec> = if group.is_compound_shape() { // The group's own appearance paints the area ([`evaluate_container`]), so the art has none. - match compound_shape_area(group, hook) { + match compound_shape_area(doc, group, hook) { Some(area) if area.is_empty() => vec![], Some(area) => vec![Arc::new(Node::path(group.id, area, Appearance::default()))], None => return Some(source.to_vec()), @@ -455,7 +469,7 @@ pub fn pathfinder_children(group: &Node, hook: Option) -> Option) -> Option, n: &Node) -> Vec<(PathData, FillRul NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. } => out.extend(crate::reshape::leaf_geometry(n)), NodeKind::Group { .. } if n.is_compound_shape() => { let hook = |c: &Node| crate::outline_object(doc, c); - match compound_shape_area(n, Some(&hook)) { + match compound_shape_area(doc, n, Some(&hook)) { Some(area) => out.push((area, FillRule::NonZero)), None => members(doc, n, out), } @@ -715,7 +729,7 @@ pub fn evaluate_container(doc: Option<&Document>, n: &Node) -> Option { let mut m = n.clone(); if pathfinder { let hook = |c: &Node| crate::outline_object(doc, c); - let result = pathfinder_children(n, Some(&hook)).unwrap_or_default(); + let result = pathfinder_children(doc, n, Some(&hook)).unwrap_or_default(); if let Some(ch) = m.children_mut() { *ch = result; } @@ -724,7 +738,7 @@ pub fn evaluate_container(doc: Option<&Document>, n: &Node) -> Option { } if !fx.is_empty() { m = crate::reshape::outlined_members(doc, &m); - let carry = crate::reshape::reshape_leaves_carrying(&mut m, &fx); + let carry = crate::reshape::reshape_leaves_carrying(&mut m, &fx, doc); // The group's own fills and strokes paint the members as a whole: their gradients follow // the effects as the members' do. if paint && carry.moves(&m.appearance) { diff --git a/crates/effects/src/group/tests.rs b/crates/effects/src/group/tests.rs index 1b876c6ff..619d06a91 100644 --- a/crates/effects/src/group/tests.rs +++ b/crates/effects/src/group/tests.rs @@ -27,7 +27,7 @@ fn area(n: &Node) -> f64 { } fn overlap_cmyk(g: &Node) -> [f32; 4] { - let faces = pathfinder_children(g, None).unwrap(); + let faces = pathfinder_children(None, g, None).unwrap(); assert_eq!(faces.len(), 3); let overlap = faces .iter() @@ -61,7 +61,7 @@ fn trap_effect_adds_an_overprinting_strip_above_the_members() { let mut g = Node::group(NodeId(10), vec![sq(1, 0.0, yellow), sq(2, 10.0, blue)]); let params = serde_json::json!({"thickness": 1.0, "tintReduction": 100}); g.appearance.effects.push(Effect { id: crate::trap::TRAP.into(), params, visible: true }); - let out = pathfinder_children(&g, None).unwrap(); + let out = pathfinder_children(None, &g, None).unwrap(); assert_eq!(out.len(), 3, "both squares and one trap"); assert_eq!((out[0].id, out[1].id), (NodeId(1), NodeId(2))); let b = out[2].geometric_bounds().unwrap(); @@ -89,17 +89,17 @@ fn soft_mix_blends_cmyk_bottom_to_top_and_drops_strokes() { #[test] fn shape_modes_follow_the_pathfinder_panel() { - let add = pathfinder_children(&group_with("pathfinder.add"), None).unwrap(); + let add = pathfinder_children(None, &group_with("pathfinder.add"), None).unwrap(); assert_eq!(add.len(), 1); assert!((area(&add[0]) - 150.0).abs() < 1e-3); // Add keeps the front object's paint, Subtract the back one's. assert_eq!(add[0].id, NodeId(2)); - let sub = pathfinder_children(&group_with("pathfinder.subtract"), None).unwrap(); + let sub = pathfinder_children(None, &group_with("pathfinder.subtract"), None).unwrap(); assert_eq!(sub[0].id, NodeId(1)); assert!((area(&sub[0]) - 50.0).abs() < 1e-3); - let int = pathfinder_children(&group_with("pathfinder.intersect"), None).unwrap(); + let int = pathfinder_children(None, &group_with("pathfinder.intersect"), None).unwrap(); assert!((area(&int[0]) - 50.0).abs() < 1e-3); - let div = pathfinder_children(&group_with("pathfinder.divide"), None).unwrap(); + let div = pathfinder_children(None, &group_with("pathfinder.divide"), None).unwrap(); assert_eq!(div.len(), 3); } @@ -112,7 +112,7 @@ fn divide_and_outline_draw_nothing_for_unpainted_members() { for effect in ["pathfinder.divide", "pathfinder.outline"] { let mut g = Node::group(NodeId(10), vec![sq(1, 0.0, Color::rgb(1.0, 0.0, 0.0)), unpainted.clone()]); g.appearance.effects.push(Effect { id: effect.into(), params: serde_json::json!({}), visible: true }); - let out = pathfinder_children(&g, None).unwrap(); + let out = pathfinder_children(None, &g, None).unwrap(); assert!(out.iter().any(|n| n.id == NodeId(1)) && out.iter().any(|n| n.id == NodeId(2)), "{effect}: {out:#?}"); for n in out.iter().filter(|n| n.id == NodeId(2)) { assert!(n.appearance.items.iter().all(|i| i.paint().is_none()), "{effect}: {n:#?}"); @@ -138,7 +138,7 @@ fn square_and(front: Option, effect: &str) -> Node { /// Returns the Pathfinder results of `g`, with type outlined as the renderer outlines it. fn results(g: &Node) -> Vec> { - pathfinder_children(g, crate::text_outliner()).unwrap() + pathfinder_children(None, g, crate::text_outliner()).unwrap() } #[test] @@ -167,7 +167,7 @@ fn type_changes_the_result_of_every_pathfinder_effect() { assert!((with_type("pathfinder.intersect") + with_type("pathfinder.minusBack") - glyph_area).abs() < 0.5, "{glyph_area}"); // Outline art nested one group deeper takes part as well. let nested: OutlineHook = &|n: &Node| crate::outline_text(n).map(|g| Node::group(n.id, vec![Arc::new(g)])); - let sub = pathfinder_children(&square_and(Some(type_h()), "pathfinder.subtract"), Some(nested)).unwrap(); + let sub = pathfinder_children(None, &square_and(Some(type_h()), "pathfinder.subtract"), Some(nested)).unwrap(); assert!((sub.iter().map(|n| area(n)).sum::() - with_type("pathfinder.subtract")).abs() < 1e-6); } @@ -243,7 +243,7 @@ fn type_in_a_group_keeps_its_look_when_the_groups_geometry_effects_outline_it() fn hidden_effect_or_plain_group_is_untouched() { let mut g = group_with("pathfinder.add"); g.appearance.effects[0].visible = false; - assert!(pathfinder_children(&g, None).is_none()); + assert!(pathfinder_children(None, &g, None).is_none()); assert!(!has_pathfinder(&sq(1, 0.0, Color::BLACK))); } @@ -320,12 +320,12 @@ fn shape_art_moves_gradients_with_a_transform_effect() { }; let mut path = Node::path(NodeId(1), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), Appearance::basic(placed.clone(), Paint::None, 0.0)); path.appearance.effects.push(moved()); - let got = starts(&shape_art(&path, None).unwrap()); + let got = starts(&shape_art(None, &path, None).unwrap()); assert!(got.len() == 1 && (got[0] - 40.0).abs() < 1e-6, "the path's gradient: {got:?}"); let mut g = Node::group(NodeId(10), vec![sq(2, 0.0, Color::BLACK)]); g.appearance.items.push(AppearanceItem::Fill(vectorcraft_doc::FillLayer::new(placed))); g.appearance.effects.push(moved()); - let got = starts(&shape_art(&g, None).unwrap()); + let got = starts(&shape_art(None, &g, None).unwrap()); assert!(!got.is_empty() && got.iter().all(|s| (s - 40.0).abs() < 1e-6), "the group's own fill: {got:?}"); } @@ -343,7 +343,7 @@ fn shape_art_moves_the_gradient_of_a_symbol_instances_art_with_its_transform() { let hook = move |_: &Node| Some(art.clone()); let mut instance = Node::new(NodeId(1), NodeKind::SymbolInstance { symbol: "s".into(), xf: vectorcraft_geom::Affine::IDENTITY }); instance.appearance.effects.push(Effect { id: "distort.transform".into(), params: serde_json::json!({"moveH": 30}), visible: true }); - let out = shape_art(&instance, Some(&hook)).unwrap(); + let out = shape_art(None, &instance, Some(&hook)).unwrap(); let Paint::Gradient(g) = out.appearance.fill_paint() else { panic!("a gradient: {:?}", out.appearance) }; assert!((g.geom.unwrap().start.x - 30.0).abs() < 1e-6, "{:?}", g.geom); assert!((out.geometric_bounds().unwrap().x0 - 30.0).abs() < 1e-6, "the art moves"); @@ -377,11 +377,11 @@ fn shape_art_moves_the_gradients_of_a_symbol_instances_art_and_a_compound_shape_ let hook = move |_: &Node| Some(art.clone()); let mut instance = Node::new(NodeId(1), NodeKind::SymbolInstance { symbol: "s".into(), xf: vectorcraft_geom::Affine::IDENTITY }); instance.appearance.effects.push(warp()); - check(&shape_art(&instance, Some(&hook)).unwrap(), Rect::new(0.0, 0.0, 10.0, 10.0), &format!("warp.{style} on a symbol instance")); + check(&shape_art(None, &instance, Some(&hook)).unwrap(), Rect::new(0.0, 0.0, 10.0, 10.0), &format!("warp.{style} on a symbol instance")); let mut shape = compound_shape(ShapeMode::Add); shape.appearance = Appearance::basic(placed.clone(), Paint::None, 0.0).into(); shape.appearance.effects.push(warp()); - check(&shape_art(&shape, None).unwrap(), Rect::new(0.0, 0.0, 15.0, 10.0), &format!("warp.{style} on a compound shape")); + check(&shape_art(None, &shape, None).unwrap(), Rect::new(0.0, 0.0, 15.0, 10.0), &format!("warp.{style} on a compound shape")); } } @@ -714,7 +714,7 @@ fn pathfinder_past_its_budget_draws_the_source_art() { // n² crossings past the cap: no evaluation (#3591), the source art as it is. for n in [130, 800] { let g = crossing_bars(n, "pathfinder.divide"); - let out = pathfinder_children(&g, None).unwrap(); + let out = pathfinder_children(None, &g, None).unwrap(); assert_eq!(out.len(), 2 * n, "{n} x {n} bars"); let ids: Vec = out.iter().map(|c| c.id).collect(); let src: Vec = g.children().unwrap().iter().map(|c| c.id).collect(); @@ -729,7 +729,7 @@ fn pathfinder_within_its_budget_still_divides() { // n x n bars divide into 3·n² pieces, and 128 x 128 bars, which overlap in exactly // `PATHFINDER_MAX_OVERLAPS` pairs, must still divide. for n in [8, 128] { - let out = pathfinder_children(&crossing_bars(n, "pathfinder.divide"), None).unwrap(); + let out = pathfinder_children(None, &crossing_bars(n, "pathfinder.divide"), None).unwrap(); assert_eq!(out.len(), 3 * n * n, "{n} x {n} bars"); } } @@ -747,7 +747,7 @@ fn pathfinder_on_many_stacked_bars_still_runs() { .collect(); let mut g = Node::group(NodeId(10), children); g.appearance.effects.push(Effect { id: "pathfinder.add".into(), params: serde_json::json!({}), visible: true }); - let out = pathfinder_children(&g, None).unwrap(); + let out = pathfinder_children(None, &g, None).unwrap(); assert_eq!(out.len(), 1); } @@ -768,9 +768,9 @@ fn compound_shapes_draw_the_area_their_shape_modes_make() { for (mode, want) in [(ShapeMode::Add, 150.0), (ShapeMode::Subtract, 50.0), (ShapeMode::Intersect, 50.0), (ShapeMode::Exclude, 100.0)] { let g = compound_shape(mode); assert!(has_pathfinder(&g), "{mode:?}"); - let area_path = compound_shape_area(&g, None).unwrap(); + let area_path = compound_shape_area(None, &g, None).unwrap(); assert!((po::area(&area_path, FillRule::NonZero).abs() - want).abs() < 0.01, "{mode:?}"); - let out = pathfinder_children(&g, None).unwrap(); + let out = pathfinder_children(None, &g, None).unwrap(); assert_eq!(out.len(), 1, "{mode:?}"); assert!(out[0].appearance.items.is_empty(), "the group's own appearance paints it"); let art = evaluate_container(None, &g).unwrap(); @@ -787,7 +787,7 @@ fn compound_shapes_draw_the_area_their_shape_modes_make() { back.extras_mut().shape_mode = ShapeMode::Subtract; ch[0] = Arc::new(back); } - assert!((po::area(&compound_shape_area(&g, None).unwrap(), FillRule::NonZero).abs() - 150.0).abs() < 0.01); + assert!((po::area(&compound_shape_area(None, &g, None).unwrap(), FillRule::NonZero).abs() - 150.0).abs() < 0.01); // Hidden members take no part; the mark on a clip group is ignored. let mut g = compound_shape(ShapeMode::Subtract); if let Some(ch) = g.children_mut() { @@ -795,10 +795,10 @@ fn compound_shapes_draw_the_area_their_shape_modes_make() { front.visible = false; ch[1] = Arc::new(front); } - assert!((po::area(&compound_shape_area(&g, None).unwrap(), FillRule::NonZero).abs() - 100.0).abs() < 0.01); + assert!((po::area(&compound_shape_area(None, &g, None).unwrap(), FillRule::NonZero).abs() - 100.0).abs() < 0.01); let mut clip = compound_shape(ShapeMode::Add); clip.kind = NodeKind::Group { children: clip.children().unwrap().clone(), clip: true }; - assert!(!clip.is_compound_shape() && compound_shape_area(&clip, None).is_none()); + assert!(!clip.is_compound_shape() && compound_shape_area(None, &clip, None).is_none()); } /// A compound shape inside another (and inside an Effect › Pathfinder group) takes part as its area, @@ -811,10 +811,10 @@ fn nested_compound_shapes_take_part_as_their_area() { let mut outer = Node::group(NodeId(20), vec![Arc::new(inner.clone()), Arc::new(cutter)]); outer.extras_mut().compound_shape = true; // (0–5 of the inner area) ∩ (2–12) = 2–5 by 10 high. - assert!((po::area(&compound_shape_area(&outer, None).unwrap(), FillRule::NonZero).abs() - 30.0).abs() < 0.01); + assert!((po::area(&compound_shape_area(None, &outer, None).unwrap(), FillRule::NonZero).abs() - 30.0).abs() < 0.01); let mut effect = Node::group(NodeId(30), vec![Arc::new(inner)]); effect.appearance.effects.push(Effect { id: "pathfinder.add".into(), params: serde_json::json!({}), visible: true }); - let out = pathfinder_children(&effect, None).unwrap(); + let out = pathfinder_children(None, &effect, None).unwrap(); assert_eq!(out.len(), 1); assert!((area(&out[0]) - 50.0).abs() < 0.01); assert_eq!(out[0].appearance.fill_paint().color(), Some(Color::rgb(0.0, 1.0, 0.0)), "styled like the compound shape"); diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index efd6d5ceb..e5b3201a4 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -80,8 +80,8 @@ pub use marks::{CROP_MARKS, crop_marks_art, has_crop_marks}; pub use pixel::{PIXEL_EFFECTS, PixelFx, PixelSpace}; pub use raster::{RASTERIZE, RasterFx, Rasterize, color_param, outset, raster_effects}; pub use reshape::{ - SYMBOL_ART_MAX_NODES, expand_outlined, gives_image_shape, has_gradient_copies, has_item_geometry, has_transform_art, has_transform_copies, - has_transform_options, image_paint, needs_outline, outline_art, outline_text, reshape, + SYMBOL_ART_MAX_NODES, expand_outlined, gives_image_shape, has_gradient_copies, has_item_geometry, has_painted_effects, has_transform_art, + has_transform_copies, has_transform_options, image_paint, needs_outline, outline_art, outline_text, reshape, }; pub use revolve::{REVOLVE, has_revolve, revolve_art, revolve_options, validate_revolve}; pub use solids::{ @@ -107,5 +107,6 @@ pub use geometry::{ #[cfg(test)] pub(crate) use geometry::{MAX_EFFECT_LINES, apply_geometry_within}; pub use params::{ - default_params, has_geometry, has_plugin_effect, is_geometry, is_raster, merged_params, new_effect, plugin_revision, scale_effect, upgrade_params, + any_plugin_paints_output, default_params, has_geometry, has_painted_plugin_effect, has_plugin_effect, is_geometry, is_raster, merged_params, + new_effect, plugin_revision, scale_effect, upgrade_params, }; diff --git a/crates/effects/src/live.rs b/crates/effects/src/live.rs index c4b6be4b7..03b335f01 100644 --- a/crates/effects/src/live.rs +++ b/crates/effects/src/live.rs @@ -43,7 +43,8 @@ fn with_hooks(doc: Option<&Document>, f: impl FnOnce(Hooks) -> R) -> R { vectorcraft_text::register_envelope_ink(); let outline = |n: &Node| outline_object(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), symbols: doc }) + let appearance = |n: &Node| bake_appearance(doc, n); + f(Hooks { outline: Some(&outline), appearance: Some(&appearance), pattern: Some(&tiles), symbols: doc }) } /// One level of evaluation of live object `n` ([`live::expand_live_hooks`]). diff --git a/crates/effects/src/params.rs b/crates/effects/src/params.rs index a178a7537..5c06fae8f 100644 --- a/crates/effects/src/params.rs +++ b/crates/effects/src/params.rs @@ -148,6 +148,19 @@ pub fn has_plugin_effect(n: &Node) -> bool { n.appearance.effects.iter().any(plugin) || n.appearance.items.iter().any(|i| i.effects().iter().any(plugin)) } +/// Does `n` have a visible live-effect plug-in effect, its own or a fill's or stroke's, whose +/// plug-in paints the pieces it returns? What such an effect draws also depends on the document's +/// swatches and patterns, which its pieces may name. +pub fn has_painted_plugin_effect(n: &Node) -> bool { + let painted = |e: &Effect| e.visible && vectorcraft_plugins::effect::plugin_id(&e.id).is_some_and(vectorcraft_plugins::effect::paints_output); + n.appearance.effects.iter().any(painted) || n.appearance.items.iter().any(|i| i.effects().iter().any(painted)) +} + +/// Is a live-effect plug-in that paints the pieces it returns installed? +pub fn any_plugin_paints_output() -> bool { + vectorcraft_plugins::registry::list().iter().any(|p| vectorcraft_plugins::effect::paints_output(&p.manifest().id)) +} + /// The plug-in registry's revision, which changes whenever a plug-in is installed or removed. pub fn plugin_revision() -> u64 { vectorcraft_plugins::registry::revision() diff --git a/crates/effects/src/reshape.rs b/crates/effects/src/reshape.rs index bf51244f4..070060dcf 100644 --- a/crates/effects/src/reshape.rs +++ b/crates/effects/src/reshape.rs @@ -103,10 +103,27 @@ fn reaches_appearance(e: &Effect) -> bool { } /// Is `n` a path or compound path that its Transform effects make into art of separate objects -/// ([`has_gradient_copies`], [`has_layered_copies`], [`has_transform_options`])? It is drawn, baked and expanded through -/// that art ([`reshape`]). +/// ([`has_gradient_copies`], [`has_layered_copies`], [`has_transform_options`]), or whose +/// live-effect plug-ins paint what they return ([`has_painted_effects`])? It is drawn, baked and +/// expanded through that art ([`reshape`]). pub fn has_transform_art(n: &Node) -> bool { - has_gradient_copies(n) || has_layered_copies(n) || has_transform_options(n) + has_gradient_copies(n) || has_layered_copies(n) || has_transform_options(n) || has_painted_effects(n) +} + +/// Is `e` a visible live-effect plug-in whose manifest declares `paintOutput`: its id's plug-in id, +/// else `None`. +fn painted_plugin(e: &Effect) -> Option<&str> { + let id = vectorcraft_plugins::effect::plugin_id(&e.id).filter(|_| e.visible)?; + vectorcraft_plugins::effect::paints_output(id).then_some(id) +} + +/// Is `n` a path or compound path with a live-effect plug-in that paints what it returns +/// ([`painted_plugin`]), its own or on a fill or stroke that paints? Each piece the plug-in returns +/// is then an object of its own with its own paints ([`paint_leaf`], [`transform_item_leaf`]). +pub fn has_painted_effects(n: &Node) -> bool { + matches!(n.kind, NodeKind::Path { guide: false, clipping: false, .. } | NodeKind::Compound { .. }) + && (n.appearance.effects.iter().any(|e| painted_plugin(e).is_some()) + || n.appearance.items.iter().any(|i| crate::paints(i) && i.effects().iter().any(|e| painted_plugin(e).is_some()))) } /// Is `n` type without inline graphics that has a fill or stroke of its own that paints and has a @@ -163,17 +180,18 @@ fn image_rows(items: &[AppearanceItem]) -> Vec { /// The shapes that the image `n`'s own fills and strokes paint ([`image_rows`]), each with its fill /// rule and the fill or stroke that paints it: the paths (or compound paths) along its frame that /// [`reshape`] makes, with each fill's or stroke's geometry effects applied as the renderer applies -/// them. Empty for anything but an image with such fills or strokes. -pub fn image_paint(n: &Node) -> Vec<(BezPath, FillRule, AppearanceItem)> { +/// them. Empty for anything but an image with such fills or strokes. The swatches and patterns that +/// live-effect plug-ins painting their pieces name are looked up in `doc`. +pub fn image_paint(doc: Option<&Document>, n: &Node) -> Vec<(BezPath, FillRule, AppearanceItem)> { if !matches!(n.kind, NodeKind::Image(_)) || !has_item_geometry(n) { return vec![]; } - let Some(art) = reshape(None, n, None) else { return vec![] }; + let Some(art) = reshape(doc, n, None) else { return vec![] }; let mut out = vec![]; art.walk(&mut |c| { let (NodeKind::Path { rule, .. } | NodeKind::Compound { rule, .. }) = &c.kind else { return }; let Some(g) = c.stroke_path() else { return }; - let ctx = GeomContext::of(c); + let ctx = GeomContext { doc, ..GeomContext::of(c) }; for item in c.appearance.items.iter().filter(|i| crate::paints(i)) { let fx = item.effects(); let shape = if crate::has_geometry(fx) && !g.elements().is_empty() { @@ -286,14 +304,17 @@ fn with_id(art: &Node, id: NodeId) -> Node { /// through the guides could hold more than [`vectorcraft_doc::MAX_SYMBOL_NODES`] objects. 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 { - outline_art_with(doc, n, symbol_art, &outline_text, &SymbolNodes::default(), false) + outline_art_with(doc, doc, n, symbol_art, &outline_text, &SymbolNodes::default(), false) } /// [`outline_art`] with live objects evaluated through `outline` ([`evaluated_with`]); with /// `apart`, also a path or compound path whose Transform effect makes copies -/// ([`has_transform_copies`]), as the one piece the copies are made from. +/// ([`has_transform_copies`]), as the one piece the copies are made from. `symbols` is the document +/// the symbols of the art come from; `paint_doc` is the one the swatches and patterns of painted +/// pieces are looked up in. fn outline_art_with( - doc: Option<&Document>, + symbols: Option<&Document>, + paint_doc: Option<&Document>, n: &Node, symbol_art: Option<&Node>, outline: OutlineHook, @@ -303,7 +324,7 @@ fn outline_art_with( let mut art = match &n.kind { NodeKind::Text(_) => outline_text(n)?, NodeKind::SymbolInstance { symbol, xf } => { - let a = match doc { + let a = match symbols { Some(d) => d.place_symbol_art(symbol, symbol_art?, *xf)?, None => { let mut a = symbol_art?.clone(); @@ -323,10 +344,10 @@ fn outline_art_with( one.appearance.effects.clear(); Node::group(n.id, vec![Arc::new(one)]) } - _ if vectorcraft_doc::live::is_live(n) => evaluated_with(doc, n, outline, nodes), + _ if vectorcraft_doc::live::is_live(n) => evaluated_with(symbols, n, outline, nodes), _ => return None, }; - outline_nested_text(&mut art, false); + outline_nested_text(paint_doc, &mut art, false); art.opacity = 1.0; art.blend = Default::default(); art.isolate = false; @@ -433,21 +454,22 @@ fn instances_as_art(doc: Option<&Document>, n: &mut Node, outline: OutlineHook, /// 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 -/// [`crate::clip_outline`] clips by. -pub(crate) fn outline_nested_text(n: &mut Node, clipping: bool) { +/// [`crate::clip_outline`] clips by. The swatches and patterns that live-effect plug-ins painting +/// their pieces name are looked up in `doc`. +pub(crate) fn outline_nested_text(doc: Option<&Document>, n: &mut Node, clipping: bool) { let clips = n.clips(); let Some(ch) = n.children_mut() else { return }; for (i, c) in ch.iter_mut().enumerate() { 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(None, c, None), + NodeKind::Text(_) => as_art(doc, c, None), _ => None, }; if let Some(o) = art { *c = Arc::new(o); } else if c.children().is_some() { - outline_nested_text(Arc::make_mut(c), clipping); + outline_nested_text(doc, Arc::make_mut(c), clipping); } } } @@ -480,7 +502,7 @@ pub fn expand_outlined(doc: Option<&Document>, n: &Node, symbol_art: Option<&Nod // effect in a group of their own (copies of copies nest), as the reference app expands them // (#5612). if has_transform_art(n) || has_transform_copies(n) { - return as_art_opts(doc, n, symbol_art, &outline_text, &SymbolNodes::default(), true); + return as_art_opts(doc, doc, n, symbol_art, &outline_text, &SymbolNodes::default(), true); } let own_paint = match n.kind { NodeKind::Text(_) => !n.appearance.items.is_empty(), @@ -494,17 +516,34 @@ pub fn expand_outlined(doc: Option<&Document>, n: &Node, symbol_art: Option<&Nod /// [`reshape`] without its checks (no geometry effects: just the art). pub(crate) fn as_art(doc: Option<&Document>, n: &Node, symbol_art: Option<&Node>) -> Option { - as_art_with(doc, n, symbol_art, &outline_text, &SymbolNodes::default()) + as_art_with(doc, doc, n, symbol_art, &outline_text, &SymbolNodes::default()) } -/// [`as_art`] with live objects evaluated through `outline` ([`evaluated_with`]). -pub(crate) fn as_art_with(doc: Option<&Document>, n: &Node, symbol_art: Option<&Node>, outline: OutlineHook, nodes: &SymbolNodes) -> Option { - as_art_opts(doc, n, symbol_art, outline, nodes, false) +/// [`as_art`] with live objects evaluated through `outline` ([`evaluated_with`]). `symbols` is the +/// document the symbols of the art come from; `paint_doc` is the one the swatches and patterns of +/// painted pieces are looked up in. +pub(crate) fn as_art_with( + symbols: Option<&Document>, + paint_doc: Option<&Document>, + n: &Node, + symbol_art: Option<&Node>, + outline: OutlineHook, + nodes: &SymbolNodes, +) -> Option { + as_art_opts(symbols, paint_doc, n, symbol_art, outline, nodes, false) } /// [`as_art_with`]; with `apart`, a path's or compound path's Transform effects make each copy an /// object of its own ([`copy_leaf`]) rather than one path of every copy. -fn as_art_opts(doc: Option<&Document>, n: &Node, symbol_art: Option<&Node>, outline: OutlineHook, nodes: &SymbolNodes, apart: bool) -> Option { +fn as_art_opts( + symbols: Option<&Document>, + paint_doc: Option<&Document>, + n: &Node, + symbol_art: Option<&Node>, + outline: OutlineHook, + nodes: &SymbolNodes, + apart: bool, +) -> Option { let fx = geometry_effects(n); let mut art = match &n.kind { NodeKind::Image(im) => { @@ -532,13 +571,13 @@ fn as_art_opts(doc: Option<&Document>, n: &Node, symbol_art: Option<&Node>, outl Node::group(n.id, rows(below).into_iter().chain([Arc::new(image)]).chain(rows(above)).collect()) } } - _ => outline_art_with(doc, n, symbol_art, outline, nodes, apart)?, + _ => outline_art_with(symbols, paint_doc, n, symbol_art, outline, nodes, apart)?, }; - reshape_leaves_apart(&mut art, &fx, Budget::EFFECTS, apart); + reshape_leaves_apart(&mut art, &fx, Budget::EFFECTS, apart, paint_doc); if apart { hoist_copies(&mut art); } - transform_items(&mut art); + transform_items(&mut art, paint_doc); // Type and symbols in perspective: the art projected onto their plane. if let Some(h) = n.projection() { project_leaves(&mut art, &h); @@ -596,20 +635,22 @@ pub(crate) fn geometry_effects(n: &Node) -> Vec<&Effect> { /// symbol instances follow a Transform effect the same way ([`transform_placed`]); other geometry /// effects leave them as they are (a raster follows a non-affine map only as a mesh warp). The /// paths keep to one [`Budget::EFFECTS`] together (or to what they cost, when that is more): a path -/// whose effect would pass what the others leave of it keeps its shape. -pub(crate) fn reshape_leaves(art: &mut Node, fx: &[&Effect]) { - reshape_leaves_within(art, fx, Budget::EFFECTS); +/// whose effect would pass what the others leave of it keeps its shape. The swatches and patterns +/// that live-effect plug-ins painting their pieces name are looked up in `doc` +/// ([`reshape_leaves_apart`]). +pub(crate) fn reshape_leaves(art: &mut Node, fx: &[&Effect], doc: Option<&Document>) { + reshape_leaves_within(art, fx, Budget::EFFECTS, doc); } /// [`reshape_leaves`], and how gradients that paint the art as a whole (a group's own fills and /// strokes) follow the effects ([`crate::Carry`]). -pub(crate) fn reshape_leaves_carrying(art: &mut Node, fx: &[&Effect]) -> crate::Carry { - reshape_leaves_within(art, fx, Budget::EFFECTS) +pub(crate) fn reshape_leaves_carrying(art: &mut Node, fx: &[&Effect], doc: Option<&Document>) -> crate::Carry { + reshape_leaves_within(art, fx, Budget::EFFECTS, doc) } /// [`reshape_leaves_carrying`] whose paths keep to `budget` together, or to what they cost. -pub(crate) fn reshape_leaves_within(art: &mut Node, fx: &[&Effect], budget: Budget) -> crate::Carry { - reshape_leaves_apart(art, fx, budget, false) +pub(crate) fn reshape_leaves_within(art: &mut Node, fx: &[&Effect], budget: Budget, doc: Option<&Document>) -> crate::Carry { + reshape_leaves_apart(art, fx, budget, false, doc) } /// `art`, the group of the one piece a path's copies are made from ([`outline_art_with`]), holding @@ -629,9 +670,11 @@ fn hoist_copies(art: &mut Node) { } /// [`reshape_leaves_within`]; with `apart`, a Transform effect's copies of each path are objects -/// of their own ([`copy_leaf`]) whatever paints them, as Expand Appearance makes them. Returns how -/// gradients that paint the art as a whole follow the effects. -fn reshape_leaves_apart(art: &mut Node, fx: &[&Effect], budget: Budget, apart: bool) -> crate::Carry { +/// of their own ([`copy_leaf`]) whatever paints them, as Expand Appearance makes them. A live-effect +/// plug-in that paints what it returns makes each path a group of its pieces ([`paint_leaf`]), +/// whose swatches and patterns must be in `doc` (without one, pieces that name one are refused). +/// Returns how gradients that paint the art as a whole follow the effects. +fn reshape_leaves_apart(art: &mut Node, fx: &[&Effect], budget: Budget, apart: bool, doc: Option<&Document>) -> crate::Carry { let mut whole = crate::Carry::default(); if fx.is_empty() { return whole; @@ -658,6 +701,17 @@ fn reshape_leaves_apart(art: &mut Node, fx: &[&Effect], budget: Budget, apart: b for e in fx { let Some(bounds) = art.geometric_bounds() else { break }; let params = merged_params(&e.id, &e.params); + if let Some(plugin) = painted_plugin(e) { + for_each_leaf(art, &mut |leaf| { + let Some((path, _)) = leaf_geometry(leaf) else { return }; + let before = Budget::of(&path); + let room = max.minus(used.minus(before)); + if let Some(made) = paint_leaf(leaf, plugin, ¶ms, bounds, room, doc) { + used = used.minus(before).plus(made); + } + }); + continue; + } // Placed gradients follow a Transform effect, Free Distort and the Warp effects // ([`crate::Carry`]): under Free Distort and Warp (`boxes`) each path's move and scale from // its bounds before the effect to its bounds after it. Under those and the other effects, @@ -843,15 +897,118 @@ fn copy_leaf(leaf: &mut Node, path: &PathData, m: Affine, copies: usize, scaling made } +/// Live-effect plug-in `plugin` (one that paints what it returns) with `params` run on `leaf`, a +/// path or compound path, as one of its object's effects with reference box `bounds`: `leaf` +/// becomes a group with its id and name of one object for each piece returned, in order, each with +/// the piece's geometry and the leaf's appearance with the piece's fills and strokes in place of +/// its own ([`vectorcraft_plugins::painted::appearance`]). The swatches and patterns the pieces +/// name must be in `doc`, and without one the pieces may name none. Returns what the pieces cost +/// ([`piece_cost`]); `None` (the leaf stays as it is) when the plug-in fails, the pieces are +/// refused, or they would pass `budget`. +fn paint_leaf(leaf: &mut Node, plugin: &str, params: &serde_json::Value, bounds: Rect, budget: Budget, doc: Option<&Document>) -> Option { + let (path, rule) = leaf_geometry(leaf)?; + let paints = vectorcraft_plugins::effect::Paints { appearance: Some(&leaf.appearance), item: None }; + let pieces = vectorcraft_plugins::effect::apply_painted(plugin, params, &path, bounds, rule, paints, doc)?; + let made = pieces.iter().map(|p| piece_cost(&p.path)).fold(Budget { anchors: 0, lines: 0 }, Budget::plus); + if !budget.fits(made) { + return None; + } + let children = pieces + .iter() + .map(|p| { + let mut one = leaf.clone(); + one.name = None; + one.appearance = vectorcraft_plugins::painted::appearance(&leaf.appearance, p).into(); + set_geometry(&mut one, p.path.clone(), p.rule); + Arc::new(one) + }) + .collect(); + let mut g = Node::group(leaf.id, children); + g.name = leaf.name.clone(); + *leaf = g; + Some(made) +} + +/// What a painted piece with geometry `path` costs: its anchors and lines, plus a share of the +/// effects' budget for the object it becomes, so that a budget of [`Budget::EFFECTS`] holds at most +/// [`MAX_OBJECTS`](vectorcraft_plugins::objects::MAX_OBJECTS) pieces however small their paths. +fn piece_cost(path: &PathData) -> Budget { + let max = vectorcraft_plugins::objects::MAX_OBJECTS; + Budget::of(path).plus(Budget { anchors: Budget::EFFECTS.anchors / max, lines: Budget::EFFECTS.lines / max }) +} + +/// Live-effect plug-in `plugin` (one that paints what it returns) with `params`, a later effect of +/// `item`, run on `leaf`, a path of [`transform_piece`] painted by copies of `item`, with reference +/// box `bounds`: `leaf` becomes a group of one path for each piece returned, in order. A piece that +/// gives fills (on a fill) or strokes (on a stroke) is painted by copies of `item`, the leaf's first +/// fill or stroke, with those paints ([`vectorcraft_plugins::painted::items`]), under the leaf's +/// other fills and strokes; one that gives none keeps the leaf's. Returns +/// what the paths cost ([`piece_cost`]); `None` (the leaf stays as it is) when the plug-in fails, +/// the pieces are refused (a swatch or pattern not in `doc`, or any without one), or they would pass +/// `budget`. +fn paint_piece( + leaf: &mut Node, + item: &AppearanceItem, + plugin: &str, + params: &serde_json::Value, + bounds: Rect, + budget: Budget, + doc: Option<&Document>, +) -> Option { + let (path, rule) = leaf_geometry(leaf)?; + let paints = vectorcraft_plugins::effect::Paints { appearance: Some(&leaf.appearance), item: Some(0) }; + let pieces = vectorcraft_plugins::effect::apply_painted(plugin, params, &path, bounds, rule, paints, doc)?; + let made = pieces.iter().map(|p| piece_cost(&p.path)).fold(Budget { anchors: 0, lines: 0 }, Budget::plus); + if !budget.fits(made) { + return None; + } + let mut children = vec![]; + for p in pieces.iter() { + let gives = match item { + AppearanceItem::Fill(_) => p.fills.is_some(), + AppearanceItem::Stroke(_) => p.strokes.is_some(), + }; + let items = if gives { + let mut items = vectorcraft_plugins::painted::items(item, p); + items.extend_from_slice(leaf.appearance.items.get(1..).unwrap_or_default()); + items + } else { + leaf.appearance.items.clone() + }; + if items.is_empty() { + continue; + } + let mut one = leaf.clone(); + one.appearance.items = items; + set_geometry(&mut one, p.path.clone(), p.rule); + children.push(Arc::new(one)); + } + *leaf = Node::group(leaf.id, children); + Some(made) +} + /// Every path and compound path under `art` (other than clipping paths) with a fill or stroke whose /// own Transform effect has Transform Patterns or Scale Strokes & Effects on -/// ([`item_transform_options`]), made into a group of objects of its own ([`transform_item_leaf`]). -/// The new paths keep to one [`Budget::EFFECTS`] together (or to what `art` costs, when that is -/// more). -fn transform_items(art: &mut Node) { +/// ([`item_transform_options`]), or whose own live-effect plug-in paints what it returns +/// ([`painted_plugin`]), made into a group of objects of its own ([`transform_item_leaf`]). The +/// new paths keep to one [`Budget::EFFECTS`] together (or to what `art` costs, when that is more). +/// The swatches and patterns painted pieces name must be in `doc`; without one they may name none. +fn transform_items(art: &mut Node, doc: Option<&Document>) { let mut used = art_cost(art); let max = Budget::EFFECTS.at_least(used); - for_each_leaf(art, &mut |leaf| transform_item_leaf(leaf, max, &mut used)); + for_each_leaf(art, &mut |leaf| transform_item_leaf(leaf, max, &mut used, doc)); +} + +/// Is `e` an effect that makes a fill's or stroke's piece of a path an object of its own: a +/// Transform effect that [`reaches_appearance`], or a live-effect plug-in that paints what it +/// returns ([`painted_plugin`])? +fn makes_piece(e: &Effect) -> bool { + reaches_appearance(e) || painted_plugin(e).is_some() +} + +/// Does `item` paint and carry an effect that [`makes_piece`]? +fn item_makes_piece(item: &AppearanceItem) -> bool { + crate::paints(item) && item.effects().iter().any(makes_piece) } /// `leaf`, a path or compound path with a fill or stroke whose own Transform effect has Transform @@ -866,8 +1023,17 @@ fn transform_items(art: &mut Node) { /// effects stay on each of them. The other fills and strokes stay together on paths of the leaf's /// geometry between them, with their own effects. `max` is the budget of the whole art and `used` /// what it has spent. -fn transform_item_leaf(leaf: &mut Node, max: Budget, used: &mut Budget) { - if matches!(leaf.kind, NodeKind::Path { clipping: true, .. }) || !leaf.appearance.items.iter().any(item_transform_options) { +/// +/// A fill or stroke whose first such effect is a live-effect plug-in that paints what it returns +/// ([`painted_plugin`]) becomes, in place of a transformed path, one path for each piece the +/// plug-in returns, painted by copies of the fill or stroke with the piece's paints +/// ([`vectorcraft_plugins::painted::items`]); the plug-in is given the geometry before it and told +/// which fill or stroke it is on. When it fails (or names a swatch or pattern not in `doc`, or any +/// without a document), the +/// fill or stroke paints that geometry as it is. The geometry effects after it then apply to all +/// the pieces as one ([`transform_piece`]). +fn transform_item_leaf(leaf: &mut Node, max: Budget, used: &mut Budget, doc: Option<&Document>) { + if matches!(leaf.kind, NodeKind::Path { clipping: true, .. }) || !leaf.appearance.items.iter().any(item_makes_piece) { return; } let Some((path, rule)) = leaf_geometry(leaf) else { return }; @@ -895,12 +1061,12 @@ fn transform_item_leaf(leaf: &mut Node, max: Budget, used: &mut Budget) { let mut pieces: Vec> = vec![]; let mut plain: Vec = vec![]; // Which items become a transformed piece of their own; every other run of items is one plain piece. - let transformed: Vec = original.appearance.items.iter().map(|i| crate::paints(i) && i.effects().iter().any(reaches_appearance)).collect(); + let transformed: Vec = original.appearance.items.iter().map(item_makes_piece).collect(); // The number of pieces after item `i` in paint order: each costs at least the leaf's geometry. let later = |i: usize| (i + 1..transformed.len()).filter(|&j| transformed[j] || j == i + 1 || transformed[j - 1]).count(); for (i, item) in original.appearance.items.iter().enumerate() { let fx = item.effects(); - let Some(at) = fx.iter().position(reaches_appearance).filter(|_| crate::paints(item)) else { + let Some(at) = fx.iter().position(makes_piece).filter(|_| crate::paints(item)) else { plain.push(item.clone()); continue; }; @@ -923,6 +1089,33 @@ fn transform_item_leaf(leaf: &mut Node, max: Budget, used: &mut Budget) { }; let Some(tb) = g.bounds() else { continue }; let params = merged_params(&fx[at].id, &fx[at].params); + if let Some(plugin) = painted_plugin(&fx[at]) { + let mut it = item.clone(); + let rest: Vec = before.iter().filter(|e| !is_geometry(&e.id)).chain(&fx[at + 1..]).cloned().collect(); + *it.effects_mut() = rest; + let mut one = Appearance { items: vec![it.clone()], ..Default::default() }; + if carry.moves(&one) { + carry.apply(&mut one); + } + let it = one.items.pop().unwrap_or(it); + let paints = vectorcraft_plugins::effect::Paints { appearance: Some(&original.appearance), item: Some(i) }; + let painted = vectorcraft_plugins::effect::apply_painted(plugin, ¶ms, &g, tb, rule, paints, doc) + .map(|pieces| pieces.iter().map(|p| (p.path.clone(), p.rule, vectorcraft_plugins::painted::items(&it, p))).collect::>()) + .filter(|pieces| room.fits(pieces.iter().map(|(p, ..)| piece_cost(p)).fold(Budget { anchors: 0, lines: 0 }, Budget::plus))); + // A failed run draws the fill or stroke on the geometry it was given. + let returned = painted.unwrap_or_else(|| vec![(g.clone(), rule, vec![it])]); + let mut group: Vec> = vec![]; + for (path, rule, items) in returned.into_iter().filter(|(_, _, items)| !items.is_empty()) { + *used = used.plus(piece_cost(&path)); + let mut p = piece(items); + set_geometry(&mut p, path, rule); + group.push(Arc::new(p)); + } + let mut p = Node::group(leaf.id, group); + transform_piece(&mut p, ctx.seed, limit, used, doc); + pieces.push(Arc::new(p)); + continue; + } let (_, copies) = crate::distort::transform_map(tb, ¶ms, ctx.seed); let options = crate::distort::TransformOptions::of(¶ms); let map = crate::distort::transform_affine(tb, ¶ms, ctx.seed); @@ -936,7 +1129,7 @@ fn transform_item_leaf(leaf: &mut Node, max: Budget, used: &mut Budget) { set_geometry(&mut p, g.clone(), rule); let (made, _) = transform_leaf(&mut p, &g, map, copies, options, room); *used = used.plus(made); - transform_piece(&mut p, ctx.seed, limit, used); + transform_piece(&mut p, ctx.seed, limit, used, doc); pieces.push(Arc::new(p)); } if !plain.is_empty() { @@ -955,9 +1148,11 @@ fn transform_item_leaf(leaf: &mut Node, max: Budget, used: &mut Budget) { /// own effects reshape all its paths ([`reshape_leaves_within`]). Each path applies its own copy of /// the effect, whose distances its Scale Strokes & Effects scaled. A Transform effect with /// Transform Patterns or Scale Strokes & Effects on transforms each path as [`transform_leaf`] does. -/// The geometry effects then leave the fills and strokes; their other effects stay. The paths keep -/// to `max` together with the rest of the art (`used`). -fn transform_piece(p: &mut Node, seed: u64, max: Budget, used: &mut Budget) { +/// A live-effect plug-in that paints what it returns ([`painted_plugin`]) makes each path one path +/// for each piece it returns ([`paint_piece`]). The geometry effects then leave the fills and +/// strokes; their other effects stay. The paths keep to `max` together with the rest of the art +/// (`used`), and the swatches and patterns painted pieces name must be in `doc`. +fn transform_piece(p: &mut Node, seed: u64, max: Budget, used: &mut Budget, doc: Option<&Document>) { let mut count = 0; for_each_leaf(p, &mut |l| { if let Some(item) = l.appearance.items.first() { @@ -973,6 +1168,12 @@ fn transform_piece(p: &mut Node, seed: u64, max: Budget, used: &mut Budget) { // What the other paths leave of the budget. let before = Budget::of(&path); let room = max.minus(used.minus(before)); + if let Some(plugin) = painted_plugin(&e) { + if let Some(made) = paint_piece(leaf, &item, plugin, &merged_params(&e.id, &e.params), bounds, room, doc) { + *used = used.minus(before).plus(made); + } + return; + } if reaches_appearance(&e) { let params = merged_params(&e.id, &e.params); let (_, copies) = crate::distort::transform_map(bounds, ¶ms, seed); @@ -987,7 +1188,7 @@ fn transform_piece(p: &mut Node, seed: u64, max: Budget, used: &mut Budget) { AppearanceItem::Stroke(s) => Some(s), AppearanceItem::Fill(_) => None, }; - let ctx = GeomContext { stroke, rule, seed, appearance: Some(&appearance), item: Some(0) }; + let ctx = GeomContext { stroke, rule, seed, appearance: Some(&appearance), item: Some(0), doc }; let mut carry = crate::Carry::default(); let out = crate::geometry::apply_geometry_carrying_within(std::slice::from_ref(&e), &path, bounds, &ctx, room, Some(&mut carry)); *used = used.minus(before).plus(Budget::of(&out)); @@ -1276,14 +1477,21 @@ 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(doc: Option<&Document>, n: &Node) -> Node { - outlined_members_with(doc, n, &outline_text, &SymbolNodes::default()) + outlined_members_with(doc, doc, n, &outline_text, &SymbolNodes::default()) } /// [`outlined_members`] with live objects and symbol instances given as art through `outline` -/// ([`evaluated_with`]). -pub(crate) fn outlined_members_with(doc: Option<&Document>, n: &Node, outline: OutlineHook, nodes: &SymbolNodes) -> Node { - let mut art = evaluated_with(doc, n, outline, nodes); - outline_nested_text(&mut art, false); +/// ([`evaluated_with`]), with the symbols of the art from `symbols` and the swatches and patterns +/// of painted pieces looked up in `paint_doc`. +pub(crate) fn outlined_members_with( + symbols: Option<&Document>, + paint_doc: Option<&Document>, + n: &Node, + outline: OutlineHook, + nodes: &SymbolNodes, +) -> Node { + let mut art = evaluated_with(symbols, n, outline, nodes); + outline_nested_text(paint_doc, &mut art, false); art } diff --git a/crates/effects/src/revolve.rs b/crates/effects/src/revolve.rs index b895aee3a..fd423689f 100644 --- a/crates/effects/src/revolve.rs +++ b/crates/effects/src/revolve.rs @@ -482,7 +482,7 @@ mod tests { let expanded = expand_revolve_art(&n).unwrap(); let count = |n: &Node| n.children().unwrap()[0].children().unwrap().len(); assert!(count(&expanded) < count(&live) * 3 / 4, "hidden back surfaces are removed"); - assert_eq!(crate::bake_appearance(&n), Some(live.clone()), "other baking retains the full surface"); + assert_eq!(crate::bake_appearance(None, &n), Some(live.clone()), "other baking retains the full surface"); n.appearance.effects[0].params["expandVisibleOnly"] = json!(false); assert_eq!(revolve_art(&n), Some(live.clone()), "the live view ignores the expansion setting"); assert_eq!(expand_revolve_art(&n), Some(live)); diff --git a/crates/effects/src/solids/tests.rs b/crates/effects/src/solids/tests.rs index 461f5610d..29bbdb3bd 100644 --- a/crates/effects/src/solids/tests.rs +++ b/crates/effects/src/solids/tests.rs @@ -330,7 +330,7 @@ fn export_and_expand_bake_faces_with_their_own_ids() { let mut n = square_node(); n.id = d.alloc_id(); n.appearance.effects.push(fx(id, json!({"bevel": "rounded"}))); - assert_eq!(crate::bake_appearance(&n), solid_art(&n), "{id}"); + assert_eq!(crate::bake_appearance(None, &n), solid_art(&n), "{id}"); let mut stroke = |_: &mut Document, _: &PathData, _: FillRule, _: &StrokeLayer| None; let art = crate::expand_leaf(&mut d, &n, &mut stroke).unwrap(); let mut ids = std::collections::HashSet::new(); @@ -356,7 +356,7 @@ fn a_twisted_tapered_hollow_inflate_exports_and_expands() { let params = json!({"twist": 120, "taper": 50, "cap": false, "bothSides": true, "rotationX": -20, "rotationY": 35}); n.appearance.effects.push(fx(INFLATE, params)); let art = solid_art(&n).unwrap(); - assert_eq!(crate::bake_appearance(&n), Some(art.clone())); + assert_eq!(crate::bake_appearance(None, &n), Some(art.clone())); assert!(faces(&art).len() > 20, "{} faces", faces(&art).len()); let mut stroke = |_: &mut Document, _: &PathData, _: FillRule, _: &StrokeLayer| None; let expanded = crate::expand_leaf(&mut d, &n, &mut stroke).unwrap(); diff --git a/crates/effects/src/tests.rs b/crates/effects/src/tests.rs index 90c215bf0..532251bd7 100644 --- a/crates/effects/src/tests.rs +++ b/crates/effects/src/tests.rs @@ -412,7 +412,7 @@ fn stacked_effects_on_art_stay_within_the_anchor_budget() { let leaf = |x: f64| Arc::new(Node::path(id, shapes::rectangle(Rect::new(x, 0.0, x + 100.0, 100.0)), Appearance::default())); let mut art = Node::group(id, (0..10).map(|k| leaf(k as f64 * 120.0)).collect()); let rough = fx("distort.roughen", json!({"size": 50, "detail": 100})); - crate::reshape::reshape_leaves_within(&mut art, &[&rough, &rough, &rough], Budget { anchors: 5_000, lines: usize::MAX }); + crate::reshape::reshape_leaves_within(&mut art, &[&rough, &rough, &rough], Budget { anchors: 5_000, lines: usize::MAX }, None); let mut total = 0; let mut reshaped = 0; for c in art.children().into_iter().flatten() { @@ -451,12 +451,12 @@ fn art_past_the_budget_keeps_its_own_gradients_in_place() { for params in [json!({"scaleH": 1000, "scaleV": 1000}), json!({"scaleH": 1000, "scaleV": 1000, "strokes": true})] { let grow = fx("distort.transform", params.clone()); let mut art = Node::group(id, vec![circle()]); - let carry = crate::reshape::reshape_leaves_within(&mut art, &[&grow], Budget::EFFECTS); + let carry = crate::reshape::reshape_leaves_within(&mut art, &[&grow], Budget::EFFECTS, None); let end = own_end(&carry).unwrap(); assert!((end - 550.0).abs() < 1e-6, "{params}: within the budget the group's gradient grows with the circle: {end}"); // No room beyond what the circle costs: scaled up, it would be drawn with more lines. let mut art = Node::group(id, vec![circle()]); - let carry = crate::reshape::reshape_leaves_within(&mut art, &[&grow], Budget { anchors: usize::MAX, lines: 0 }); + let carry = crate::reshape::reshape_leaves_within(&mut art, &[&grow], Budget { anchors: usize::MAX, lines: 0 }, None); let b = art.geometric_bounds().unwrap(); assert!(b.width() < 101.0 && b.height() < 101.0, "{params}: the circle keeps its shape: {b:?}"); assert_eq!(own_end(&carry), Some(100.0), "{params}: the group's gradient stays where it was placed"); @@ -465,7 +465,7 @@ fn art_past_the_budget_keeps_its_own_gradients_in_place() { let mut inner = Node::group(NodeId(2), vec![square]); inner.appearance.items.push(AppearanceItem::Fill(FillLayer::new(placed.clone()))); let mut art = Node::group(id, vec![circle(), Arc::new(inner)]); - let carry = crate::reshape::reshape_leaves_within(&mut art, &[&grow], Budget { anchors: usize::MAX, lines: 0 }); + let carry = crate::reshape::reshape_leaves_within(&mut art, &[&grow], Budget { anchors: usize::MAX, lines: 0 }, None); assert_eq!(own_end(&carry), Some(100.0), "{params}: the art's own gradient stays"); let inner = &art.children().unwrap()[1]; let Paint::Gradient(g) = inner.appearance.fill_paint() else { panic!("a gradient") }; @@ -509,7 +509,7 @@ fn a_hidden_path_past_the_budget_leaves_a_layers_gradients_free_to_move() { let corners = json!({"corners": [[-4.5, -4.5], [5.5, -4.5], [5.5, 5.5], [-4.5, 5.5]]}); for grow in [fx("distort.transform", json!({"scaleH": 1000, "scaleV": 1000})), fx("distort.freeDistort", corners)] { let mut art = layer(); - let carry = crate::reshape::reshape_leaves_within(&mut art, &[&grow], budget); + let carry = crate::reshape::reshape_leaves_within(&mut art, &[&grow], budget, None); let circle = art.children().unwrap()[1].geometric_bounds().unwrap(); assert!(circle.width() < 101.0, "{}: the hidden circle keeps its shape: {circle:?}", grow.id); let square = art.children().unwrap()[0].geometric_bounds().unwrap(); @@ -518,7 +518,7 @@ fn a_hidden_path_past_the_budget_leaves_a_layers_gradients_free_to_move() { carry.apply(&mut own); assert_eq!(end(&own), Some(550.0), "{}: the layer's gradient, as the art", grow.id); let mut art = Node::group(NodeId(9), vec![Arc::new(layer())]); - crate::reshape::reshape_leaves_within(&mut art, &[&grow], budget); + crate::reshape::reshape_leaves_within(&mut art, &[&grow], budget, None); assert_eq!(end(&art.children().unwrap()[0].appearance), Some(550.0), "{}: the layer's gradient, inside the art", grow.id); } } @@ -549,7 +549,7 @@ fn edited_freeform_points_follow_a_transform_effect_and_free_distort() { inner.appearance.items.push(AppearanceItem::Fill(FillLayer::new(freeform.clone()))); let mut art = Node::group(NodeId(9), vec![square(Appearance::basic(freeform.clone(), Paint::None, 0.0)), Arc::new(inner)]); art.appearance.items.push(AppearanceItem::Fill(FillLayer::new(freeform.clone()))); - let carry = crate::reshape::reshape_leaves_within(&mut art, &[&e], Budget::EFFECTS); + let carry = crate::reshape::reshape_leaves_within(&mut art, &[&e], Budget::EFFECTS, None); let members = art.children().unwrap(); assert_eq!(xs(&members[0].appearance), Some(vec![30.0, 130.0]), "{}: the path's points", e.id); assert_eq!(xs(&members[1].appearance), Some(vec![30.0, 130.0]), "{}: the inner group's points", e.id); @@ -625,7 +625,7 @@ fn reshape_leaves_within_optimizes_transform_before_terminal_offset_path() { let offset = fx("path.offsetPath", json!({"offset": 4.0, "joins": "round"})); let start = std::time::Instant::now(); - crate::reshape::reshape_leaves_within(&mut art, &[&transform, &offset], Budget::EFFECTS); + crate::reshape::reshape_leaves_within(&mut art, &[&transform, &offset], Budget::EFFECTS, None); let elapsed = start.elapsed(); assert!(elapsed < std::time::Duration::from_secs(2)); @@ -652,7 +652,7 @@ fn reshape_leaves_within_preserves_order_when_not_terminal_or_not_style_safe() { let mut art_evenodd = Node::group(id, vec![Arc::new(leaf)]); let transform = fx("distort.transform", json!({"moveH": 2.0, "moveV": 2.0, "copies": 2})); let offset = fx("path.offsetPath", json!({"offset": 4.0})); - crate::reshape::reshape_leaves_within(&mut art_evenodd, &[&transform, &offset], Budget::EFFECTS); + crate::reshape::reshape_leaves_within(&mut art_evenodd, &[&transform, &offset], Budget::EFFECTS, None); let p = art_evenodd.children().unwrap()[0].path_data().unwrap(); assert_eq!(p.subpaths.len(), 1, "EvenOdd does not swap; copies union into 1 outer subpath"); @@ -660,7 +660,7 @@ fn reshape_leaves_within_preserves_order_when_not_terminal_or_not_style_safe() { let leaf_round = Arc::new(Node::path(id, shapes::rectangle(Rect::new(0.0, 0.0, 20.0, 20.0)), Appearance::default())); let mut art_round = Node::group(id, vec![leaf_round]); let round_corners = fx("stylize.roundCorners", json!({"radius": 2.0})); - crate::reshape::reshape_leaves_within(&mut art_round, &[&transform, &offset, &round_corners], Budget::EFFECTS); + crate::reshape::reshape_leaves_within(&mut art_round, &[&transform, &offset, &round_corners], Budget::EFFECTS, None); let p = art_round.children().unwrap()[0].path_data().unwrap(); assert_eq!(p.subpaths.len(), 1, "Non-terminal Offset Path does not swap; copies union into 1 outer subpath"); @@ -668,7 +668,7 @@ fn reshape_leaves_within_preserves_order_when_not_terminal_or_not_style_safe() { let leaf_miter = Arc::new(Node::path(id, shapes::rectangle(Rect::new(0.0, 0.0, 20.0, 20.0)), Appearance::default())); let mut art_miter = Node::group(id, vec![leaf_miter]); let offset_miter = fx("path.offsetPath", json!({"offset": 4.0, "joins": "miter"})); - crate::reshape::reshape_leaves_within(&mut art_miter, &[&transform, &offset_miter], Budget::EFFECTS); + crate::reshape::reshape_leaves_within(&mut art_miter, &[&transform, &offset_miter], Budget::EFFECTS, None); let p = art_miter.children().unwrap()[0].path_data().unwrap(); assert_eq!(p.subpaths.len(), 1, "Miter join does not swap; copies union into 1 outer subpath"); } diff --git a/crates/engine/src/cmd/compoundshape.rs b/crates/engine/src/cmd/compoundshape.rs index 52c19a9a5..bc93b399a 100644 --- a/crates/engine/src/cmd/compoundshape.rs +++ b/crates/engine/src/cmd/compoundshape.rs @@ -189,7 +189,7 @@ fn make(s: &mut Session, p: &Value) -> Result { /// that has a shape. A shape taken from inside a group, or from the art of a blend, an envelope or /// a warped group, also takes that object's opacity and blend mode. fn top_style(d: &Document, n: &Node, hook: effects::OutlineHook) -> Option