From 8b6e358f0c89a4626a1c0fee2492030a7298f9d4 Mon Sep 17 00:00:00 2001 From: Don Omondi <282319807+mockydevs@users.noreply.github.com> Date: Sat, 7 Nov 2026 07:31:19 -0500 Subject: [PATCH 1/2] Alt + keypad codes: with NumLock off the keypad keys count as digits and don't move the caret Follow-up to #5939. With NumLock off the keypad sends Insert, End, arrows, Clear, Home and Page Up/Down; Windows still composes the code from them, but the app also moved the caret, so the character landed elsewhere. With Alt held, those keys without the extended-key flag now count as digits and are taken out of the frame; the separate navigation keys keep their meaning. --- apps/vectorcraft/src/windows_alt_codes.rs | 190 +++++++++++++++++----- docs/type-parity.md | 5 +- 2 files changed, 154 insertions(+), 41 deletions(-) diff --git a/apps/vectorcraft/src/windows_alt_codes.rs b/apps/vectorcraft/src/windows_alt_codes.rs index a4ef28dbb..69b5f1d6a 100644 --- a/apps/vectorcraft/src/windows_alt_codes.rs +++ b/apps/vectorcraft/src/windows_alt_codes.rs @@ -13,6 +13,13 @@ //! same undo step as the typing round it. The keypad digits themselves are taken out of the frame, //! so they don't run a shortcut bound to Alt and a digit. //! +//! With NumLock off the keypad keys arrive as Insert, End, Down, Page Down, Left, Clear, Right, +//! Home, Up and Page Up, and Windows still composes the code from them. They are told from the +//! separate navigation keys by the extended-key flag, which only the separate keys set +//! (), +//! so with Alt held they count as digits and are taken out of the frame too, rather than moving +//! the caret before the character arrives. +//! //! egui reports a keypad digit as the same key as the digit above the letters, so the other //! platforms, whose systems have no such convention, can't tell them apart either. @@ -28,10 +35,20 @@ const WM_CHAR: u32 = 0x0102; const WM_SYSKEYDOWN: u32 = 0x0104; const WM_SYSKEYUP: u32 = 0x0105; const WM_SYSCHAR: u32 = 0x0106; -/// `VK_NUMPAD0` to `VK_NUMPAD9` (`winuser.h`): keypad digits with NumLock on. With it off the keys -/// are Insert, End, … and type no code. +/// `VK_NUMPAD0` to `VK_NUMPAD9` (`winuser.h`): keypad digits with NumLock on. const VK_NUMPAD0: usize = 0x60; const VK_NUMPAD9: usize = 0x69; +/// The keypad's virtual keys with NumLock off, by digit (`winuser.h`): `VK_INSERT`, `VK_END`, +/// `VK_DOWN`, `VK_NEXT`, `VK_LEFT`, `VK_CLEAR`, `VK_RIGHT`, `VK_HOME`, `VK_UP` and `VK_PRIOR`. +const NUMLOCK_OFF_KEYS: [usize; 10] = [0x2D, 0x23, 0x28, 0x22, 0x25, 0x0C, 0x27, 0x24, 0x26, 0x21]; +/// The keys egui reports for the keypad with NumLock off, by digit (Clear has no egui key, so it +/// comes as the keypad's own key, the digit). +const NUMLOCK_OFF_EGUI_KEYS: [Key; 10] = + [Key::Insert, Key::End, Key::ArrowDown, Key::PageDown, Key::ArrowLeft, Key::Num5, Key::ArrowRight, Key::Home, Key::ArrowUp, Key::PageUp]; +const DIGIT_KEYS: [Key; 10] = [Key::Num0, Key::Num1, Key::Num2, Key::Num3, Key::Num4, Key::Num5, Key::Num6, Key::Num7, Key::Num8, Key::Num9]; +/// Bit 24 of a key message's `lParam`: an extended key, such as the navigation keys beside the +/// keypad (the keypad's own keys clear it). +const EXTENDED_KEY: isize = 1 << 24; /// `VK_MENU`, `VK_LMENU` and `VK_RMENU`: Alt. const ALT_KEYS: [usize; 3] = [0x12, 0xA4, 0xA5]; @@ -51,23 +68,42 @@ struct AltCodes { high: Option, /// Characters the codes typed, for the next frame. typed: String, - /// Keypad digit presses and releases with Alt held since the last frame, which egui reports - /// as Alt and a digit: taken out of the frame. - keypad_events: usize, + /// The digits of the keypad presses and releases with Alt held since the last frame, which + /// egui reports as Alt and a digit (or, with NumLock off, Alt and a navigation key): taken + /// out of the frame. + keypad_events: Vec, +} + +/// The digit a keypad key stands for in an Alt code: `wparam` is its virtual-key code and +/// `lparam` the message's flags. +fn keypad_digit(wparam: usize, lparam: isize) -> Option { + if (VK_NUMPAD0..=VK_NUMPAD9).contains(&wparam) { + return Some(wparam - VK_NUMPAD0); + } + if lparam & EXTENDED_KEY != 0 { + return None; + } + NUMLOCK_OFF_KEYS.iter().position(|&k| k == wparam) } impl AltCodes { - /// Note a keyboard message `message` with `wparam` (the virtual-key code, or a UTF-16 unit). - fn message(&mut self, message: u32, wparam: usize) { - let keypad = (VK_NUMPAD0..=VK_NUMPAD9).contains(&wparam); - match message { + /// Note a keyboard message `message` with `wparam` (the virtual-key code, or a UTF-16 unit) + /// and `lparam` (its flags). + fn message(&mut self, message: u32, wparam: usize, lparam: isize) { + let keypad = match message { + WM_SYSKEYDOWN | WM_SYSKEYUP => keypad_digit(wparam, lparam), + _ => None, + }; + if let Some(d) = keypad { // Windows sends a key pressed with Alt held as a system key. - WM_SYSKEYDOWN if keypad => { + if message == WM_SYSKEYDOWN { self.digits += 1; - self.keypad_events += 1; self.armed = false; } - WM_SYSKEYUP if keypad => self.keypad_events += 1, + self.keypad_events.push(d); + return; + } + match message { WM_KEYDOWN | WM_SYSKEYDOWN if ALT_KEYS.contains(&wparam) => {} // Any other key ends the sequence and keeps its own meaning. WM_KEYDOWN | WM_SYSKEYDOWN => self.reset(), @@ -99,14 +135,23 @@ impl AltCodes { /// out the keypad digits typed with Alt held. fn drain(&mut self, events: &mut Vec) { let mut keypad = std::mem::take(&mut self.keypad_events); - if keypad > 0 { + if !keypad.is_empty() { events.retain(|e| { - let digit = matches!(e, Event::Key { key: Key::Num0 | Key::Num1 | Key::Num2 | Key::Num3 | Key::Num4 | Key::Num5 | Key::Num6 | Key::Num7 | Key::Num8 | Key::Num9, modifiers, .. } if modifiers.alt); - if digit && keypad > 0 { - keypad -= 1; - return false; + let Event::Key { key, modifiers, .. } = e else { + return true; + }; + if !modifiers.alt { + return true; + } + // The first keypad key still to take out that egui reports as `key`. + let stands_for = |d: &usize| DIGIT_KEYS.get(*d) == Some(key) || NUMLOCK_OFF_EGUI_KEYS.get(*d) == Some(key); + match keypad.iter().position(stands_for) { + Some(i) => { + keypad.remove(i); + false + } + None => true, } - true }); } if !self.typed.is_empty() { @@ -129,7 +174,7 @@ pub fn msg_hook(msg: *const std::ffi::c_void) { if (WM_KEYDOWN..=WM_SYSCHAR).contains(&msg.message) { CODES.with(|c| { if let Ok(mut c) = c.try_borrow_mut() { - c.message(msg.message, msg.wParam.0); + c.message(msg.message, msg.wParam.0, msg.lParam.0); } }); } @@ -152,14 +197,14 @@ mod tests { /// Alt held, keypad digits `code`, Alt released, and the character Windows then posts. fn alt_code(c: &mut AltCodes, code: &str, posted: u16) { - c.message(WM_SYSKEYDOWN, ALT); + c.message(WM_SYSKEYDOWN, ALT, 0); for d in code.bytes() { let vk = VK_NUMPAD0 + usize::from(d - b'0'); - c.message(WM_SYSKEYDOWN, vk); - c.message(WM_SYSKEYUP, vk); + c.message(WM_SYSKEYDOWN, vk, 0); + c.message(WM_SYSKEYUP, vk, 0); } - c.message(WM_KEYUP, ALT); - c.message(WM_CHAR, usize::from(posted)); + c.message(WM_KEYUP, ALT, 0); + c.message(WM_CHAR, usize::from(posted), 0); } fn key(key: Key, alt: bool, pressed: bool) -> Event { @@ -186,11 +231,11 @@ mod tests { fn a_character_without_a_code_before_it_is_left_to_winit() { let mut c = AltCodes::default(); // A plain key press's character (winit types it), and Alt pressed and released alone. - c.message(WM_KEYDOWN, 0x41); - c.message(WM_CHAR, usize::from(b'a')); - c.message(WM_SYSKEYDOWN, ALT); - c.message(WM_KEYUP, ALT); - c.message(WM_CHAR, usize::from(b'b')); + c.message(WM_KEYDOWN, 0x41, 0); + c.message(WM_CHAR, usize::from(b'a'), 0); + c.message(WM_SYSKEYDOWN, ALT, 0); + c.message(WM_KEYUP, ALT, 0); + c.message(WM_CHAR, usize::from(b'b'), 0); let mut events = vec![]; c.drain(&mut events); assert!(events.is_empty(), "{events:?}"); @@ -199,12 +244,12 @@ mod tests { #[test] fn another_key_while_alt_is_held_cancels_the_code() { let mut c = AltCodes::default(); - c.message(WM_SYSKEYDOWN, ALT); - c.message(WM_SYSKEYDOWN, VK_NUMPAD0 + 1); + c.message(WM_SYSKEYDOWN, ALT, 0); + c.message(WM_SYSKEYDOWN, VK_NUMPAD0 + 1, 0); // Alt+F: a shortcut, not a code. - c.message(WM_SYSKEYDOWN, 0x46); - c.message(WM_KEYUP, ALT); - c.message(WM_CHAR, 0x41); + c.message(WM_SYSKEYDOWN, 0x46, 0); + c.message(WM_KEYUP, ALT, 0); + c.message(WM_CHAR, 0x41, 0); let mut events = vec![]; c.drain(&mut events); assert!(!events.iter().any(|e| matches!(e, Event::Text(_))), "{events:?}"); @@ -213,13 +258,80 @@ mod tests { #[test] fn a_code_above_the_basic_plane_arrives_as_two_halves() { let mut c = AltCodes::default(); - c.message(WM_SYSKEYDOWN, ALT); - c.message(WM_SYSKEYDOWN, VK_NUMPAD0 + 1); - c.message(WM_KEYUP, ALT); - c.message(WM_CHAR, 0xD83D); - c.message(WM_CHAR, 0xDE00); + c.message(WM_SYSKEYDOWN, ALT, 0); + c.message(WM_SYSKEYDOWN, VK_NUMPAD0 + 1, 0); + c.message(WM_KEYUP, ALT, 0); + c.message(WM_CHAR, 0xD83D, 0); + c.message(WM_CHAR, 0xDE00, 0); let mut events = vec![]; c.drain(&mut events); assert_eq!(events, vec![Event::Text("😀".into())]); } + + /// Alt held, keypad digits `code` with NumLock off (keypad Insert, End, …, flags without the + /// extended-key bit), Alt released, and the character Windows then posts. + fn alt_code_numlock_off(c: &mut AltCodes, code: &str, posted: u16) { + c.message(WM_SYSKEYDOWN, ALT, 0); + for d in code.bytes() { + let vk = NUMLOCK_OFF_KEYS[usize::from(d - b'0')]; + c.message(WM_SYSKEYDOWN, vk, 0); + c.message(WM_SYSKEYUP, vk, 0); + } + c.message(WM_KEYUP, ALT, 0); + c.message(WM_CHAR, usize::from(posted), 0); + } + + #[test] + fn with_numlock_off_the_keypad_keys_type_the_code_and_leave_the_caret_alone() { + let mut c = AltCodes::default(); + alt_code_numlock_off(&mut c, "0176", 0xB0); + // What egui reports for keypad 0, 1, 7 and 6 with NumLock off and Alt held, a press and a + // release each, then a plain Home typed after Alt came up. + let mut events = vec![]; + for k in [Key::Insert, Key::End, Key::Home, Key::ArrowRight] { + events.push(key(k, true, true)); + events.push(key(k, true, false)); + } + events.push(key(Key::Home, false, true)); + c.drain(&mut events); + assert_eq!(events, vec![key(Key::Home, false, true), Event::Text("°".into())]); + } + + #[test] + fn with_numlock_off_keypad_five_counts_too() { + let mut c = AltCodes::default(); + // Keypad 5 is Clear, which egui reports as the keypad's 5; 0151 uses every other kind. + alt_code_numlock_off(&mut c, "0151", 0x2014); + alt_code_numlock_off(&mut c, "0234", 0xEA); + let mut events = + vec![key(Key::Num5, true, true), key(Key::ArrowDown, true, true), key(Key::PageDown, true, true), key(Key::ArrowLeft, true, true)]; + c.drain(&mut events); + assert_eq!(events, vec![Event::Text("—ê".into())]); + } + + #[test] + fn the_separate_navigation_keys_still_move_the_caret_and_cancel_the_code() { + let mut c = AltCodes::default(); + c.message(WM_SYSKEYDOWN, ALT, 0); + c.message(WM_SYSKEYDOWN, NUMLOCK_OFF_KEYS[1], 0); + c.message(WM_SYSKEYUP, NUMLOCK_OFF_KEYS[1], 0); + // Alt+Home on the separate Home key (extended): a shortcut, not a digit. + c.message(WM_SYSKEYDOWN, NUMLOCK_OFF_KEYS[7], EXTENDED_KEY); + c.message(WM_SYSKEYUP, NUMLOCK_OFF_KEYS[7], EXTENDED_KEY); + c.message(WM_KEYUP, ALT, 0); + c.message(WM_CHAR, 0x41, 0); + let mut events = vec![key(Key::End, true, true), key(Key::End, true, false), key(Key::Home, true, true), key(Key::Home, true, false)]; + c.drain(&mut events); + assert_eq!(events, vec![key(Key::Home, true, true), key(Key::Home, true, false)]); + } + + #[test] + fn numlock_off_keys_without_alt_are_left_alone() { + let mut c = AltCodes::default(); + c.message(WM_KEYDOWN, NUMLOCK_OFF_KEYS[7], 0); + c.message(WM_KEYUP, NUMLOCK_OFF_KEYS[7], 0); + let mut events = vec![key(Key::Home, false, true), key(Key::Home, false, false)]; + c.drain(&mut events); + assert_eq!(events.len(), 2); + } } diff --git a/docs/type-parity.md b/docs/type-parity.md index 4b48fd84b..89820f910 100644 --- a/docs/type-parity.md +++ b/docs/type-parity.md @@ -1,6 +1,6 @@ # Type parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-07 · **Change:** minor (Typographer's Quotes sets a lone typed `”` as the opening quote where one opens, for the macOS Serbian Cyrillic layout, which has no straight quote, #5247; Indian-language hyphenation review and license allowlist, #4947; Bulgarian among the languages, with its quote marks and the fonts' Bulgarian forms, #5945; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-07 · **Change:** minor (Alt + keypad codes with NumLock off no longer move the caret, follow-up to #5939) · **Target:** Adobe Illustrator 2026 (30.x) Type is Illustrator's largest single area by use (weight 13 of 105 across "Type core" and "Type advanced" in [target-app-parity.md](target-app-parity.md)). This checklist lists the features one by one. Status is from the @@ -52,7 +52,7 @@ against Illustrator's public documentation; **estimated** unless noted. | Hidden characters (Show Hidden Characters) | done | `type.hiddenCharacters` | | Spell check (Check Spelling, Auto Spell Check, custom dictionary) | missing | menu stubs; needs an openly licensed dictionary | | Smart Punctuation, Change Case, Find & Replace | done | | -| Alt + numeric-keypad codes while typing (Windows: `Alt+0176` → °) | partial | #5108: on Windows the character Windows posts when Alt comes up after keypad digits is typed at the caret like any other, in the typing's undo step, and the keypad digits typed with Alt run no shortcut (`windows_alt_codes` tests; not yet tried on a Windows machine). `type.insert {code}` (Windows Alt+0 code, Windows-1252 for 128–159) and `{unicode}` (hex code point) insert a character by its code (`menucmds::insert_a_character_by_its_code`). macOS and Linux have no such system convention and egui reports a keypad digit as the top-row digit, so there it does nothing; a floating panel window doesn't receive the character | +| Alt + numeric-keypad codes while typing (Windows: `Alt+0176` → °) | partial | #5108: on Windows the character Windows posts when Alt comes up after keypad digits is typed at the caret like any other, in the typing's undo step, and the keypad digits typed with Alt run no shortcut; tried on Windows with NumLock on. With NumLock off the keypad keys (Insert, End, arrows, Clear, Home, Page Up/Down without the extended-key flag) count as digits too and don't move the caret (`windows_alt_codes` tests; NumLock off not yet retried on a Windows machine). `type.insert {code}` (Windows Alt+0 code, Windows-1252 for 128–159) and `{unicode}` (hex code point) insert a character by its code (`menucmds::insert_a_character_by_its_code`). macOS and Linux have no such system convention and egui reports a keypad digit as the top-row digit, so there it does nothing; a floating panel window doesn't receive the character | | Caret: the arrows and Delete step over whole clusters (Thai consonant with its marks, combining accents, emoji); Backspace removes one character | done | in text with right-to-left characters the arrows move in visual order and stop on both sides of a character drawn with no width, such as a zero width non-joiner | | Touch Type tool | done | moves, scales and rotates one character through its kerning, baseline shift, scales and rotation (`text.touchType`); a character with a preceding logical character on the same line can move along the line in either reading direction; the first logical character moves only up and down. Positive `dx` follows reading order. These direction rules are VectorCraft's choices. | | Retype (identify fonts in outlines/images) | missing | Illustrator's uses a cloud model | @@ -82,6 +82,7 @@ Character Styles menu New/Redefine and footer New capture the first selected cha | Date | Change | Summary | |---|---|---| +| 2026-11-07 | minor | Alt + numeric-keypad codes with NumLock off: the keypad keys arrive as Insert, End, arrows, Clear, Home and Page Up/Down, which Windows still composes into the code; with Alt held they now count as digits and are taken out of the frame instead of moving the caret, while the separate navigation keys (extended-key flag) keep their meaning (follow-up to #5939). Scores and estimates unchanged. | | 2026-11-07 | minor | Indian-language hyphenation (#4947): eleven MIT-licensed pattern sets, with grapheme and shaping-cluster protection in both composers and existing paragraph limits. Assamese and Sanskrit are selectable; Sanskrit automatic patterns remain pending an allowlisted source. Scores and estimates unchanged. | | 2026-11-06 | minor | Alt + numeric-keypad codes (`Alt+0176` → °) type their character at the caret on Windows, and `type.insert` inserts a character by `code` or `unicode` (#5108); scores and estimates unchanged | | 2026-11-06 | minor | Snap to Glyph › Glyph Bounds: each glyph's box gives four corners a drawn point lands on, also on a curved side (`B`), and its box and edges turn with rotated type (#5112); scores and estimates unchanged | From d8074562d1d22ff651cfd941b5677538807c3899 Mon Sep 17 00:00:00 2001 From: Don Omondi <282319807+mockydevs@users.noreply.github.com> Date: Sun, 8 Nov 2026 03:45:22 -0500 Subject: [PATCH 2/2] Alt + keypad codes: docs say the Windows path is unit-tested only; fix two comments The type parity row no longer says the NumLock-on path was tried on Windows: neither path has been run on a Windows machine yet. The comment on the NumLock-off egui keys says Clear comes as the top-row 5, and the test comment names the keys 0151 and 0234 actually use. --- apps/vectorcraft/src/windows_alt_codes.rs | 5 +++-- docs/type-parity.md | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/vectorcraft/src/windows_alt_codes.rs b/apps/vectorcraft/src/windows_alt_codes.rs index 69b5f1d6a..b370bf214 100644 --- a/apps/vectorcraft/src/windows_alt_codes.rs +++ b/apps/vectorcraft/src/windows_alt_codes.rs @@ -42,7 +42,7 @@ const VK_NUMPAD9: usize = 0x69; /// `VK_DOWN`, `VK_NEXT`, `VK_LEFT`, `VK_CLEAR`, `VK_RIGHT`, `VK_HOME`, `VK_UP` and `VK_PRIOR`. const NUMLOCK_OFF_KEYS: [usize; 10] = [0x2D, 0x23, 0x28, 0x22, 0x25, 0x0C, 0x27, 0x24, 0x26, 0x21]; /// The keys egui reports for the keypad with NumLock off, by digit (Clear has no egui key, so it -/// comes as the keypad's own key, the digit). +/// comes as the top-row digit key, `Num5`). const NUMLOCK_OFF_EGUI_KEYS: [Key; 10] = [Key::Insert, Key::End, Key::ArrowDown, Key::PageDown, Key::ArrowLeft, Key::Num5, Key::ArrowRight, Key::Home, Key::ArrowUp, Key::PageUp]; const DIGIT_KEYS: [Key; 10] = [Key::Num0, Key::Num1, Key::Num2, Key::Num3, Key::Num4, Key::Num5, Key::Num6, Key::Num7, Key::Num8, Key::Num9]; @@ -300,7 +300,8 @@ mod tests { #[test] fn with_numlock_off_keypad_five_counts_too() { let mut c = AltCodes::default(); - // Keypad 5 is Clear, which egui reports as the keypad's 5; 0151 uses every other kind. + // Keypad 5 is Clear, which egui reports as the top-row 5. 0151 uses Insert, End and Clear; + // 0234 uses Insert, Down, Page Down and Left. alt_code_numlock_off(&mut c, "0151", 0x2014); alt_code_numlock_off(&mut c, "0234", 0xEA); let mut events = diff --git a/docs/type-parity.md b/docs/type-parity.md index 89820f910..5675a804d 100644 --- a/docs/type-parity.md +++ b/docs/type-parity.md @@ -52,7 +52,7 @@ against Illustrator's public documentation; **estimated** unless noted. | Hidden characters (Show Hidden Characters) | done | `type.hiddenCharacters` | | Spell check (Check Spelling, Auto Spell Check, custom dictionary) | missing | menu stubs; needs an openly licensed dictionary | | Smart Punctuation, Change Case, Find & Replace | done | | -| Alt + numeric-keypad codes while typing (Windows: `Alt+0176` → °) | partial | #5108: on Windows the character Windows posts when Alt comes up after keypad digits is typed at the caret like any other, in the typing's undo step, and the keypad digits typed with Alt run no shortcut; tried on Windows with NumLock on. With NumLock off the keypad keys (Insert, End, arrows, Clear, Home, Page Up/Down without the extended-key flag) count as digits too and don't move the caret (`windows_alt_codes` tests; NumLock off not yet retried on a Windows machine). `type.insert {code}` (Windows Alt+0 code, Windows-1252 for 128–159) and `{unicode}` (hex code point) insert a character by its code (`menucmds::insert_a_character_by_its_code`). macOS and Linux have no such system convention and egui reports a keypad digit as the top-row digit, so there it does nothing; a floating panel window doesn't receive the character | +| Alt + numeric-keypad codes while typing (Windows: `Alt+0176` → °) | partial | #5108: on Windows the character Windows posts when Alt comes up after keypad digits is typed at the caret like any other, in the typing's undo step, and the keypad digits typed with Alt run no shortcut. With NumLock off the keypad keys (Insert, End, arrows, Clear, Home, Page Up/Down without the extended-key flag) count as digits too and don't move the caret (`windows_alt_codes` unit tests; neither NumLock on nor off has been tried on a Windows machine yet). `type.insert {code}` (Windows Alt+0 code, Windows-1252 for 128–159) and `{unicode}` (hex code point) insert a character by its code (`menucmds::insert_a_character_by_its_code`). macOS and Linux have no such system convention and egui reports a keypad digit as the top-row digit, so there it does nothing; a floating panel window doesn't receive the character | | Caret: the arrows and Delete step over whole clusters (Thai consonant with its marks, combining accents, emoji); Backspace removes one character | done | in text with right-to-left characters the arrows move in visual order and stop on both sides of a character drawn with no width, such as a zero width non-joiner | | Touch Type tool | done | moves, scales and rotates one character through its kerning, baseline shift, scales and rotation (`text.touchType`); a character with a preceding logical character on the same line can move along the line in either reading direction; the first logical character moves only up and down. Positive `dx` follows reading order. These direction rules are VectorCraft's choices. | | Retype (identify fonts in outlines/images) | missing | Illustrator's uses a cloud model |