Select Text by Font: limit it to artboards or layers (#5529)

Squashed from #5578 onto main.
This commit is contained in:
ruchirguitar
2026-11-09 02:03:01 +07:00
committed by bflatastic
parent 2ead0afaa0
commit b1f52d1369
33 changed files with 280 additions and 13 deletions
+147 -2
View File
@@ -10,8 +10,9 @@ use vectorcraft_color::Paint;
use vectorcraft_doc::{AppearanceItem, Document, Node, NodeId, NodeKind, TextKind, TextObject, TextRun};
use vectorcraft_geom::Rect;
use super::select::on_artboard;
use super::typecmd::refresh_bounds;
use super::{CommandSpec, Result, Session, always, bad, bool_or, cmd, has_doc, str_param};
use super::{CommandSpec, Result, Session, always, bad, bool_or, cmd, has_doc, index, str_param};
pub fn specs() -> Vec<CommandSpec> {
vec![
@@ -38,7 +39,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Select Text by Font",
[],
None,
"{family, style?} select the text objects using the font, and the instances of the symbols whose art uses it, that are not hidden or locked, in a hidden or locked group or layer, or on a locked artboard → {count}",
"{family, style?, scope?: \"document\" (default) | {artboards: [index, …] (0-based)} | {layers: [id, …] (layers or sublayers)}} select the text objects using the font, and the instances of the symbols whose art uses it, that are not hidden or locked, in a hidden or locked group or layer, or on a locked artboard; with scope, only those on one of the artboards (as All on Active Artboard takes them: their bounds overlap it or their center is on it) or in one of the layers → {count}",
has_doc,
select_font
),
@@ -713,6 +714,7 @@ fn select_font(s: &mut Session, p: &Value) -> Result<Value> {
let style = str_param(p, "style").map(str::to_string);
let ids = scope(s, false, None)?;
let d = &s.doc()?.doc;
let within = FontScope::read(d, p)?;
// Only text the Selection tool can reach: not hidden or locked, in a hidden or locked group
// or layer, or on a locked artboard.
let editable = d.editable();
@@ -758,11 +760,65 @@ fn select_font(s: &mut Session, p: &Value) -> Result<Value> {
});
}
drop(editable);
let symbols = d.symbol_boxes();
hits.retain(|id| within.holds(d, &symbols, *id));
drop(symbols);
let n = hits.len();
s.select(|_, sel| sel.set(hits))?;
Ok(json!({ "count": n }))
}
/// Where Select Text by Font looks: its `scope`.
enum FontScope {
Document,
/// The artboards' rectangles.
Artboards(Vec<Rect>),
/// The layers and sublayers, with what they hold.
Layers(BTreeSet<NodeId>),
}
impl FontScope {
fn read(d: &Document, p: &Value) -> Result<Self> {
const C: &str = "select.font";
let expected = || bad(C, "`scope` must be \"document\", {artboards: [index, …]} or {layers: [id, …]}");
let scope = match p.get("scope") {
None | Some(Value::Null) => return Ok(Self::Document),
Some(Value::String(v)) if v == "document" => return Ok(Self::Document),
Some(Value::Object(m)) if m.len() == 1 => m,
Some(_) => return Err(expected()),
};
let list = |key: &str| -> Result<Option<&Vec<Value>>> {
match scope.get(key) {
None => Ok(None),
Some(Value::Array(a)) if !a.is_empty() => Ok(Some(a)),
Some(_) => Err(bad(C, format!("`scope.{key}` must be a non-empty array"))),
}
};
if let Some(a) = list("artboards")? {
let rect = |v: &Value| v.as_u64().and_then(|i| d.artboards.get(index(i))).map(|b| b.rect);
return a.iter().map(|v| rect(v).ok_or_else(|| bad(C, format!("no such artboard {v}")))).collect::<Result<_>>().map(Self::Artboards);
}
if let Some(a) = list("layers")? {
let layer = |v: &Value| v.as_u64().map(NodeId).filter(|id| d.node(*id).is_some_and(Node::is_layer));
return a
.iter()
.map(|v| layer(v).ok_or_else(|| bad(C, format!("{v} names no layer or sublayer"))))
.collect::<Result<_>>()
.map(Self::Layers);
}
Err(expected())
}
/// Is object `id` in the scope.
fn holds(&self, d: &Document, symbols: &vectorcraft_doc::SymbolBoxes, id: NodeId) -> bool {
match self {
Self::Document => true,
Self::Artboards(rects) => d.node(id).is_some_and(|n| rects.iter().any(|r| on_artboard(n, symbols, *r))),
Self::Layers(layers) => d.ancestry(id).is_some_and(|a| a.iter().any(|i| layers.contains(i))),
}
}
}
fn font_list(_: &mut Session, p: &Value) -> Result<Value> {
let db = vectorcraft_text::FontDb::global();
match str_param(p, "family") {
@@ -911,6 +967,95 @@ mod tests {
assert!(s.doc().unwrap().selection.objects.is_empty());
}
/// Select Text by Font with an artboard scope takes the text on the artboards, as All on
/// Active Artboard does: overlapping one, or centered on it, a straddling one included.
#[test]
fn select_font_in_artboards() {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
s.execute("artboard.new", &json!({"x": 300, "y": 0, "width": 200, "height": 200})).unwrap();
s.execute("artboard.new", &json!({"x": 600, "y": 0, "width": 200, "height": 200})).unwrap();
let text = |s: &mut Session, x: i32| {
NodeId(s.execute("text.create", &json!({"x": x, "y": 50, "text": "Serif", "font": "Source Serif 4"})).unwrap()["id"].as_u64().unwrap())
};
let first = text(&mut s, 10);
let straddling = text(&mut s, 180);
let second = text(&mut s, 350);
let third = text(&mut s, 650);
let _off = text(&mut s, 1000);
let select = |s: &mut Session, scope: Value| {
s.execute("select.font", &json!({"family": "Source Serif 4", "scope": scope})).unwrap();
s.doc().unwrap().selection.objects.clone()
};
assert_eq!(select(&mut s, json!({"artboards": [0]})), [first, straddling]);
assert_eq!(select(&mut s, json!({"artboards": [1, 2]})), [second, third]);
assert_eq!(select(&mut s, json!("document")).len(), 5);
// The same objects All on Active Artboard takes.
s.execute("select.allOnArtboard", &json!({"artboard": 0})).unwrap();
assert_eq!(s.doc().unwrap().selection.objects, [first, straddling]);
// Hidden text stays out.
s.execute("select.set", &json!({"ids": [third.0]})).unwrap();
s.execute("object.hide", &json!({})).unwrap();
assert_eq!(select(&mut s, json!({"artboards": [1, 2]})), [second]);
for bad in [json!({"artboards": [3]}), json!({"artboards": []}), json!({"artboards": ["x"]}), json!({"pages": [0]}), json!("all"), json!(7)] {
assert!(s.execute("select.font", &json!({"family": "Source Serif 4", "scope": bad})).is_err(), "{bad}");
}
}
/// Select Text by Font with a layer scope takes the text in the layers, their sublayers
/// included, and still leaves out hidden and locked text.
#[test]
fn select_font_in_layers() {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
let text = |s: &mut Session, y: i32| {
NodeId(s.execute("text.create", &json!({"x": 10, "y": y, "text": "Serif", "font": "Source Serif 4"})).unwrap()["id"].as_u64().unwrap())
};
let first_layer = s.doc().unwrap().doc.layers[0].id;
let a = text(&mut s, 20);
let second_layer = s.execute("layer.new", &json!({})).unwrap()["id"].as_u64().unwrap();
let b = text(&mut s, 40);
let sub = s.execute("layer.newSublayer", &json!({"parent": second_layer})).unwrap()["id"].as_u64().unwrap();
s.execute("layer.setCurrent", &json!({"id": sub})).unwrap();
let in_sub = text(&mut s, 60);
let locked = text(&mut s, 80);
s.execute("select.set", &json!({"ids": [locked.0]})).unwrap();
s.execute("object.lock", &json!({})).unwrap();
let select = |s: &mut Session, layers: Value| {
s.execute("select.font", &json!({"family": "Source Serif 4", "scope": {"layers": layers}})).unwrap();
let mut v = s.doc().unwrap().selection.objects.clone();
v.sort();
v
};
let d = &s.doc().unwrap().doc;
assert_eq!(d.ancestry(in_sub).unwrap()[1], NodeId(sub), "the text is in the sublayer");
assert_eq!(select(&mut s, json!([first_layer.0])), [a]);
assert_eq!(select(&mut s, json!([second_layer])), [b, in_sub]);
assert_eq!(select(&mut s, json!([sub])), [in_sub]);
assert_eq!(select(&mut s, json!([first_layer.0, sub])), [a, in_sub]);
// Not a layer: an error, the selection as it was.
assert!(s.execute("select.font", &json!({"family": "Source Serif 4", "scope": {"layers": [a.0]}})).is_err());
assert!(s.execute("select.font", &json!({"family": "Source Serif 4", "scope": {"layers": [u64::MAX]}})).is_err());
assert_eq!(s.doc().unwrap().selection.objects.len(), 2);
}
/// A symbol instance whose art uses the font is selected when the instance is in scope.
#[test]
fn select_font_scope_takes_symbol_instances_in_it() {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
s.execute("artboard.new", &json!({"x": 300, "y": 0, "width": 200, "height": 200})).unwrap();
let id = s.execute("text.create", &json!({"x": 10, "y": 50, "text": "Serif", "font": "Source Serif 4"})).unwrap()["id"].clone();
s.execute("symbol.new", &json!({"name": "Mark", "ids": [id]})).unwrap();
let instance = s.doc().unwrap().selection.objects[0];
let select = |s: &mut Session, i: u64| {
s.execute("select.font", &json!({"family": "Source Serif 4", "scope": {"artboards": [i]}})).unwrap()["count"].clone()
};
assert_eq!(select(&mut s, 0), 1);
assert_eq!(s.doc().unwrap().selection.objects, [instance]);
assert_eq!(select(&mut s, 1), 0);
}
/// Replace Font leaves hidden and locked text in its font, and text inside a hidden or locked
/// group or layer, and says so. Text on a visible, unlocked template layer is changed.
#[test]
+4
View File
@@ -230,6 +230,10 @@ fn structured_junk() {
("document.export", json!({"format": "bmp", "rect": {"x": 1}, "artboard": 3})),
("document.export", json!({"format": "svg", "rect": [0, 0, 10, 10]})),
("select.add", json!({"ids": [[1], {"a": 1}]})),
("select.font", json!({"family": "Source Sans 3", "scope": {"artboards": [0, u64::MAX, -1, "x", null, 1.5]}})),
("select.font", json!({"family": "Source Sans 3", "scope": {"layers": [0, u64::MAX, -1, [1], {"a": 1}]}})),
("select.font", json!({"family": "Source Sans 3", "scope": {"artboards": [], "layers": [1]}})),
("select.font", json!({"family": "Source Sans 3", "scope": 7})),
("select.anchors", json!({"id": 2, "anchors": [[99, 99], [-1, 0], ["a", "b"]], "mode": "set"})),
("select.anchorsMany", json!({"items": [{"id": 2, "anchors": [[0, 1000]]}, null, 5]})),
("select.anchorsMany", json!({"items": [{"id": 2, "anchors": [[0, 1000], [0, 0]]}, {"id": u64::MAX}], "mode": "toggle"})),
+94 -10
View File
@@ -1,7 +1,8 @@
//! Type → Find Font…: the fonts the document uses (missing ones marked), Find (select the text
//! using a font), Change / Change All to another font from the list Replace With Font From shows
//! (the document's fonts, the recent fonts or every installed font), and Find in Folder… (the
//! missing fonts' files, in the Missing Fonts dialog: `ui.findFontsInFolder`).
//! missing fonts' files, in the Missing Fonts dialog: `ui.findFontsInFolder`). Find In limits Find
//! to the active artboard or the selected layers.
use egui::Ui;
use serde_json::{Value, json};
@@ -23,9 +24,10 @@ pub fn open_on_missing(app: &mut VectorcraftApp) {
open_at(app, first.unwrap_or(0));
}
/// Open the dialog with the document's font `selected` (an index into `text.fonts`) chosen, and
/// System's font ([`system_family`]) to change to.
/// Open the dialog with the document's font `selected` (an index into `text.fonts`) chosen,
/// System's font ([`system_family`]) to change to, and the last Find In choice (Document at first).
fn open_at(app: &mut VectorcraftApp, selected: usize) {
let find_in = FIND_IN.iter().find(|f| **f == app.ui.find_font_in).copied().unwrap_or("document");
let used = fonts(app);
let source = "system";
let rows = replacement_rows(app, source, &used);
@@ -33,7 +35,7 @@ fn open_at(app: &mut VectorcraftApp, selected: usize) {
let style = rows.iter().find(|r| r.family.eq_ignore_ascii_case(&family)).map(|r| r.style.clone()).unwrap_or_default();
app.ui.dialog = Some(Dialog::new(
"findFont",
json!({ "selected": selected, "family": family, "style": style, "selectionOnly": false, "replaceFrom": source, "rows": rows_json(&rows) }),
json!({ "selected": selected, "family": family, "style": style, "selectionOnly": false, "replaceFrom": source, "rows": rows_json(&rows), "findIn": find_in }),
));
}
@@ -210,6 +212,42 @@ fn set_source(app: &VectorcraftApp, d: &mut Dialog, source: &str, used: &[Value]
d.fields.insert("style".into(), json!(pick.map(|r| r.style.clone()).unwrap_or_default()));
}
/// Find In's choices (the `findIn` field): the whole document, the active artboard, or the layers
/// highlighted in the Layers panel (else the current layer).
const FIND_IN: [&str; 3] = ["document", "artboard", "layers"];
const FIND_IN_LABELS: [&str; 3] = ["Document", "Artboard", "Layers"];
/// The `scope` Find passes `select.font` for Find In choice `find_in`. A document without
/// artboards, or without a layer to look in, is looked through whole.
fn find_scope(app: &VectorcraftApp, find_in: &str) -> Value {
let Some(st) = app.session.active() else { return json!("document") };
let n = st.doc.artboards.len();
match find_in {
"artboard" if n > 0 => json!({ "artboards": [crate::panels::artboards::selected(app, n)] }),
"layers" => {
let is_layer = |id: &vectorcraft_doc::NodeId| st.doc.node(*id).is_some_and(vectorcraft_doc::Node::is_layer);
let mut layers: Vec<u64> = st.highlighted_rows().into_iter().filter(is_layer).map(|id| id.0).collect();
if layers.is_empty() {
layers.extend(st.current_layer().map(|id| id.0));
}
if layers.is_empty() { json!("document") } else { json!({ "layers": layers }) }
}
_ => json!("document"),
}
}
/// Run the dialog's button `button` (find, change or changeAll) on the document font `from`.
fn run_button(app: &mut VectorcraftApp, d: &Dialog, button: &str, from: &Value) -> Result<Value, String> {
match button {
"find" => {
let scope = find_scope(app, &d.str("findIn"));
app.run("select.font", json!({ "family": from["family"], "style": from["style"], "scope": scope }))
}
"change" => change(app, d, from, false),
_ => change(app, d, from, true),
}
}
fn fonts(app: &mut VectorcraftApp) -> Vec<Value> {
app.session.execute("text.fonts", &json!({})).ok().and_then(|v| v.as_array().cloned()).unwrap_or_default()
}
@@ -342,6 +380,14 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
}
}
ui.add_space(14.0);
ui.horizontal(|ui| {
widgets::field_label(ui, tl!("Find In"));
let at = FIND_IN.iter().position(|s| *s == d.str("findIn")).unwrap_or(0);
if let Some(i) = widgets::dropdown_named(ui, "ff-find-in", Some(tl!("Find In")), FIND_IN_LABELS[at], &FIND_IN_LABELS, 120.0) {
d.fields.insert("findIn".into(), json!(FIND_IN[i]));
}
});
ui.add_space(6.0);
// An empty list (Recent before any font was applied) leaves nothing to change to.
let replace = sel < list.len() && can_replace(&d, families.as_deref());
ui.horizontal(|ui| {
@@ -362,14 +408,10 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
});
});
});
app.ui.find_font_in = d.str("findIn");
let sel = d.fields.get("selected").and_then(Value::as_u64).unwrap_or(0) as usize;
if let (Some(a), Some(from)) = (act, list.get(sel)) {
let r = match a {
"find" => app.run("select.font", json!({ "family": from["family"], "style": from["style"] })),
"change" => change(app, &d, from, false),
_ => change(app, &d, from, true),
};
match r {
match run_button(app, &d, a, from) {
// The rows are a snapshot: the document's fonts and the recent fonts have moved on.
Ok(_) if a != "find" => {
let used = fonts(app);
@@ -433,6 +475,48 @@ mod tests {
assert_eq!(fonts(&mut app)[0]["family"], "Source Sans 3");
}
/// Find In passes Find the active artboard, or the selected layers (else the current
/// layer), as `select.font`'s scope.
#[test]
fn find_in_limits_find_to_the_artboard_or_the_layers() {
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
app.session.execute("artboard.new", &json!({"x": 200, "y": 0, "width": 100, "height": 100})).unwrap();
let text = |app: &mut VectorcraftApp, x: i32| {
app.session.execute("text.create", &json!({"x": x, "y": 40, "text": "Hi", "font": "Inter"})).unwrap()["id"].as_u64().unwrap()
};
let first = text(&mut app, 10);
let layer = app.session.execute("layer.new", &json!({})).unwrap()["id"].as_u64().unwrap();
let second = text(&mut app, 210);
let third = text(&mut app, 20);
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
// Choose Find In on the open dialog, draw a frame, then press Find.
let find = |app: &mut VectorcraftApp, find_in: &str| {
open(app);
app.ui.dialog.as_mut().unwrap().fields.insert("findIn".into(), json!(find_in));
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| show(app, ui.ctx()));
out.textures_delta.clear();
let d = app.ui.dialog.clone().unwrap();
let from = fonts(app).into_iter().find(|f| f["family"] == "Inter").unwrap();
run_button(app, &d, "find", &from).unwrap();
let mut ids: Vec<u64> = app.session.active().unwrap().selection.objects.iter().map(|id| id.0).collect();
ids.sort_unstable();
ids
};
open(&mut app);
assert_eq!(app.ui.dialog.as_ref().unwrap().str("findIn"), "document");
assert_eq!(find(&mut app, "document"), [first, second, third]);
// Without a view, the active artboard is the first.
assert_eq!(find(&mut app, "artboard"), [first, third]);
assert_eq!(find_scope(&app, "layers"), json!({ "layers": [layer] }));
assert_eq!(find(&mut app, "layers"), [second, third]);
// Closing and opening the dialog again keeps the last choice.
app.ui.dialog = None;
open(&mut app);
assert_eq!(app.ui.dialog.as_ref().unwrap().str("findIn"), "layers");
}
/// Change All leaves locked text in its font, and the status bar says so.
#[test]
fn change_all_clears_the_skip_warning_after_unlocking_text() {
+1
View File
@@ -2699,6 +2699,7 @@ digits Malayalam Малаялам
Reset Transformation Нулиране на трансформацията
Replace With Font Замяна с шрифт
Replace With Font From Замяна с шрифт от
Find In Търсене в
System Система
Replaces the saved preset of that name. Заменя записаната предварителна настройка с това име.
Replace… Замяна…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4000,6 +4000,7 @@ effect Regular Obično
Find in Folder… Pronađi u folderu…
Replace With Font Zamijeni fontom
Replace With Font From Zamijeni fontom iz
Find In Traži u
System Sistem
— substituted by {style} — zamijenjeno stilom {style}
— {n} characters from another font — {n} znakova iz drugog fonta
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3406,6 +3406,7 @@ digits Malayalam Malaiàlam
Reset Transformation Restableix la transformació
Replace With Font Reemplaça per un tipus de lletra
Replace With Font From Reemplaça per un tipus de lletra de
Find In Cerca a
System Sistema
Replace “{name}”, the file this document was opened from? Voleu reemplaçar “{name}”, el fitxer amb què es va obrir aquest document?
Replaces the saved preset of that name. Reemplaça el preajust desat amb aquest nom.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4050,6 +4050,7 @@ effect Regular Pravidelný
Find in Folder… Najít ve složce…
Replace With Font Nahradit písmem
Replace With Font From Nahradit písmem z
Find In Hledat v
System Systém
— substituted by {style} — nahrazeno řezem {style}
— {n} characters from another font — znaky z jiného písma: {n}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3766,6 +3766,7 @@ effect Regular Regular
Find in Folder… Find i mappe…
Replace With Font Erstat med skrifttype
Replace With Font From Erstat med skrifttype fra
Find In Søg i
System System
— substituted by {style} — erstattet af {style}
— {n} characters from another font — {n} tegn fra en anden skrifttype
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2731,6 +2731,7 @@ digits Malayalam Malayalam
Reset Transformation Transformation zurücksetzen
Replace With Font Durch Schrift ersetzen
Replace With Font From Ersetzende Schrift aus
Find In Suchen in
System System
Replaces the saved preset of that name. Ersetzt die gespeicherte Vorgabe dieses Namens.
Replace… Ersetzen…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3341,6 +3341,7 @@ digits Malayalam Μαλαγιάλαμ
Reset Transformation Επαναφορά μετασχηματισμού
Replace With Font Αντικατάσταση με γραμματοσειρά
Replace With Font From Αντικατάσταση με γραμματοσειρά από
Find In Αναζήτηση σε
System Σύστημα
Replaces the saved preset of that name. Αντικαθιστά την αποθηκευμένη προεπιλογή με αυτό το όνομα.
Require a password to open the document Απαίτηση κωδικού για το άνοιγμα εγγράφου
Can't render this file because it contains an unexpected character in line 168 and column 5.
+1
View File
@@ -2774,6 +2774,7 @@ digits Malayalam Malayálam
Reset Transformation Restablecer transformación
Replace With Font Reemplazar con fuente
Replace With Font From Reemplazar con fuente de
Find In Buscar en
System Sistema
Replaces the saved preset of that name. Reemplaza el preajuste guardado con ese nombre.
Replace… Reemplazar…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3969,6 +3969,7 @@ effect Regular Normaali
Find in Folder… Etsi kansiosta…
Replace With Font Korvaa fontilla
Replace With Font From Korvaavan fontin lähde
Find In Etsi kohteesta
System Järjestelmä
— substituted by {style} — korvattu tyylillä {style}
— {n} characters from another font — {n} merkkiä toisesta fontista
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2766,6 +2766,7 @@ digits Malayalam Malayalam
Reset Transformation Réinitialiser la transformation
Replace With Font Remplacer par la police
Replace With Font From Remplacer par une police de
Find In Rechercher dans
System Système
Replaces the saved preset of that name. Remplace le paramètre prédéfini enregistré sous ce nom.
Replace… Remplacer…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3306,6 +3306,7 @@ mark-style Japanese Japán
Reset Transformation Átalakítás visszaállítása
Replace With Font Helyettesítő betűtípus
Replace With Font From Helyettesítő betűtípus forrása
Find In Keresés helye
System Rendszer
Replace “{name}”, the file this document was opened from? Felülírod ezt a fájlt, amelyből a dokumentumot megnyitottad: „{name}”?
Replaces the saved preset of that name. Felülírja az azonos nevű mentett előbeállítást.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3766,6 +3766,7 @@ effect Regular Reguler
Find in Folder… Temukan di folder…
Replace With Font Ganti dengan font
Replace With Font From Ganti dengan font dari
Find In Cari di
System Sistem
— substituted by {style} — digantikan oleh {style}
— {n} characters from another font — {n} karakter dari font lain
Can't render this file because it contains an unexpected character in line 2270 and column 5.
+1
View File
@@ -2807,6 +2807,7 @@ digits Malayalam Malayalam
Reset Transformation Reimposta trasformazione
Replace With Font Sostituisci con font
Replace With Font From Sostituisci con font da
Find In Cerca in
System Sistema
Replaces the saved preset of that name. Sostituisce il preset salvato con quel nome.
Replace… Sostituisci…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2793,6 +2793,7 @@ mark-style Japanese 日本式
Reset Transformation 変形をリセット
Replace With Font 置換するフォント
Replace With Font From 置換するフォントの一覧
Find In 検索範囲
System システム
Replace it with another file, or ignore it: its images keep showing the preview saved with the document. 別のファイルに置き換えるか、無視してください。無視した場合、画像にはドキュメントに保存されたプレビューが表示され続けます。
Replaces the saved preset of that name. 同じ名前の保存済みプリセットを置き換えます。
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3559,6 +3559,7 @@ effect Regular ჩვეულებრივი
Find in Folder… საქაღალდეში ძებნა…
Replace With Font ჩანაცვლება შრიფტით
Replace With Font From ჩანაცვლების შრიფტის წყარო
Find In ძებნის არე
System სისტემა
— substituted by {style} — ჩანაცვლებულია: {style}
— {n} characters from another font — სხვა შრიფტის სიმბოლოები: {n}
Can't render this file because it is too large.
+1
View File
@@ -2782,6 +2782,7 @@ mark-style Japanese 일본식
Reset Transformation 변형 재설정
Replace With Font 바꿀 글꼴
Replace With Font From 바꿀 글꼴 목록
Find In 찾을 위치
System 시스템
Replace it with another file, or ignore it: its images keep showing the preview saved with the document. 다른 파일로 바꾸거나 무시하세요. 무시하면 해당 이미지는 문서에 저장된 미리 보기로 계속 표시됩니다.
Replaces the saved preset of that name. 같은 이름으로 저장된 사전 설정을 대체합니다.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3780,6 +3780,7 @@ effect Regular Regelmatig
Find in Folder… Zoeken in map…
Replace With Font Vervangen door lettertype
Replace With Font From Vervangen door lettertype uit
Find In Zoeken in
System Systeem
— substituted by {style} — vervangen door {style}
— {n} characters from another font — {n} tekens van een ander lettertype
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3889,6 +3889,7 @@ effect Regular Normalny
Find in Folder… Znajdź w folderze…
Replace With Font Zastąp czcionką
Replace With Font From Zastąp czcionką z
Find In Szukaj w
System System
— substituted by {style} — zastąpiona przez {style}
— {n} characters from another font — znaki z innej czcionki: {n}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2314,6 +2314,7 @@ digits Malayalam Malaiala
Reset Transformation Redefinir Transformação
Replace With Font Substituir Com Fonte
Replace With Font From Substituir Com Fonte De
Find In Localizar em
System Sistema
Replace it with another file, or ignore it: its images keep showing the preview saved with the document. Substitua por outro arquivo, ou ignore-o: suas imagens continuam mostrando a pré-visualização salva com o documento.
Replaces the saved preset of that name. Substitui o predefinido salvo com esse nome.
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -2802,6 +2802,7 @@ digits Malayalam Малаялам
Reset Transformation Сбросить трансформацию
Replace With Font Заменить шрифтом
Replace With Font From Заменить шрифтом из
Find In Искать в
System Система
Replaces the saved preset of that name. Заменяет сохранённый набор с таким именем.
Replace… Заменить…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2710,6 +2710,7 @@ digits Malayalam Malayalam
Reset Transformation Återställ transformering
Replace With Font Ersätt med typsnitt
Replace With Font From Ersätt med typsnitt från
Find In Sök i
System System
Replaces the saved preset of that name. Ersätter den sparade förinställningen med samma namn.
Replace… Ersätt…
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -2579,6 +2579,7 @@ digits Malayalam Malayalam
Reset Transformation Dönüşümü Sıfırla
Replace With Font Yazı Tipiyle Değiştir
Replace With Font From Değiştirilecek Yazı Tipi Kaynağı
Find In Arama yeri
System Sistem
Replaces the saved preset of that name. Bu adın kayıtlı ön ayarını değiştirir.
Replace… Değiştir…
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -2799,6 +2799,7 @@ digits Malayalam Малаялам
Reset Transformation Скинути трансформацію
Replace With Font Замінити шрифтом
Replace With Font From Замінити шрифтом з
Find In Шукати в
System Система
Replaces the saved preset of that name. Замінює збережений набір із таким ім’ям.
Replace… Замінити…
Can't render this file because it contains an unexpected character in line 182 and column 5.
+1
View File
@@ -3074,6 +3074,7 @@ mark-style Japanese Kiểu Nhật
Reset Transformation Đặt lại biến đổi
Replace With Font Thay thế bằng phông chữ
Replace With Font From Thay thế bằng phông chữ từ
Find In Tìm trong
System Hệ thống
Replace “{name}”, the file this document was opened from? Thay thế “{name}”, tệp mà tài liệu này được mở từ đó?
Replaces the saved preset of that name. Thay thế thiết lập sẵn đã lưu có tên đó.
Can't render this file because it contains an unexpected character in line 204 and column 5.
+1
View File
@@ -2677,6 +2677,7 @@ digits Malayalam 马拉雅拉姆数字
Reset Transformation 重置变换
Replace With Font 替换为字体
Replace With Font From 替换字体来源
Find In 查找范围
System 系统
Replaces the saved preset of that name. 替换同名已存预设。
Replace… 替换…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3680,6 +3680,7 @@ digits Malayalam 馬來亞拉姆數字
Reset Transformation 重設變形
Replace With Font 取代為字體
Replace With Font From 取代字體來源
Find In 尋找範圍
System 系統
Replaces the saved preset of that name. 取代同名的已儲存預設集。
Replace… 取代…
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -370,6 +370,9 @@ pub struct UiState {
/// That popup's first text field is still to take the keyboard.
#[serde(skip)]
pub transient_focus: bool,
/// Type › Find Font…'s last Find In choice (`find_font::FIND_IN`), kept while the app runs.
#[serde(skip)]
pub find_font_in: String,
pub control_bar: bool,
/// The Control bar is docked to the bottom of the window instead of the top
/// (`window.control.dock`).
@@ -974,6 +977,7 @@ impl Default for UiState {
open_panel: None,
transient_panel: None,
transient_focus: false,
find_font_in: String::new(),
control_bar: false,
control_bar_bottom: false,
toolbar: true,
+1
View File
@@ -42,6 +42,7 @@ M7 type: Touch Type moves a right-to-left character along its line and up from i
| 2026-11-05 | minor | Selection Tools add-on: Select Linked, Embedded, Missing and Modified select the images and placed documents with that link status (Object › Plug-ins › Selection Tools and the Links panel menu; Shift adds), and Select by Link Status… lists them by name, status and format to pick from, optionally zooming to them; what is hidden, locked, on a template layer or outside the isolated group is left out and the status bar says how many (#5526, a VectorCraft addition); scores and estimates unchanged (estimated). |
| 2026-11-05 | minor | Pencil and Paintbrush: a new stroke with Keep selected off leaves nothing selected, the earlier selection included, while Keep selected on selects only the new path (#5752); scores and estimates unchanged. |
| 2026-11-05 | minor | Layers panel: a layer whose Layer Options › Print is off shows its name italic, as the reference application shows non-printing layers, and keeps it while the row is dragged (#5739); scores and estimates unchanged. |
| 2026-11-04 | minor | M7: Select Text by Font takes a scope: the document, chosen artboards or chosen layers; Find Font's Find In offers Document, Artboard (the active one) and Layers (the selected ones, else the current one) (#5529, a VectorCraft addition); estimates unchanged. |
| 2026-11-04 | minor | M7: Find Font's Replace With Font lists family and style rows with a search, and the list and the document's font list scroll on their own (#4972); scores and estimates unchanged. |
| 2026-11-04 | minor | Selection Tools add-on: Select by Artboards selects the objects overlapping, fully inside or fully outside several chosen artboards, from a dialog that lists, searches and multi-selects the artboards and fits one in the window on double-click (#5525, #5548, #5549, a VectorCraft addition); scores and estimates unchanged. |
| 2026-11-04 | minor | M4: raster/PSD exports default to document resolution; CLI run/convert expose --ppi, matching the engine and MCP precedence. Scores and hours unchanged. |
File diff suppressed because one or more lines are too long
+2 -1
View File
@@ -34,7 +34,7 @@ against Illustrator's public documentation; **estimated** unless noted.
| Character size and horizontal scale after object/group scaling | done | #1034: fields and setters compensate the stored affine; point size follows the transformed em-height axis, not the glyph ink bounds. Range edits, keyboard increments, leading and baseline shift use document points; rotation, reflection and shear remain editable. |
| Preserve font sizes while scaling a group | missing | Separate enhancement; scaling still scales text with the other artwork. |
| OpenType panel, Glyphs panel, alternates | done | Highlight Alternate Glyphs preference (#394, off by default) paints pink behind OpenType substitutions on screen when on (required shaping such as Arabic joining forms and the lam-alef ligature, including a lam-alef older fonts file under `liga`, is not highlighted; an optional feature that changes the ligature is); stylistic sets ss01–ss20, stylistic and titling alternates, proportional and lining figures, superscript and subscript are character-style features (`text.setStyle {features}`, #1607); the OpenType panel's Position menu sets superscript, subscript, numerator or denominator glyphs (`sups`, `subs`, `numr`, `dnom`). The OpenType panel's Stylistic Sets menu lists the selected font's `ss01`–`ss20` features, using a font-supplied name where available and `Set N` otherwise; several sets can be active, and selecting an active one removes it. For a mixed-font selection it uses the first selected character's font, a VectorCraft design choice ([public help](https://helpx.adobe.com/illustrator/desktop/design-with-text/special-characters-glyphs/add-stylistic-sets-to-selected-text.html), [OpenType feature specification](https://learn.microsoft.com/en-us/typography/opentype/spec/features_pt)). The Glyphs panel highlights the character selected with the Type tool (a range's first character, else the one before the caret) and scrolls it into view as the selection moves (#4908; `panels::glyphs` tests); its Show menu lists Unicode ranges and Alternates for Current Selection, the selected character's alternate glyphs from the font's `GSUB` table, one of which a double-click draws for that character (`text.setGlyphAlternate`); the glyph-cell pop-up of alternates is not there yet |
| Font menu: samples, live preview, filters, favourites, recent families; font matching by any name; Find Font | done | #3515: recently applied families appear first, once each; missing families are omitted and the existing Number of Recent Fonts setting applies; #339; script filter: Japanese, Latin, Thai; the font scan on Linux also reads the folders fontconfig's configuration lists, such as TeX fonts under `/usr/share/texmf/fonts` (#1548, fixes #1529; not verified on Linux) Replace With Font lists family and style rows, one click setting both, with a search over them; its list scrolls on its own, apart from the document's font list (#4972). |
| Font menu: samples, live preview, filters, favourites, recent families; font matching by any name; Find Font | done | #3515: recently applied families appear first, once each; missing families are omitted and the existing Number of Recent Fonts setting applies; #339; script filter: Japanese, Latin, Thai; the font scan on Linux also reads the folders fontconfig's configuration lists, such as TeX fonts under `/usr/share/texmf/fonts` (#1548, fixes #1529; not verified on Linux); Select Text by Font takes a scope: the document, chosen artboards or chosen layers; Find Font's Find In offers Document, Artboard (the active one) and Layers (the selected ones, else the current one) (#5529, a VectorCraft addition); Replace With Font lists family and style rows, one click setting both, with a search over them; its list scrolls on its own, apart from the document's font list (#4972). |
| Missing glyphs set in an installed font made for their script (Thai, Lao, Khmer, Myanmar first; then any font that has them) | done | Thai: Leelawadee UI, Tahoma, Thonburi, Sukhumvit Set, Noto Sans Thai, TLWG |
| Full font-name matching (OpenType name ID 4) | done | #1258: loaded/cataloged faces and Missing Fonts searches; localized names and variable default faces; Windows font-service QA pending |
| Variable fonts | done | named instances as styles; Character panel › Variable Font sets each axis (`fontAxes`); the font menu's Font Technology › Variable lists only variable font families, with the script, kind and ★ filters ([Find, apply, and adjust variable fonts](https://helpx.adobe.com/illustrator/desktop/design-with-text/fonts-and-scripts/find-apply-and-adjust-variable-fonts.html)); a family with a variable file beside static ones counts as variable. Evidence: `font_menu::tests::the_variable_filter_lists_only_variable_families`, `tests_axes::a_family_with_variation_axes_is_variable`, `tests_sysfonts::a_family_with_a_variable_file_beside_static_ones_is_variable` |
@@ -87,6 +87,7 @@ Character Styles menu New/Redefine and footer New capture the first selected cha
| 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 |
| 2026-11-05 | minor | Character panel › Show Snap to Glyph Options scrolls the panel down to the Snap to Glyph section, which comes after every character attribute and could sit out of sight, and the section starts with a Snap to Glyph checkbox (View › Snap to Glyph) so its dimmed options can be turned on there (#5273); scores and estimates unchanged |
| 2026-11-05 | minor | Release Type on a Path joins the Type Tools add-on: type on a path splits into point type and its path (#5532, beyond Illustrator). Scores and estimates unchanged. |
| 2026-11-04 | minor | Select Text by Font takes a scope: the document, chosen artboards or chosen layers; Find Font's Find In offers Document, Artboard (the active one) and Layers (the selected ones, else the current one) (#5529, a VectorCraft addition); scores and estimates unchanged. |
| 2026-11-04 | minor | Typographer's Quotes: a lone typed `”` (U+201D) is set like a straight `"` where a quote opens (at the start, after a space), taking the document's opening quote, and stays `”` where one closes: the macOS Serbian Cyrillic layout has no straight quote and types `”` on Shift+2, which now gives „…” (#5247). `“ „` and the single quotes `‘ ’ ‚` stay as typed (that layout's apostrophe key types `’`, and `’90s` keeps its apostrophe); pasted text keeps its quotes; with the option off `”` stays as typed. Evidence: `docsetup::a_typed_closing_double_quote_opens_where_a_quote_opens`; scores unchanged |
| 2026-11-04 | minor | A Type-range Copy or Cut publishes styled art alongside plain text, so a later canvas Paste keeps its character formatting after editing ends (#5636). Scores and estimates unchanged. |
| 2026-11-03 | minor | Glyphs panel adds Alternates for Current Selection. A chosen alternate changes one character's drawn glyph while keeping its Unicode text; character-style capture does not copy that per-character choice. Scores and estimates unchanged. |