From ba5eab7349e02277096b0b5e3fc70d3485e260a1 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 11 Nov 2026 00:36:07 -0800 Subject: [PATCH 1/3] Docs: Edit Symbol keeps dynamic symbol instances linked The Dynamic symbols entry still said Edit Symbol expands the symbol's own art. Since #5064, Edit Symbol opens symbol editing mode: every instance stays linked and a dynamic symbol's instances keep their changes for the objects the edited art still has. --- docs/target-app-parity.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index a8bc235f2..2103ed624 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1896,7 +1896,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 opacity and blend for any object of the symbol's art, and its own fill and stroke for an object that draws them, 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: on-canvas editing beyond a solid fill and stroke on a path, compound path or type object inside a dynamic instance; 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 opacity and blend for any object of the symbol's art, and its own fill and stroke for an object that draws them, 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 keeps the instances linked and keeps each change whose object the edited art still has; 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: on-canvas editing beyond a solid fill and stroke on a path, compound path or type object inside a dynamic instance; 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) From 68042f0924174ab543f4bf4729a2ba29477bbd63 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 11 Nov 2026 00:57:15 -0800 Subject: [PATCH 2/3] Docs: the Dynamic symbols evidence lists the Edit Symbol tests --- docs/target-app-parity.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 2103ed624..8d2d365fb 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1896,7 +1896,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 opacity and blend for any object of the symbol's art, and its own fill and stroke for an object that draws them, 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 keeps the instances linked and keeps each change whose object the edited art still has; 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: on-canvas editing beyond a solid fill and stroke on a path, compound path or type object inside a dynamic instance; 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 opacity and blend for any object of the symbol's art, and its own fill and stroke for an object that draws them, 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 keeps the instances linked and keeps each change whose object the edited art still has; 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: on-canvas editing beyond a solid fill and stroke on a path, compound path or type object inside a dynamic instance; 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, `crates/engine/src/tests/symboledit.rs`. Score unchanged at 87% (estimated); hours 5–9 → 4–8. ## Document Tools add-on: Export All Open Documents (2026-11-01) From 1851f8769a34fd305bad2c039378e9cd03debf15 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 11 Nov 2026 01:05:16 -0800 Subject: [PATCH 3/3] Glyphs: text.setGlyphAlternate with gid null reports the glyph drawn With no glyph choice left on the character, text.setGlyphAlternate returned the font's default glyph for it: 0 for a character the font lacks, which a fallback font draws, and the lowercase glyph for a letter under All Caps. It now reports the glyph the character is drawn with, as text.glyphAlternates does. --- crates/engine/src/cmd/textedit.rs | 20 ++++++++----- crates/engine/src/tests/textedit.rs | 44 +++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/crates/engine/src/cmd/textedit.rs b/crates/engine/src/cmd/textedit.rs index 689a73ea0..86cc05081 100644 --- a/crates/engine/src/cmd/textedit.rs +++ b/crates/engine/src/cmd/textedit.rs @@ -1276,19 +1276,25 @@ fn set_glyph_alternate(s: &mut Session, p: &Value) -> Result { // out (overflowing text) is shaped alone in its writing direction, with the rest of its grapheme // in the grapheme's own styles. let plain = t.plain_text(); - let unchosen = holder.as_ref().filter(|_| !styled).and_then(|_| { - let mut probe = t.clone(); - edit::style_range(&mut probe.runs, bytes.start, bytes.end, |st| st.glyph_alternate = None); - let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &probe); - own_glyph(&plain, &lay, ch, &bytes, &face).unwrap_or_else(|| shaped_alone(&probe, ch, &bytes, &face).0) - }); + let mut probe = t.clone(); + edit::style_range(&mut probe.runs, bytes.start, bytes.end, |st| st.glyph_alternate = None); + let probe_lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &probe); + let unchosen = holder + .as_ref() + .filter(|_| !styled) + .and_then(|_| own_glyph(&plain, &probe_lay, ch, &bytes, &face).unwrap_or_else(|| shaped_alone(&probe, ch, &bytes, &face).0)); let substituted = unchosen.is_some_and(|g| g != default); let choice = match gid.filter(|g| *g != default || substituted) { None => None, Some(g) if g == default || face.alternates(ch).iter().any(|a| a.gid == g) => Some(face.choice(ch, g)), Some(g) => return Err(bad(C, format!("glyph {g} is not an alternate of {ch} in {} {}", face.family, face.style))), }; - let drawn = choice.as_ref().map_or(unchosen.unwrap_or(default), |c| c.gid); + // Without a choice, the glyph drawn in whatever face draws the character, as + // `text.glyphAlternates` reports it (a fallback face's glyph, the capital of All Caps). + let drawn = match &choice { + Some(c) => c.gid, + None => drawn_glyph(&plain, &probe_lay, ch, &bytes, &face).or_else(|| shaped_alone(&probe, ch, &bytes, &face).1).unwrap_or(default), + }; // The run holding the character. The request changes nothing when the choice is the one the // character's run holds and that run holds the character alone; otherwise the edit splits the // run, so that the following text does not take the character into a ligature. diff --git a/crates/engine/src/tests/textedit.rs b/crates/engine/src/tests/textedit.rs index e584e3e32..69f7bc644 100644 --- a/crates/engine/src/tests/textedit.rs +++ b/crates/engine/src/tests/textedit.rs @@ -3436,6 +3436,50 @@ fn glyph_alternates_query_reports_the_fallback_glyph_that_is_drawn() { assert_eq!(query["chosen"], json!(glyph.gid), "an overflowing character reports the fallback glyph it is drawn with"); } +/// `text.setGlyphAlternate` with `gid: null` reports the glyph the character is drawn with, as +/// `text.glyphAlternates` does: the fallback face's glyph for a character the font lacks, laid out +/// or overflowing, and the capital that All Caps draws for an a. +#[test] +fn a_glyph_choice_taken_away_reports_the_glyph_drawn() { + use vectorcraft_text::test_fonts::{alternates_family, alternates_font}; + let _fonts = crate::adding_fonts(); + let db = vectorcraft_text::FontDb::global(); + db.add_font(alternates_font(true).expect("the test font builds")); + let face = db.face(alternates_family(true), "Regular").expect("the test face"); + let mut s = session(); + let reset = |s: &mut Session, id: NodeId, start: usize| { + s.execute("text.setGlyphAlternate", &json!({"id": id.0, "start": start, "gid": null})).unwrap()["gid"].clone() + }; + let chosen = |s: &mut Session, id: NodeId, start: usize| { + s.execute("text.glyphAlternates", &json!({"id": id.0, "start": start})).unwrap()["chosen"].clone() + }; + + let ch = 'Ƃ'; + assert_eq!(face.glyph_for(ch), 0); + let point = json!({"x": 0, "y": 0, "text": ch.to_string(), "font": alternates_family(true)}); + let id = NodeId(s.execute("text.create", &point).unwrap()["id"].as_u64().unwrap()); + let drawn = vectorcraft_text::layout(db, &obj(&s, id)).glyphs[0].gid; + assert_ne!(drawn, 0); + assert_eq!(reset(&mut s, id, 0), json!(drawn), "the fallback glyph"); + assert_eq!(chosen(&mut s, id, 0), json!(drawn)); + + let area = json!({"x": 0, "y": 300, "text": ch.to_string(), "font": alternates_family(true), "area": {"width": 200, "height": 2}}); + let boxed = NodeId(s.execute("text.create", &area).unwrap()["id"].as_u64().unwrap()); + assert!(vectorcraft_text::layout(db, &obj(&s, boxed)).glyphs.is_empty(), "the text overflows"); + assert_eq!(reset(&mut s, boxed, 0), json!(drawn), "the overflowing character's fallback glyph"); + + let caps = NodeId( + s.execute("text.create", &json!({"x": 100, "y": 100, "text": "xaay", "size": 20, "font": alternates_family(true)})).unwrap()["id"] + .as_u64() + .unwrap(), + ); + s.execute("text.setRangeStyle", &json!({"id": caps.0, "start": 0, "end": 4, "allCaps": true})).unwrap(); + let drawn = vectorcraft_text::layout(db, &obj(&s, caps)).glyphs.iter().find(|g| g.byte == 1).expect("the a is laid out").gid; + assert_ne!(drawn, face.glyph_for('a'), "All Caps draws the a as A"); + assert_eq!(reset(&mut s, caps, 1), json!(drawn), "the capital drawn"); + assert_eq!(chosen(&mut s, caps, 1), json!(drawn)); +} + /// A fallback face draws Ka with the vowel sign I in one cluster, the vowel sign first. The glyph /// reported for Ka is the fallback face's glyph for Ka, laid out or overflowing, not the vowel /// sign's.