From bddb834231ea0c6fa26e2823ae68fabb703227a8 Mon Sep 17 00:00:00 2001 From: jo-quast Date: Thu, 5 Nov 2026 12:17:33 +0700 Subject: [PATCH] Part of #394: each run hyphenates in its own language; Default Language sets new type's Squashed from #2818 onto main. --- crates/doc/src/text/char_style.rs | 7 ++- crates/engine/src/cmd/create.rs | 7 ++- crates/engine/src/cmd/prefscmds.rs | 42 ++++++++++++++---- crates/engine/src/prefs.rs | 13 +++++- crates/engine/src/tests/prefs.rs | 54 ++++++++++++++++++++++++ crates/text/src/hyphen.rs | 45 ++++++++++++++++++++ crates/text/src/layout.rs | 2 +- crates/text/src/layout/breaking.rs | 7 +-- crates/text/src/layout/wrapping_tests.rs | 1 + crates/text/src/shape.rs | 4 ++ crates/text/src/tests/typo.rs | 27 ++++++++++++ docs/gaps.md | 11 +++-- docs/target-app-parity.md | 5 ++- docs/ui-parity.md | 3 +- 14 files changed, 204 insertions(+), 24 deletions(-) diff --git a/crates/doc/src/text/char_style.rs b/crates/doc/src/text/char_style.rs index 731480fed..67ccb60a4 100644 --- a/crates/doc/src/text/char_style.rs +++ b/crates/doc/src/text/char_style.rs @@ -143,8 +143,11 @@ pub struct CharStyle { pub no_break: bool, /// Language (Character panel): the BCP 47 tag of one of [`crate::setup::LANGUAGES`] these /// characters are shaped in, so that the font uses that language's forms (OpenType `locl`). - /// `None`: the default language, English: USA, shaped without a language. New type takes the - /// document's language (Document Setup). Saved only when set. + /// `None`: the default language, English: USA, shaped without a language, which is what text + /// saved without one reads as. A tag the list doesn't have, from a file, is kept and has no + /// hyphenation. New type takes the document's language (Document Setup), else the default + /// language of Preferences › Hyphenation. Saved only when set. Each run hyphenates in its own + /// language (`vectorcraft_text::hyphen`). #[serde(default, skip_serializing_if = "Option::is_none")] pub language: Option, /// Another glyph of the font for the run's first character (Glyphs panel › Show › Alternates diff --git a/crates/engine/src/cmd/create.rs b/crates/engine/src/cmd/create.rs index 00ea88ea6..ab3dc966c 100644 --- a/crates/engine/src/cmd/create.rs +++ b/crates/engine/src/cmd/create.rs @@ -833,7 +833,10 @@ pub(crate) fn new_type_style(s: &Session, p: &Value) -> CharStyle { None if !s.paint.fill.is_none() && s.paint.fill != vectorcraft_color::Paint::solid(vectorcraft_color::Color::WHITE) => s.paint.fill.clone(), None => vectorcraft_color::Paint::solid(vectorcraft_color::Color::BLACK), }; - // In the document's language (Document Setup), none for the default. - style.language = s.doc().ok().and_then(|d| vectorcraft_doc::setup::char_language(&d.doc.setup.language).ok().flatten()).map(str::to_string); + // In the language Document Setup chose, else Preferences › Hyphenation › Default Language; none + // for the default. Type already in a document keeps its own. + let chosen = |name: &str| vectorcraft_doc::setup::char_language(name).ok().flatten(); + let document = s.doc().ok().and_then(|d| chosen(&d.doc.setup.language)); + style.language = document.or_else(|| chosen(&s.prefs.hyphenation_language)).map(str::to_string); style } diff --git a/crates/engine/src/cmd/prefscmds.rs b/crates/engine/src/cmd/prefscmds.rs index ad24afacb..029f3608f 100644 --- a/crates/engine/src/cmd/prefscmds.rs +++ b/crates/engine/src/cmd/prefscmds.rs @@ -260,14 +260,40 @@ pub const PREF_SPECS: &[PrefSpec] = &[ "", "Default Language", choice(&[ - ("English: USA", "English: USA"), - ("English: UK", "English: UK"), - ("French", "French"), - ("German", "German"), - ("Spanish", "Spanish"), - ("Italian", "Italian"), - ("Dutch", "Dutch"), - ("Portuguese", "Portuguese") + ("en-US", "English: USA"), + ("en-GB", "English: UK"), + ("de", "German"), + ("de-CH", "German: Swiss"), + ("fr", "French"), + ("it", "Italian"), + ("es", "Spanish"), + ("ca", "Catalan"), + ("pt", "Portuguese"), + ("nl", "Dutch"), + ("da", "Danish"), + ("sv", "Swedish"), + ("fi", "Finnish"), + ("nb", "Norwegian"), + ("pl", "Polish"), + ("cs", "Czech"), + ("ru", "Russian"), + ("sr-Cyrl", "Serbian: Cyrillic"), + ("sr-Latn", "Serbian: Latin"), + ("hr", "Croatian"), + ("tr", "Turkish"), + ("ja", "Japanese"), + ("zh", "Chinese"), + ("hi", "Hindi"), + ("mr", "Marathi"), + ("gu", "Gujarati"), + ("bn", "Bengali"), + ("pa", "Punjabi"), + ("ta", "Tamil"), + ("te", "Telugu"), + ("kn", "Kannada"), + ("ml", "Malayalam"), + ("or", "Odia"), + ("ur", "Urdu") ]) ), p!("hyphenationExceptions", "Hyphenation", "", "Exceptions (comma separated)", text), diff --git a/crates/engine/src/prefs.rs b/crates/engine/src/prefs.rs index 5deb00ddd..ce7c5e4a6 100644 --- a/crates/engine/src/prefs.rs +++ b/crates/engine/src/prefs.rs @@ -10,6 +10,14 @@ pub enum DrawMode { Inside, } +/// A saved Default Language as its tag: older preferences hold the language's name, and one the list +/// no longer has reads as the default. +fn language_tag<'de, D: serde::Deserializer<'de>>(d: D) -> std::result::Result { + let saved = ::deserialize(d)?; + let tag = vectorcraft_doc::setup::char_language(&saved).ok().flatten(); + Ok(tag.or_else(|| vectorcraft_doc::setup::LANGUAGES.first().map(|(_, t, _)| *t)).unwrap_or_default().to_string()) +} + /// Application preferences (Edit → Preferences). Every field is reachable through `prefs.get` / /// `prefs.set {key, value}` (camelCase keys, validated against [`cmd::prefscmds::PREF_SPECS`]). #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] @@ -114,6 +122,9 @@ pub struct Prefs { pub show_slice_numbers: bool, pub slice_line_color: String, // Hyphenation + /// Default Language: the BCP 47 tag of the language new type takes when its document's language + /// is the default ([`vectorcraft_doc::setup::LANGUAGES`]). + #[serde(deserialize_with = "language_tag")] pub hyphenation_language: String, pub hyphenation_exceptions: String, /// Type › Options › Additional Fonts Folder: a folder (read with its subfolders) whose fonts @@ -404,7 +415,7 @@ impl Default for Prefs { snap_perpendicular: true, show_slice_numbers: true, slice_line_color: s("#ff3f3f"), - hyphenation_language: s("English: USA"), + hyphenation_language: s("en-US"), hyphenation_exceptions: String::new(), fonts_folder: String::new(), plugins_folder: String::new(), diff --git a/crates/engine/src/tests/prefs.rs b/crates/engine/src/tests/prefs.rs index 87b943d6c..ae8609cd6 100644 --- a/crates/engine/src/tests/prefs.rs +++ b/crates/engine/src/tests/prefs.rs @@ -582,6 +582,60 @@ fn set_pref(s: &mut Session, key: &str, value: Value) { s.execute("prefs.set", &json!({"key": key, "value": value})).unwrap(); } +/// New type with the session's defaults: its id and language. +fn new_type(s: &mut Session) -> (NodeId, Option) { + let id = NodeId(s.execute("text.create", &json!({"x": 10, "y": 50, "text": "Hello"})).unwrap()["id"].as_u64().unwrap()); + (id, type_language(s, id)) +} + +fn type_language(s: &Session, id: NodeId) -> Option { + let Some(NodeKind::Text(t)) = s.doc().unwrap().doc.node(id).map(|n| &n.kind) else { panic!("not text") }; + t.runs[0].style.language.clone() +} + +/// Preferences › Hyphenation › Default Language (#394) is the language of new type, stored as its +/// tag. Type already there keeps its language, and a language chosen in Document Setup comes first. +#[test] +fn default_language_is_the_language_of_new_type() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap(); + let (before, language) = new_type(&mut s); + assert_eq!(language, None, "the default language isn't stored"); + + set_pref(&mut s, "hyphenationLanguage", json!("German")); + assert_eq!(s.prefs.hyphenation_language, "de", "a name is stored as its tag"); + assert_eq!(new_type(&mut s).1.as_deref(), Some("de")); + assert_eq!(type_language(&s, before), None, "type already there stays as it is"); + + s.execute("document.setup", &json!({"language": "French"})).unwrap(); + assert_eq!(new_type(&mut s).1.as_deref(), Some("fr"), "the document's own language comes first"); + + set_pref(&mut s, "hyphenationLanguage", json!("en-US")); + s.execute("document.setup", &json!({"language": "English: USA"})).unwrap(); + assert_eq!(new_type(&mut s).1, None); +} + +/// Preferences saved before the language was a tag hold its name; a language no longer in the list +/// reads as the default. +#[test] +fn a_saved_default_language_reads_as_its_tag() { + let read = |v: &str| serde_json::from_value::(json!({"hyphenationLanguage": v})).unwrap().hyphenation_language; + assert_eq!(read("German"), "de"); + assert_eq!(read("English: USA"), "en-US"); + assert_eq!(read("de"), "de"); + assert_eq!(read("Klingon"), "en-US"); + assert_eq!(Prefs::default().hyphenation_language, "en-US"); +} + +/// The preference offers every language the Character panel and Document Setup do. +#[test] +fn default_language_offers_every_language() { + let spec = PREF_SPECS.iter().find(|p| p.key == "hyphenationLanguage").unwrap(); + let crate::cmd::prefscmds::PrefKind::Choice(options) = spec.kind else { panic!("a choice") }; + let languages: Vec<(&str, &str)> = vectorcraft_doc::setup::LANGUAGES.iter().map(|(name, tag, _)| (*tag, *name)).collect(); + assert_eq!(options, languages.as_slice()); +} + fn gesture(s: &mut Session, v: ViewInfo, events: &[(vectorcraft_tools::PointerKind, f64, f64)]) { for (k, x, y) in events { s.pointer(&vectorcraft_tools::PointerEvent::new(*k, *x, *y), v).unwrap(); diff --git a/crates/text/src/hyphen.rs b/crates/text/src/hyphen.rs index 16aa674ca..bcf758706 100644 --- a/crates/text/src/hyphen.rs +++ b/crates/text/src/hyphen.rs @@ -7,6 +7,11 @@ //! Preferences › Hyphenation › Exceptions (comma separated) override the points for named words: //! `typography` never breaks, `ty-pog-ra-phy` breaks only where the hyphens mark. See //! [`set_hyphenation_exceptions`]. +//! +//! Each run hyphenates in its own language ([`vectorcraft_doc::CharStyle::language`]). English is +//! the only language with break points so far: text in any other language, or in a language a +//! file names that isn't known, doesn't hyphenate, apart from the words in the Exceptions list +//! ([`language_has_patterns`]). use std::collections::HashMap; use std::sync::{Mutex, MutexGuard, OnceLock}; @@ -144,6 +149,12 @@ fn onset_pair(a: char, b: char) -> bool { ) } +/// Whether text in `language` (a [`vectorcraft_doc::CharStyle::language`]: a BCP 47 tag, `None` for +/// the default, English: USA) has break points of its own. +pub fn language_has_patterns(language: Option<&str>) -> bool { + language.is_none_or(|l| l.get(..2).is_some_and(|p| p.eq_ignore_ascii_case("en")) && matches!(l.as_bytes().get(2), None | Some(b'-'))) +} + /// Allowed hyphenation points in `word`, as char indices (a hyphen goes *before* the char), with /// the default Hyphenation options. pub fn hyphen_points(word: &str) -> Vec { @@ -157,12 +168,21 @@ pub fn hyphen_points(word: &str) -> Vec { /// (other than a trailing apostrophe) are not hyphenated. Preferences exceptions override the /// pattern and these options for a matching word. pub fn hyphen_points_with(word: &str, opts: &HyphenationOptions) -> Vec { + hyphen_points_in(word, opts, true) +} + +/// [`hyphen_points_with`] for a word in a language with or without break points of its own +/// ([`language_has_patterns`]): without them, only a word in the Exceptions list breaks. +pub fn hyphen_points_in(word: &str, opts: &HyphenationOptions, patterns: bool) -> Vec { if let Some(ex) = exception_for(word) { return match ex { Exception::Never => vec![], Exception::Only(pts) => lowercase_points_to_word(word, &pts), }; } + if !patterns { + return vec![]; + } let c: Vec = word.chars().collect(); let n = c.len(); let longer_than = usize::try_from(opts.words_longer_than).unwrap_or(usize::MAX); @@ -235,6 +255,31 @@ mod tests { with_hyphenation_exceptions_for_test(source, f); } + /// #394: English in any region has break points; other languages, and a tag from a file that + /// isn't a known language, have none. + #[test] + fn only_english_has_break_points() { + for l in [None, Some("en-US"), Some("en-GB"), Some("EN-gb"), Some("en")] { + assert!(language_has_patterns(l), "{l:?}"); + } + for l in [Some("de"), Some("fr"), Some("pt"), Some("eng"), Some("x-unknown"), Some(""), Some("é")] { + assert!(!language_has_patterns(l), "{l:?}"); + } + } + + /// #394: a language without break points leaves words whole, but the Exceptions list is the + /// user's own and applies in every language. + #[test] + fn a_language_without_break_points_keeps_only_the_exceptions() { + let opts = HyphenationOptions::default(); + assert_eq!(hyphen_points_in("typography", &opts, true), vec![4, 7]); + assert!(hyphen_points_in("typography", &opts, false).is_empty()); + with_exceptions("ty-pog-ra-phy, happen", || { + assert_eq!(hyphen_points_in("typography", &opts, false), vec![2, 5, 7]); + assert!(hyphen_points_in("happen", &opts, false).is_empty()); + }); + } + /// #394 Preferences › Hyphenation › Exceptions contract (parse + override). #[test] fn hyphenation_exceptions_override_the_pattern() { diff --git a/crates/text/src/layout.rs b/crates/text/src/layout.rs index b814506e6..50e1ce0b2 100644 --- a/crates/text/src/layout.rs +++ b/crates/text/src/layout.rs @@ -14,7 +14,7 @@ use vectorcraft_geom::{ArcPath, PathData}; use crate::GlyphBend; use crate::composer::{Breakpoint, Params, compose}; use crate::fontdb::FontDb; -use crate::hyphen::hyphen_points_with; +use crate::hyphen::hyphen_points_in; use crate::justify::{Flex, LineFit, apply_desired, fit_line, is_word_space}; use crate::kashida; use crate::shape::{ diff --git a/crates/text/src/layout/breaking.rs b/crates/text/src/layout/breaking.rs index 54d8941e8..90d6555a0 100644 --- a/crates/text/src/layout/breaking.rs +++ b/crates/text/src/layout/breaking.rs @@ -1,7 +1,7 @@ use super::region::Region; use super::{ Affine, BezPath, Breakpoint, Burasagari, EPS, FirstBaseline, FontDb, HyphenationOptions, Kinsoku, LeadingModel, SGlyph, hyphen_glyph, - hyphen_points_with, no_line_end, no_line_start, soft_line_start, + hyphen_points_in, no_line_end, no_line_start, soft_line_start, }; /// Vertical metrics of a line: (ascent, descent, leading, cap height, x height). @@ -518,7 +518,8 @@ fn soft_kinsoku_lets_small_kana_and_the_prolonged_sound_mark_start_a_line() { } } -/// Glyph indices inside `g[ws..we]` (a word) where a hyphenated break may go under `opts`. +/// Glyph indices inside `g[ws..we]` (a word) where a hyphenated break may go under `opts`, in the +/// language of the word's first letter. fn hyphen_breaks(text: &str, g: &[SGlyph], ws: usize, we: usize, opts: &HyphenationOptions) -> Vec { // Strip leading/trailing punctuation (quotes, commas). let (mut a, mut b) = (ws, we); @@ -542,7 +543,7 @@ fn hyphen_breaks(text: &str, g: &[SGlyph], ws: usize, we: usize, opts: &Hyphenat starts.entry(g[k].byte).or_insert(k); } } - hyphen_points_with(word, opts).into_iter().filter_map(|ci| starts.get(offs.get(ci)?).copied()).collect() + hyphen_points_in(word, opts, g[a].hyphenates).into_iter().filter_map(|ci| starts.get(offs.get(ci)?).copied()).collect() } /// Break candidates for the every-line composer: hyphenation points under `hyphenate` (the diff --git a/crates/text/src/layout/wrapping_tests.rs b/crates/text/src/layout/wrapping_tests.rs index 9f1a048b3..cdf585539 100644 --- a/crates/text/src/layout/wrapping_tests.rs +++ b/crates/text/src/layout/wrapping_tests.rs @@ -39,6 +39,7 @@ fn synthetic_glyphs(specs: &[(usize, usize, f64, char)]) -> Vec { no_break: false, vertical_roman: false, cursive: false, + hyphenates: true, }) .collect() } diff --git a/crates/text/src/shape.rs b/crates/text/src/shape.rs index 6ecd515db..f37c7443e 100644 --- a/crates/text/src/shape.rs +++ b/crates/text/src/shape.rs @@ -80,6 +80,8 @@ pub(crate) struct SGlyph { /// decided for the whole segment it was shaped in ([`joins`]): on a curved path it is bent /// along the path so its joins meet. pub cursive: bool, + /// The glyph's language has hyphenation break points ([`crate::hyphen::language_has_patterns`]). + pub hyphenates: bool, } /// Does `script` join its letters (a cursive script, whose neighbouring glyphs touch)? @@ -947,6 +949,7 @@ fn inline_glyph( no_break: st.no_break, vertical_roman: false, cursive: false, + hyphenates: false, } } @@ -1327,6 +1330,7 @@ fn shape_segment(text: &str, context: &Range, seg: &Segment, feats: &OtFe no_break: gst.no_break, vertical_roman: gst.standard_vertical_roman, cursive, + hyphenates: crate::hyphen::language_has_patterns(gst.language.as_deref()), }); } } diff --git a/crates/text/src/tests/typo.rs b/crates/text/src/tests/typo.rs index f92ce67a6..478bc9c97 100644 --- a/crates/text/src/tests/typo.rs +++ b/crates/text/src/tests/typo.rs @@ -1013,6 +1013,33 @@ fn hyphenation_rules() { } } +/// #394: each run hyphenates in its own language. English breaks words, a language without break +/// points doesn't, and text saved without a language breaks as it always did. +#[test] +fn each_run_hyphenates_in_its_own_language() { + let text = "Extraordinarily comprehensive typographical considerations notwithstanding everything."; + let frame = Rect::new(0.0, 0.0, 120.0, 600.0); + let hyphens = |language: Option<&str>| { + let st = CharStyle { language: language.map(str::to_string), ..style(14.0) }; + let mut t = area(text, st, frame, Justify::Left); + t.para.hyphenate = true; + layout(db(), &t).glyphs.iter().filter(|g| g.len == 0).count() + }; + let saved_without_one = hyphens(None); + assert!(saved_without_one > 0, "some words hyphenated"); + assert_eq!(hyphens(Some("en-GB")), saved_without_one); + assert_eq!(hyphens(Some("de")), 0, "German has no break points yet"); + assert_eq!(hyphens(Some("x-from-a-file")), 0, "a tag that isn't known has no hyphenation"); + + // both in one paragraph: the English run breaks and the German one doesn't + let mut t = area(&format!("{text} {text}"), style(14.0), frame, Justify::Left); + t.para.hyphenate = true; + edit::style_range(&mut t.runs, text.len(), 2 * text.len() + 1, |s| s.language = Some("de".into())); + let l = layout(db(), &t); + let at: Vec = l.glyphs.iter().filter(|g| g.len == 0).map(|g| g.byte).collect(); + assert!(!at.is_empty() && at.iter().all(|b| *b < text.len()), "hyphens only in the English run: {at:?}"); +} + #[test] fn hyphenated_layout_adds_hyphens() { let text = "Extraordinarily comprehensive typographical considerations notwithstanding everything."; diff --git a/docs/gaps.md b/docs/gaps.md index 2c730d0b8..7dc084a0a 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Gaps -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-05 · **Change:** minor (document tabs are reordered by dragging them along the tab strip; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-05 · **Change:** minor (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 @@ -28,6 +28,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| +| 2026-11-05 | minor | G12: Hyphenation › Default Language sets the language of new type unless Document Setup chose one, stored as a tag; each run hyphenates in its own language, English only so far, so 2 Preferences options stay unused (#394) | | 2026-11-03 | minor | New gradients use CMYK default stops in CMYK documents, including Gradient tool creation. Scores and estimates unchanged. | | 2026-11-02 | minor | Font-family fields step selected text with the mouse wheel (#5253); no scored gap or estimate reassessed. | | 2026-10-24 | minor | G12: General › Corner Radius is the Rounded Rectangle tool's radius, a change while the tool is active reaches it, and the radius ↑/↓ set while drawing (or a Tool Options value) becomes the preference, within 0–1296 pt (#394, `crates/engine/src/tests/constrain_tools.rs`); no entry closed, estimate unchanged. | @@ -437,9 +438,11 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall - **Done:** User Interface › Open Documents As Tabs: with the box cleared, each document opened or created afterwards opens in a floating window of its own (G7's Float in Window); documents already open stay where they are. Estimate unchanged. -- **Missing:** 3 Preferences options stored but not acted on (#394), each waiting on a subsystem of its own: - Hyphenation › Default Language (hyphenation is English-only, with no per-language - dictionaries), Plug-ins & Scratch Disks › Primary and Secondary (documents are kept in memory; nothing pages to +- **Done:** Hyphenation › Default Language is the language of new type, unless Document Setup chose one, and each + run hyphenates in its own language (Character panel). Only English has break points so far: text in another + language stays whole, apart from the Exceptions list (#394). +- **Missing:** 2 Preferences options stored but not acted on (#394), each waiting on a subsystem of its own: + Plug-ins & Scratch Disks › Primary and Secondary (documents are kept in memory; nothing pages to disk), and Clipboard Handling › Legacy vector clipboard with its Preserve Paths / Preserve Appearance and Overprints choice (Illustrator's own clipboard flavour, which has no public specification). Also Properties panel per context, context menus beyond the canvas, Layers panel row menus and Collect for Export, SVG diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 086ad771b..343b650e4 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -23,6 +23,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| +| 2026-11-05 | minor | G12: Hyphenation › Default Language sets the language of new type unless Document Setup chose one, stored as a tag; each run hyphenates in its own language, English only so far, so 2 Preferences options stay unused (#394) | | 2026-11-05 | minor | Type Tools add-on: Release Type on a Path (a VectorCraft addition, #5532): evidence added; scores unchanged. | | 2026-11-04 | minor | Path Precision Tools add-on (a VectorCraft addition): Set Corners… and `addon.pathtools.cornerSet` `minAngle`/`maxAngle` round only the corners whose angle is in a range (#5545); evidence added; scores unchanged. | | 2026-11-04 | minor | Path Precision Tools add-on (a VectorCraft addition): Set Corners… and `addon.pathtools.cornerSet` `every` round all, the odd-numbered or the even-numbered anchors of each path (#5546); evidence added; scores unchanged. | @@ -182,7 +183,7 @@ dominate; 3D, graphs and automation are specialist). They sum to 105. Feature de | Export for Screens, Asset Export, slices, Save for Web | 3 | 85% | anti-aliasing ghost borders (#983); TIFF choices and settings are implemented | 2–4 | estimated | | Print, colour management, separations, flattener | 3 | 80% | print preview fidelity | 2–4 | estimated | | Automation (Actions, Variables, scripting, batch) | 3 | 60% | a scripting surface (Illustrator: JavaScript/ExtendScript), Batch, image and graph variables, dataset import/export | 8–14 | estimated; MCP `add_text` exposes existing vertical type; score and hours unchanged | -| UI chrome (panels, Properties, workspaces, preferences) | 6 | 80% | Properties per context, 4 Preferences options stored but unused (#394, audited 2026-10-30: Open Documents As Tabs, hyphenation Default Language, Scratch Disks, legacy vector clipboard), SVG Interactivity, context menus beyond the canvas, accessibility | 13–20 | estimated; floating panels in OS windows of their own on the desktop (#4135), score and hours unchanged; macOS save Format menu implemented (#866), score and hours unchanged; Eyedropper cursor and precise setting checked (#1269), OS visual QA unverified; numeric-field focus and normal text drags regression-tested (#1326, in part); movements below the click threshold still select all; artboard navigation synchronizes the tool selection before destructive actions; status messages and readouts use stable responsive allocations sized for localized labels and ordinary coordinates (sample widths cached by language/unit/DPI; regression and contributor original-file evidence below; scores/hours unchanged) | +| UI chrome (panels, Properties, workspaces, preferences) | 6 | 80% | Properties per context, 3 Preferences options stored but unused (#394, audited 2026-10-30: Open Documents As Tabs, Scratch Disks, legacy vector clipboard), SVG Interactivity, context menus beyond the canvas, accessibility | 13–20 | estimated; floating panels in OS windows of their own on the desktop (#4135), score and hours unchanged; macOS save Format menu implemented (#866), score and hours unchanged; Eyedropper cursor and precise setting checked (#1269), OS visual QA unverified; numeric-field focus and normal text drags regression-tested (#1326, in part); movements below the click threshold still select all; artboard navigation synchronizes the tool selection before destructive actions; status messages and readouts use stable responsive allocations sized for localized labels and ordinary coordinates (sample widths cached by language/unit/DPI; regression and contributor original-file evidence below; scores/hours unchanged) | | Libraries, Links, Package | 2 | 82% | shared libraries, linked graphics, gradients and patterns as library items (no cloud by design) | 2–3 | estimated | | Generative AI (Text to Vector, Generative Recolor, Shape Fill, Concept to Vector, Prompt to Edit, Rewrite Text) | 2 | 0% | a pluggable provider and the panels; needs an owner decision on models | 30–60 | measured (absent) | | **Feature depth** | **105** | **~75%** | | **~250–420** | | @@ -679,7 +680,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - The desktop app keeps the Color panel's Show Options or Hide Options between sessions, and `ui.colorPanel` sets its mode and options as its panel menu does. - Preferences that act (#394, in part): Show Tool Tips, Double Click To Isolate, Zoom with Mouse Wheel (the wheel zooms about the pointer, Shift-wheel scrolls up and down, Cmd/Ctrl-wheel sideways; Alt/Option-wheel and a trackpad pinch zoom about the pointer either way), Use Precise Cursors; Selection & Anchor Display › Tolerance (objects, anchors and handles), Object Selection by Path Only, Command Click to Select Objects Behind (cycling down the stack), Zoom to Selection, anchor Size, Highlight anchors on mouse over; Type › Number of Recent Fonts (up to 15); Guides & Grid › guide and grid Color and Style (lines or dots) and Grids In Back. Then Snap to Point (its distance, for moves, drawn points and reference points), Move Locked and Hidden Artwork with Artboard, Enable Rubber Band for the Pen and Curvature tools, Handles style, Show handles when multiple anchors are selected, Hide Corner Widget for angles greater than; General › Transform Pattern Tiles (the transforms' and dialogs' Transform Patterns, Align and Distribute, art moved or duplicated with an artboard, and objects moved whole with direct-selected anchors) and Select Same Tint %; Type › Size/Leading, Tracking and Baseline Shift increments (`type.step`, Cmd+Shift+. and Cmd+Shift+, for the size, the Type tool's Alt+arrows), Enable Missing Glyph Protection and Fill New Type Objects With Placeholder Text. Then Smart Guides › Color, Alignment Guides, Anchor/Path Labels, Measurement Labels, Transform Tools, Object Highlighting (with Smart Guides on) and Snapping Tolerance (4 px by default, for every smart guide snap), then Construction Guides and their Angles (#506: a drawn point near a line at one of those angles through the point it leaves from lands on it). Then Type › Type Object Selection by Path Only (type is picked on the baseline of any of its lines, area type's frame or the path of type on a path, no longer anywhere in its bounds; the Type tools and the Eyedropper still take a click among the characters). Then General › Anti-aliased Artwork (off, the canvas draws the art with hard edges). Then Guides & Grid › Show Pixel Grid (Above 600% Zoom), a line at every document pixel over the art in Pixel Preview, turning with a rotated view as the document grid now does too. Then General › Show The Home Screen When No Documents Are Open (off, closing the last document leaves an empty window; the Home button still opens the screen). Then Units › Numbers Without Units Are Points (on by default: a number typed with no unit into a field in picas is read in points, a typed unit still wins; dimmed unless a unit is Picas). Then User Interface › Auto-Collapse Iconic Panels (off by default: on, a click away from a panel popped out of the icon column puts it away). Then Performance › Animated Zoom (on, with GPU Performance, which it needs and which dims it when off: the Zoom tool dragged sideways zooms about the press, right in and left out, and held still zooms on, Alt out; off, a drag zooms to the area dragged across, now drawn as a marquee), User Interface › Large Tabs (taller document tabs) and Scale Cursor Proportional to UI (off, the tool cursors keep their size whatever the UI Scaling), and File Handling › Display Bitmaps as Anti-aliased Images in Pixel Preview (off by default: Pixel Preview draws images with each pixel taking its nearest image pixel). Type › Auto Size New Area Type works too (new area type gets Auto Size). They reach agents too (`prefs.set`, then `pointer_gesture` or `press_key`; `ui.wheel` on the control channel). Then Smart Guides › Spacing Guides (moves space the selection evenly with its row or column, see Drawing aids). Then Hyphenation › Exceptions (comma-separated words that never break, or break only at their hyphens; `prefs.set` and restore push them into the hyphenator, #394). Then Type › Show Font Names in English (off, font menus label a family by its native-language name when the font has one; documents still use the English family, #394). Then General › Display Print Size at 100% Zoom (off by default: View › Actual Size is one document point per screen point; on, one document inch fills 96 screen points, an inch at the reference density display scaling is set against; `view.actualSize` / `prefs.set`, #394). Then Guides & Grid › Gridline every and Subdivisions (changing either sets the grid of every open document; new and opened documents take that grid too, except a native file and the native document an SVG, PDF, `.ai` or EPS file carries, which open with the grid they were saved with; `file.new`, `document.open`, `file.newFromTemplate`, `file.revert`, #394). Then Appearance of Black › On Screen (Display All Blacks Accurately by default: pure CMYK black shows through the CMYK profile, a dark grey; Display All Blacks as Rich Black shows it on the canvas as RGB black, also in CMYK documents drawn by ink planes, while rich blacks, black tints and images show as before; `RenderOptions::rich_black`, #394). Then Appearance of Black › Printing / Exporting (Output All Blacks Accurately by default; Output All Blacks as Rich Black draws pure CMYK black as RGB black in RGB raster exports and Print as Bitmap; CMYK ink exports and the document stay unchanged; `prefs.blackOutput` / `richBlack`, #394). Then Units › Identify Objects By (Object Name by default: Layers, Variables and Layer Options show and edit free names; XML ID shows and edits AI-style XML ids, `_` for a space and `_xHH_` escapes, and `layer.setProps` / rename refuse invalid ids; the document still stores the object name, #394). Then Units › East Asian Type (the bullet below). Then User Interface › Open Documents As Tabs (see Window › Arrange). Three stored options still do nothing yet (the audit below). - Preferences › Type › Highlight Substituted Fonts (on by default) paints pink behind text drawn in a substitute for a missing font in every document, and turning it off removes that highlight everywhere (`prefs.set highlightSubstitutedFonts`); Document Setup keeps Highlight Substituted Glyphs ([help](https://helpx.adobe.com/illustrator/desktop/design-with-text/fonts-and-scripts/edit-text-without-replacing-missing-fonts.html)). - - Preferences audit (2026-10-30, #394): every Preferences dialog option is read by shipped code, by its field or its key, except Hyphenation › Default Language, Plug-ins & Scratch Disks › Primary and Secondary, and Clipboard Handling › Legacy vector clipboard and its mode, which are stored only (Units › East Asian Type and User Interface › Open Documents As Tabs, stored only on the day of the audit, act now: East Asian Type since 2026-10-31, Open Documents As Tabs as the Window › Arrange bullet says); [gaps.md G12](gaps.md#g12-preferences-and-panels-that-dont-act-yet) says what each waits on. + - Preferences audit (2026-10-30, #394): every Preferences dialog option is read by shipped code, by its field or its key, except Plug-ins & Scratch Disks › Primary and Secondary, and Clipboard Handling › Legacy vector clipboard and its mode, which are stored only (Units › East Asian Type and User Interface › Open Documents As Tabs, stored only on the day of the audit, act now: East Asian Type since 2026-10-31, Open Documents As Tabs as the Window › Arrange bullet says); [gaps.md G12](gaps.md#g12-preferences-and-panels-that-dont-act-yet) says what each waits on. - Units › East Asian Type (#394, `unitsAsianType`): Points by default; it offers every unit and Q and Ha, a quarter of a millimetre each (0.709 pt: the [Illustrator Scripting Guide](https://ai-scripting.docsforadobe.dev/scripting/measurementUnits/); `q` and `h` are the field suffixes [InDesign's notation table](https://helpx.adobe.com/in/indesign/desktop/layout-and-grid-tools/rulers-and-measure-tools/change-ruler-measurement-units.html) lists), and only it lists them: rulers, Document Setup and the other Units preferences keep their ten. The public pages do not say which fields follow it, so no field uses it yet and that waits for a source. The model stays in points; any length field reads `13q`/`13h`. Evidence: `q_and_ha_are_a_quarter_millimetre`, `east_asian_type_unit_*` (`crates/engine/src/tests/units.rs`). - Preferences audit (2026-10-30, #394): every Preferences dialog option is read by shipped code, by its field or its key, except User Interface › Open Documents As Tabs, Units › East Asian Type, Hyphenation › Default Language, Plug-ins & Scratch Disks › Primary and Secondary, and Clipboard Handling › Legacy vector clipboard and its mode, which are stored only; [gaps.md G12](gaps.md#g12-preferences-and-panels-that-dont-act-yet) says what each waits on. - With Performance › Real-time Drawing and Editing off, the bounding box moves with a Selection-tool move drag and the object moves when the button is released ([help](https://web.archive.org/web/20220324053041id_/https://helpx.adobe.com/illustrator/using/real-time-editing.html); `real_time_drawing_off_moves_the_bounding_box_with_a_move_drag`). diff --git a/docs/ui-parity.md b/docs/ui-parity.md index ef71d5ce0..7b5011024 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -30,6 +30,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| +| 2026-11-05 | minor | G12: Hyphenation › Default Language sets the language of new type unless Document Setup chose one, stored as a tag; each run hyphenates in its own language, English only so far, so 2 Preferences options stay unused (#394) | | 2026-11-05 | minor | Gradient panel: Delete Stop sits at the right end of the ramp, so it shows whenever the ramp does instead of under the stop fields, where a short panel hid it (#5403); scores and estimates unchanged | | 2026-11-04 | minor | Artboards panel: a Shift- or Ctrl/Cmd-click on a row selects several artboards with any tool, switching to the Artboard tool first, so a preset, orientation, size or Fill set in Properties or the Control bar goes to every selected artboard; Fill used to set the active artboard only (#5698); scores and estimates unchanged | | 2026-11-04 | minor | Isolation mode: once the layer, or a group, around the isolated object is locked or hidden, a click or a marquee with the Selection tool no longer selects the isolated art (part of #5309); scores and estimates unchanged | @@ -367,7 +368,7 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po | Swatch, graphic style and symbol library panels: each library opens in a panel of its own, several at once, that floats, groups, docks and closes like any panel; opening an open library brings its panel forward; Previous / Next Library change the library in place; the panel menu's Persistent reopens the panel at startup with its view and find field (a library loaded from a file is read again; when its file is missing the panel reopens showing the file's path with Locate Library…, and the status bar names the file) | done | #2603; panel ids `library::`, `ui.libraryPanel {panel, library?, view?, showFind?, persistent?}`, `ui.libraryPanel.locate {panel, path?}`; the library panels have no sort option, as before | | DXF export/import preserves intended curve closure | done | #1299; R13–2018 closed SPLINE bit set/read; an open curve ending at its start stays open; knots and control points unchanged; native CIRCLE/ARC export and CNC preset remain pending | | macOS save sheet: choose the format from a menu | done | #866; the save panel is VectorCraft's own `NSSavePanel` with its Format menu set before the sheet opens (`apps/vectorcraft/src/mac_save_format.rs`); #1855 had withdrawn the menu that was added after the sheet began, which made Save As, Save a Copy and the first Save show no sheet | -| Preferences: every option acts | partial | 3 stored but unused (#394, audited 2026-10-30): Hyphenation › Default Language, Scratch Disks, Legacy vector clipboard; the rest act (Units › East Asian Type since 2026-10-31; Open Documents As Tabs: cleared, new documents open in floating windows). General › Corner Radius drives the Rounded Rectangle tool and follows its ↑/↓ (2026-10-24) | +| Preferences: every option acts | partial | 2 stored but unused (#394, audited 2026-10-30): Scratch Disks, Legacy vector clipboard; the rest act (Units › East Asian Type since 2026-10-31; Open Documents As Tabs: cleared, new documents open in floating windows). General › Corner Radius drives the Rounded Rectangle tool and follows its ↑/↓ (2026-10-24) | | Preferences › General › Constrain Angle | done | arrow keys and Shift drags along the turned axes; the Rectangle, Rounded Rectangle, Ellipse and grid tools, point and area type and the shape size dialogs draw along them with the bounding box turned; Shift-drawn polygons and stars keep the turned base orientation; the Line Segment tool's Shift steps count from it (#394, ); arcs, spirals, flares and Pen paths are not turned | | Tabs panel: a stop drags from where the press landed, a stop just added included | done | #1458 | | Outline view keeps template layers in Preview, dimmed | done | #1486; a picture placed to be traced stays visible under the outlines |