diff --git a/crates/effects/src/bake.rs b/crates/effects/src/bake.rs index cdde7d135..a2cefbd74 100644 --- a/crates/effects/src/bake.rs +++ b/crates/effects/src/bake.rs @@ -333,16 +333,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; } @@ -357,7 +357,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); @@ -376,11 +376,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; @@ -530,9 +530,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); } @@ -543,9 +544,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()); @@ -601,15 +602,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 e16742270..7f933f412 100644 --- a/crates/effects/src/bake/tests.rs +++ b/crates/effects/src/bake/tests.rs @@ -67,7 +67,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"]); @@ -82,7 +82,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") }; @@ -98,7 +98,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, .. })); @@ -131,7 +131,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:?}"); @@ -142,7 +142,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() { @@ -157,7 +157,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/group.rs b/crates/effects/src/group.rs index aa222809d..d34a7fcef 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) { + // 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))) + } + NodeKind::Group { .. } | NodeKind::Layer { .. } if !fx.is_empty() => match compound_shape_art_in(doc, n, hook, nodes) { Some(area) => Some(reshaped(&area, n)), None => { let mut m = crate::reshape::outlined_members_with(None, n, outline, nodes); - crate::reshape::reshape_leaves(&mut m, &fx); + crate::reshape::reshape_leaves(&mut m, &fx, doc); without_geometry_effects(&mut m); Some(m) } @@ -209,8 +220,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); - crate::reshape::reshape_leaves(&mut art, &fx); + crate::reshape::outline_nested_text(doc, &mut art, false); + crate::reshape::reshape_leaves(&mut art, &fx, doc); (art.opacity, art.blend, art.isolate) = (n.opacity, n.blend, n.isolate); art.appearance.effects = n.appearance.effects.clone(); without_geometry_effects(&mut art); @@ -270,12 +281,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; } @@ -284,7 +295,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)); } @@ -310,13 +321,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) @@ -422,15 +433,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()), @@ -442,7 +454,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), } @@ -702,7 +714,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; } @@ -711,7 +723,7 @@ pub fn evaluate_container(doc: Option<&Document>, n: &Node) -> Option { } if !fx.is_empty() { m = crate::reshape::outlined_members(doc, &m); - crate::reshape::reshape_leaves_in(&mut m, &fx, doc); + crate::reshape::reshape_leaves(&mut m, &fx, doc); } m.appearance.effects.retain(|e| !is_pathfinder(&e.id) && !crate::is_geometry(&e.id)); if paint { diff --git a/crates/effects/src/group/tests.rs b/crates/effects/src/group/tests.rs index b67279bbb..d04a1f4ec 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))); } @@ -328,7 +328,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(); @@ -343,7 +343,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"); } } @@ -361,7 +361,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); } @@ -382,9 +382,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(); @@ -401,7 +401,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() { @@ -409,10 +409,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, @@ -425,10 +425,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 b251b15a6..1d85d9814 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -101,5 +101,6 @@ pub use geometry::{GeomContext, apply_geometry, apply_geometry_bez, apply_geomet #[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, + 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 fbf22ff6b..75272c3e1 100644 --- a/crates/effects/src/params.rs +++ b/crates/effects/src/params.rs @@ -147,6 +147,14 @@ 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)) +} + /// 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 a84d37bd0..af3d5d655 100644 --- a/crates/effects/src/reshape.rs +++ b/crates/effects/src/reshape.rs @@ -336,7 +336,7 @@ fn outline_art_with( _ if vectorcraft_doc::live::is_live(n) => evaluated_with(doc, n, outline, nodes), _ => return None, }; - outline_nested_text(&mut art, false); + outline_nested_text(doc, &mut art, false); art.opacity = 1.0; art.blend = Default::default(); art.isolate = false; @@ -443,21 +443,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); } } } @@ -606,20 +607,16 @@ 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); -} - -/// [`reshape_leaves`], with the swatches and patterns that painted live-effect plug-ins name looked -/// up in `doc` ([`reshape_leaves_apart`]). -pub(crate) fn reshape_leaves_in(art: &mut Node, fx: &[&Effect], doc: Option<&Document>) { - reshape_leaves_apart(art, fx, Budget::EFFECTS, false, doc); +/// 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`] whose paths keep to `budget` together, or to what they cost. -pub(crate) fn reshape_leaves_within(art: &mut Node, fx: &[&Effect], budget: Budget) { - reshape_leaves_apart(art, fx, budget, false, None); +pub(crate) fn reshape_leaves_within(art: &mut Node, fx: &[&Effect], budget: Budget, doc: Option<&Document>) { + 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 @@ -1247,7 +1244,7 @@ pub(crate) fn outlined_members(doc: Option<&Document>, n: &Node) -> Node { /// ([`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); + outline_nested_text(doc, &mut art, false); art } diff --git a/crates/effects/src/revolve.rs b/crates/effects/src/revolve.rs index b2528818b..d2ad1e66c 100644 --- a/crates/effects/src/revolve.rs +++ b/crates/effects/src/revolve.rs @@ -434,7 +434,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 a9997bb5d..e5f42b4e8 100644 --- a/crates/effects/src/solids/tests.rs +++ b/crates/effects/src/solids/tests.rs @@ -325,7 +325,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(); @@ -351,7 +351,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 ea3843f0e..f9ca9aa1f 100644 --- a/crates/effects/src/tests.rs +++ b/crates/effects/src/tests.rs @@ -394,7 +394,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() { @@ -473,7 +473,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)); @@ -500,7 +500,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"); @@ -508,7 +508,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"); @@ -516,7 +516,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