Symbols: Replace Symbol clears the old symbol's dynamic overrides

An override on a dynamic symbol instance names an object of its symbol's
art by path. Replace Symbol kept every override whose path the new
symbol's art also had, so a change made to one object of the old symbol
was applied to an unrelated object of the new one. An instance given a
different symbol now drops its overrides; an instance replaced with its
own symbol keeps them. Undo brings them back.
This commit is contained in:
dotCooCoo
2026-11-01 16:24:33 -08:00
parent 639c9d6ce9
commit 2adf5a9a61
5 changed files with 34 additions and 6 deletions
+1 -1
View File
@@ -247,7 +247,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Replace Symbol",
[],
None,
"{name, ids?|id?} swap the symbol of the selected instances (they keep their appearance changes to the objects at paths a dynamic new symbol's art has) → {ids}",
"{name, ids?|id?} swap the symbol of the selected instances (an instance given a different symbol drops its appearance changes; one given its own symbol keeps them) → {ids}",
has_selection,
symbol_replace
),
+8 -3
View File
@@ -590,14 +590,19 @@ pub(super) fn symbol_replace(s: &mut Session, p: &Value) -> Result<Value> {
s.edit("Replace Symbol", |d, _| {
let new = natural_size(d, &name);
for id in &ids {
let old = match d.node(*id).map(|n| &n.kind) {
Some(NodeKind::SymbolInstance { symbol, .. }) => natural_size(d, symbol),
let (old, other) = match d.node(*id).map(|n| &n.kind) {
Some(NodeKind::SymbolInstance { symbol, .. }) => (natural_size(d, symbol), *symbol != name),
_ => continue,
};
if let Some(NodeKind::SymbolInstance { symbol, xf }) = d.node_mut(*id).map(|n| &mut n.kind) {
let Some(n) = d.node_mut(*id) else { continue };
if let NodeKind::SymbolInstance { symbol, xf } = &mut n.kind {
*symbol = name.clone();
*xf *= Affine::scale_non_uniform(new.0 / old.0, new.1 / old.1);
}
// An override's path names an object of the old symbol's art.
if other && !n.extras().symbol_overrides.is_empty() {
n.extras_mut().symbol_overrides.clear();
}
}
fit_overrides(d, &ids);
Ok(())
@@ -111,6 +111,28 @@ fn redefining_the_symbol_keeps_the_changes_whose_objects_remain() {
assert_eq!(overrides(&s, inst), 0);
}
#[test]
fn replace_symbol_drops_the_changes_made_for_the_old_symbol() {
let mut s = session();
// "Other": two blue squares in the same places, the instance taken off the canvas.
let (a, b) = (square(&mut s, 10.0), square(&mut s, 60.0));
let first = id(&s.execute("symbol.new", &json!({"name": "Other", "ids": [a.0, b.0], "type": "dynamic"})).unwrap());
s.execute("edit.clear", &json!({"ids": [first.0]})).unwrap();
let inst = pair(&mut s, "dynamic");
s.execute("symbol.instance.setAppearance", &json!({"id": inst.0, "path": [1], "fill": "#ff0000"})).unwrap();
// The same symbol: nothing changes.
s.execute("symbol.replace", &json!({"name": "Pair", "ids": [inst.0]})).unwrap();
assert_eq!(overrides(&s, inst), 1);
assert_eq!(pixel(&s, 80, 30), RED);
s.execute("symbol.replace", &json!({"name": "Other", "ids": [inst.0]})).unwrap();
assert_eq!(overrides(&s, inst), 0, "the change named an object of Pair's art");
assert_eq!(pixel(&s, 80, 30), BLUE, "Other's art draws as the symbol has it");
s.execute("edit.undo", &json!({})).unwrap();
assert!(matches!(&node(&s, inst).kind, NodeKind::SymbolInstance { symbol, .. } if symbol == "Pair"));
assert_eq!(overrides(&s, inst), 1);
assert_eq!(pixel(&s, 80, 30), RED);
}
#[test]
fn reset_and_static_drop_the_changes_and_undo_brings_them_back() {
let mut s = session();
+2 -1
View File
@@ -1586,7 +1586,8 @@ already walks Image Trace. Short agent entry points:
a path that names no object. `symbol.instance.reset {ids?}` drops the selected instances' changes (the Properties
panel's Reset). Redefining the symbol (`symbol.update`) reshapes every instance and keeps each change whose path the
new art still has; `symbol.breakLink` bakes the changes into the expanded art; `symbol.edit` edits the symbol's own
art, and each edit there keeps the changes whose path the art still has; making the symbol static drops them.
art, and each edit there keeps the changes whose path the art still has; making the symbol static drops them, and
so does `symbol.replace` with a different symbol (a replace with the instance's own symbol keeps them).
```json
{"name":"run_command","arguments":{"command":"symbol.instance.setAppearance","params":{"id":42,"path":[1],"fill":"#d0021b","opacity":80}}}
+1 -1
View File
@@ -1454,7 +1454,7 @@ A VectorCraft addition, not a feature of the reference app, so no area score mov
## Dynamic symbols (2026-11-01)
Symbols area (#4938), from the public symbol documentation ([create and place symbols](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/create-and-place-symbols.html), [edit symbols](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/edit-symbols.html)). Symbol Options has Symbol Type (Dynamic Symbol / Static Symbol; the dialog's default is Dynamic, `symbol.new`'s `type` defaults to static so scripts behave as before). An instance of a dynamic symbol keeps its own fill, stroke, opacity and blend for any object of the symbol's art, named by a child-index path (`symbol.instance.setAppearance`), and stays an instance: the canvas, SVG, PDF, EPS, legacy `.ai`, EMF/WMF and DXF writers, effects and Expand draw the art with those changes. Redefining the symbol reshapes every instance and keeps each change whose object the new art still has; Break Link bakes the changes into the art; Edit Symbol expands the symbol's own art; Reset (`symbol.instance.reset`: the Properties panel's Quick Actions and the Symbols panel menu) and making the symbol static drop them. The type is stored on the symbol and the changes on the instance in the native format, both left out when unset, so documents without them save byte for byte as before. Remaining: Direct Selection of an object inside a dynamic instance on the canvas (with the Fill/Stroke controls then setting its change), and reading and writing the type and changes in `.ai` files. Evidence: `crates/engine/src/tests/dynamic_symbols.rs`, `crates/doc/src/symbol_overrides.rs` tests. Score unchanged at 87% (estimated); hours 5–9 → 4–8.
Symbols area (#4938), from the public symbol documentation ([create and place symbols](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/create-and-place-symbols.html), [edit symbols](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/edit-symbols.html)). Symbol Options has Symbol Type (Dynamic Symbol / Static Symbol; the dialog's default is Dynamic, `symbol.new`'s `type` defaults to static so scripts behave as before). An instance of a dynamic symbol keeps its own fill, stroke, opacity and blend for any object of the symbol's art, named by a child-index path (`symbol.instance.setAppearance`), and stays an instance: the canvas, SVG, PDF, EPS, legacy `.ai`, EMF/WMF and DXF writers, effects and Expand draw the art with those changes. Redefining the symbol reshapes every instance and keeps each change whose object the new art still has; Break Link bakes the changes into the art; Edit Symbol expands the symbol's own art; Reset (`symbol.instance.reset`: the Properties panel's Quick Actions and the Symbols panel menu) and making the symbol static drop them, and Replace Symbol drops them when it gives the instance a different symbol. The type is stored on the symbol and the changes on the instance in the native format, both left out when unset, so documents without them save byte for byte as before. Remaining: Direct Selection of an object inside a dynamic instance on the canvas (with the Fill/Stroke controls then setting its change), and reading and writing the type and changes in `.ai` files. Evidence: `crates/engine/src/tests/dynamic_symbols.rs`, `crates/doc/src/symbol_overrides.rs` tests. Score unchanged at 87% (estimated); hours 5–9 → 4–8.
## Document Tools add-on: Export All Open Documents (2026-11-01)