diff --git a/crates/engine/src/cmd/pathops.rs b/crates/engine/src/cmd/pathops.rs index d400ad217..795925259 100644 --- a/crates/engine/src/cmd/pathops.rs +++ b/crates/engine/src/cmd/pathops.rs @@ -644,12 +644,19 @@ pub(crate) fn outlined_stroke( /// the path, each stroke becomes its outline (or brush art), grouped when there are several. /// As on the canvas, the path's geometry effects ([`reshaped`]) shape the path first, and each /// stroke's own geometry effects then shape the path that stroke outlines; [`outlined_stroke`] -/// gets the stroke without them. `None` when `l` has no stroke to outline. +/// gets the stroke without them. A path whose Transform effect copies it with a gradient becomes +/// the art the canvas draws for it, each copy with its own gradient and its strokes outlined. +/// `None` when `l` has no stroke to outline. pub(crate) fn outline_strokes(d: &mut Document, brushes: &[vectorcraft_brush::Brush], l: &Node) -> Option { - use vectorcraft_render::effects::{GeomContext, apply_geometry_with, has_geometry, is_geometry}; + use vectorcraft_render::effects::{GeomContext, apply_geometry_with, has_geometry, has_gradient_copies, is_geometry, reshape}; if !l.appearance.items.iter().any(|i| matches!(i, AppearanceItem::Stroke(st) if outlinable(st))) { return None; } + if has_gradient_copies(l) { + let mut art = d.reid(&reshape(l, None)?); + outline_member_strokes(d, brushes, &mut art); + return Some(art); + } let reshaped = reshaped(l); let l = reshaped.as_ref().unwrap_or(l); let (path, rule) = node_path(l)?; @@ -680,6 +687,18 @@ pub(crate) fn outline_strokes(d: &mut Document, brushes: &[vectorcraft_brush::Br assemble(d, l, parts) } +/// Outline the strokes of every member of group `n`, however deep ([`outline_strokes`]). +fn outline_member_strokes(d: &mut Document, brushes: &[vectorcraft_brush::Brush], n: &mut Node) { + let Some(children) = n.children_mut() else { return }; + for c in children.iter_mut() { + if let Some(o) = outline_strokes(d, brushes, c) { + *c = Arc::new(o); + } else if c.children().is_some() { + outline_member_strokes(d, brushes, Arc::make_mut(c)); + } + } +} + /// A plain copy of path `l` (new ids, no transparency, name or mask) painted with `items`. pub(crate) fn styled_copy(d: &mut Document, l: &Node, items: Vec) -> Node { let mut f = d.reid(l); diff --git a/crates/engine/src/tests_outlinestroke.rs b/crates/engine/src/tests_outlinestroke.rs index 62d315850..c6f6a11f9 100644 --- a/crates/engine/src/tests_outlinestroke.rs +++ b/crates/engine/src/tests_outlinestroke.rs @@ -280,6 +280,29 @@ fn a_geometry_effect_on_the_object_shapes_the_path_it_outlines() { assert_eq!(outlined_as_drawn(json!({"color": "#33aa55"}), Value::Null), as_drawn()); } +/// The canvas paints a gradient fill or stroke on each copy that the object's Transform effect +/// makes by itself; the outlined result draws the same, with the gradient fitted to each copy. +#[test] +fn each_transform_copy_keeps_its_own_gradient() { + let mut differ = vec![]; + for on_stroke in [false, true] { + for (command, p) in outlining() { + let mut s = session(); + let fill = if on_stroke { json!({"none": true}) } else { json!({"gradient": {"kind": "linear"}}) }; + square_with(&mut s, fill, Value::Null, &[("distort.transform", json!({"moveH": 120, "copies": 1}))]); + if on_stroke { + s.execute("paint.setStroke", &json!({"gradient": {"kind": "linear"}})).unwrap(); + s.execute("stroke.set", &json!({"weight": 12})).unwrap(); + } + let before = render(&s); + outlined_by(&mut s, command, &p); + differ.push((on_stroke, command, differing(&before, &render(&s), 64))); + } + } + let none_differ: Vec<_> = [false, true].into_iter().flat_map(|on_stroke| outlining().map(|(command, _)| (on_stroke, command, 0))).collect(); + assert_eq!(differ, none_differ); +} + #[test] fn brushed_strokes_outline_to_their_brush_art() { let mut s = session();