diff --git a/crates/format/tests/it/native_v3.rs b/crates/format/tests/it/native_v3.rs index 4dbd33c196..afc3bea9d2 100644 --- a/crates/format/tests/it/native_v3.rs +++ b/crates/format/tests/it/native_v3.rs @@ -220,7 +220,7 @@ fn all_four_composers_round_trip() { use vectorcraft_doc::{TextKind, TextObject}; let one = |c: Composer| { let mut d = Document::new(200.0, 200.0); - let mut t = TextObject::point(kurbo::Point::ZERO, "ક\u{ACD}ક", Default::default()); + let mut t = TextObject::point(vectorcraft_geom::Point::ZERO, "ક\u{ACD}ક", Default::default()); t.para.composer = c; t.kind = TextKind::Point; let id = d.alloc_id(); diff --git a/crates/mcp/src/ui_catalog.rs b/crates/mcp/src/ui_catalog.rs index deb43ae968..28c1fc36fd 100644 --- a/crates/mcp/src/ui_catalog.rs +++ b/crates/mcp/src/ui_catalog.rs @@ -392,7 +392,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "type.lineComposer", "Toggle Line Composer", "Cmd+Alt+Shift+C", - "{} the selected type's paragraphs (or those the Type tool's selection touches) use the Single-line Composer when they all use the Every-line Composer, else the Every-line Composer, each keeping its shaping: Middle Eastern & South Asian or Standard (text.setFormat {composer}) → \"singleLine\" | \"everyLine\" | \"standardSingleLine\" | \"standardEveryLine\"", + "{} the selected type's paragraphs (or those the Type tool's selection touches) use the Single-line Composer when they all use the Every-line Composer, else the Every-line Composer; paragraphs that all use a Standard composer keep it, any other mix takes the Middle Eastern & South Asian one (text.setFormat {composer}) → \"singleLine\" | \"everyLine\" | \"standardSingleLine\" | \"standardEveryLine\"", ), ( "type.hyphenate", diff --git a/crates/text/src/features.rs b/crates/text/src/features.rs index e25d225dc1..99c27b68a8 100644 --- a/crates/text/src/features.rs +++ b/crates/text/src/features.rs @@ -164,6 +164,7 @@ impl OtFeatures { extra: u32::MAX >> (32 - EXTRA_FEATURES.len()), extra_values: [0; EXTRA_FEATURES.len()], swash_value: 0, + unshaped_complex: false, }; change(&mut off); change(&mut on); diff --git a/crates/text/src/kashida.rs b/crates/text/src/kashida.rs index c36eacbf2d..8db784a2bc 100644 --- a/crates/text/src/kashida.rs +++ b/crates/text/src/kashida.rs @@ -78,10 +78,11 @@ fn joining(c: char) -> Joining { } /// Is the cluster of glyph `a` joined to the cluster of glyph `b` that follows it in `text`: both -/// right to left in one face, `a`'s last character other than a mark joining the letter after it, -/// and `b` starting with a letter that joins the one before it? +/// right to left in one face and set as joining letters (not by a Standard composer), `a`'s last +/// character other than a mark joining the letter after it, and `b` starting with a letter that +/// joins the one before it? fn joined(text: &str, a: &SGlyph, b: &SGlyph) -> bool { - if a.face.id() != b.face.id() || a.level != b.level || !a.level.is_rtl() || a.byte + a.len != b.byte { + if a.face.id() != b.face.id() || a.level != b.level || !a.level.is_rtl() || a.byte + a.len != b.byte || !(a.cursive && b.cursive) { return false; } let last = text.get(a.byte..a.byte + a.len).and_then(|s| s.chars().rev().find(|&c| joining(c) != Joining::Transparent)); diff --git a/crates/text/src/layout.rs b/crates/text/src/layout.rs index 5ff18b4076..159248750c 100644 --- a/crates/text/src/layout.rs +++ b/crates/text/src/layout.rs @@ -22,7 +22,7 @@ use crate::shape::{ Punct, SGlyph, Tcy, cap_x_heights, hyphen_glyph, is_cjk, mark_word_ends, no_line_end, no_line_start, punct, run_face, shape_range, shape_runs, soft_line_start, style_metrics, }; -use crate::{Composer, FirstBaseline, InlineGlyph, LayoutOptions, LineInfo, ListMarker, OtFeatures, PositionedGlyph, TextLayout, VerticalAlign}; +use crate::{FirstBaseline, InlineGlyph, LayoutOptions, LineInfo, ListMarker, OtFeatures, PositionedGlyph, TextLayout, VerticalAlign}; mod bidi; mod breaking; diff --git a/crates/text/src/layout/flow.rs b/crates/text/src/layout/flow.rs index 38df882e07..0bd9b3b6b9 100644 --- a/crates/text/src/layout/flow.rs +++ b/crates/text/src/layout/flow.rs @@ -8,9 +8,9 @@ use super::vertical::{ EM_CENTER, MojikumiRules, align_shift, compress_punctuation, dot_quarter, glyph_em, halve_punctuation, punct_half, space_japanese_and_latin, }; use super::{ - Affine, BidiInfo, Burasagari, CharStyle, Composer, Ctx, EPS, Flex, Justify, Kinsoku, KinsokuShoriType, LeadingModel, LineFit, LineInfo, - ListMarker, ParaStyle, Params, Point, Punct, Range, SGlyph, SingleWordJustify, TextObject, apply_desired, cap_x_heights, compose, fit_line, - hyphen_glyph, is_cjk, is_word_space, kashida, mark_word_ends, no_line_end, no_line_start, style_metrics, + Affine, BidiInfo, Burasagari, CharStyle, Ctx, EPS, Flex, Justify, Kinsoku, KinsokuShoriType, LeadingModel, LineFit, LineInfo, ListMarker, + ParaStyle, Params, Point, Punct, Range, SGlyph, SingleWordJustify, TextObject, apply_desired, cap_x_heights, compose, fit_line, hyphen_glyph, + is_cjk, is_word_space, kashida, mark_word_ends, no_line_end, no_line_start, style_metrics, }; use crate::justify::is_format_control; use crate::shape::{keep_inseparable_pairs, keep_korean_words}; diff --git a/crates/text/src/shape.rs b/crates/text/src/shape.rs index 16809291bc..67ae8362c7 100644 --- a/crates/text/src/shape.rs +++ b/crates/text/src/shape.rs @@ -123,7 +123,8 @@ pub(crate) fn joins(script: Script) -> bool { } /// Is `script` one a Standard composer leaves unshaped: Indic (conjuncts, half forms, marks placed -/// by the font's layout tables), Arabic-like ([`joins`]) and Hebrew? +/// by the font's layout tables), the joining scripts ([`joins`]: Arabic, Syriac, N'Ko, Mongolian…) +/// and Hebrew? fn is_complex_script(script: Script) -> bool { joins(script) || matches!( @@ -1395,11 +1396,12 @@ fn shape_segment(text: &str, context: &Range, seg: &Segment, feats: &OtFe let text_seg = &text[range.clone()]; // The segment's script: that of its first character of a script of its own. let first_script = text_seg.chars().map(|c| c.script()).find(|s| !matches!(s, Script::Common | Script::Inherited)); - let cursive = first_script.is_some_and(joins); // A Standard composer sets a complex script glyph by glyph (no layout tables, nominal glyphs). // shortcut: decided for the whole segment by its first strong script, as `cursive` is; segments // already split where the script changes. let unshaped = feats.unshaped_complex && first_script.is_some_and(is_complex_script); + // Unshaped letters don't join, so they are not bent along a path as joined ones are. + let cursive = first_script.is_some_and(joins) && !unshaped; let full = st.size.max(0.0); // Superscript/subscript and small capitals shrink the glyphs; line metrics keep the full size. let (_, pos_shift) = st.position.scale_shift(full); @@ -1593,15 +1595,17 @@ fn shape_segment(text: &str, context: &Range, seg: &Segment, feats: &OtFe let c = st.digits.map(c); let chars: Vec = if upper { c.to_uppercase().collect() } else { vec![c] }; for u in chars { - let g = cmap.map(u).unwrap_or_default(); - let adv = gm.advance_width(g).unwrap_or(face.upem as f32 * 0.5); + // Format characters (joiners, direction marks) have no glyph of their own in most + // fonts, and the shaper hides them: draw the space's glyph with no advance. + let (g, adv) = if crate::justify::is_format_control(u) { + (cmap.map(' ').unwrap_or_default(), 0.0) + } else { + let g = cmap.map(u).unwrap_or_default(); + (g, gm.advance_width(g).unwrap_or(face.upem as f32 * 0.5)) + }; raw.push((g.to_u32(), cl, adv.round() as i32, 0, 0)); } } - // Shaped right-to-left text comes out in visual order, which layout expects. - if unshaped && level.is_rtl() { - raw.reverse(); - } } } shaped diff --git a/crates/text/src/tests/composers.rs b/crates/text/src/tests/composers.rs index 7b4988e822..57236d8708 100644 --- a/crates/text/src/tests/composers.rs +++ b/crates/text/src/tests/composers.rs @@ -33,8 +33,8 @@ fn g(c: char) -> u32 { u32::from(source_sans_gid(c).expect("a Source Sans glyph")) } -/// Under the ME&SA composers the conjunct forms (Devanagari: the vowel sign I drawn before its -/// syllable and Ka's half form; Gujarati: the half form); under the Standard ones each character +/// Under the ME&SA composers the conjunct forms (the vowel sign I drawn before its syllable and Ka's +/// half form, in Devanagari and in Gujarati); under the Standard ones each character /// has its own glyph, in text order, as the font maps it. #[test] fn standard_composers_set_indic_text_glyph_by_glyph() { @@ -42,7 +42,7 @@ fn standard_composers_set_indic_text_glyph_by_glyph() { let ids = |v: Vec<(u32, usize)>| v.into_iter().map(|(gid, _)| gid).collect::>(); for composer in [Composer::SingleLine, Composer::EveryLine] { assert_eq!(ids(drawn(&db, DEVANAGARI, composer)), [g('i'), g('k'), g('K')], "{composer:?} Devanagari"); - assert_eq!(ids(drawn(&db, GUJARATI, composer)), [g('k'), g('K'), g('i')], "{composer:?} Gujarati"); + assert_eq!(ids(drawn(&db, GUJARATI, composer)), [g('i'), g('k'), g('K')], "{composer:?} Gujarati"); } for composer in [Composer::StandardSingleLine, Composer::StandardEveryLine] { let plain = [g('K'), g('.'), g('K'), g('i')]; @@ -94,3 +94,18 @@ fn composers_split_into_breaking_and_shaping() { } assert_eq!(Composer::default(), EveryLine, "new type and old documents are ME&SA Every-line"); } + +/// A joiner or non-joiner has no glyph in most fonts and the shaper hides it; a Standard composer +/// draws it as no width at all, not as a missing-glyph box. +#[test] +fn standard_composers_give_format_characters_no_width() { + let db = db(); + let text = "क\u{200C}क"; + for composer in [Composer::StandardSingleLine, Composer::StandardEveryLine] { + let l = layout(&db, &object(text, composer)); + let widths: Vec = l.glyphs.iter().map(|g| g.advance).collect(); + assert_eq!(widths.len(), 3, "{composer:?}: {widths:?}"); + assert_eq!(widths[1], 0.0, "{composer:?}: the non-joiner takes no room: {widths:?}"); + assert!(widths[0] > 0.0 && widths[2] > 0.0, "{composer:?}: {widths:?}"); + } +} diff --git a/crates/ui-egui/src/menus/ui_commands.rs b/crates/ui-egui/src/menus/ui_commands.rs index 029ddf955a..2c9d328039 100644 --- a/crates/ui-egui/src/menus/ui_commands.rs +++ b/crates/ui-egui/src/menus/ui_commands.rs @@ -393,7 +393,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "type.lineComposer", "Toggle Line Composer", "Cmd+Alt+Shift+C", - "{} the selected type's paragraphs (or those the Type tool's selection touches) use the Single-line Composer when they all use the Every-line Composer, else the Every-line Composer, each keeping its shaping: Middle Eastern & South Asian or Standard (text.setFormat {composer}) → \"singleLine\" | \"everyLine\" | \"standardSingleLine\" | \"standardEveryLine\"", + "{} the selected type's paragraphs (or those the Type tool's selection touches) use the Single-line Composer when they all use the Every-line Composer, else the Every-line Composer; paragraphs that all use a Standard composer keep it, any other mix takes the Middle Eastern & South Asian one (text.setFormat {composer}) → \"singleLine\" | \"everyLine\" | \"standardSingleLine\" | \"standardEveryLine\"", ), ( "type.hyphenate", diff --git a/crates/ui-egui/src/panels/paragraph.rs b/crates/ui-egui/src/panels/paragraph.rs index 3c1a0fe9c8..6d4e5c4c69 100644 --- a/crates/ui-egui/src/panels/paragraph.rs +++ b/crates/ui-egui/src/panels/paragraph.rs @@ -54,13 +54,15 @@ pub(crate) fn align(app: &mut VectorcraftApp, justify: &str) -> Result Result { text_style(app).ok_or("select some type first")?; - let shared = shared_composer(app); - let to_single = shared.is_some_and(|c| !c.is_single_line()); - let id = composer_id(shared.unwrap_or_default().with_breaking(to_single)); + let to_single = shared_para(app, |p| p.composer.is_single_line()) == Some(false); + let standard = shared_para(app, |p| p.composer.shapes_complex_scripts()) == Some(false); + let base = if standard { Composer::StandardEveryLine } else { Composer::EveryLine }; + let id = composer_id(base.with_breaking(to_single)); format(app, json!({ "composer": id })); Ok(json!(id)) } diff --git a/crates/ui-egui/src/panels/paragraph/tests.rs b/crates/ui-egui/src/panels/paragraph/tests.rs index 518df239ae..f700d404c1 100644 --- a/crates/ui-egui/src/panels/paragraph/tests.rs +++ b/crates/ui-egui/src/panels/paragraph/tests.rs @@ -97,11 +97,16 @@ fn toggling_the_line_composer_keeps_the_shaping() { assert_eq!(toggle_composer(&mut app).unwrap(), json!(first), "{from}"); assert_eq!(toggle_composer(&mut app).unwrap(), json!(second), "{from} again"); } - // Paragraphs that differ take Every-line, as before the Standard composers existed. + // Paragraphs that break differently take Every-line, as before the Standard composers existed. app.run("text.setFormat", json!({"id": id, "composer": "standardSingleLine"})).unwrap(); app.run("text.setFormat", json!({"id": id, "start": 2, "end": 3, "composer": "singleLine"})).unwrap(); app.session.set_tool_option("selectAll", &json!(true)); assert_eq!(toggle_composer(&mut app).unwrap(), json!("everyLine")); + // Paragraphs that both break every-line but differ in shaping: Single-line, Middle Eastern & South Asian. + app.run("text.setFormat", json!({"id": id, "composer": "everyLine"})).unwrap(); + app.run("text.setFormat", json!({"id": id, "start": 2, "end": 3, "composer": "standardEveryLine"})).unwrap(); + app.session.set_tool_option("selectAll", &json!(true)); + assert_eq!(toggle_composer(&mut app).unwrap(), json!("singleLine")); } /// The Paragraph panel menu lists the four composers, and ticks the one every selected paragraph has. @@ -126,15 +131,23 @@ fn the_paragraph_menu_sets_each_of_the_four_composers() { _ => {} } } - let texts = items(&mut app); - for label in [ - "Middle Eastern & South Asian Single-line Composer", - "Middle Eastern & South Asian Every-line Composer", - "Standard Single-line Composer", - "Standard Every-line Composer", - ] { - assert!(texts.iter().any(|t| t == label), "{label} in {texts:?}"); + let labels = [ + ("singleLine", "Middle Eastern & South Asian Single-line Composer"), + ("everyLine", "Middle Eastern & South Asian Every-line Composer"), + ("standardSingleLine", "Standard Single-line Composer"), + ("standardEveryLine", "Standard Every-line Composer"), + ]; + // The menu lists all four, each row ticked (and only that one) when every selected paragraph has it. + for (value, _) in labels { + app.run("text.setFormat", json!({"id": id, "composer": value})).unwrap(); + app.session.set_tool_option("selectAll", &json!(true)); + let texts = items(&mut app); + for (v, label) in labels { + let row = if v == value { format!("✓ {label}") } else { format!(" {label}") }; + assert!(texts.iter().any(|t| *t == row), "{value}: {row:?} in {texts:?}"); + } } + // Choosing a row sets its composer: the row is a `text.setFormat {composer}` with the row's id. for (value, want) in [ ("standardEveryLine", Composer::StandardEveryLine), ("singleLine", Composer::SingleLine), @@ -142,6 +155,7 @@ fn the_paragraph_menu_sets_each_of_the_four_composers() { ] { app.run("text.setFormat", json!({"id": id, "composer": value})).unwrap(); assert_eq!(shared_composer(&app), Some(want), "{value}"); + assert_eq!(composer_id(want), value); } } diff --git a/docs/gaps.md b/docs/gaps.md index 201823b983..e49e9ba252 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Gaps -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-13 · **Change:** minor (3D: the seams of white-light Extrude & Bevel, Inflate and Rotate, and the face seams of the 3D (Classic) effects, have round joins instead of miters; previously: PDF import: preserve Type 3 font text runs as named groups , or as live text in TextAs::Text mode if ToUnicode exists on non-colour fonts (TextAs::Outlines produces grouped outlines), #6740; scores and estimates unchanged; previously: icon-strip flyouts also resize from the top edge and the canvas-side top corner, keeping the moved top in workspaces (storytold/craft-libs#1094); scores and estimates unchanged; previously: #6865: oversized editing artboard import; Glyphs panel: Alternates for Current Selection applies an OpenType feature value (`salt=2`) to the selected letter alone, so an Arabic word stays joined, and Entire Font lists the unencoded glyphs, #5514; #6404: transform-aware Eyedropper character copying; scores and estimates unchanged; previously: G5: built-in Energy Beams brush library; estimate 5–15 → 4–14 h; previously minor: G5: built-in Skill Icons symbol library; estimate 6–16 → 5–15 h; previously minor: ruler double-click adds a guide at the pointer, #5239; G5: built-in Status Effects graphic style library; estimate 7–17 → 6–16 h; MCP docs catch-up for UI Frames and Map Markers; previously minor: G5: built-in Inventory Icons symbol library; estimate 8–18 → 7–17 h; previously minor: G9: `.ai` save names a variable font instance's design-space setting, #6388; previously minor: G5: built-in Game Trails brush library; estimate 9–19 → 8–18 h; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: docs tidy-up, no content change: stray notes at the top moved into the revision table; scores and estimates unchanged; previously minor: G5: built-in Game HUD graphic style library; estimate 10–20 → 9–19 h; previously minor: G5: built-in Map Markers symbol library; estimate 11–21 → 10–20 h; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview; estimate 1–2 h → 0; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: built-in UI Frames symbol library with default 9-slice; estimate 11–21 h; previously minor: G5: dynamic symbol part on-canvas takes opacity, blend, gradient and pattern; docs Overlap/auto-corners sync; estimate 12–22 h; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap, Pattern brush auto corners and Global Edit Shift-click leave-out moved from Missing to Done (code already shipped); previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; G9: EPS gradients drawn as abutting strips import as one gradient, without seams; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; opening a slow file shows a progress window, part of #5930; Hindi catalog and contributor loanword preferences; agent wording pass complete; wider native proofreading welcome; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); scores and estimates unchanged; a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window; scores and estimates unchanged; large images in CMYK documents are shrunk before they are separated into ink planes (#4155); scores and estimates unchanged; `.ai` paths with a Calligraphic brush open brushed, with the 6D Art Pen's channels, and Windows reads the pen's tilt and barrel rotation, #372; scores and estimates unchanged; Compound Shapes add-on, beyond Illustrator; scores and estimates unchanged; Contour add-on, beyond Illustrator; scores and estimates unchanged; a slow first frame is waited for instead of restarting without the graphics adapter, and the menu bar no longer waits for the installed fonts' scan; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-14 · **Change:** minor (Type: the Middle Eastern & South Asian composers are done as a per-paragraph choice with the Standard composers (#4935); scores and estimates unchanged; previously minor: 3D: the seams of white-light Extrude & Bevel, Inflate and Rotate, and the face seams of the 3D (Classic) effects, have round joins instead of miters; previously: PDF import: preserve Type 3 font text runs as named groups , or as live text in TextAs::Text mode if ToUnicode exists on non-colour fonts (TextAs::Outlines produces grouped outlines), #6740; scores and estimates unchanged; previously: icon-strip flyouts also resize from the top edge and the canvas-side top corner, keeping the moved top in workspaces (storytold/craft-libs#1094); scores and estimates unchanged; previously: #6865: oversized editing artboard import; Glyphs panel: Alternates for Current Selection applies an OpenType feature value (`salt=2`) to the selected letter alone, so an Arabic word stays joined, and Entire Font lists the unencoded glyphs, #5514; #6404: transform-aware Eyedropper character copying; scores and estimates unchanged; previously: G5: built-in Energy Beams brush library; estimate 5–15 → 4–14 h; previously minor: G5: built-in Skill Icons symbol library; estimate 6–16 → 5–15 h; previously minor: ruler double-click adds a guide at the pointer, #5239; G5: built-in Status Effects graphic style library; estimate 7–17 → 6–16 h; MCP docs catch-up for UI Frames and Map Markers; previously minor: G5: built-in Inventory Icons symbol library; estimate 8–18 → 7–17 h; previously minor: G9: `.ai` save names a variable font instance's design-space setting, #6388; previously minor: G5: built-in Game Trails brush library; estimate 9–19 → 8–18 h; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: docs tidy-up, no content change: stray notes at the top moved into the revision table; scores and estimates unchanged; previously minor: G5: built-in Game HUD graphic style library; estimate 10–20 → 9–19 h; previously minor: G5: built-in Map Markers symbol library; estimate 11–21 → 10–20 h; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview; estimate 1–2 h → 0; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: built-in UI Frames symbol library with default 9-slice; estimate 11–21 h; previously minor: G5: dynamic symbol part on-canvas takes opacity, blend, gradient and pattern; docs Overlap/auto-corners sync; estimate 12–22 h; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap, Pattern brush auto corners and Global Edit Shift-click leave-out moved from Missing to Done (code already shipped); previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; G9: EPS gradients drawn as abutting strips import as one gradient, without seams; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; opening a slow file shows a progress window, part of #5930; Hindi catalog and contributor loanword preferences; agent wording pass complete; wider native proofreading welcome; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); scores and estimates unchanged; a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window; scores and estimates unchanged; large images in CMYK documents are shrunk before they are separated into ink planes (#4155); scores and estimates unchanged; `.ai` paths with a Calligraphic brush open brushed, with the 6D Art Pen's channels, and Windows reads the pen's tilt and barrel rotation, #372; scores and estimates unchanged; Compound Shapes add-on, beyond Illustrator; scores and estimates unchanged; Contour add-on, beyond Illustrator; scores and estimates unchanged; a slow first frame is waited for instead of restarting without the graphics adapter, and the menu bar no longer waits for the installed fonts' scan; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) ## Feature submission evidence @@ -34,6 +34,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| +| 2026-11-14 | minor | Type: Middle Eastern & South Asian and Standard composers per paragraph (#4935): Paragraph panel menu, paragraph styles, `text.setFormat {composer}` and the native file; the Standard composers set Indic, Arabic and Hebrew type glyph by glyph. `.ai`, EPS and PDF files carry no composer. Scores and estimates unchanged. | | 2026-11-13 | minor | PDF import: preserve Type 3 font text runs as named groups ``, or as live text in `TextAs::Text` mode if `/ToUnicode` exists on non-colour fonts (`TextAs::Outlines` produces grouped outlines) (#6740); scores and estimates unchanged. | | 2026-11-08 | minor | #3380, deletion only: preserve gradient placement and exact surviving geometry; consume owned subpaths with a shared original-node snapshot; consolidate status/evidence and document command/helper contracts. Compound behavior and the existing M2 status entry are retained. Endpoint highlighting remains open; scores and estimates unchanged. | | 2026-11-07 | minor | #3380 (deletion only): independent surviving path objects; transformed appearance, stacking, selection, cubic handles and Undo/Redo covered by `delete_path_pieces`; compound semantics retained. Endpoint highlighting remains open; scores and estimates unchanged. | @@ -447,7 +448,7 @@ macOS cursor delivery (#2292) is narrowed to a native host adapter using the exi own pixels until a document takes them in; estimate unchanged. - **Missing:** CJK vertical composition (custom mojikumi sets and the Mojikumi Settings dialog, kinsoku settings #633, manual tate-chu-yoko), Middle Eastern - features beyond bidi, digit types, Character Direction and Kashidas (Middle Eastern composers, a split caret), spell check (its engine is done, #4936: `text.spelling.check` and + features beyond bidi, digit types, Character Direction and Kashidas (a split caret; the Middle Eastern & South Asian composers are done, #4935), spell check (its engine is done, #4936: `text.spelling.check` and `text.spelling.suggest` on craft-proof; the custom dictionary and Ignore All commands; Auto Spell Check, `edit.autoSpellCheck`, with a right-click on an underlined word offering its suggestions, Ignore All and Add to Dictionary, and Spelling › Auto Spell Check in the context menu of type; the Edit Custom Dictionary dialog; the Check Spelling dialog remains), Touch Type, Retype, Snap to Glyph; optional preservation of font sizes during group scaling (#1034 enhancement). Itemized in [type-parity.md](type-parity.md). diff --git a/docs/mcp.md b/docs/mcp.md index e435dfb1a2..9ff3548778 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -10485,8 +10485,9 @@ attributes as `para.composer`, saved only when it is not Every-line, which is th Standard composers existed). `singleLine` and `everyLine` shape Indic, Arabic and Hebrew type with the font's layout tables, as all type is set; `standardSingleLine` and `standardEveryLine` break lines the same way but set such type glyph by glyph as the font maps its characters (no conjuncts, reordering, mark placement or joining forms), for legacy -fonts that keep a script on Latin character codes. Latin and other scripts keep their kerning and ligatures under every -composer. An older VectorCraft cannot open a file that uses a Standard composer. `.ai`, EPS and PDF files carry no composer. +fonts that keep a script on Latin character codes. The scripts a Standard composer leaves unshaped are Indic, Arabic and the +other joining scripts (Syriac, N'Ko, Mongolian, Adlam…) and Hebrew; Latin, Greek, Cyrillic, CJK, Thai and the rest keep their +kerning and ligatures under every composer. Letters left unshaped do not join, so Insert Kashidas adds no tatweel between them. An older VectorCraft cannot open a file that uses a Standard composer. `.ai`, EPS and PDF files carry no composer. Every-line, the default, picks the breaks of the whole paragraph together in area type: justified text gets even word spacing, ragged text (left, centre or right aligned) an even rag, so it may move a word that fits to the next line to avoid a lone short word on the last line or a full line next to a short one. @@ -10500,8 +10501,9 @@ no room for all of it, or when the search for its breaks passes VectorCraft's ow 2,000,000 baseline steps for one paragraph, 2,000,000 lines and 4,000,000 steps for all the paragraphs of a frame). The UI command `type.lineComposer` (Cmd+Alt+Shift+C, in remote mode `press_key {key: "C", mods: {cmd: true, alt: true, shift: true}}` or `invoke_menu`) sets the Single-line form when every paragraph of the selected type (or every paragraph the -Type tool's selection touches) uses an Every-line one, else the Every-line form, keeping each paragraph's shaping (`singleLine` -or `everyLine`; a Standard composer toggles between `standardSingleLine` and `standardEveryLine`), and returns the composer it set. +Type tool's selection touches) uses an Every-line one, else the Every-line form. Paragraphs that all use a Standard composer +keep it (`standardSingleLine` or `standardEveryLine`); any other mix takes the Middle Eastern & South Asian one (`singleLine` +or `everyLine`). It returns the composer it set. ```json {"name":"run_command","arguments":{"command":"text.setFormat","params":{"ids":[7],"composer":"singleLine"}}} diff --git a/docs/type-parity.md b/docs/type-parity.md index f49594adc5..ed343131af 100644 --- a/docs/type-parity.md +++ b/docs/type-parity.md @@ -62,7 +62,7 @@ against Illustrator's public documentation; **estimated** unless noted. | Middle Eastern: bidirectional layout, Paragraph Direction | done | PDF export keeps harakat and zero-advance Noto Arabic dot glyphs in one compatible batch per layout line, with per-glyph vertical offsets (#6309). `crates/pdf/src/tests/realtext.rs` (`mark_text_*`) checks decoded cluster mappings, mixed Arabic/Latin/digits, contour placement against outline export, vertical columns, differing path rotations and PDF/X-1a/3/4. Editable-text import still needs ActualText support (#6165); separately styled or differently rotated clusters and external viewer search/copy remain outside this export-only evidence. | | Middle Eastern and Indic: digit types (Character panel menu › Digits: Default, Arabic, Hindi, Farsi; Devanagari, Bengali, Gurmukhi, Gujarati, Odia, Tamil, Telugu, Kannada, Malayalam) | done | `text.setFormat` / `text.setRangeStyle {digits}` (`"devanagari"`, `"bengali"`, `"gurmukhi"`, `"gujarati"`, `"odia"`, `"tamil"`, `"telugu"`, `"kannada"`, `"malayalam"` added, #4942); the text keeps the typed digits, the glyphs and SVG export follow the style; carried by character styles. Not yet: `.ai` export of the setting; Indic digits typed as such stay as typed under another system. Evidence: `text::tests::indic_digits_map_to_their_scripts_block`, `tests::bidi::indic_digits_draw_the_scripts_own_digits`, `typecmd::area_tests::indic_digits_round_trip_through_the_commands_and_the_file` | | Middle Eastern: Kashidas | done | Paragraph Insert Kashidas sets the length for justified Arabic area type; Character-menu Kashidas allows or excludes selected characters, on by default. A kashida is inserted only between two allowed joined characters. The character switch is retained in character styles and the native file; the length is retained in paragraph styles and the native file. Paint-only harakat (coloured or hidden marks) stay in one shaping cluster so letters keep joining; when the character-level pass matches, a glyph may take a later in-cluster character's run only if GDEF names it a mark (class 3) and that character is a harakat—otherwise the cluster start (a mark may miss its colour; a base never takes a mark's). Inserted tatweels take the base letter's paint—not a trailing mark's. Fonts without GDEF and some AAT/`morx` ligatures (e.g. Geeza Pro Allah) can leave a mark painted as its letter. No Character-panel harakat colour command yet. Evidence: `tests::harakat`. [Public help](https://helpx.adobe.com/illustrator/desktop/design-with-text/fonts-and-scripts/justify-text-using-kashida-and-hyphenation.html). | -| Middle Eastern & South Asian composers and Standard composers (Paragraph panel menu, paragraph styles, `text.setFormat {composer}`) | done / partial | #4935: per paragraph, four choices. Middle Eastern & South Asian Single-line and Every-line are how all type was already set (complex-script shaping on) and stay the default, so existing documents do not change. Standard Single-line and Every-line break lines the same way and set Indic, Arabic and Hebrew type glyph by glyph as the font maps its characters (no conjuncts, reordering, mark placement or joining), for legacy fonts that keep a script on Latin character codes; Latin and other scripts keep their kerning and ligatures. The native file and paragraph styles keep the choice (`standardSingleLine` / `standardEveryLine`; an older VectorCraft cannot read a file that uses them). `.ai`, EPS and PDF import and export carry no composer: this app has no recorded format for it, so a Standard paragraph opened from or saved to those files comes back Middle Eastern & South Asian. Evidence: `tests::composers`, `tests::panelcmds::text_set_format_takes_the_four_composers`, `native_v3::all_four_composers_round_trip` | +| Middle Eastern & South Asian composers and Standard composers (Paragraph panel menu, paragraph styles, `text.setFormat {composer}`) | done / partial | #4935: per paragraph, four choices. Middle Eastern & South Asian Single-line and Every-line are how all type was already set (complex-script shaping on) and stay the default, so existing documents do not change. Standard Single-line and Every-line break lines the same way and set Indic, Arabic and Hebrew type glyph by glyph as the font maps its characters (no conjuncts, reordering, mark placement or joining), for legacy fonts that keep a script on Latin character codes. Indic, Arabic and the other joining scripts and Hebrew are the scripts left unshaped (their letters do not join, so Insert Kashidas adds no tatweel); Latin, Greek, Cyrillic, CJK, Thai and the rest keep their kerning and ligatures. The native file and paragraph styles keep the choice (`standardSingleLine` / `standardEveryLine`; an older VectorCraft cannot read a file that uses them). `.ai`, EPS and PDF import and export carry no composer: this app has no recorded format for it, so a Standard paragraph opened from or saved to those files comes back Middle Eastern & South Asian. Evidence: `tests::composers`, `tests::panelcmds::text_set_format_takes_the_four_composers`, `native_v3::all_four_composers_round_trip` | | Middle Eastern: split caret | missing | Character Direction is done; editable bidirectional SVG text remains incomplete. | | CJK: input methods (marked text, candidate window) | done | | | CJK: kinsoku (Hard, Soft, None), the predefined mojikumi sets (None, YakumonoHankaku, GyomatsuYakumonoHankaku, GyomatsuYakumonoZenkaku, YakumonoZenkaku, with VectorCraft's own spacing for each) and Line-end Punctuation Half Width, tate-chu-yoko (auto) | done / partial | custom kinsoku sets, custom mojikumi sets and the Mojikumi Settings dialog missing |