diff --git a/crates/engine/src/cmd/brushsym.rs b/crates/engine/src/cmd/brushsym.rs index 7386f7718..6f7661afc 100644 --- a/crates/engine/src/cmd/brushsym.rs +++ b/crates/engine/src/cmd/brushsym.rs @@ -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, diff --git a/crates/engine/src/cmd/brushsym/symbols.rs b/crates/engine/src/cmd/brushsym/symbols.rs index 9bfbb902a..8eb21dd64 100644 --- a/crates/engine/src/cmd/brushsym/symbols.rs +++ b/crates/engine/src/cmd/brushsym/symbols.rs @@ -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 { +fn instances_of(d: &Document, name: Option<&str>) -> Vec { let mut v = vec![]; d.walk(|n| { if let NodeKind::SymbolInstance { symbol, .. } = &n.kind diff --git a/crates/engine/src/cmd/symboledit.rs b/crates/engine/src/cmd/symboledit.rs index 4cb58788b..0c365919b 100644 --- a/crates/engine/src/cmd/symboledit.rs +++ b/crates/engine/src/cmd/symboledit.rs @@ -419,17 +419,12 @@ fn each_changed_instance(n: &mut Node, name: &str, f: &mut dyn FnMut(&mut Vec], bool), new: (&[Arc], 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| { follow_paths(o, old, new); @@ -439,7 +434,7 @@ fn follow_objects(d: &mut Document, name: &str, old: (&[Arc], 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; diff --git a/crates/engine/src/tests/symboledit.rs b/crates/engine/src/tests/symboledit.rs index 9a3ce144c..8cd11aaa2 100644 --- a/crates/engine/src/tests/symboledit.rs +++ b/crates/engine/src/tests/symboledit.rs @@ -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, 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();