Symbols: an instance in an opacity mask follows an edit of its dynamic symbol

The appearance changes of the instances on the layers now follow the edited objects through the same pass as the instances in other symbols, graph designs, pattern tiles and brushes. That pass also reaches instances used as opacity-mask art, which keep their changes on the same objects when an edit reorders them and drop the change to an object the edit deletes.
This commit is contained in:
dotCooCoo
2026-11-02 12:04:20 +07:00
committed by bflatastic
parent 5c678ed52c
commit 294d13d857
4 changed files with 52 additions and 20 deletions
+45
View File
@@ -581,6 +581,51 @@ fn instances_in_pattern_tiles_graph_designs_and_brushes_follow_an_edit_of_their_
assert!(stored.as_array().is_some_and(|a| a.iter().any(|e| e["name"] == "Pair Band" && e["note"] == "kept")), "{stored}");
}
/// An instance of a dynamic symbol that is the opacity mask of an object on a layer keeps its
/// appearance changes on the same objects when the edit reorders them, and drops the change to an
/// object the edit deleted.
#[test]
fn an_instance_in_an_opacity_mask_follows_an_edit_of_its_dynamic_symbol() {
let mut s = Session::new();
run(&mut s, "file.new", json!({"width": 400, "height": 300}));
let a = id(&run(&mut s, "shape.rectangle", json!({"x": 10, "y": 10, "width": 40, "height": 40})));
let b = id(&run(&mut s, "shape.rectangle", json!({"x": 60, "y": 10, "width": 40, "height": 40})));
let inst = id(&run(&mut s, "symbol.new", json!({"name": "Pair", "ids": [a.0, b.0], "type": "dynamic"})));
run(&mut s, "symbol.instance.setAppearance", json!({"id": inst.0, "path": [0], "fill": "#00ff00"}));
run(&mut s, "symbol.instance.setAppearance", json!({"id": inst.0, "path": [1], "fill": "#ff0000"}));
let masked = id(&run(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 120, "height": 60})));
run(&mut s, "select.set", json!({"ids": [inst.0]}));
run(&mut s, "object.arrange.bringToFront", json!({}));
run(&mut s, "transparency.makeOpacityMask", json!({"ids": [masked.0, inst.0]}));
// The changes of the instance of "Pair" in the masked object's mask, by path and color.
let held = |s: &Session| -> Vec<(Vec<u32>, String)> {
let d = &s.doc().unwrap().doc;
let mask = d.node(masked).and_then(|n| n.extras().mask.clone()).expect("the opacity mask");
let mut out = vec![];
vectorcraft_doc::swatches::walk_with_masks(&mask.art, &mut |n| {
if matches!(&n.kind, NodeKind::SymbolInstance { symbol, .. } if symbol == "Pair") {
out.extend(n.extras().symbol_overrides.iter().map(|o| (o.path.clone(), o.fill.as_ref().and_then(|p| p.color()).unwrap().to_hex())));
}
});
out
};
assert_eq!(held(&s), vec![(vec![0], "#00ff00".to_string()), (vec![1], "#ff0000".to_string())]);
let other = id(&run(&mut s, "symbol.place", json!({"name": "Pair", "x": 200, "y": 200})));
run(&mut s, "symbol.edit", json!({"id": other.0}));
let art = layer_art(&s);
assert_eq!(art.len(), 2);
// Sending the second object to the back: each change follows its object.
run(&mut s, "select.set", json!({"ids": [art[1].0]}));
run(&mut s, "object.arrange.sendToBack", json!({}));
assert_eq!(held(&s), vec![(vec![1], "#00ff00".to_string()), (vec![0], "#ff0000".to_string())]);
// Deleting the first object: the change to it goes.
run(&mut s, "select.set", json!({"ids": [art[0].0]}));
run(&mut s, "edit.clear", json!({}));
assert_eq!(held(&s), vec![(vec![0], "#ff0000".to_string())]);
run(&mut s, "symbol.exitEditing", json!({}));
assert_eq!(held(&s), vec![(vec![0], "#ff0000".to_string())]);
}
#[test]
fn undo_back_into_an_earlier_edit_leaves_it_to_its_own_isolation() {
let (mut s, first, second) = two_instances();