diff --git a/crates/engine/src/cmd/textedit.rs b/crates/engine/src/cmd/textedit.rs index 329d471e9..095cad361 100644 --- a/crates/engine/src/cmd/textedit.rs +++ b/crates/engine/src/cmd/textedit.rs @@ -1280,19 +1280,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 e04c2c728..359373911 100644 --- a/crates/engine/src/tests/textedit.rs +++ b/crates/engine/src/tests/textedit.rs @@ -3467,6 +3467,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. diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 4227b8088..8aeae51f1 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1898,7 +1898,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, `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)