mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 00:17:42 +00:00
Text Tweaker: refuse ranges that aren't whole bytes, make no step when nothing changes (#4924)
A `start` or `end` such as -1 or 1.5 was read as absent and changed text before the range; it is now refused. Characters that already have the value are left out, so a run that changes nothing returns count 0 and makes no undo step. docs/mcp.md cites the public formatting-type page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
bflatastic
co-authored by
Claude Opus 5.5
parent
0dba3afea9
commit
bf58f605fe
@@ -89,29 +89,58 @@ fn text_tweaker(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mut rng = Rng::new(whole_or(p, "seed", 0, C)?);
|
||||
let skip = switch_or(p, "skipSpaces", true, C)?;
|
||||
let setup = s.doc()?.doc.setup.clone();
|
||||
// A range given as anything but whole bytes is refused, not read as all of the text.
|
||||
for k in ["start", "end"] {
|
||||
if p.get(k).is_some_and(|v| !v.is_null() && v.as_u64().is_none()) {
|
||||
return Err(bad(C, format!("`{k}` must be a whole number of bytes")));
|
||||
}
|
||||
}
|
||||
// At a caret, all of the object's text.
|
||||
let ranges = type_ranges(s, p, C, |text, _| Some((0, text.len())))?;
|
||||
// Each object's changed characters: (byte, its change), worked out before the edit.
|
||||
let mut plans: Vec<(NodeId, Vec<(usize, CharChange)>)> = vec![];
|
||||
let mut count = 0;
|
||||
let (mut seen, mut count) = (0, 0);
|
||||
for (id, a, b, _) in ranges {
|
||||
let t = text_ref(s, id)?;
|
||||
let text = t.plain_text();
|
||||
let chars: Vec<usize> =
|
||||
text.char_indices().filter(|&(i, c)| (a..b).contains(&i) && !(skip && (c.is_whitespace() || c.is_control()))).map(|(i, _)| i).collect();
|
||||
count += chars.len();
|
||||
if count > MAX_CHARS {
|
||||
seen += chars.len();
|
||||
if seen > MAX_CHARS {
|
||||
return Err(bad(C, format!("too many characters: at most {MAX_CHARS}")));
|
||||
}
|
||||
// Each run's end byte, to find the style a character has now.
|
||||
let ends: Vec<usize> = t
|
||||
.runs
|
||||
.iter()
|
||||
.scan(0, |off, r| {
|
||||
*off += r.text.len();
|
||||
Some(*off)
|
||||
})
|
||||
.collect();
|
||||
let n = chars.len();
|
||||
let mut changes = Vec::with_capacity(n);
|
||||
let mut run = 0;
|
||||
for (i, at) in chars.into_iter().enumerate() {
|
||||
let random = if shape == "random" { unit(&mut rng) } else { 0.0 };
|
||||
let value = from + (to - from) * amount(shape, i, n, random);
|
||||
let mut change = CharChange::parse(&json!({ key: value }), C, &setup)?;
|
||||
change.localize(&t, C)?;
|
||||
while ends.get(run).is_some_and(|&e| e <= at) {
|
||||
run += 1;
|
||||
}
|
||||
// A character that already has the value is left out, so a run that changes nothing
|
||||
// makes no step.
|
||||
if let Some(r) = t.runs.get(run) {
|
||||
let mut style = r.style.clone();
|
||||
change.apply(&mut style);
|
||||
if style == r.style {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
changes.push((at, change));
|
||||
}
|
||||
count += changes.len();
|
||||
if !changes.is_empty() {
|
||||
plans.push((id, changes));
|
||||
}
|
||||
|
||||
@@ -62,7 +62,8 @@ fn a_straight_ramp_sizes_each_letter_and_skips_spaces() {
|
||||
select(&mut s, &[id]);
|
||||
let n = steps(&s);
|
||||
let out = s.execute(C, &json!({"attribute": "size", "from": 10, "to": 40})).unwrap();
|
||||
assert_eq!(out, json!({"count": 4, "ids": [id.0]}));
|
||||
// `b` is 20 pt already: three characters change.
|
||||
assert_eq!(out, json!({"count": 3, "ids": [id.0]}));
|
||||
assert_eq!(steps(&s), n + 1, "one undo step");
|
||||
let sizes: Vec<(char, f64)> = styles(&s, id).into_iter().map(|(c, st)| (c, st.size)).collect();
|
||||
assert_eq!(sizes, [('a', 10.0), ('b', 20.0), (' ', 20.0), ('c', 30.0), ('d', 40.0)], "the space keeps its size");
|
||||
@@ -108,7 +109,8 @@ fn each_object_gets_its_own_ramp() {
|
||||
let (a, b) = (text(&mut s, "abc"), text(&mut s, "de"));
|
||||
select(&mut s, &[a, b]);
|
||||
let out = s.execute(C, &json!({"attribute": "rotation", "from": 0, "to": 30})).unwrap();
|
||||
assert_eq!(out["count"], json!(5));
|
||||
// Each first character is upright already.
|
||||
assert_eq!(out["count"], json!(3));
|
||||
let turns = |id| styles(&s, id).iter().map(|(_, st)| st.rotation).collect::<Vec<_>>();
|
||||
assert_eq!((turns(a), turns(b)), (vec![0.0, 15.0, 30.0], vec![0.0, 30.0]));
|
||||
}
|
||||
@@ -161,6 +163,19 @@ fn empty_text_and_only_spaces_change_nothing() {
|
||||
assert_eq!(steps(&s), n, "no step");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn characters_that_already_have_the_value_are_left_out() {
|
||||
let mut s = session();
|
||||
let id = text(&mut s, "abc");
|
||||
select(&mut s, &[id]);
|
||||
let p = json!({"attribute": "rotation", "from": 0, "to": 30});
|
||||
// The first character already has 0°.
|
||||
assert_eq!(s.execute(C, &p).unwrap(), json!({"count": 2, "ids": [id.0]}));
|
||||
let (before, n) = (s.doc().unwrap().doc.clone(), steps(&s));
|
||||
assert_eq!(s.execute(C, &p).unwrap(), json!({"count": 0, "ids": []}), "again: nothing to change");
|
||||
assert_eq!((s.doc().unwrap().doc.clone(), steps(&s)), (before, n), "no step");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_params_are_refused_without_a_change() {
|
||||
let mut s = session();
|
||||
@@ -179,6 +194,9 @@ fn bad_params_are_refused_without_a_change() {
|
||||
json!({"seed": -1}),
|
||||
json!({"skipSpaces": "yes"}),
|
||||
json!({"ids": [999_999]}),
|
||||
json!({"id": id.0, "start": -1}),
|
||||
json!({"id": id.0, "start": 1, "end": 1.5}),
|
||||
json!({"end": "2"}),
|
||||
] {
|
||||
assert!(s.execute(C, &p).is_err(), "{p}");
|
||||
assert_eq!(s.doc().unwrap().doc, before, "{p}");
|
||||
|
||||
+2
-1
@@ -6838,7 +6838,8 @@ there is no step. → `{symbols: [name…], ids: [instance id…], skipped: [{la
|
||||
Type Tools is a built-in add-on for type: its command is `addon.type.textTweaker`, and in the desktop app it lives in
|
||||
Object › Plug-ins › Type Tools (#4924). Text Tweaker gives one character attribute to the characters letter by letter,
|
||||
from a start value to an end value along a shape, as one undo step; the text stays live and editable. Each value is set
|
||||
as `text.setRangeStyle` sets it (the Character panel), in its units and ranges, so on scaled type it is the size shown.
|
||||
as `text.setRangeStyle` sets it (the Character panel), in its units and ranges ([formatting type](https://helpx.adobe.com/illustrator/using/formatting-type.html)), so on scaled
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user