Fix #4924: Text Tweaker ramps or randomises one character attribute letter by letter

A built-in Type Tools add-on (Object › Plug-ins › Type Tools › Text Tweaker…,
`addon.type.textTweaker`): size, baseline shift, rotation, horizontal or vertical
scale, tracking, leading or stroke weight go from a start value to an end value
along a straight ramp, an ease, a bow, a spike or at random with a seed, set as
the Character panel sets them, while the text stays live. One undo step, a
dialog with Preview, catalog rows in all 26 languages, docs/mcp.md and the
parity docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ruchirguitar
2026-11-03 20:07:58 +07:00
committed by bflatastic
co-authored by Claude Opus 5.5
parent 78a5b8b642
commit 8850b923ce
39 changed files with 830 additions and 6 deletions
+3 -1
View File
@@ -9,7 +9,7 @@
**Stage: alpha** · next: beta, ~13 points (ready for real work ~62% → ~75%, and reliable `.ai` exchange) and ~170–270 h away
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-02 · **Change:** minor (font-family fields accept the mouse wheel, #5253; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (Progress log row for the Type Tools add-on's Text Tweaker, #4924; headline numbers unchanged; previously: font-family fields accept the mouse wheel, #5253; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adobe Illustrator workflow. It runs
on macOS, Windows, Linux, FreeBSD and the web (WASM), and agents can drive all of it over MCP, the CLI and a JSON
@@ -138,6 +138,7 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap
- 2026-11-01: New Symbols from Layers joins the Document Tools add-on (Object › Plug-ins › Document Tools): one symbol of each top-level layer's art (the layers highlighted in the Layers panel when two or more are, else every layer), named after the layer, made as New Symbol makes one; Convert Layers to Symbols also puts an instance of each in place of its layer's art. Empty layers are skipped and reported; one undo step. A VectorCraft addition: headline numbers unchanged.
- 2026-11-01: Document Tools, a built-in add-on (Object › Plug-ins › Document Tools), starts with Save a Timestamped Copy: a copy of the document named `<name>_YYYY-MM-DD_HH-MM` (UTC; `-2`, `-3`… when that name is taken) goes next to it, or into a folder picked for a document never saved, written as File › Save a Copy writes it, so the document keeps its path, title and modified state. Off on the web, which has no clock. A VectorCraft addition: headline numbers unchanged.
- 2026-11-01: Divide into Dashes joins the Path Precision Tools add-on (Object › Plug-ins › Path Precision Tools): the selected paths, or only their direct-selected segments, are cut into separate open paths of a dash length measured along the curve, with an optional gap (left empty, or kept as pieces), each keeping the path's appearance; Fit to Corners and Ends stretches or squeezes the lengths so every run starts and ends on a dash. A dialog with Preview, one undo step, at most 100000 pieces. A VectorCraft addition: headline numbers unchanged.
- 2026-11-03: Text Tweaker, the first tool of the Type Tools add-on (Object › Plug-ins › Type Tools): one character attribute (size, baseline shift, rotation, horizontal or vertical scale, tracking, leading or stroke weight) goes letter by letter from a start value to an end value along a straight ramp, an ease, a bow, a spike or at random with a seed, set as the Character panel sets it, while the text stays live; a dialog with Preview, one undo step (`addon.type.textTweaker`, #4924). A VectorCraft addition: headline numbers unchanged.
- 2026-11-01: Group Overlapping Objects joins the Arrange Tools add-on (Object › Plug-ins › Arrange Tools): the selected objects whose visual bounds overlap or touch, chains included, go into one group each, within their own layer or group, at the stacking place of their frontmost member; objects that overlap nothing stay as they are. One undo step, a sweep along x so thousands of loose pieces group quickly (at most 20000 objects). A VectorCraft addition: headline numbers unchanged.
- 2026-10-31: Export for Screens works as the reference app's dialog: Artboards and Assets tabs with a grid or list of thumbnails, Clear Selection and Prefix, Create Sub-folders by Scale or Format (on by default), Export PDFs As Single File (the chosen artboards as one PDF's pages, `pdfs: "single"`) or Multiple Files, iOS and Android rows, format presets in a ☰ menu, and a count of the files the export writes (#4895). About 55 community PRs and fixes landed the same day: Find Font, Replace Font, Outline Stroke and Expand leave hidden and locked art alone (with a warning); DXF export writes LINE and ELLIPSE entities and outlines only the strokes a CAD line can't draw; Blob Brush Angle and Roundness; Shift-constrained Perspective Selection; Grid Repeat Options Preview; a gradient mesh from a file budgeted at about a million quads; width-profile strokes keep their shape zoomed out; `.ai`/EPS import fidelity (area type, envelopes, transparency in dictionaries, character styles); PDF import finds hyphenated and CMap-suffixed font names; Windows renders follow the process's core affinity; Enter or Escape in a floating panel's field no longer reaches the canvas (#4803); Properties shows Blend Options for any selected blend (#4817); Artwork Cleanup finds doubled type (#4835) and joins stacked lines into one object (Join Point Text, #4826). Headline numbers unchanged.
- 2026-10-31: Arrange Tools, a built-in add-on (Object › Plug-ins › Arrange Tools): Arrange in Grid lays the selected objects out in rows and columns, a row or a column with gaps, from the selection's top-left or centred on the artboard; Swap Objects swaps two objects' places (and, with Swap Objects and Sizes, their sizes); Replace with Top Object puts a copy of the topmost selected object in place of each of the others, kept, stretched or fitted in proportion; Set Size of Each gives every selected object an exact width and/or height about a reference point; Zoom to Selection fits the view to the selection. Each is one undo step with a previewed dialog where it has options. A VectorCraft addition: headline numbers unchanged.
@@ -215,6 +216,7 @@ The full earlier record of what landed is kept in [docs/roadmap.md](docs/roadmap
| Date | Change | Summary |
|---|---|---|
| 2026-11-03 | minor | Progress log row for the Type Tools add-on's Text Tweaker (#4924); headline numbers unchanged. |
| 2026-11-02 | minor | Closed font-family fields accept wheel input for selected text in Character, Properties and Control (#5253); scores and estimates unchanged. |
| 2026-11-01 | minor | Progress log row for the Color Effects add-on's Find Nearest Swatch (#4923); headline numbers unchanged. |
| 2026-11-01 | minor | Progress log row for the Path Precision Tools add-on's Expand Dashes into Paths (#4922); headline numbers unchanged. |
+2
View File
@@ -7,6 +7,7 @@ mod color;
mod document;
mod pathtools;
mod pen;
mod typetools;
use super::CommandSpec;
@@ -18,5 +19,6 @@ pub fn specs() -> Vec<CommandSpec> {
v.extend(document::specs());
v.extend(pathtools::specs());
v.extend(pen::specs());
v.extend(typetools::specs());
v
}
+165
View File
@@ -0,0 +1,165 @@
//! Type Tools (`addon.type.*`), a built-in add-on: Text Tweaker gives one character attribute to
//! the selected characters letter by letter, from a start value to an end value along a shape
//! (straight, eased, a bow, a spike, or at random), while the text stays live. Values are set as
//! the Character panel sets them (`text.setRangeStyle`), in its units and ranges
//! (<https://helpx.adobe.com/illustrator/using/formatting-type.html>). Its submenu
//! (Object › Plug-ins › Type Tools) and dialog are the UI's.
#[cfg(test)]
mod tests;
use serde_json::{Value, json};
use vectorcraft_color::recolor::Rng;
use vectorcraft_doc::{NodeId, TextRun};
use vectorcraft_text::edit;
use super::super::menucmds::whole_or;
use super::super::object::choice;
use super::super::textedit::{CharChange, text_mut, text_ref, type_ranges};
use super::super::typecmd::refresh_bounds;
use super::super::{CommandSpec, bad, cmd, has_selection, number_or, switch_or};
use crate::{EngineError, Result, Session};
/// An attribute Text Tweaker sets: its `text.setRangeStyle` param, the values it takes and its
/// default start and end.
type Attribute = (&'static str, (f64, f64), (f64, f64));
/// The attributes Text Tweaker sets.
pub const ATTRIBUTES: [Attribute; 8] = [
("size", (0.1, 1296.0), (12.0, 36.0)),
("baselineShift", (-1296.0, 1296.0), (0.0, 12.0)),
("rotation", (-360.0, 360.0), (0.0, 30.0)),
("hScale", (1.0, 10000.0), (100.0, 150.0)),
("vScale", (1.0, 10000.0), (100.0, 150.0)),
("tracking", (-1000.0, 10000.0), (0.0, 200.0)),
("leading", (0.1, 5000.0), (12.0, 36.0)),
("strokeWidth", (0.0, 1000.0), (0.0, 2.0)),
];
/// The shapes the values follow from the first character to the last.
pub const SHAPES: [&str; 5] = ["straight", "ease", "bow", "spike", "random"];
/// The most characters one run changes.
const MAX_CHARS: usize = 100_000;
pub fn specs() -> Vec<CommandSpec> {
vec![cmd!(
"addon.type.textTweaker",
"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}",
has_selection,
text_tweaker
)]
}
/// Where along `shape` (0 at `from`, 1 at `to`) character `i` of `n` lands; `random` for the
/// random shape.
fn amount(shape: &str, i: usize, n: usize, random: f64) -> f64 {
let t = if n > 1 { i as f64 / (n - 1) as f64 } else { 0.0 };
match shape {
"ease" => t * t * (3.0 - 2.0 * t),
"bow" => (std::f64::consts::PI * t).sin(),
"spike" => 1.0 - (2.0 * t - 1.0).abs(),
"random" => random,
_ => t,
}
}
/// A random number from 0 to 1.
fn unit(rng: &mut Rng) -> f64 {
(rng.next_u64() >> 11) as f64 / (1u64 << 53) as f64
}
fn text_tweaker(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "addon.type.textTweaker";
let attribute = choice(p, C, "attribute", &ATTRIBUTES.map(|a| a.0))?.unwrap_or("size");
let Some(&(key, (lo, hi), (from0, to0))) = ATTRIBUTES.iter().find(|a| a.0 == attribute) else {
return Err(bad(C, format!("unknown attribute `{attribute}`")));
};
let mut ends = [0.0; 2];
for (end, (k, default)) in ends.iter_mut().zip([("from", from0), ("to", to0)]) {
let v = number_or(p, k, default, C)?;
if !(lo..=hi).contains(&v) {
return Err(bad(C, format!("`{k}` must be from {lo} to {hi} for {key}")));
}
*end = v;
}
let [from, to] = ends;
let shape = choice(p, C, "shape", &SHAPES)?.unwrap_or("straight");
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();
// 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;
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 {
return Err(bad(C, format!("too many characters: at most {MAX_CHARS}")));
}
let n = chars.len();
let mut changes = Vec::with_capacity(n);
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)?;
changes.push((at, change));
}
if !changes.is_empty() {
plans.push((id, changes));
}
}
if plans.is_empty() {
return Ok(json!({ "count": 0, "ids": [] }));
}
let ids: Vec<u64> = plans.iter().map(|(id, _)| id.0).collect();
s.edit("Text Tweaker", |d, _| {
for (id, changes) in &plans {
let t = text_mut(d, *id).ok_or(EngineError::NoNode(*id))?;
t.runs = tweak(std::mem::take(&mut t.runs), changes);
refresh_bounds(t);
}
Ok(())
})?;
Ok(json!({ "count": count, "ids": ids }))
}
/// `runs` with each character at a byte of `changes` (in order) changed: its run split into
/// characters, the changes applied, and runs that came out alike merged again.
fn tweak(runs: Vec<TextRun>, changes: &[(usize, CharChange)]) -> Vec<TextRun> {
let mut out = Vec::with_capacity(runs.len() + changes.len() * 2);
let mut next = changes.iter().peekable();
let mut off = 0;
for r in runs {
let end = off + r.text.len();
if r.inline.is_some() || next.peek().is_none_or(|(at, _)| *at >= end) {
// An inline graphic is one character: its style changes whole.
if let Some((_, change)) = next.next_if(|(at, _)| (off..end).contains(at)) {
let mut r = r;
change.apply(&mut r.style);
out.push(r);
} else {
out.push(r);
}
off = end;
continue;
}
for (i, c) in r.text.char_indices() {
let mut piece = TextRun::new(c.to_string(), r.style.clone());
if let Some((_, change)) = next.next_if(|(at, _)| *at == off + i) {
change.apply(&mut piece.style);
}
out.push(piece);
}
off = end;
}
edit::normalize(&mut out);
out
}
@@ -0,0 +1,195 @@
//! Text Tweaker: the values each shape gives each character, what it skips and changes, one undo
//! step, and bad params refused without a change.
use serde_json::json;
use vectorcraft_doc::{CharStyle, NodeId, NodeKind, TextObject};
use super::amount;
use crate::Session;
const C: &str = "addon.type.textTweaker";
fn session() -> Session {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 600, "height": 400})).unwrap();
s
}
fn text(s: &mut Session, words: &str) -> NodeId {
NodeId(s.execute("text.create", &json!({"x": 40, "y": 100, "text": words, "size": 20})).unwrap()["id"].as_u64().unwrap())
}
fn select(s: &mut Session, ids: &[NodeId]) {
s.select(|_, sel| sel.set(ids.iter().copied())).unwrap();
}
fn obj(s: &Session, id: NodeId) -> TextObject {
match &s.doc().unwrap().doc.node(id).unwrap().kind {
NodeKind::Text(t) => (**t).clone(),
_ => panic!("not text"),
}
}
/// Each character of `id` with its style.
fn styles(s: &Session, id: NodeId) -> Vec<(char, CharStyle)> {
obj(s, id).runs.iter().flat_map(|r| r.text.chars().map(|c| (c, r.style.clone())).collect::<Vec<_>>()).collect()
}
fn steps(s: &Session) -> usize {
s.doc().unwrap().history.undo.len()
}
fn near(a: &[f64], b: &[f64]) -> bool {
a.len() == b.len() && a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-9)
}
#[test]
fn the_shapes_run_from_the_start_value_to_the_end_value() {
let at = |shape, n| (0..n).map(|i| amount(shape, i, n, 0.5)).collect::<Vec<_>>();
assert!(near(&at("straight", 5), &[0.0, 0.25, 0.5, 0.75, 1.0]));
assert!(near(&at("ease", 5), &[0.0, 0.15625, 0.5, 0.84375, 1.0]), "slow at both ends");
let bow = at("bow", 5);
assert!(near(&[bow[0], bow[2], bow[4]], &[0.0, 1.0, 0.0]) && (bow[1] - 0.5f64.sqrt()).abs() < 1e-9, "a smooth arc");
assert!(near(&at("spike", 5), &[0.0, 0.5, 1.0, 0.5, 0.0]), "straight up and down");
assert!(near(&at("random", 3), &[0.5; 3]));
assert!(near(&at("straight", 1), &[0.0]), "one character takes the start value");
}
#[test]
fn a_straight_ramp_sizes_each_letter_and_skips_spaces() {
let mut s = session();
let id = text(&mut s, "ab cd");
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]}));
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");
assert_eq!(obj(&s, id).plain_text(), "ab cd", "the text stays live");
s.execute("edit.undo", &json!({})).unwrap();
assert!(styles(&s, id).iter().all(|(_, st)| st.size == 20.0), "undone");
}
#[test]
fn spaces_can_count_too() {
let mut s = session();
let id = text(&mut s, "a b");
select(&mut s, &[id]);
s.execute(C, &json!({"attribute": "baselineShift", "from": 0, "to": 10, "skipSpaces": false})).unwrap();
let shifts: Vec<f64> = styles(&s, id).iter().map(|(_, st)| st.baseline_shift).collect();
assert_eq!(shifts, [0.0, 5.0, 10.0]);
}
#[test]
fn every_attribute_is_set_as_the_character_panel_sets_it() {
let mut s = session();
for (attribute, get) in [
("size", (|st: &CharStyle| st.size) as fn(&CharStyle) -> f64),
("baselineShift", |st| st.baseline_shift),
("rotation", |st| st.rotation),
("hScale", |st| st.h_scale),
("vScale", |st| st.v_scale),
("tracking", |st| st.tracking),
("leading", |st| st.leading.unwrap_or(-1.0)),
("strokeWidth", |st| st.stroke_width),
] {
let id = text(&mut s, "xyz");
select(&mut s, &[id]);
s.execute(C, &json!({"attribute": attribute, "from": 2, "to": 6})).unwrap();
let got: Vec<f64> = styles(&s, id).iter().map(|(_, st)| get(st)).collect();
assert!(near(&got, &[2.0, 4.0, 6.0]), "{attribute}: {got:?}");
}
}
#[test]
fn each_object_gets_its_own_ramp() {
let mut s = session();
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));
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]));
}
#[test]
fn a_range_changes_only_its_characters() {
let mut s = session();
let id = text(&mut s, "abcde");
s.execute(C, &json!({"id": id.0, "start": 1, "end": 4, "attribute": "tracking", "from": 100, "to": 300})).unwrap();
let tracking: Vec<f64> = styles(&s, id).iter().map(|(_, st)| st.tracking).collect();
assert_eq!(tracking, [0.0, 100.0, 200.0, 300.0, 0.0]);
}
#[test]
fn random_values_stay_in_range_and_repeat_with_the_seed() {
let mut s = session();
let id = text(&mut s, "randomness");
select(&mut s, &[id]);
let run = |s: &mut Session, seed| {
s.execute(C, &json!({"attribute": "vScale", "from": 50, "to": 150, "shape": "random", "seed": seed})).unwrap();
styles(s, id).iter().map(|(_, st)| st.v_scale).collect::<Vec<_>>()
};
let first = run(&mut s, 7);
assert!(first.iter().all(|v| (50.0..=150.0).contains(v)), "{first:?}");
assert!(first.windows(2).any(|w| w[0] != w[1]), "values vary");
assert_eq!(run(&mut s, 7), first, "same seed, same values");
assert_ne!(run(&mut s, 8), first, "another seed, other values");
}
#[test]
fn values_follow_the_text_transform_as_the_character_panel_does() {
let mut s = session();
let id = text(&mut s, "ab");
select(&mut s, &[id]);
s.execute("object.scale", &json!({"sx": 200, "sy": 200})).unwrap();
s.execute(C, &json!({"attribute": "size", "from": 20, "to": 40})).unwrap();
let sizes: Vec<f64> = styles(&s, id).iter().map(|(_, st)| st.size).collect();
// Shown sizes are the asked ones: stored sizes are half, the text being scaled twice.
let scale = obj(&s, id).style_scale().unwrap().points;
assert!(near(&sizes.iter().map(|v| v * scale).collect::<Vec<_>>(), &[20.0, 40.0]), "{sizes:?} at {scale}");
}
#[test]
fn empty_text_and_only_spaces_change_nothing() {
let mut s = session();
let id = text(&mut s, " ");
select(&mut s, &[id]);
let n = steps(&s);
assert_eq!(s.execute(C, &json!({})).unwrap(), json!({"count": 0, "ids": []}));
assert_eq!(steps(&s), n, "no step");
}
#[test]
fn bad_params_are_refused_without_a_change() {
let mut s = session();
let id = text(&mut s, "abc");
select(&mut s, &[id]);
let (before, n) = (s.doc().unwrap().doc.clone(), steps(&s));
for p in [
json!({"attribute": "colour"}),
json!({"attribute": 3}),
json!({"shape": "zigzag"}),
json!({"attribute": "size", "from": 0}),
json!({"attribute": "size", "to": 5000}),
json!({"attribute": "hScale", "from": -1}),
json!({"from": "big"}),
json!({"to": f64::MAX}),
json!({"seed": -1}),
json!({"skipSpaces": "yes"}),
json!({"ids": [999_999]}),
] {
assert!(s.execute(C, &p).is_err(), "{p}");
assert_eq!(s.doc().unwrap().doc, before, "{p}");
assert_eq!(steps(&s), n, "{p}");
}
}
#[test]
fn it_needs_a_selection() {
let mut s = session();
text(&mut s, "abc");
select(&mut s, &[]);
assert!(s.execute(C, &json!({})).is_err());
}
+4 -4
View File
@@ -154,14 +154,14 @@ pub fn specs() -> Vec<CommandSpec> {
]
}
fn text_mut(d: &mut vectorcraft_doc::Document, id: NodeId) -> Option<&mut TextObject> {
pub(crate) fn text_mut(d: &mut vectorcraft_doc::Document, id: NodeId) -> Option<&mut TextObject> {
match d.node_mut(id).map(|n| &mut n.kind) {
Some(NodeKind::Text(t)) => Some(t),
_ => None,
}
}
fn text_ref(s: &Session, id: NodeId) -> Result<TextObject> {
pub(crate) fn text_ref(s: &Session, id: NodeId) -> Result<TextObject> {
match s.doc()?.doc.node(id).map(|n| &n.kind) {
Some(NodeKind::Text(t)) => Ok((**t).clone()),
Some(_) => Err(EngineError::Other(format!("node {} is not text", id.0))),
@@ -708,7 +708,7 @@ impl CharChange {
&& self.font_height_reference.is_none()
}
fn localize(&mut self, t: &TextObject, cmd: &str) -> Result<()> {
pub(crate) fn localize(&mut self, t: &TextObject, cmd: &str) -> Result<()> {
use super::typecmd::local_type_value;
self.size = self.size.map(|v| local_type_value(t, v, (0.1, 1296.0), false, cmd)).transpose()?;
self.leading = self.leading.map(|v| v.map(|v| local_type_value(t, v, (0.1, 5000.0), false, cmd)).transpose()).transpose()?;
@@ -1255,7 +1255,7 @@ pub(crate) fn editing_range(s: &mut Session) -> Result<Option<(NodeId, usize, us
/// objects in `ids`, else the text the Type tool has selected, else all the text of every selected
/// type object. At a caret, `at_caret` gives the range from the plain text and the caret's byte
/// (None: nothing changes there). Each range comes with whether it came from a caret.
fn type_ranges(
pub(crate) fn type_ranges(
s: &mut Session,
p: &Value,
c: &str,
+6
View File
@@ -653,6 +653,12 @@ fn structured_junk() {
("addon.pen.reshape", json!({"id": 2, "segment": 0, "t": "x", "dx": 1, "dy": 1, "mode": "symmetric"})),
("addon.pen.nextSegment", json!({"curved": "yes"})),
("addon.pen.closeJoin", json!({"ids": [u64::MAX]})),
// Type Tools add-on.
("addon.type.textTweaker", json!({"attribute": "size", "from": 1296, "to": 0.1, "shape": "random", "seed": u64::MAX})),
("addon.type.textTweaker", json!({"attribute": "tracking", "from": f64::MAX, "to": -1e308, "shape": "spike"})),
("addon.type.textTweaker", json!({"attribute": "rotation", "from": -360, "to": 360, "shape": "bow", "skipSpaces": false, "ids": [u64::MAX]})),
("addon.type.textTweaker", json!({"attribute": "hScale", "id": 2, "start": u64::MAX, "end": 0, "shape": "ease"})),
("addon.type.textTweaker", json!({"attribute": ["size"], "from": "x", "seed": 1e308, "skipSpaces": "no", "ids": "all"})),
("addon.cleanup.breakText", json!({"by": "glyphs", "ids": [0, 1, 2, 3, 4, 5, 6, 7, 8, u64::MAX]})),
("addon.cleanup.breakText", json!({"by": ["lines"]})),
("addon.cleanup.breakText", json!({"by": "paragraphs"})),
+2
View File
@@ -7,6 +7,7 @@ pub mod color;
pub mod document;
pub mod pathtools;
pub mod pen;
pub mod typetools;
use crate::menus::Item;
@@ -19,6 +20,7 @@ pub fn plugin_menu() -> Vec<Item> {
Item::Sub("Document Tools", document::menu()),
Item::Sub("Path Precision Tools", pathtools::menu()),
Item::Sub("Smart Pen", pen::menu()),
Item::Sub("Type Tools", typetools::menu()),
Item::Sep,
]
}
+96
View File
@@ -0,0 +1,96 @@
//! Type Tools in the interface: its submenu (Object › Plug-ins › Type Tools) and Text Tweaker's
//! dialog, which picks an attribute, its start and end values and a shape, previews on the canvas
//! and runs `addon.type.textTweaker` on OK.
use egui::Ui;
use serde_json::{Value, json};
use crate::VectorcraftApp;
use crate::dialogs::{DialogSpec, form};
use crate::menus::Item;
use crate::state::Dialog;
use crate::widgets;
/// The command the dialog runs, and its kind.
pub const COMMAND: &str = "addon.type.textTweaker";
pub const KIND: &str = "textTweaker";
/// An attribute: (param, label, unit, decimals, values taken, default start and end), as the
/// command takes it.
type Attribute = (&'static str, &'static str, &'static str, usize, (f64, f64), (f64, f64));
const ATTRIBUTES: [Attribute; 8] = [
("size", "Font Size", "pt", 1, (0.1, 1296.0), (12.0, 36.0)),
("baselineShift", "Baseline Shift", "pt", 1, (-1296.0, 1296.0), (0.0, 12.0)),
("rotation", "Character Rotation", "°", 1, (-360.0, 360.0), (0.0, 30.0)),
("hScale", "Horizontal Scale", "%", 0, (1.0, 10000.0), (100.0, 150.0)),
("vScale", "Vertical Scale", "%", 0, (1.0, 10000.0), (100.0, 150.0)),
("tracking", "Tracking", "", 0, (-1000.0, 10000.0), (0.0, 200.0)),
("leading", "Leading", "pt", 1, (0.1, 5000.0), (12.0, 36.0)),
("strokeWidth", "Stroke Weight", "pt", 2, (0.0, 1000.0), (0.0, 2.0)),
];
const SHAPES: [(&str, &str); 5] = [("straight", "Straight"), ("ease", "Ease In Out"), ("bow", "Bow"), ("spike", "Spike"), ("random", "Random")];
/// The seeds the dialog offers.
const SEEDS: u64 = 1_000_000;
const LABEL_W: f32 = 90.0;
const FIELD_W: f32 = 90.0;
/// Object › Plug-ins › Type Tools.
pub fn menu() -> Vec<Item> {
vec![Item::Cmd("Text Tweaker…", COMMAND, Value::Null)]
}
/// The attribute row of param `key` (the first, Font Size, for an unknown one).
fn attribute(key: &str) -> &'static Attribute {
ATTRIBUTES.iter().find(|a| a.0 == key).unwrap_or(&ATTRIBUTES[0])
}
/// The dialog's fields to start with: Font Size from 12 to 36 pt in a straight ramp, and a new
/// seed each time the document has changed, so running it again varies a random result.
pub(crate) fn fields(app: &VectorcraftApp) -> Value {
let seed = app.session.active().map_or(0, |st| st.revision % SEEDS);
json!({
"attribute": "size", "__shown": "size", "from": 12, "to": 36, "shape": "straight", "seed": seed, "skipSpaces": true,
})
}
pub(crate) const SPEC: DialogSpec =
DialogSpec { heading: |_| tl!("Text Tweaker").to_string(), body, confirm, preview: true, min_width: 320.0, ..DialogSpec::FORM };
fn body(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog) -> bool {
let widths = (LABEL_W, 170.0);
let options: Vec<(&str, &str)> = ATTRIBUTES.iter().map(|a| (a.0, a.1)).collect();
form::choice(ui, d, "attribute", tl!("Attribute:"), widths, &options);
let &(key, _, unit, decimals, (lo, hi), (from, to)) = attribute(&d.str("attribute"));
// Another attribute starts from its own values.
if d.str("__shown") != key {
d.fields.insert("__shown".into(), json!(key));
d.fields.insert("from".into(), json!(from));
d.fields.insert("to".into(), json!(to));
}
for (field, label) in [("from", tl!("From:")), ("to", tl!("To:"))] {
widgets::label_row(ui, label, LABEL_W, |ui| {
if let Some(v) = widgets::range_field(ui, ("text-tweaker", field), d.f64(field, lo), lo..=hi, unit, decimals, FIELD_W) {
d.fields.insert(field.into(), json!(v));
}
});
}
form::choice(ui, d, "shape", tl!("Shape:"), widths, &SHAPES);
ui.add_enabled_ui(d.str("shape") == "random", |ui| {
widgets::label_row(ui, tl!("Seed:"), LABEL_W, |ui| {
if let Some(v) = widgets::range_field(ui, ("text-tweaker", "seed"), d.f64("seed", 0.0), 0.0..=(SEEDS - 1) as f64, "", 0, FIELD_W) {
d.fields.insert("seed".into(), json!(v as u64));
}
});
});
form::check(ui, d, "skipSpaces", tl!("Skip Spaces"));
form::preview(app, ui, d, "Text Tweaker", COMMAND, form::params(d));
false
}
/// OK: run the command (keep its preview) and close.
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
form::commit_preview(app, COMMAND, form::params(d))
}
#[cfg(test)]
mod tests;
@@ -0,0 +1,62 @@
use serde_json::json;
use vectorcraft_doc::{NodeId, NodeKind};
use vectorcraft_engine::Session;
use super::KIND;
use crate::tests_labels::painted_text;
use crate::{VectorcraftApp, menus};
/// An app with one type object, selected.
fn app() -> (VectorcraftApp, NodeId) {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
let id = NodeId(app.run("text.create", json!({"x": 40, "y": 100, "text": "abc", "size": 20})).unwrap()["id"].as_u64().unwrap());
app.run("select.set", json!({"ids": [id.0]})).unwrap();
(app, id)
}
fn sizes(app: &VectorcraftApp, id: NodeId) -> Vec<f64> {
match &app.session.doc().unwrap().doc.node(id).unwrap().kind {
NodeKind::Text(t) => t.runs.iter().flat_map(|r| r.text.chars().map(|_| r.style.size).collect::<Vec<_>>()).collect(),
_ => panic!("not text"),
}
}
fn open(app: &mut VectorcraftApp) {
let entries = menus::menu_entries(app);
let item = entries.iter().find(|e| e.path == ["Object", "Plug-ins", "Type Tools"] && e.label == "Text Tweaker…").unwrap();
let (id, p) = menus::click_target(&item.label, item.command.as_deref().unwrap(), &item.params);
menus::invoke(app, &id, p);
}
#[test]
fn text_tweaker_opens_its_dialog_previews_and_runs_on_ok() {
let (mut app, id) = app();
open(&mut app);
assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(KIND));
let shown = painted_text(&mut app, |app, ui| crate::dialogs::show(app, ui.ctx()));
for s in ["Text Tweaker", "Attribute:", "Font Size", "From:", "To:", "Shape:", "Straight", "Seed:", "Skip Spaces", "Preview"] {
assert!(shown.contains(s), "{s} in {shown}");
}
app.ui.dialog.as_mut().unwrap().fields.insert("preview".into(), json!(true));
painted_text(&mut app, |app, ui| crate::dialogs::show(app, ui.ctx()));
assert_eq!(sizes(&app, id), [12.0, 24.0, 36.0], "previewed");
crate::dialogs::cancel(&mut app);
assert_eq!(sizes(&app, id), [20.0; 3], "rolled back");
open(&mut app);
let n = app.session.doc().unwrap().history.undo.len();
crate::dialogs::confirm(&mut app).unwrap();
assert!(app.ui.dialog.is_none());
assert_eq!(app.session.doc().unwrap().history.undo.len(), n + 1, "one undo step");
assert_eq!(sizes(&app, id), [12.0, 24.0, 36.0]);
}
#[test]
fn another_attribute_starts_from_its_own_values() {
let (mut app, _) = app();
open(&mut app);
app.ui.dialog.as_mut().unwrap().fields.insert("attribute".into(), json!("rotation"));
painted_text(&mut app, |app, ui| crate::dialogs::show(app, ui.ctx()));
let d = app.ui.dialog.as_ref().unwrap();
assert_eq!((d.fields["from"].clone(), d.fields["to"].clone()), (json!(0.0), json!(30.0)));
}
+1
View File
@@ -408,6 +408,7 @@ registry! {
DivideDashes: [crate::addons::pathtools::dashes::KIND] => crate::addons::pathtools::dashes::SPEC,
NearestSwatch: [crate::addons::color::nearest::KIND] => crate::addons::color::nearest::SPEC,
SwatchLegend: [crate::addons::color::legend::KIND] => crate::addons::color::legend::SPEC,
TextTweaker: [crate::addons::typetools::KIND] => crate::addons::typetools::SPEC,
ArrangeTools: [crate::addons::arrange::GRID, crate::addons::arrange::REPLACE, crate::addons::arrange::SIZE, crate::addons::arrange::OPACITY, crate::addons::arrange::SORT] => crate::addons::arrange::SPEC,
ShortcutImport: [shortcut_import::KIND] => DialogSpec::window(shortcut_import::show, shortcut_import::confirm),
}
+10
View File
@@ -6112,3 +6112,13 @@ breakText Glyphs Глифи
Create Swatch Legend Създаване на легенда на образците
Create Swatch Legend… Създаване на легенда на образците…
HEX HEX
# Type Tools add-on (Object › Plug-ins › Type Tools).
Type Tools Инструменти за текст
Text Tweaker… Настройка на текста…
Text Tweaker Настройка на текста
Attribute: Атрибут:
From: От:
To: До:
Bow Дъга
Spike Връх
Skip Spaces Пропускане на интервалите
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -6146,3 +6146,13 @@ shape Open Otvoren
Create Swatch Legend Kreiraj legendu uzoraka boja
Create Swatch Legend… Kreiraj legendu uzoraka boja…
HEX HEX
# Type Tools add-on (Object › Plug-ins › Type Tools).
Type Tools Alati za tekst
Text Tweaker… Podešavanje teksta…
Text Tweaker Podešavanje teksta
Attribute: Atribut:
From: Od:
To: Do:
Bow Luk
Spike Šiljak
Skip Spaces Preskoči razmake
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -6246,3 +6246,13 @@ breakText Glyphs Glifs
Create Swatch Legend Crea una llegenda de mostres
Create Swatch Legend… Crea una llegenda de mostres…
HEX HEX
# Type Tools add-on (Object › Plug-ins › Type Tools).
Type Tools Eines de text
Text Tweaker… Ajustador de text…
Text Tweaker Ajustador de text
Attribute: Atribut:
From: De:
To: A:
Bow Arc
Spike Pic
Skip Spaces Omet els espais
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -6260,3 +6260,13 @@ breakText Glyphs Glyfy
Create Swatch Legend Vytvořit legendu vzorníku
Create Swatch Legend… Vytvořit legendu vzorníku…
HEX HEX
# Type Tools add-on (Object › Plug-ins › Type Tools).
Type Tools Nástroje textu
Text Tweaker… Ladění textu…
Text Tweaker Ladění textu
Attribute: Atribut:
From: Od:
To: Do:
Bow Oblouk
Spike Špička
Skip Spaces Přeskočit mezery
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -6128,3 +6128,13 @@ breakText Glyphs Glyffer
Create Swatch Legend Opret farveprøveforklaring
Create Swatch Legend… Opret farveprøveforklaring…
HEX HEX
# Type Tools add-on (Object › Plug-ins › Type Tools).
Type Tools Tekstværktøjer
Text Tweaker… Tekstjustering…
Text Tweaker Tekstjustering
Attribute: Attribut:
From: Fra:
To: Til:
Bow Bue
Spike Spids
Skip Spaces Spring mellemrum over
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -5846,3 +5846,13 @@ breakText Glyphs Glyphen
Create Swatch Legend Farbfeldlegende erstellen
Create Swatch Legend… Farbfeldlegende erstellen…
HEX HEX
# Type Tools add-on (Object › Plug-ins › Type Tools).
Type Tools Textwerkzeuge
Text Tweaker… Text-Tweaker…
Text Tweaker Text-Tweaker
Attribute: Attribut:
From: Von:
To: Bis:
Bow Bogen
Spike Spitze
Skip Spaces Leerzeichen überspringen
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -6212,3 +6212,13 @@ breakText Glyphs Γλύφοι
Create Swatch Legend Δημιουργία υπομνήματος δειγμάτων χρωμάτων
Create Swatch Legend… Δημιουργία υπομνήματος δειγμάτων χρωμάτων…
HEX HEX
# Type Tools add-on (Object › Plug-ins › Type Tools).
Type Tools Εργαλεία κειμένου
Text Tweaker… Ρύθμιση κειμένου…
Text Tweaker Ρύθμιση κειμένου
Attribute: Ιδιότητα:
From: Από:
To: Έως:
Bow Τόξο
Spike Αιχμή
Skip Spaces Παράλειψη κενών
Can't render this file because it contains an unexpected character in line 163 and column 5.
+10
View File
@@ -6202,3 +6202,13 @@ breakText Glyphs Glifos
Create Swatch Legend Crear leyenda de muestras
Create Swatch Legend… Crear leyenda de muestras…
HEX HEX
# Type Tools add-on (Object › Plug-ins › Type Tools).
Type Tools Herramientas de texto
Text Tweaker… Ajustador de texto…
Text Tweaker Ajustador de texto
Attribute: Atributo:
From: Desde:
To: Hasta:
Bow Arco
Spike Pico
Skip Spaces Omitir espacios
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -6290,3 +6290,13 @@ breakText Glyphs Merkit
Create Swatch Legend Luo värimallien selite
Create Swatch Legend… Luo värimallien selite…
HEX HEX
# Type Tools add-on (Object › Plug-ins › Type Tools).
Type Tools Tekstityökalut
Text Tweaker… Tekstin säätö…
Text Tweaker Tekstin säätö
Attribute: Määrite:
From: Alku:
To: Loppu:
Bow Kaari
Spike Piikki
Skip Spaces Ohita välilyönnit
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -6255,3 +6255,13 @@ breakText Glyphs Glyphes
Create Swatch Legend Créer une légende des nuances
Create Swatch Legend… Créer une légende des nuances…
HEX HEX
# Type Tools add-on (Object › Plug-ins › Type Tools).
Type Tools Outils de texte
Text Tweaker… Ajusteur de texte…
Text Tweaker Ajusteur de texte
Attribute: Attribut :
From: De :
To: À :
Bow Arc
Spike Pointe
Skip Spaces Ignorer les espaces
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -6244,3 +6244,13 @@ breakText Glyphs Glifák
Create Swatch Legend Színmintajelmagyarázat létrehozása
Create Swatch Legend… Színmintajelmagyarázat létrehozása…
HEX HEX
# Type Tools add-on (Object › Plug-ins › Type Tools).
Type Tools Szövegeszközök
Text Tweaker… Szöveghangoló…
Text Tweaker Szöveghangoló
Attribute: Jellemző:
From: Ettől:
To: Eddig:
Bow Ív
Spike Csúcs
Skip Spaces Szóközök kihagyása
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -6128,3 +6128,13 @@ breakText Glyphs Glif
Create Swatch Legend Buat Legenda Swatch
Create Swatch Legend… Buat Legenda Swatch…
HEX HEX
# Type Tools add-on (Object › Plug-ins › Type Tools).
Type Tools Alat Teks
Text Tweaker… Penyetel Teks…
Text Tweaker Penyetel Teks
Attribute: Atribut:
From: Dari:
To: Ke:
Bow Busur
Spike Puncak
Skip Spaces Lewati Spasi
Can't render this file because it contains an unexpected character in line 2211 and column 5.
+10
View File
@@ -5880,3 +5880,13 @@ breakText Glyphs Glifi
Create Swatch Legend Crea legenda campioni
Create Swatch Legend… Crea legenda campioni…
HEX HEX
# Type Tools add-on (Object › Plug-ins › Type Tools).
Type Tools Strumenti testo
Text Tweaker… Regolatore testo…
Text Tweaker Regolatore testo
Attribute: Attributo:
From: Da:
To: A:
Bow Arco
Spike Picco
Skip Spaces Salta gli spazi
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -5945,3 +5945,13 @@ breakText Glyphs 字形
Create Swatch Legend スウォッチの凡例を作成
Create Swatch Legend… スウォッチの凡例を作成…
HEX HEX
# Type Tools add-on (Object › Plug-ins › Type Tools).
Type Tools テキストツール
Text Tweaker… テキストの調整…
Text Tweaker テキストの調整
Attribute: 属性:
From: 開始:
To: 終了:
Bow 弓形
Spike 山形
Skip Spaces スペースをスキップ
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -6248,3 +6248,13 @@ breakText Glyphs გლიფები
Create Swatch Legend ფერის ნიმუშების ლეგენდის შექმნა
Create Swatch Legend… ფერის ნიმუშების ლეგენდის შექმნა…
HEX HEX
# Type Tools add-on (Object › Plug-ins › Type Tools).
Type Tools ტექსტის ხელსაწყოები
Text Tweaker… ტექსტის მორგება…
Text Tweaker ტექსტის მორგება
Attribute: ატრიბუტი:
From: საწყისი:
To: საბოლოო:
Bow რკალი
Spike პიკი
Skip Spaces ჰარების გამოტოვება
Can't render this file because it is too large.
+10
View File
@@ -5841,3 +5841,13 @@ breakText Glyphs 글리프
Create Swatch Legend 견본 범례 만들기
Create Swatch Legend… 견본 범례 만들기…
HEX HEX
# Type Tools add-on (Object › Plug-ins › Type Tools).
Type Tools 텍스트 도구
Text Tweaker… 텍스트 조정…
Text Tweaker 텍스트 조정
Attribute: 속성:
From: 시작:
To: 끝:
Bow 활 모양
Spike 뾰족한 모양
Skip Spaces 공백 건너뛰기
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -6172,3 +6172,13 @@ breakText Glyphs Glyphs
Create Swatch Legend Stalenlegenda maken
Create Swatch Legend… Stalenlegenda maken…
HEX HEX
# Type Tools add-on (Object › Plug-ins › Type Tools).
Type Tools Teksthulpmiddelen
Text Tweaker… Tekst bijstellen…
Text Tweaker Tekst bijstellen
Attribute: Kenmerk:
From: Van:
To: Tot:
Bow Boog
Spike Piek
Skip Spaces Spaties overslaan
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -6115,3 +6115,13 @@ breakText Glyphs Glify
Create Swatch Legend Utwórz legendę próbek
Create Swatch Legend… Utwórz legendę próbek…
HEX HEX
# Type Tools add-on (Object › Plug-ins › Type Tools).
Type Tools Narzędzia tekstu
Text Tweaker… Strojenie tekstu…
Text Tweaker Strojenie tekstu
Attribute: Atrybut:
From: Od:
To: Do:
Bow Łuk
Spike Szpic
Skip Spaces Pomiń spacje
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -6333,3 +6333,13 @@ swatch Yellow Green Verde-amarelado
Create Swatch Legend Criar Legenda de Amostras
Create Swatch Legend… Criar Legenda de Amostras…
HEX HEX
# Type Tools add-on (Object › Plug-ins › Type Tools).
Type Tools Ferramentas de texto
Text Tweaker… Ajustador de texto…
Text Tweaker Ajustador de texto
Attribute: Atributo:
From: De:
To: Até:
Bow Arco
Spike Pico
Skip Spaces Ignorar espaços
Can't render this file because it contains an unexpected character in line 6 and column 47.
+10
View File
@@ -6349,3 +6349,13 @@ breakText Glyphs Глифы
digits Hindi Хинди
font-script Japanese Японский
shape Open Открытый
# Type Tools add-on (Object › Plug-ins › Type Tools).
Type Tools Инструменты текста
Text Tweaker… Настройка текста…
Text Tweaker Настройка текста
Attribute: Атрибут:
From: От:
To: До:
Bow Дуга
Spike Пик
Skip Spaces Пропускать пробелы
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -6316,3 +6316,13 @@ breakText Glyphs Glyfer
# The strings the report listed as missing for sv.
Font Size: Teckenstorlek:
digits Hindi Hindi
# Type Tools add-on (Object › Plug-ins › Type Tools).
Type Tools Textverktyg
Text Tweaker… Textjustering…
Text Tweaker Textjustering
Attribute: Attribut:
From: Från:
To: Till:
Bow Båge
Spike Spets
Skip Spaces Hoppa över mellanslag
Can't render this file because it contains an unexpected character in line 5 and column 25.
+10
View File
@@ -6217,3 +6217,13 @@ breakText Glyphs Glifler
Create Swatch Legend Renk Örneği Açıklaması Oluştur
Create Swatch Legend… Renk Örneği Açıklaması Oluştur…
HEX HEX
# Type Tools add-on (Object › Plug-ins › Type Tools).
Type Tools Metin Araçları
Text Tweaker… Metin Ayarlayıcı…
Text Tweaker Metin Ayarlayıcı
Attribute: Öznitelik:
From: Başlangıç:
To: Bitiş:
Bow Yay
Spike Sivri Uç
Skip Spaces Boşlukları Atla
Can't render this file because it contains an unexpected character in line 5 and column 25.
+10
View File
@@ -5860,3 +5860,13 @@ breakText Glyphs Гліфи
Create Swatch Legend Створити легенду зразків
Create Swatch Legend… Створити легенду зразків…
HEX HEX
# Type Tools add-on (Object › Plug-ins › Type Tools).
Type Tools Інструменти тексту
Text Tweaker… Налаштування тексту…
Text Tweaker Налаштування тексту
Attribute: Атрибут:
From: Від:
To: До:
Bow Дуга
Spike Пік
Skip Spaces Пропускати пробіли
Can't render this file because it contains an unexpected character in line 177 and column 5.
+10
View File
@@ -5861,3 +5861,13 @@ breakText Glyphs Glyph
Create Swatch Legend Tạo chú giải mẫu màu
Create Swatch Legend… Tạo chú giải mẫu màu…
HEX HEX
# Type Tools add-on (Object › Plug-ins › Type Tools).
Type Tools Công cụ văn bản
Text Tweaker… Tinh chỉnh văn bản…
Text Tweaker Tinh chỉnh văn bản
Attribute: Thuộc tính:
From: Từ:
To: Đến:
Bow Cung
Spike Đỉnh nhọn
Skip Spaces Bỏ qua khoảng trắng
Can't render this file because it contains an unexpected character in line 199 and column 5.
+10
View File
@@ -6101,3 +6101,13 @@ breakText Glyphs 字形
Create Swatch Legend 创建色板图例
Create Swatch Legend… 创建色板图例…
HEX HEX
# Type Tools add-on (Object › Plug-ins › Type Tools).
Type Tools 文字工具
Text Tweaker… 文字微调…
Text Tweaker 文字微调
Attribute: 属性:
From: 起始:
To: 结束:
Bow 弓形
Spike 尖峰
Skip Spaces 跳过空格
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -5898,3 +5898,13 @@ breakText Glyphs 字符
Create Swatch Legend 建立色票圖例
Create Swatch Legend… 建立色票圖例…
HEX HEX
# Type Tools add-on (Object › Plug-ins › Type Tools).
Type Tools 文字工具
Text Tweaker… 文字微調…
Text Tweaker 文字微調
Attribute: 屬性:
From: 起始:
To: 結束:
Bow 弓形
Spike 尖峰
Skip Spaces 略過空格
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -71,6 +71,7 @@ pub(super) fn menu_dialog(app: &VectorcraftApp, id: &str) -> Option<(&'static st
crate::addons::pathtools::dashes::COMMAND => (crate::addons::pathtools::dashes::KIND, crate::addons::pathtools::dashes::fields()),
crate::addons::color::nearest::COMMAND => (crate::addons::color::nearest::KIND, crate::addons::color::nearest::fields()),
crate::addons::color::legend::COMMAND => (crate::addons::color::legend::KIND, crate::addons::color::legend::fields()),
crate::addons::typetools::COMMAND => (crate::addons::typetools::KIND, crate::addons::typetools::fields(app)),
"artboard.rearrange" => (crate::dialogs::rearrange_artboards::KIND, crate::dialogs::rearrange_artboards::open_fields(app)),
_ => return None,
})
+27
View File
@@ -6777,3 +6777,30 @@ there is no step. → `{symbols: [name…], ids: [instance id…], skipped: [{la
```json
{"name":"run_command","arguments":{"command":"addon.document.symbolsFromLayers","params":{"replace":true}}}
```
## Type Tools add-on
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.
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.
| Parameter | Values (default) |
|---|---|
| `attribute` | `size` (pt 0.1…1296), `baselineShift` (pt −1296…1296), `rotation` (° −360…360), `hScale`, `vScale` (% 1…10000), `tracking` (1/1000 em −1000…10000), `leading` (pt 0.1…5000), `strokeWidth` (pt 0…1000; shows only on characters with a stroke) (`size`) |
| `from`, `to` | the first and last character's values, in the attribute's units and range (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` (the same, in straight lines), `random` (each character anywhere from `from` to `to`) (`straight`) |
| `seed` | whole number (0): `random` with the same seed and text gives the same values |
| `skipSpaces` | true: spaces and line breaks keep their values and don't count |
→ `{count (characters changed), ids}`. Text Tweaker… opens a dialog (`ui.menuDialog {command}`, dialog kind
`textTweaker`) whose fields are the command's parameters, with Preview; another attribute starts from its own
default values, and the seed starts new whenever the document has changed.
```json
{"name":"run_command","arguments":{"command":"addon.type.textTweaker","params":{"attribute":"baselineShift","from":0,"to":18,"shape":"bow"}}}
{"name":"run_command","arguments":{"command":"addon.type.textTweaker","params":{"attribute":"rotation","from":-15,"to":15,"shape":"random","seed":4}}}
```
+6 -1
View File
@@ -1,6 +1,6 @@
# Parity with Adobe Illustrator
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (font-family fields accept the mouse wheel, #5253; Units › East Asian Type acts, with Q and Ha units; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (Type Tools add-on: Text Tweaker, #4924; font-family fields accept the mouse wheel, #5253; Units › East Asian Type acts, with Q and Ha units; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
## Feature submission evidence
@@ -19,6 +19,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard
| Date | Change | Summary |
|---|---|---|
| 2026-11-03 | minor | Type Tools add-on: Text Tweaker (a VectorCraft addition, #4924): evidence added; scores unchanged. |
| 2026-11-02 | minor | Legacy `.ai` saves (version 7) write type with rotated characters as outlines that keep each character's rotation, with a warning unless `outlineText` is on (#5186); scores unchanged. |
| 2026-10-31 | minor | Object › Path › Smooth smooths a path along its length before refitting it, so Auto-Smooth takes a shaky line to one smooth curve instead of leaving its jags, and with anchor points direct-selected it smooths only that section; scores unchanged. |
| 2026-10-31 | minor | New Document: More Settings has the Save Preset button beside the name too, saving its settings (artboard layout included) as a preset (#4893); scores unchanged. |
@@ -1500,6 +1501,10 @@ A VectorCraft addition, not a feature of the reference app, so no area score mov
A VectorCraft addition, not a feature of the reference app, so no area score moves. Object › Plug-ins › Arrange Tools › Sort Objects… sorts the top-level selected objects (or `ids`, at most 20000) by one attribute (width, height, area, x or y position, opacity or stacking order; small to large, large to small, or a random order from a seed) and gives them another in that order, the first the smallest value: the objects trade the values they have, or with Distribute Evenly the values step evenly from the smallest to the largest. Lining objects up left to right by size, restacking them so the largest is at the back, or giving them heights that step with their x position are each one run. Sizes and places are measured and set as `object.setBounds` measures them (Use Preview Bounds); a width, height or area changes about each object's centre (an area in proportion), and stacking order trades places only among objects of the same layer or group. One undo step; no step when nothing changes. Tests: `sort_*` in `cmd::addons::arrange::tests` in `vectorcraft-engine` (each attribute sorted by and applied, both directions, distribute, layers kept apart, the seeded random order, `ids`, no step when in order, bad params), `sort_objects_opens_its_dialog_and_runs_on_ok` in `vectorcraft-ui-egui` and `command_sweep`. Commands: `docs/mcp.md` › Arrange Tools add-on.
## Type Tools add-on: Text Tweaker (2026-11-03)
A VectorCraft addition, not a feature of the reference app, so no area score moves. Object › Plug-ins › Type Tools › Text Tweaker… gives one character attribute (size, baseline shift, character rotation, horizontal or vertical scale, tracking, leading or stroke weight) to the selected characters letter by letter, from a start value to an end value along a straight ramp, an ease in and out, a bow, a spike or at random with a seed, while the text stays live and editable (#4924). Values are set as the Character panel sets them, in its units and ranges, so scaled type takes the sizes shown; spaces and line breaks are skipped unless asked for, each type object gets its own ramp, the range is the Type tool's selection or all the text of the selected type objects, and the run is one undo step from a dialog with Preview. Tests: `cmd::addons::typetools::tests` in `vectorcraft-engine` (each shape, each attribute, skipped spaces, ranges, one ramp per object, seeded random values, scaled type, one undo step, bad params refused), `addons::typetools::tests` in `vectorcraft-ui-egui` (the submenu, the dialog, Preview and Cancel, OK) and `command_sweep`. Commands: `docs/mcp.md` › Type Tools add-on.
## Path Precision Tools add-on: Divide into Dashes (2026-11-01)
A VectorCraft addition, not a feature of the reference app, so no area score moves. Object › Plug-ins › Path Precision Tools › Divide into Dashes… cuts the selected paths into separate open paths along the curve (arc length, not straight distance): each `dash` long (0.01 to 1000000 pt, 10 by default) with `gap` between them (0 by default: the pieces touch); the gap pieces are left out unless Remove Gaps is off. With direct-selected segments (or anchors) only those runs are divided, the pattern running on across segments that follow one another, and the rest of such a path stays as pieces of its own. Fit to Corners and Ends gives every run between corners and ends a whole number of dashes, all stretched or squeezed alike so it starts and ends on a dash (a closed path without corners holds whole periods); otherwise the lengths are exact and each run's last piece is what is left. Every piece keeps the path's appearance and is selected; one undo step; at most 100000 pieces (more is refused). The dialog takes lengths in any unit and previews on the canvas. Tests: `dashes::tests` in `vectorcraft-pathops` (exact and fitted lengths, curves measured along them, picked segments on open and closed paths, the piece cap and bad patterns), `divide_into_dashes_*` in `cmd::addons::pathtools::tests` in `vectorcraft-engine` (gaps removed or kept, fitted lengths, the stroke kept, one undo step, selected segments only, bad params and too many pieces refused), `divide_into_dashes_opens_its_dialog_previews_and_runs_on_ok` in `vectorcraft-ui-egui` and `command_sweep`. Commands: `docs/mcp.md` › Path Precision Tools add-on.