From ee9cce43dc5236bbbd673bebe233f03ef04ad77c Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 4 Nov 2026 16:29:03 -0800 Subject: [PATCH] Symbols: an instance fill with no opacity keeps the art's swatch links A symbol instance whose fill has an opacity of 0 no longer counts as stained. Expand Appearance and the other places that build an instance's art used to rebuild every solid paint as an unlinked color even though the tint changed nothing, so the art lost its global and spot swatch links. --- crates/brush/src/colorize.rs | 5 +++-- crates/brush/src/tests.rs | 20 ++++++++++++++++++++ crates/engine/src/tests/expand.rs | 28 ++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/crates/brush/src/colorize.rs b/crates/brush/src/colorize.rs index 179c0de51..a62dce9ec 100644 --- a/crates/brush/src/colorize.rs +++ b/crates/brush/src/colorize.rs @@ -132,9 +132,10 @@ pub fn tint_node(n: &mut Node, color: &Color, amount: f32) { } /// The stain instance `inst` puts on its symbol's art: its visible fill colour and that fill's -/// opacity (how far the art mixes towards it). `None` for an unstained instance. +/// opacity (how far the art mixes towards it). `None` for an unstained instance, including one +/// whose fill has an opacity of 0, so its art keeps its paints and their swatch links. pub fn stain(inst: &Node) -> Option<(Color, f32)> { - let f = inst.appearance.fill().filter(|f| f.visible)?; + let f = inst.appearance.fill().filter(|f| f.visible && f.opacity > 0.0)?; Some((f.paint.color()?, f.opacity)) } diff --git a/crates/brush/src/tests.rs b/crates/brush/src/tests.rs index 7871b61af..22aad6603 100644 --- a/crates/brush/src/tests.rs +++ b/crates/brush/src/tests.rs @@ -764,6 +764,26 @@ fn stain_mixes_type_strokes_too() { assert_eq!(run_paints(&[art]), [(Paint::solid(blue), Paint::solid(blue))]); } +/// An instance whose fill has an opacity of 0 does not stain its symbol's art, so the art keeps its +/// swatch links. +#[test] +fn a_fill_with_no_opacity_leaves_the_symbol_art_linked() { + let spot = Paint::Solid { color: Color::cmyk(1.0, 0.7, 0.0, 0.4), swatch: Some("Spot".into()), tint: 1.0 }; + let art = Node::path(NodeId(1), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), Appearance::basic(spot.clone(), Paint::None, 0.0)); + let mut inst = Node::new(NodeId(2), NodeKind::SymbolInstance { symbol: "Dot".into(), xf: Affine::IDENTITY }); + inst.appearance = Appearance::basic(Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0).into(); + if let Some(f) = inst.appearance.fill_mut() { + f.opacity = 0.0; + } + assert_eq!(stain(&inst), None); + assert_eq!(instance_art(&art, &inst).appearance.fill_paint(), spot); + // Any opacity above 0 still stains. + if let Some(f) = inst.appearance.fill_mut() { + f.opacity = 0.01; + } + assert_ne!(instance_art(&art, &inst).appearance.fill_paint(), spot); +} + #[test] fn library_defaults_store_and_find() { let mut d = Document::new(100.0, 100.0); diff --git a/crates/engine/src/tests/expand.rs b/crates/engine/src/tests/expand.rs index c84fb9588..090cb9828 100644 --- a/crates/engine/src/tests/expand.rs +++ b/crates/engine/src/tests/expand.rs @@ -75,6 +75,34 @@ fn symbol_instance_tint_survives_expanding_an_object_effect() { assert!(differing(&before, &after, 8) < 200, "expanding changed {} pixels", differing(&before, &after, 8)); } +/// An instance fill with an opacity of 0 does not tint the symbol's art, so expanding keeps the +/// art's global swatch link. +#[test] +fn a_symbol_instance_fill_with_no_opacity_keeps_swatch_links_when_expanded() { + let mut s = session(); + let sq = square(&mut s); + run(&mut s, "swatch.new", json!({"name": "Brand", "color": "#2a6fb0", "global": true})); + run(&mut s, "paint.setFill", json!({"ids": [sq.0], "swatch": "Brand"})); + let id = NodeId(run(&mut s, "symbol.new", json!({"name": "Linked Square"}))["id"].as_u64().unwrap()); + run(&mut s, "appearance.addFill", json!({"ids": [id.0]})); + let fill = node(&s, id).appearance.items.len() - 1; + run(&mut s, "appearance.setItem", json!({"ids": [id.0], "index": fill, "color": "#00ff00", "opacity": 0})); + run(&mut s, "effect.apply", json!({"ids": [id.0], "item": null, "effect": "path.offsetPath", "params": {"offset": 6}})); + assert!(expandable(&s)); + + run(&mut s, "effect.expandAppearance", json!({})); + let n = node(&s, id); + assert!(!matches!(n.kind, NodeKind::SymbolInstance { .. })); + let mut linked = 0; + n.walk(&mut |part| { + if let vectorcraft_color::Paint::Solid { swatch: Some(name), .. } = part.appearance.fill_paint() { + assert_eq!(name, "Brand"); + linked += 1; + } + }); + assert!(linked > 0, "the expanded art keeps the Brand swatch link"); +} + #[test] fn two_fills_and_an_offset_stroke_become_three_paths_that_look_the_same() { let mut s = session();