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();