From af14fe3d5904c4659b8891068fe0c46824583c9a Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 4 Nov 2026 02:19:23 -0800 Subject: [PATCH] Align Text Tweaker ranges to character boundaries --- crates/engine/src/cmd/addons/typetools.rs | 3 ++- crates/engine/src/cmd/addons/typetools/tests.rs | 10 ++++++++++ docs/mcp.md | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/cmd/addons/typetools.rs b/crates/engine/src/cmd/addons/typetools.rs index 320dce6b9..a478b8c00 100644 --- a/crates/engine/src/cmd/addons/typetools.rs +++ b/crates/engine/src/cmd/addons/typetools.rs @@ -46,7 +46,7 @@ pub fn specs() -> Vec { "Text Tweaker", ["Object", "Plug-ins", "Type Tools"], None, - "{attribute?: \"size\" (pt 0.1..1296) | \"baselineShift\" (pt −1296..1296) | \"rotation\" (° −360..360) | \"hScale\" (% 1..10000) | \"vScale\" (% 1..10000) | \"tracking\" (1/1000 em −1000..10000) | \"leading\" (pt 0.1..5000) | \"strokeWidth\" (pt 0..1000) (\"size\"), from?: the first character's value, to?: the last character's value (each in the attribute's units and range; defaults: size 12 → 36, baselineShift 0 → 12, rotation 0 → 30, hScale and vScale 100 → 150, tracking 0 → 200, leading 12 → 36, strokeWidth 0 → 2), shape?: \"straight\" (an even ramp) | \"ease\" (slow at both ends) | \"bow\" (from at both ends, rising smoothly to `to` in the middle) | \"spike\" (from at both ends, straight up to `to` in the middle) | \"random\" (each character anywhere from `from` to `to`) (\"straight\"), seed?: whole number (0; random: the same seed and text give the same values), skipSpaces?: true (spaces and line breaks keep their values and don't count), id?, start?: byte, end?: byte, ids?: [id…] (instead of id)} give the characters one character attribute letter by letter, from `from` to `to` along the shape, as the Character panel sets it, as one undo step; the text stays live and editable. The range of `id`, else the type objects in `ids`, else the text the Type tool has selected (at a caret: all its text), else every selected type object; each object gets its own ramp. Stroke weight only shows on characters with a stroke → {count (characters changed), ids}", + "{attribute?: \"size\" (pt 0.1..1296) | \"baselineShift\" (pt −1296..1296) | \"rotation\" (° −360..360) | \"hScale\" (% 1..10000) | \"vScale\" (% 1..10000) | \"tracking\" (1/1000 em −1000..10000) | \"leading\" (pt 0.1..5000) | \"strokeWidth\" (pt 0..1000) (\"size\"), from?: the first character's value, to?: the last character's value (each in the attribute's units and range; defaults: size 12 → 36, baselineShift 0 → 12, rotation 0 → 30, hScale and vScale 100 → 150, tracking 0 → 200, leading 12 → 36, strokeWidth 0 → 2), shape?: \"straight\" (an even ramp) | \"ease\" (slow at both ends) | \"bow\" (from at both ends, rising smoothly to `to` in the middle) | \"spike\" (from at both ends, straight up to `to` in the middle) | \"random\" (each character anywhere from `from` to `to`) (\"straight\"), seed?: whole number (0; random: the same seed and text give the same values), skipSpaces?: true (spaces and line breaks keep their values and don't count), id?, start?: byte, end?: byte (range endpoints inside a character round down to its start), ids?: [id…] (instead of id)} give the characters one character attribute letter by letter, from `from` to `to` along the shape, as the Character panel sets it, as one undo step; the text stays live and editable. The range of `id`, else the type objects in `ids`, else the text the Type tool has selected (at a caret: all its text), else every selected type object; each object gets its own ramp. Stroke weight only shows on characters with a stroke → {count (characters changed), ids}", has_selection, text_tweaker )] @@ -97,6 +97,7 @@ fn text_tweaker(s: &mut Session, p: &Value) -> Result { for (id, a, b, _) in ranges { let t = text_ref(s, id)?; let text = t.plain_text(); + let (a, b) = (edit::floor_char(&text, a), edit::floor_char(&text, b)); let chars: Vec = text.char_indices().filter(|&(i, c)| (a..b).contains(&i) && !(skip && (c.is_whitespace() || c.is_control()))).map(|(i, _)| i).collect(); seen += chars.len(); diff --git a/crates/engine/src/cmd/addons/typetools/tests.rs b/crates/engine/src/cmd/addons/typetools/tests.rs index 6c56f6769..7f05cb823 100644 --- a/crates/engine/src/cmd/addons/typetools/tests.rs +++ b/crates/engine/src/cmd/addons/typetools/tests.rs @@ -124,6 +124,16 @@ fn a_range_changes_only_its_characters() { assert_eq!(tracking, [0.0, 100.0, 200.0, 300.0, 0.0]); } +#[test] +fn a_range_inside_a_multibyte_character_uses_character_boundaries() { + let mut s = session(); + let id = text(&mut s, "éx"); + let out = s.execute(C, &json!({"id": id.0, "start": 1, "end": 2, "attribute": "tracking", "from": 100, "to": 300})).unwrap(); + assert_eq!(out["count"], 1); + let tracking: Vec = styles(&s, id).iter().map(|(_, st)| st.tracking).collect(); + assert_eq!(tracking, [100.0, 0.0]); +} + #[test] fn random_values_stay_in_range_and_repeat_with_the_seed() { let mut s = session(); diff --git a/docs/mcp.md b/docs/mcp.md index 837b0ad24..6cfb9dd57 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -6921,6 +6921,7 @@ type it is the size shown. It changes the range of `id` (`start`/`end` bytes), else all the text of the type objects in `ids`, else the text the Type tool has selected (at a caret: all of that object's text), else all the text of every selected type object; each object gets its own ramp, and at most 100000 characters change in one run. A run that changes nothing makes no step. +A `start` or `end` byte inside a character rounds down to that character's start. | Parameter | Values (default) | |---|---|