Expand Appearance: tinted symbols keep their colors under a color adjustment

Expand Appearance on a symbol instance with a fill tint and a color
adjustment applied the adjustment to the untinted art, so the expanded
colors differed from the canvas. The expand path now passes the tinted
instance art to adjust_in_document.
This commit is contained in:
dotCooCoo
2026-11-10 23:05:53 -08:00
parent 9f44e37274
commit de5323efb6
4 changed files with 37 additions and 5 deletions
+5 -3
View File
@@ -468,12 +468,14 @@ pub fn adjust(n: &Node, outer: Option<&ColorMap>, swatches: &SwatchKinds, images
}
/// [`adjust`] for export and Expand Appearance: recoloured embedded images become new images in
/// `d` (named by their content), and a symbol instance stands for its symbol's art.
pub fn adjust_in_document(d: &mut Document, n: &Node) -> Option<Node> {
/// `d` (named by their content), and a symbol instance stands for its symbol's art: `symbol_art`
/// when given, otherwise the symbol's art with the instance's own appearance changes.
pub fn adjust_in_document(d: &mut Document, n: &Node, symbol_art: Option<Node>) -> Option<Node> {
let art;
let src = match &n.kind {
NodeKind::SymbolInstance { symbol, .. } if has_adjustment(n) => {
let symbol = d.symbols.iter().find(|s| s.name == *symbol).map(|s| vectorcraft_doc::symbol_overrides::dynamic_art(&s.art, n));
let symbol = symbol_art
.or_else(|| d.symbols.iter().find(|s| s.name == *symbol).map(|s| vectorcraft_doc::symbol_overrides::dynamic_art(&s.art, n)));
art = crate::reshape::as_art(Some(d), n, symbol.as_ref())?;
&art
}
+1 -1
View File
@@ -112,7 +112,7 @@ fn bake_node(d: &mut Document, n: &Node) -> Option<Node> {
}
// Colour adjustments: the object recoloured (inside it too), then baked as it is.
if has_adjustment(n)
&& let Some(m) = adjust_in_document(d, n)
&& let Some(m) = adjust_in_document(d, n, None)
{
return Some(bake_node(d, &m).unwrap_or(m));
}
+8 -1
View File
@@ -814,8 +814,15 @@ fn expanded_node(d: &mut Document, mut n: Node, brushes: &[vectorcraft_brush::Br
let mut changed = false;
// Is `n` in `out` yet?
let mut listed_n = false;
// A symbol instance's fill tints its art before the adjustment, as the canvas draws it.
let symbol_art = match &n.kind {
NodeKind::SymbolInstance { symbol, .. } if effects::has_adjustment(&n) => {
d.symbols.iter().find(|s| s.name == *symbol).map(|s| vectorcraft_render::instance_art(&s.art, &n))
}
_ => None,
};
if effects::has_adjustment(&n)
&& let Some(m) = effects::adjust_in_document(d, &n)
&& let Some(m) = effects::adjust_in_document(d, &n, symbol_art)
{
out.push((id, 0));
listed_n = true;
+23
View File
@@ -75,6 +75,29 @@ fn symbol_instance_tint_survives_expanding_an_object_effect() {
assert!(differing(&before, &after, 8) < 200, "expanding changed {} pixels", differing(&before, &after, 8));
}
/// The canvas tints a symbol's art with the instance fill before the instance's color adjustment,
/// and expanding gives the same colors.
#[test]
fn symbol_instance_tint_comes_before_a_color_adjustment_when_expanded() {
let mut s = session();
square(&mut s);
let id = NodeId(run(&mut s, "symbol.new", json!({"name": "Tinted 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": 50}));
run(&mut s, "effect.apply", json!({"ids": [id.0], "item": null, "effect": "adjust.hueSaturation", "params": {"hue": 120}}));
let before = render(&s);
assert_ne!(before.pixel(100, 100), [255, 255, 255, 255], "the symbol is visible");
assert!(expandable(&s));
run(&mut s, "effect.expandAppearance", json!({}));
assert!(!matches!(node(&s, id).kind, NodeKind::SymbolInstance { .. }));
let after = render(&s);
let (b, a) = (before.pixel(100, 100), after.pixel(100, 100));
assert!(b.iter().zip(a.iter()).all(|(x, y)| x.abs_diff(*y) <= 8), "expanding changed the color from {b:?} to {a:?}");
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]