From 8afb606a2eb8f992cfe1488d115910f4eeffd5a5 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Tue, 20 Oct 2026 20:31:22 -0700 Subject: [PATCH] Outline Stroke: each Transform copy of a gradient path keeps its own gradient A path whose Transform effect makes copies and that has a gradient fill or stroke is drawn on the canvas as separate copies, each painting its gradient by itself. Outline Stroke merged the copies into one path, so the gradient stretched across all of them. It now outlines the strokes of the art the canvas draws, as Expand Appearance does. Expand's Stroke option and Flatten Transparency's Convert All Strokes to Outlines change the same way. --- crates/engine/src/cmd/pathops.rs | 23 +++++++++++++++++++++-- crates/engine/src/tests_outlinestroke.rs | 23 +++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) 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();