From 2f72a998b97ae485f924fd263433a93bf7afa884 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 8 Nov 2026 11:05:27 -0800 Subject: [PATCH] Plug-ins: a painted fill or stroke effect keeps a placed gradient on its moved shape A Transform or Free Distort before a painted effect on a fill or stroke moved the shape but left the placed gradient at its old position, both on pieces that keep the fill or stroke and on the geometry drawn after a refused result. The inherited fill or stroke now takes the same gradient move as the Transform branch. --- crates/effects/src/reshape.rs | 5 +++++ crates/engine/src/tests/plugins.rs | 35 ++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/crates/effects/src/reshape.rs b/crates/effects/src/reshape.rs index 09f92581c..58929126a 100644 --- a/crates/effects/src/reshape.rs +++ b/crates/effects/src/reshape.rs @@ -1082,6 +1082,11 @@ fn transform_item_leaf(leaf: &mut Node, max: Budget, used: &mut Budget, doc: Opt let mut it = item.clone(); let rest: Vec = before.iter().filter(|e| !is_geometry(&e.id)).chain(&fx[at + 1..]).cloned().collect(); *it.effects_mut() = rest; + let mut one = Appearance { items: vec![it.clone()], ..Default::default() }; + if carry.moves(&one) { + carry.apply(&mut one); + } + let it = one.items.pop().unwrap_or(it); let paints = vectorcraft_plugins::effect::Paints { appearance: Some(&original.appearance), item: Some(i) }; let painted = vectorcraft_plugins::effect::apply_painted(plugin, ¶ms, &g, tb, rule, paints, doc) .map(|pieces| pieces.iter().map(|p| (p.path.clone(), p.rule, vectorcraft_plugins::painted::items(&it, p))).collect::>()) diff --git a/crates/engine/src/tests/plugins.rs b/crates/engine/src/tests/plugins.rs index 79e8eba32..23491c6d2 100644 --- a/crates/engine/src/tests/plugins.rs +++ b/crates/engine/src/tests/plugins.rs @@ -574,6 +574,41 @@ fn a_second_painted_effect_on_a_fill_keeps_the_piece_s_other_fills() { assert!(paints.contains(&vec![fill("#0000ff"), fill("#00ff00")]), "{paints:?}"); } +/// A Transform effect before a painted effect on a fill moves the fill's placed gradient: a piece +/// that keeps the fill, and the fill drawn on the moved shape after a refused result, both have the +/// gradient at its moved position. +#[test] +fn a_painted_effect_on_a_fill_keeps_a_placed_gradient_with_its_moved_shape() { + let mut gone = solid("#00ff00"); + gone["swatch"] = json!("Not a swatch"); + for (name, out, want) in [ + ("keeps", json!({"objects": [{"path": square_at(40.0)}]}).to_string(), square_at(40.0).bounds()), + ("refused", json!({"objects": [{"path": square_at(40.0), "fills": [gone]}]}).to_string(), square_at(110.0).bounds()), + ] { + let mut s = session(); + let id = NodeId(run(&mut s, "shape.rectangle", json!({"x": 10, "y": 40, "width": 20, "height": 20}))["id"].as_u64().unwrap()); + run(&mut s, "paint.setStroke", json!({"ids": [id.0], "none": true})); + let stops = json!([{"offset": 0, "color": "#ffffff"}, {"offset": 1, "color": "#000000"}]); + run(&mut s, "paint.setFill", json!({"ids": [id.0], "gradient": {"kind": "linear", "stops": stops}})); + run(&mut s, "select.set", json!({"ids": [id.0]})); + run(&mut s, "paint.setGradientGeom", json!({"start": [14, 50], "end": [26, 50], "stroke": false})); + let plugin = format!("test.engine.carry{name}"); + install_wat(&mut s, &tpl::returning(&painting(&plugin, true), &out)); + let fill = node(&s, id).appearance.items.iter().position(|i| i.is_fill()).unwrap(); + run(&mut s, "effect.apply", json!({"ids": [id.0], "effect": "distort.transform", "params": {"moveH": 100}, "item": fill})); + run(&mut s, "effect.apply", json!({"ids": [id.0], "effect": format!("plugin.{plugin}"), "item": fill})); + let st = s.doc().unwrap(); + let art = vectorcraft_render::effects::reshape(Some(&st.doc), st.doc.node(id).unwrap(), None).unwrap(); + let mut seen = vec![]; + art.walk(&mut |n| { + if let (Some(p), Some(Paint::Gradient(g))) = (n.path_data(), n.appearance.items.iter().find(|i| i.is_fill()).map(|i| i.paint())) { + seen.push((p.bounds(), g.geom.map(|g| (g.start.x, g.end.x)))); + } + }); + assert_eq!(seen, [(want, Some((114.0, 126.0)))], "{name}"); + } +} + /// The geometry of a painted result that names a swatch counts only when the document has that /// swatch: without a document, and with one that lacks it, the effect keeps its input. #[test]