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.
This commit is contained in:
dotCooCoo
2026-10-20 20:31:22 -07:00
parent a5f3298a0c
commit 8afb606a2e
2 changed files with 44 additions and 2 deletions
+21 -2
View File
@@ -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<Node> {
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<AppearanceItem>) -> Node {
let mut f = d.reid(l);
+23
View File
@@ -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();