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
+1 -1
View File
@@ -35,7 +35,7 @@ use brushes::{
};
use dynamic::{symbol_instance_reset, symbol_instance_set_appearance};
use symbols::selected_instances;
pub(crate) use symbols::{fit_instances_of, instance_in_place, instances_of, natural_size, normalize, place_xf, set_natural_size, store_symbol};
pub(crate) use symbols::{instance_in_place, natural_size, normalize, place_xf, set_natural_size, store_symbol};
use symbols::{
symbol_adjust, symbol_break_link, symbol_delete, symbol_duplicate, symbol_list, symbol_new, symbol_options, symbol_place, symbol_replace,
symbol_select_instances, symbol_set_current, symbol_spray, symbol_unused, symbol_update,
+1 -8
View File
@@ -95,14 +95,7 @@ pub(crate) fn normalize(mut art: Node) -> Result<(Node, (f64, f64), Point)> {
Ok((art, (w, h), c))
}
/// After the art of symbol `name` changed: each of its instances keeps its appearance changes to
/// the objects the art still has.
pub(crate) fn fit_instances_of(d: &mut Document, name: &str) {
let ids = instances_of(d, Some(name));
fit_overrides(d, &ids);
}
pub(crate) fn instances_of(d: &Document, name: Option<&str>) -> Vec<NodeId> {
fn instances_of(d: &Document, name: Option<&str>) -> Vec<NodeId> {
let mut v = vec![];
d.walk(|n| {
if let NodeKind::SymbolInstance { symbol, .. } = &n.kind
+5 -11
View File
@@ -419,17 +419,12 @@ fn each_changed_instance(n: &mut Node, name: &str, f: &mut dyn FnMut(&mut Vec<Sy
}
/// Each instance's appearance changes to objects of symbol `name`'s art follow those objects
/// through an edit of the editing layer (see [`follow_paths`]). The instances in the art of the
/// through an edit of the editing layer (see [`follow_paths`]), and keep only the changes to
/// objects the art still has. That covers the instances on the layers and in the art of the
/// document's other symbols, graph designs, pattern tiles (the tiles pattern editing's Cancel puts
/// back included) and brushes follow too, and keep only the changes to objects the art still has.
/// back included) and brushes, opacity-mask art included. The editing layer holds no instance of
/// the symbol, since [`sync`] refuses one.
fn follow_objects(d: &mut Document, name: &str, old: (&[Arc<Node>], bool), new: (&[Arc<Node>], bool)) {
for id in super::brushsym::instances_of(d, Some(name)) {
let Some(n) = d.node_mut(id) else { continue };
if n.extras().symbol_overrides.is_empty() {
continue;
}
follow_paths(&mut n.extras_mut().symbol_overrides, old, new);
}
let art = d.symbols.iter().find(|s| s.name == name).filter(|s| s.dynamic).map(|s| s.art.clone());
let mut follow = |o: &mut Vec<SymbolOverride>| {
follow_paths(o, old, new);
@@ -439,7 +434,7 @@ fn follow_objects(d: &mut Document, name: &str, old: (&[Arc<Node>], bool), new:
}
};
let symbols = d.symbols.iter_mut().filter(|s| s.name != name).map(|s| &mut s.art);
let trees = symbols.chain(d.graph_designs.iter_mut().map(|g| &mut g.art));
let trees = d.layers.iter_mut().chain(symbols).chain(d.graph_designs.iter_mut().map(|g| &mut g.art));
let patterns = d.patterns.iter_mut().chain(d.pattern_edit.iter_mut().filter_map(|pe| pe.original.as_mut()));
for root in trees.chain(patterns.flat_map(|p| &mut p.art)) {
if holds_changed_instance(root, name) {
@@ -522,7 +517,6 @@ pub(crate) fn sync(before: &Document, d: &mut Document) -> Result<()> {
};
d.symbols[index].art = Arc::new(new_art);
follow_objects(d, &se.symbol, (&old, old_group), (&art, group));
super::brushsym::fit_instances_of(d, &se.symbol);
if let Some(e) = d.symbol_edit.as_mut() {
e.changed = true;
e.group = group;
+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();