Outline Stroke: random Transform effects keep the values the canvas drew

Random in a Transform effect takes its seed from the object's id. The
copies of a gradient path that its Transform effect makes were given new
ids before their strokes were outlined, so a random Transform on the
stroke moved each outline elsewhere than the canvas drew it. The copies
are now outlined first and given new ids after.

A fill with a random Transform effect stayed on a copy of the path with
a new id, so it drew elsewhere too. It now paints the outline its
geometry effects make of the path, on a copy of its own.

outlines_keep_the_random_transforms_the_canvas_drew checks Outline
Stroke, Expand and Flatten Transparency against the canvas for both
cases and for a random Transform that makes the copies.
This commit is contained in:
dotCooCoo
2026-10-21 00:16:52 -07:00
parent 8afb606a2e
commit 33917ff2fd
3 changed files with 60 additions and 9 deletions
+6 -3
View File
@@ -366,7 +366,7 @@ pub(crate) fn reshaped(l: &Node) -> Option<Node> {
/// Path `l` (outline `path`, box `bounds`) as its fill `item` paints it on the canvas, with its
/// outline and box: when the fill has visible geometry effects, `l` with the outline they make of
/// `path` as a plain path, and that outline's box.
fn fill_shape(l: &Node, item: &AppearanceItem, path: &PathData, bounds: Rect) -> Option<(Node, PathData, Rect)> {
pub(crate) fn fill_shape(l: &Node, item: &AppearanceItem, path: &PathData, bounds: Rect) -> Option<(Node, PathData, Rect)> {
use vectorcraft_render::effects::{GeomContext, apply_geometry_with, has_geometry};
if !has_geometry(item.effects()) {
return Some((l.clone(), path.clone(), bounds));
@@ -595,10 +595,13 @@ fn moves_only(a: Affine) -> bool {
(m0 - 1.0).abs() <= eps && m1.abs() <= eps && m2.abs() <= eps && (m3 - 1.0).abs() <= eps
}
/// Is `e` a visible Transform effect with Random on (which takes its seed from the object's id)?
pub(crate) fn seeded(e: &vectorcraft_doc::Effect) -> bool {
e.visible && e.id == "distort.transform" && e.params.get("random").and_then(Value::as_bool) == Some(true)
}
/// Does `n` have a visible Transform effect with Random on, its own or a fill's or stroke's?
fn seeded_here(n: &Node) -> bool {
let seeded =
|e: &vectorcraft_doc::Effect| e.visible && e.id == "distort.transform" && e.params.get("random").and_then(Value::as_bool) == Some(true);
n.appearance.effects.iter().any(seeded) || n.appearance.items.iter().any(|i| i.effects().iter().any(seeded))
}
+21 -6
View File
@@ -13,7 +13,7 @@ use vectorcraft_pathops as po;
use vectorcraft_pathops::{BoolOp, PathfinderOp};
use super::edit::selected_roots;
use super::expand::reshaped;
use super::expand::{fill_shape, reshaped, seeded};
use super::*;
pub fn specs() -> Vec<CommandSpec> {
@@ -644,8 +644,11 @@ 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. 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.
/// gets the stroke without them. A fill with a random Transform effect paints the outline its
/// geometry effects make of the path, on a copy of its own. 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 before the art gets new ids. Random in a Transform effect takes its seed
/// from the object's id, so these effects give the outline the values the canvas drew.
/// `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, has_gradient_copies, is_geometry, reshape};
@@ -653,9 +656,9 @@ pub(crate) fn outline_strokes(d: &mut Document, brushes: &[vectorcraft_brush::Br
return None;
}
if has_gradient_copies(l) {
let mut art = d.reid(&reshape(l, None)?);
let mut art = reshape(l, None)?;
outline_member_strokes(d, brushes, &mut art);
return Some(art);
return Some(d.reid(&art));
}
let reshaped = reshaped(l);
let l = reshaped.as_ref().unwrap_or(l);
@@ -665,7 +668,19 @@ pub(crate) fn outline_strokes(d: &mut Document, brushes: &[vectorcraft_brush::Br
let mut fills: Vec<AppearanceItem> = vec![];
for item in &l.appearance.items {
match item {
AppearanceItem::Fill(f) if f.visible && !f.paint.is_none() => fills.push(item.clone()),
AppearanceItem::Fill(f) if f.visible && !f.paint.is_none() => {
let shaped = f.effects.iter().any(seeded).then(|| path.bounds().and_then(|b| fill_shape(l, item, &path, b))).flatten();
let Some((shape, ..)) = shaped else {
fills.push(item.clone());
continue;
};
if !fills.is_empty() {
parts.push(styled_copy(d, l, std::mem::take(&mut fills)));
}
let mut plain = item.clone();
plain.effects_mut().retain(|e| !is_geometry(&e.id));
parts.push(styled_copy(d, &shape, vec![plain]));
}
AppearanceItem::Stroke(st) if outlinable(st) => {
if !fills.is_empty() {
parts.push(styled_copy(d, l, std::mem::take(&mut fills)));
+33
View File
@@ -303,6 +303,39 @@ fn each_transform_copy_keeps_its_own_gradient() {
assert_eq!(differ, none_differ);
}
/// Random in a Transform effect takes its seed from the object's id. The outline draws as the
/// canvas drew the object with Random in a Transform effect that copies a gradient square, in a
/// Transform effect on the stroke of such a square, and in a Transform effect on the fill of a
/// plain square.
#[test]
fn outlines_keep_the_random_transforms_the_canvas_drew() {
let random = json!({"moveH": 30, "moveV": 30, "rotate": 40, "random": true});
let cases = [
("random copies", json!({"gradient": {"kind": "linear"}}), json!({"moveH": 120, "rotate": 30, "copies": 1, "random": true}), None),
("copies, random stroke", json!({"gradient": {"kind": "linear"}}), json!({"moveH": 120, "copies": 1}), Some(1)),
("random fill", json!({"color": "#33aa55"}), json!({}), Some(0)),
];
let mut differ = vec![];
for (case, fill, object, item) in &cases {
for (command, p) in outlining() {
let mut s = session();
square_with(&mut s, fill.clone(), Value::Null, &[]);
if object != &json!({}) {
s.execute("effect.apply", &json!({"effect": "distort.transform", "params": object})).unwrap();
}
s.execute("stroke.set", &json!({"weight": 12})).unwrap();
if let Some(item) = item {
s.execute("effect.apply", &json!({"effect": "distort.transform", "params": random, "item": item})).unwrap();
}
let before = render(&s);
outlined_by(&mut s, command, &p);
differ.push((*case, command, differing(&before, &render(&s), 64)));
}
}
let none_differ: Vec<_> = cases.iter().flat_map(|(case, ..)| outlining().map(|(command, _)| (*case, command, 0))).collect();
assert_eq!(differ, none_differ);
}
#[test]
fn brushed_strokes_outline_to_their_brush_art() {
let mut s = session();