Merge pull request #1809 from Messhias/feat/394-identify-objects-by

Part of #394: Identify Objects By (Object Name / XML ID)
This commit is contained in:
BFlat authored and GitHub committed 2026-10-14 05:38:49 +07:00
commit a23f5711af
12 files changed
+198 -61

No files matched your search

+1
View File
@@ -48,6 +48,7 @@ pub use symbols::SymbolBoxes;
pub mod text;
pub mod trace;
pub mod variables;
pub mod xml_id;
use std::collections::BTreeMap;
use std::sync::Arc;
+6
View File
@@ -978,6 +978,12 @@ impl Node {
_ => format!("<{}>", self.kind_label()),
}
}
/// [`Self::display_name`], or its XML id when Preferences › Units › Identify Objects By is XML ID.
pub fn identify_label(&self, xml_id: bool) -> String {
let name = self.display_name();
if xml_id { crate::xml_id::from_object_name(&name) } else { name }
}
/// Path data for path nodes.
pub fn path_data(&self) -> Option<&PathData> {
match &self.kind {
+117
View File
@@ -0,0 +1,117 @@
//! Object names ↔ XML IDs (Preferences › Units › Identify Objects By).
//!
//! Illustrator's public documentation: with XML ID selected, the Layers panel shows and edits
//! names that conform to XML naming (letter, underscore or colon first; no spaces). Underscores
//! stand for spaces and `_xHH_` / `_xHHHH_` escape other characters, as in `.ai` `AI10_ArtUID`
//! values ([edit dynamic objects](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/edit-dynamic-objects.html)).
/// An object name from its XML id: `_` is a space and `_xHH_` / `_xHHHH_` a character; ids made
/// unique end with `_` and 20 or more digits (stripped).
pub fn to_object_name(id: &str) -> String {
let id = strip_unique(id);
let mut out = String::with_capacity(id.len());
let mut rest = id;
while let Some(c) = rest.chars().next() {
if c == '_' {
if let Some((ch, n)) = escaped(rest) {
out.push(ch);
rest = rest.get(n..).unwrap_or_default();
continue;
}
out.push(' ');
} else {
out.push(c);
}
rest = rest.get(c.len_utf8()..).unwrap_or_default();
}
out
}
/// The XML id for an object name (the inverse of [`to_object_name`]).
pub fn from_object_name(name: &str) -> String {
let mut out = String::with_capacity(name.len().saturating_mul(2));
for (i, c) in name.chars().enumerate() {
if c == ' ' {
out.push('_');
} else if c == '_' {
out.push_str("_x5F_");
} else if i == 0 && c.is_ascii_digit() {
out.push_str(&format!("_x{:X}_", u32::from(c)));
} else if c.is_alphanumeric() || c == '.' || c == '-' || c == ':' {
out.push(c);
} else {
out.push_str(&format!("_x{:X}_", u32::from(c)));
}
}
if out.is_empty() || !starts_name(&out) {
out.insert(0, '_');
}
out
}
/// Whether `id` is a usable XML ID for Identify Objects By › XML ID: non-empty, no whitespace,
/// starts with a letter, `_` or `:`, and otherwise letters, digits, `.`, `-`, `_` or `:`.
pub fn is_valid(id: &str) -> bool {
let id = id.trim();
if id.is_empty() || id.chars().any(char::is_whitespace) {
return false;
}
let mut chars = id.chars();
let Some(first) = chars.next() else {
return false;
};
if !(first.is_alphabetic() || first == '_' || first == ':') {
return false;
}
chars.all(|c| c.is_alphanumeric() || matches!(c, '_' | '.' | '-' | ':'))
}
fn starts_name(id: &str) -> bool {
id.starts_with(|c: char| c.is_alphabetic() || c == '_' || c == ':')
}
/// `_xHHHH_` at the start of `s`: the character and the escape's length.
fn escaped(s: &str) -> Option<(char, usize)> {
let body = s.strip_prefix("_x")?;
let end = body.find('_')?;
let hex = body.get(..end)?;
if hex.is_empty() || hex.len() > 6 {
return None;
}
let c = char::from_u32(u32::from_str_radix(hex, 16).ok()?)?;
Some((c, end + 3))
}
/// `id` without the `_000…_` suffix that makes it unique.
fn strip_unique(id: &str) -> &str {
let Some(body) = id.strip_suffix('_') else {
return id;
};
let Some(at) = body.rfind('_') else {
return id;
};
let digits = body.get(at + 1..).unwrap_or_default();
if digits.len() >= 20 && digits.bytes().all(|b| b.is_ascii_digit()) { body.get(..at).unwrap_or(id) } else { id }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_ai_style_ids() {
for (name, id) in [
("Layer 1", "Layer_1"),
("My Rect #2", "My_Rect__x23_2"),
("1abc (paren)", "_x31_abc__x28_paren_x29_"),
("Ünï 日本", "Ünï_日本"),
("a_b", "a_x5F_b"),
] {
assert_eq!(from_object_name(name), id, "encode {name}");
assert_eq!(to_object_name(id), name, "decode {id}");
}
assert_eq!(to_object_name("S_00000092429200007574234970000005974767392385616558_"), "S");
assert!(is_valid("Layer_1") && is_valid("_x31_abc") && is_valid(":root"));
assert!(!is_valid("My Rect") && !is_valid("1abc") && !is_valid(""));
}
}
+21 -8
View File
@@ -80,7 +80,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Layer Options…",
["Window", "Layers"],
None,
"{ids?|id? (default: the highlighted rows, else the current layer), name?, visible?, locked?, template?, printable?, preview?, dimImages?: 0..100|false, color?: index 0..26|\"#rrggbb\"|preset name} Layer Options; rows that are not layers take name, visible and locked. Template on also locks the layer and dims its images to 50% (unless given); off unlocks it and stops dimming. One undo step",
"{ids?|id? (default: the highlighted rows, else the current layer), name? (with prefs.identifyObjectsBy xmlId: a valid XML ID, stored as the object name), visible?, locked?, template?, printable?, preview?, dimImages?: 0..100|false, color?: index 0..26|\"#rrggbb\"|preset name} Layer Options; rows that are not layers take name, visible and locked. Template on also locks the layer and dims its images to 50% (unless given); off unlocks it and stops dimming. One undo step",
has_doc,
set_props
),
@@ -297,7 +297,9 @@ fn layer_color_param(v: &Value) -> std::result::Result<LayerColor, String> {
}
/// Layer Options in `p` applied to layer or row `n` (validated first by [`check_options`]).
fn apply_options(n: &mut Node, p: &Value) {
/// `xml_id`: Preferences › Units › Identify Objects By › XML ID — `name` is an XML id and is
/// stored as the object name ([`vectorcraft_doc::xml_id`]).
fn apply_options(n: &mut Node, p: &Value, xml_id: bool) {
if let Some(v) = str_param(p, "name") {
if v.trim().is_empty() {
// A layer keeps a name; an object goes back to its generated `<Kind>` one.
@@ -305,7 +307,8 @@ fn apply_options(n: &mut Node, p: &Value) {
n.name = None;
}
} else {
n.name = Some(v.chars().take(512).collect());
let name = if xml_id { vectorcraft_doc::xml_id::to_object_name(v) } else { v.to_string() };
n.name = Some(name.chars().take(512).collect());
}
}
if let Some(v) = p.get("visible").and_then(Value::as_bool) {
@@ -354,7 +357,9 @@ fn apply_options(n: &mut Node, p: &Value) {
}
/// Bad values in Layer Options params, as an error for `cmd`.
fn check_options(cmd: &str, p: &Value) -> Result<()> {
/// `xml_id`: Preferences › Units › Identify Objects By › XML ID — a non-empty `name` must be a
/// valid XML id.
fn check_options(cmd: &str, p: &Value, xml_id: bool) -> Result<()> {
if let Some(v) = p.get("color") {
layer_color_param(v).map_err(|e| bad(cmd, e))?;
}
@@ -368,6 +373,12 @@ fn check_options(cmd: &str, p: &Value) -> Result<()> {
{
return Err(bad(cmd, "`dimImages` is a percentage or false"));
}
if xml_id
&& let Some(name) = str_param(p, "name").filter(|n| !n.trim().is_empty())
&& !vectorcraft_doc::xml_id::is_valid(name)
{
return Err(bad(cmd, "Identify Objects By is XML ID: the name must be a valid XML ID (start with a letter, underscore or colon; no spaces)"));
}
Ok(())
}
@@ -446,12 +457,13 @@ fn clip_toggle(s: &mut Session, p: &Value) -> Result<Value> {
/// A new layer with the options in `p`, at `parent`/`index`; it becomes current and highlighted.
fn add_layer(s: &mut Session, p: &Value, cmd: &str, label: &str, place: impl FnOnce(&Document) -> Result<(Option<NodeId>, usize)>) -> Result<Value> {
check_options(cmd, p)?;
let xml_id = s.prefs.identify_objects_by == "xmlId";
check_options(cmd, p, xml_id)?;
let name = str_param(p, "name").filter(|n| !n.trim().is_empty()).map(str::to_string);
let id = s.edit(label, |d, _| {
let (parent, index) = place(d)?;
let mut n = make_layer(d, name.as_deref());
apply_options(&mut n, p);
apply_options(&mut n, p, xml_id);
Ok(d.insert(parent, index, n)?)
})?;
let st = s.doc_mut()?;
@@ -568,14 +580,15 @@ fn highlight(s: &mut Session, p: &Value) -> Result<Value> {
fn set_props(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "layer.setProps";
check_options(C, p)?;
let xml_id = s.prefs.identify_objects_by == "xmlId";
check_options(C, p, xml_id)?;
let ids = rows_param(s, p, C)?;
if ids.is_empty() {
return Err(bad(C, "no rows"));
}
s.edit("Layer Options", |d, sel| {
for id in &ids {
apply_options(d.node_mut(*id).ok_or(EngineError::NoNode(*id))?, p);
apply_options(d.node_mut(*id).ok_or(EngineError::NoNode(*id))?, p, xml_id);
}
sel.deselect_uneditable(d);
Ok(())
+19
View File
@@ -238,6 +238,25 @@ fn visibility_and_lock_per_row_are_undoable() {
assert_eq!(node(&s, sub).display_name(), "Sub", "a layer keeps its name");
}
/// Preferences › Units › Identify Objects By › XML ID (#394): `layer.setProps` takes a valid XML
/// id and stores the object name; Object Name (the default) keeps free text. The Layers label is
/// the XML id when that preference is on.
#[test]
fn identify_objects_by_xml_id_names_layers() {
let mut s = session();
let a = rect(&mut s, 0.0);
run(&mut s, "layer.setProps", json!({"id": a.0, "name": "My Rect #2"}));
assert_eq!(node(&s, a).display_name(), "My Rect #2");
assert_eq!(node(&s, a).identify_label(true), "My_Rect__x23_2");
run(&mut s, "prefs.set", json!({"key": "identifyObjectsBy", "value": "xmlId"}));
assert!(s.execute("layer.setProps", &json!({"id": a.0, "name": "My Rect"})).is_err(), "spaces are refused");
run(&mut s, "layer.setProps", json!({"id": a.0, "name": "My_Rect__x23_2"}));
assert_eq!(node(&s, a).display_name(), "My Rect #2", "the XML id is stored as the object name");
run(&mut s, "prefs.set", json!({"key": "identifyObjectsBy", "value": "objectName"}));
run(&mut s, "layer.setProps", json!({"id": a.0, "name": "My Rect"}));
assert_eq!(node(&s, a).display_name(), "My Rect");
}
#[test]
fn object_set_props_deselects_the_objects_it_hides_or_locks() {
let mut s = session();
+2 -39
View File
@@ -123,46 +123,9 @@ pub(super) fn close(o: &mut Obj, stack: &mut Vec<V>, base: usize) {
}
}
/// An object name from its XML id (`AI10_ArtUID`): `_` stands for a space and `_xHH_` for a
/// character; ids made unique end with `_` and 20 or more digits.
/// An object name from its XML id (`AI10_ArtUID`).
pub(super) fn xml_name(id: &str) -> String {
let id = strip_unique(id);
let mut out = String::with_capacity(id.len());
let mut rest = id;
while let Some(c) = rest.chars().next() {
if c == '_' {
if let Some((ch, n)) = escaped(rest) {
out.push(ch);
rest = rest.get(n..).unwrap_or_default();
continue;
}
out.push(' ');
} else {
out.push(c);
}
rest = rest.get(c.len_utf8()..).unwrap_or_default();
}
out
}
/// `_xHHHH_` at the start of `s`: the character and the escape's length.
fn escaped(s: &str) -> Option<(char, usize)> {
let body = s.strip_prefix("_x")?;
let end = body.find('_')?;
let hex = body.get(..end)?;
if hex.is_empty() || hex.len() > 6 {
return None;
}
let c = char::from_u32(u32::from_str_radix(hex, 16).ok()?)?;
Some((c, end + 3))
}
/// `id` without the `_000…_` suffix that makes it unique.
fn strip_unique(id: &str) -> &str {
let Some(body) = id.strip_suffix('_') else { return id };
let Some(at) = body.rfind('_') else { return id };
let digits = body.get(at + 1..).unwrap_or_default();
if digits.len() >= 20 && digits.bytes().all(|b| b.is_ascii_digit()) { body.get(..at).unwrap_or(id) } else { id }
vectorcraft_doc::xml_id::to_object_name(id)
}
#[cfg(test)]
+8 -4
View File
@@ -33,9 +33,10 @@ fn color_field(c: LayerColor) -> String {
}
/// The fields for `n` (a layer or any other row).
fn fields_of(n: &Node) -> serde_json::Map<String, Value> {
/// `xml_id`: Preferences › Units › Identify Objects By › XML ID.
fn fields_of(n: &Node, xml_id: bool) -> serde_json::Map<String, Value> {
let mut f = serde_json::Map::new();
f.insert("name".into(), json!(n.display_name()));
f.insert("name".into(), json!(n.identify_label(xml_id)));
f.insert("visible".into(), json!(n.visible));
f.insert("locked".into(), json!(n.locked));
f.insert("layer".into(), json!(n.is_layer()));
@@ -70,7 +71,8 @@ pub fn open(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
}
};
let first = ids.first().and_then(|id| st.doc.node(*id)).ok_or("no such layer or object")?;
let mut f = fields_of(first);
let xml_id = app.session.prefs.identify_objects_by == "xmlId";
let mut f = fields_of(first, xml_id);
f.insert("ids".into(), json!(ids.iter().map(|i| i.0).collect::<Vec<_>>()));
f.insert("mode".into(), json!("edit"));
app.ui.dialog = Some(Dialog { kind: KIND.into(), fields: f });
@@ -93,8 +95,10 @@ pub fn open_new(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
}
let sublayer = p.get("sublayer").and_then(Value::as_bool).unwrap_or(false);
let color = LayerColor::Preset((layers.len() % LAYER_COLORS.len()) as u8);
let name = format!("Layer {i}");
let name = if app.session.prefs.identify_objects_by == "xmlId" { vectorcraft_doc::xml_id::from_object_name(&name) } else { name };
let fields = json!({
"mode": if sublayer { "newSublayer" } else { "new" }, "layer": true, "name": format!("Layer {i}"), "color": color_field(color),
"mode": if sublayer { "newSublayer" } else { "new" }, "layer": true, "name": name, "color": color_field(color),
"template": false, "locked": false, "visible": true, "printable": true, "preview": true, "dimImages": false, "dimPercent": 50,
});
app.ui.dialog = Some(Dialog::new(KIND, fields));
+13 -5
View File
@@ -169,7 +169,8 @@ fn drag_ghost(ui: &Ui, view: &View, src: NodeId, pos: egui::Pos2) {
if view.opts.thumb(n) {
x = row_thumb(&ui, view, n, r, x).right() + 4.0;
}
let name = painted_name(n, &n.display_name(), crate::i18n::current());
let label = n.identify_label(view.identify_xml_id);
let name = if view.identify_xml_id { label } else { painted_name(n, &label, crate::i18n::current()) };
ui.painter().text(egui::pos2(x, r.center().y), egui::Align2::LEFT_CENTER, name, egui::FontId::proportional(13.0), t.text);
}
@@ -329,6 +330,8 @@ struct View<'a> {
column: Option<ColumnDrag>,
/// The row to scroll into view.
reveal: Option<NodeId>,
/// Preferences › Units › Identify Objects By › XML ID.
identify_xml_id: bool,
}
/// What one frame of the panel decided.
@@ -390,6 +393,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
h: app.ui.layers_panel.row(),
column: ui.data(|d| d.get_temp(key("column"))),
reveal,
identify_xml_id: app.session.prefs.identify_objects_by == "xmlId",
};
let mut out = Out::default();
ui.add_space(6.0);
@@ -685,8 +689,9 @@ fn row(ui: &mut Ui, view: &View, item: &Row, out: &mut Out) {
x = th.right() + 4.0;
th
});
// Name.
let name = if doc.mask_edit.is_some_and(|m| m.layer == n.id) { "<Opacity Mask>".to_string() } else { n.display_name() };
// Name (Preferences › Units › Identify Objects By: Object Name or XML ID).
let xml_id = view.identify_xml_id;
let name = if doc.mask_edit.is_some_and(|m| m.layer == n.id) { "<Opacity Mask>".to_string() } else { n.identify_label(xml_id) };
let font = egui::FontId::proportional(13.0);
let renaming: Option<(u64, String)> = ui.data(|d| d.get_temp(rename_id()));
let name_rect = egui::Rect::from_min_max(egui::pos2(x - 2.0, r.center().y - 10.0), egui::pos2(r.right() - 44.0, r.center().y + 10.0));
@@ -713,10 +718,13 @@ fn row(ui: &mut Ui, view: &View, item: &Row, out: &mut Out) {
_ => {
let painter = ui.painter().with_clip_rect(name_rect);
// Generated names ("<Path>", "<Opacity Mask>") are translated where painted; the stored name stays English.
// XML IDs stay as ids (not translated).
let shown = if doc.mask_edit.is_some_and(|m| m.layer == n.id) {
tl!("<Opacity Mask>").to_string()
} else if xml_id {
name.clone()
} else {
painted_name(n, &name, crate::i18n::current())
painted_name(n, &n.display_name(), crate::i18n::current())
};
let text = painter.text(egui::pos2(x, r.center().y), egui::Align2::LEFT_CENTER, shown, font, if item.dim { t.text_dim } else { t.text });
name_end = text.right();
@@ -758,7 +766,7 @@ fn row(ui: &mut Ui, view: &View, item: &Row, out: &mut Out) {
let on_name = ui.input(|i| i.pointer.interact_pos()).is_some_and(|p| p.x <= name_end.max(name_rect.left() + 20.0) && p.x >= name_rect.left());
let on_thumb = thumb_rect.is_some_and(|th| ui.input(|i| i.pointer.interact_pos()).is_some_and(|p| th.contains(p)));
if on_name && !on_thumb {
ui.data_mut(|d| d.insert_temp(rename_id(), (n.id.0, n.display_name())));
ui.data_mut(|d| d.insert_temp(rename_id(), (n.id.0, n.identify_label(view.identify_xml_id))));
} else {
out.actions.push(("ui.layerOptions".into(), json!({"ids": [n.id.0]})));
}
+2 -1
View File
@@ -97,6 +97,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
return;
};
// Owned snapshots: the rows below run commands, which borrow the app mutably.
let xml_id = app.session.prefs.identify_objects_by == "xmlId";
let vars: Vec<VarRow> = st
.doc
.variables
@@ -106,7 +107,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
let bound: Vec<u64> = st.doc.variables.objects_of(&v.name).iter().map(|i| i.0).collect();
let object = match bound.as_slice() {
[] => String::new(),
[one] => st.doc.node(NodeId(*one)).map(|n| n.display_name()).unwrap_or_default(),
[one] => st.doc.node(NodeId(*one)).map(|n| n.identify_label(xml_id)).unwrap_or_default(),
_ => tl!("Multiple objects").to_string(),
};
VarRow { name: v.name.clone(), kind: v.kind, bound, object }
+2 -1
View File
@@ -1,6 +1,6 @@
# Gaps
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (G12: Appearance of Black › Printing / Exporting acts, #394; ~16 → ~15 prefs still inert; control transport deadlines, #1726; G1: Average and Join in one step with a corner or smooth choice added as missing (it was listed as done in ui-parity.md); Join after Average, #1533; guide objects, #1534; Keyboard Shortcuts, #1539; G9 and G11 fixes, #1528, #1530, #1544, #1553, #1512, #1537; estimates unchanged; Direct Selection anchor precedence, #1508; dialog slider rails, #1483; template layers in Outline view, #1486; Tabs panel stop drags, #1458; type that does not fit its frame is reported, #1471; 3D on outlined type, #1481, Twist range, #1524, twisted walls, #1525; print-size images, #1517; French catalog, #1511; estimates unchanged; G1: Split Into Grid options, #1462, Transform Objects and Reflect's angle, #1448 and #1460, Pathfinder on a group, #1450, value fields, #1457 and #1465 in part; G9: grayscale TIFFs with alpha, #1466 in part; G6: unknown parameter keys refused, #1442; G10: window surfaces past 8192 px, #1459 in part; G15: many-object edits, #1429 in part; estimates unchanged; G16: Vietnamese catalog added, complete under the tests; three key languages remain, 60–100 → 55–90 h; G3: Extrude & Bevel, Inflate and Rotate added, 45–75 → 25–45 h; G9: `.ai` compound path paint, #1410, overprint kept on import, #1388, text-only PDF parts, #1387; G1: Transform dialogs preview, #1412, transforms of direct-selected anchors, #1408, corner drags, #1229; G11: strokes wider than their shape, #1396; estimates unchanged; G9: `.ai` editing-data follow-ups (gradient stops, type placement, short area-type frames), #1390, #1391, #1392; G15: symbol instance art kept between frames, #1386; G1: dialog keyboard focus, #1367 and #1395; estimates unchanged; G9: placed (linked) files read from `.ai` editing data, #1264; G9: gradient shapes import as paths, #1304, and white constant alpha masks, #1286 in part; G15: linear deselect on hide or lock, #1353; estimates unchanged; G7: custom ruler origin done, #1212; G1: Artboard object clicks create a fitted artboard (#1227); remaining interaction pass and estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; G16: Korean catalog added, 70–110 → 60–100 h; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; G11: valid selection after Simplify and Add Anchor Points (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; G10: RPM runtime dependency metadata (#1275); estimates unchanged; G15: texture-effect distance searches optimized (#1257); idle-machine/platform drag QA still pending; estimate unchanged; G1: Curvature point display improved (#1236, in part); estimates unchanged; G4: full-name font matching (#1258); Windows font-service QA pending; estimate unchanged; G11: no-op anchor Align keeps history and saved state (#1180 follow-up); estimates unchanged; G1: dedicated Eyedropper cursor and precise-cursor checks (#1269); estimates unchanged; G1: Eyedropper reads visible locked and outside-isolation sources (#1251); estimate unchanged; G11: Direct Selection segment Delete (#1278); estimates unchanged; G1: painted strokes regain the existing default after None/zero-width inheritance (#1253); estimate unchanged; G15: large-selection edits, cached live effects and faster Pathfinder, #1173; G4: proportional vertical metrics done, #966; G11: Shape Builder merge seams on grids of lines fixed, #893; scaled text controls corrected, #1034; group font-size preservation deferred; G9: `.ai` gradient matrices and meshes from the editing data; estimate unchanged; G9: `.ai` art styles from the editing data; estimate unchanged; G9: `.ai` editing-data follow-ups (gradient stops, type placement, short area-type frames), #1390, #1391, #1392; G15: symbol instance art kept between frames, #1386; G1: dialog keyboard focus, #1367 and #1395; estimates unchanged; G9: placed (linked) files read from `.ai` editing data, #1264; G9: gradient shapes import as paths, #1304, and white constant alpha masks, #1286 in part; G15: linear deselect on hide or lock, #1353; estimates unchanged; G7: custom ruler origin done, #1212; G1: Artboard object clicks create a fitted artboard (#1227); remaining interaction pass and estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; G16: Korean catalog added, 70–110 → 60–100 h; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closeLine truncated
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (G12: Identify Objects By acts, #394; ~16 → ~15 prefs still inert; G12: Appearance of Black › Printing / Exporting acts, #394; ~16 → ~15 prefs still inert; control transport deadlines, #1726; G1: Average and Join in one step with a corner or smooth choice added as missing (it was listed as done in ui-parity.md); Join after Average, #1533; guide objects, #1534; Keyboard Shortcuts, #1539; G9 and G11 fixes, #1528, #1530, #1544, #1553, #1512, #1537; estimates unchanged; Direct Selection anchor precedence, #1508; dialog slider rails, #1483; template layers in Outline view, #1486; Tabs panel stop drags, #1458; type that does not fit its frame is reported, #1471; 3D on outlined type, #1481, Twist range, #1524, twisted walls, #1525; print-size images, #1517; French catalog, #1511; estimates unchanged; G1: Split Into Grid options, #1462, Transform Objects and Reflect's angle, #1448 and #1460, Pathfinder on a group, #1450, value fields, #1457 and #1465 in part; G9: grayscale TIFFs with alpha, #1466 in part; G6: unknown parameter keys refused, #1442; G10: window surfaces past 8192 px, #1459 in part; G15: many-object edits, #1429 in part; estimates unchanged; G16: Vietnamese catalog added, complete under the tests; three key languages remain, 60–100 → 55–90 h; G3: Extrude & Bevel, Inflate and Rotate added, 45–75 → 25–45 h; G9: `.ai` compound path paint, #1410, overprint kept on import, #1388, text-only PDF parts, #1387; G1: Transform dialogs preview, #1412, transforms of direct-selected anchors, #1408, corner drags, #1229; G11: strokes wider than their shape, #1396; estimates unchanged; G9: `.ai` editing-data follow-ups (gradient stops, type placement, short area-type frames), #1390, #1391, #1392; G15: symbol instance art kept between frames, #1386; G1: dialog keyboard focus, #1367 and #1395; estimates unchanged; G9: placed (linked) files read from `.ai` editing data, #1264; G9: gradient shapes import as paths, #1304, and white constant alpha masks, #1286 in part; G15: linear deselect on hide or lock, #1353; estimates unchanged; G7: custom ruler origin done, #1212; G1: Artboard object clicks create a fitted artboard (#1227); remaining interaction pass and estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; G16: Korean catalog added, 70–110 → 60–100 h; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; G11: valid selection after Simplify and Add Anchor Points (#1272); estimates unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; G10: RPM runtime dependency metadata (#1275); estimates unchanged; G15: texture-effect distance searches optimized (#1257); idle-machine/platform drag QA still pending; estimate unchanged; G1: Curvature point display improved (#1236, in part); estimates unchanged; G4: full-name font matching (#1258); Windows font-service QA pending; estimate unchanged; G11: no-op anchor Align keeps history and saved state (#1180 follow-up); estimates unchanged; G1: dedicated Eyedropper cursor and precise-cursor checks (#1269); estimates unchanged; G1: Eyedropper reads visible locked and outside-isolation sources (#1251); estimate unchanged; G11: Direct Selection segment Delete (#1278); estimates unchanged; G1: painted strokes regain the existing default after None/zero-width inheritance (#1253); estimate unchanged; G15: large-selection edits, cached live effects and faster Pathfinder, #1173; G4: proportional vertical metrics done, #966; G11: Shape Builder merge seams on grids of lines fixed, #893; scaled text controls corrected, #1034; group font-size preservation deferred; G9: `.ai` gradient matrices and meshes from the editing data; estimate unchanged; G9: `.ai` art styles from the editing data; estimate unchanged; G9: `.ai` editing-data follow-ups (gradient stops, type placement, short area-type frames), #1390, #1391, #1392; G15: symbol instance art kept between frames, #1386; G1: dialog keyboard focus, #1367 and #1395; estimates unchanged; G9: placed (linked) files read from `.ai` editing data, #1264; G9: gradient shapes import as paths, #1304, and white constant alpha masks, #1286 in part; G15: linear deselect on hide or lock, #1353; estimates unchanged; G7: custom ruler origin done, #1212; G1: Artboard object clicks create a fitted artboard (#1227); remaining interaction pass and estimate unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; G16: Korean catalog added, 70–110 → 60–100 h; Czech render-site localization follow-up; Line truncated
Every known shortfall against Illustrator, one entry each, **ranked by how much it stops a professional from
switching**. This is the work list: unless you were given a task, pick the highest-ranked gap you can make
@@ -134,6 +134,7 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
## G12 Preferences and panels that don't act yet
- **Missing:** ~15 Preferences options stored but not acted on (#394: Highlight Alternate Glyphs, Open
Documents As Tabs, Real-time Drawing, hyphenation language, Appearance of Black for output, devices…), Properties panel per
Documents As Tabs, Real-time Drawing, hyphenation language, devices…), Properties panel per
context, context menus beyond the canvas, Layers panel row menus and Collect for Export, SVG Interactivity.
- **Impact:** settings that silently do nothing erode trust.
+4 -1
View File
@@ -1991,7 +1991,10 @@ three: General (rulers, positions and sizes, the Info panel, dialog distances, c
`numbersWithoutUnitsArePoints` (on by default) makes the desktop app's length fields in picas read a number typed
with no unit in points (`12` is 12 pt, `2p6` and `1p` stay picas, `12 mm` millimetres); off, it is in picas. Fields in
other units read it in their unit either way, and the Preferences dialog dims the option unless a unit is Picas.
Command params are in points either way.
Command params are in points either way. `identifyObjectsBy` (`objectName` by default, or `xmlId`) sets how Layers,
Variables and Layer Options show and edit object names: Object Name keeps free text; XML ID shows AI-style XML ids
(`My Rect #2` as `My_Rect__x23_2`) and `layer.setProps` / rename require a valid XML ID (letter, underscore or colon
first; no spaces), stored back as the object name.
`prefs.list` marks the length preferences (`keyboardIncrement`, `cornerRadius`, `pasteOffset`, `gridlineEvery`,
`typeSizeIncrement`, `baselineShiftIncrement`) with `measure: "general"|"type"`; they are kept in points and also take
a string with a unit. Unit names: `Points`, `Picas`, `Inches`, `Millimeters`, `Centimeters`, `Pixels`, `Feet & Inches`,
+3 -2
View File
@@ -1,6 +1,6 @@
# Parity with Adobe Illustrator
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (Appearance of Black › Printing / Exporting (`blackOutput`) acts on RGB raster export and Print as Bitmap, #394, scores unchanged; per-corner radius fields with a link toggle, split from #1594; control transport deadlines, #1726; Color panel: the mode picked last comes back after a restart, #1675; Image Trace: shared edges, memory, thin lines, strokes, corners, and the Shapes, Auto Grouping and Gradients options, measured in `image-trace-quality.md`, score unchanged; 2026-10-13 merges: Polish interface, #1498; compact native saves, #1492; PDF and EPS paste as vector art, #1499; import fixes for `.ai`, PDF, EPS and SVG, #1528, #1530, #1531, #1544, #1551, #1553; guide objects, #1534; Join after Average, #1533; Keyboard Shortcuts lists commands outside the menus, #1539; correction: Average and Join with a corner or smooth choice is missing; scores unchanged; late 2026-10-12 merges: type that does not fit its frame is reported, #1471; Tabs panel drags, #1458; template layers in Outline view, #1486; dialog slider rails, #1483; Direct Selection anchor precedence, #1508; print-size images, #1517; 3D on outlined type, #1481; French catalog, #1511; scores unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; Localization: Vietnamese catalog added, 63% → 71%, hours 60–100 → 55–90, full readiness unchanged at ~62%; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unchanged; Type core: Middle Eastern digit types done, Character panel menu › Digits; PDF export warns when the pixel capLine truncated
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (Units › Identify Objects By (`identifyObjectsBy`) acts on Layers, Variables and Layer Options, #394, scores unchanged; Appearance of Black › Printing / Exporting (`blackOutput`) acts on RGB raster export and Print as Bitmap, #394, scores unchanged; per-corner radius fields with a link toggle, split from #1594; control transport deadlines, #1726; Color panel: the mode picked last comes back after a restart, #1675; Image Trace: shared edges, memory, thin lines, strokes, corners, and the Shapes, Auto Grouping and Gradients options, measured in `image-trace-quality.md`, score unchanged; 2026-10-13 merges: Polish interface, #1498; compact native saves, #1492; PDF and EPS paste as vector art, #1499; import fixes for `.ai`, PDF, EPS and SVG, #1528, #1530, #1531, #1544, #1551, #1553; guide objects, #1534; Join after Average, #1533; Keyboard Shortcuts lists commands outside the menus, #1539; correction: Average and Join with a corner or smooth choice is missing; scores unchanged; late 2026-10-12 merges: type that does not fit its frame is reported, #1471; Tabs panel drags, #1458; template layers in Outline view, #1486; dialog slider rails, #1483; Direct Selection anchor precedence, #1508; print-size images, #1517; 3D on outlined type, #1481; French catalog, #1511; scores unchanged; Split Into Grid options, #1462; Transform Objects and Reflect across an angle, #1448 and #1460; Pathfinder panel on a group, #1450; unknown parameter keys refused, #1442; grayscale TIFFs with alpha, #1466 in part; Localization: Vietnamese catalog added, 63% → 71%, hours 60–100 → 55–90, full readiness unchanged at ~62%; scores and hours unchanged; Image Trace places edges to a fraction of a pixel in every mode, measured in `image-trace-quality.md`, score unchanged; 3D and Materials: Extrude & Bevel, Inflate and Rotate added, 3D ~8% → ~25% (estimated), menu stubs 25 → 22; Affinity text frames too short for their first line show it, #1423; Transform dialogs preview, #1412, and the transform tools and commands act on direct-selected anchors, #1408; `.ai` compound path paint, #1410; strokes wider than their shape, #1396; corner drags hide the bounding box, #1229; scores and hours unchanged; PDF and `.ai` import keep overprint, #1388, scores unchanged; `.ai` editing-data follow-ups: gradient stops, type of objects with an appearance, short area-type frames (#1390, #1391, #1392); dialog keyboard focus on every open (#1382, #1395); art flush with an artboard (#1375); font rescan (#1376); symbol instance redraw cache (#1386); scores and hours unchanged; grid tool options, #1377 in part, scores unchanged; `.ai` placed (linked) files read from the editing data, #1264, scores unchanged; gradient shapes import as paths, #1304; white constant alpha masks, #1286 in part; Live Corners widget click selects its corner, #1239; scores and hours unchanged; custom ruler origin, #1212; Artboard click-to-fit verified for objects, groups, visible bounds, undo and cancellation (#1227); scores and hours unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; Localization: Korean catalog added, 54% → 63%, full readiness unchanged at ~62%; desktop print transport adopts craft-print, Windows 7 fallback retained, scores unchanged; Image Trace: an image over 64 megapixels traces scaled down instead of being refused, scores unchanged; Czech render-site localization follow-up; scores and estimates unchanged; Document Setup: Edit Artboards closes the modal after applying valid settings; pointer-click regressions; scores and hours unchanged; Artboards: Escape exits editing; an active drag cancels first; shortcut-routing regression; scores and hours unchanged; Path operations: Simplify preview/counts and component selection (#1272); scores unchanged; G2: Film Grain added; Photoshop effects 29/57; 28 filters remain; Platforms: RPM runtime capabilities and glibc floor (#1275); scores unchanged; Raster effects: squared-distance texture redraws (#1257); pixels unchanged; scores and hours unchanged; Drawing: Curvature point display improved (#1236, in part); scores unchanged; Type core: full-name font matching (#1258), scores and hours unchanged; Selection: no-op anchor Align preserves history and saved state (#1180 follow-up); scores unchanged; UI chrome: Eyedropper cursor and precise-setting regressions (#1269); scores unchanged; Colour: Eyedropper reads visible locked and outside-isolation sources (#1251); scores and hours unchanged; Selection: segment click/Delete preserves neighboring edges (#1278); scores unchanged; Colour: unpainted zero-width strokes regain 1 pt when painted (#1253); scores and hours unchanged; graphs: designs and markers, data, Graph Type and label options; UI chrome: native macOS save Format menu, #866, scores unchanged; compound paths inherit the backmost appearance and member fill-rule edits reach their owner, #1197; scores unLine truncated
The authoritative parity assessment. [`ROADMAP.md`](../ROADMAP.md) summarizes it; [`gaps.md`](gaps.md) is
the work list made from it. Deep areas have their own checklists: [UI and interaction](ui-parity.md),
@@ -326,7 +326,7 @@ about 1–1.5 agent-hours each, so the other rows (estimated on the older scale)
| Export for Screens, Asset Export, slices, Save for Web | 3 | 85% | polish only (all four are done) | 2–4 |
| Print, colour management, separations, flattener | 3 | 80% | print preview fidelity (flattener presets in PDF and print, overprint in PDF and composite print, Print dialog, presets, PostScript, marks, separations, Print Tiling, print overprint/flattener/bitmap options and printer profile are done) | 1–2 |
| Automation | 3 | 65% | scripting surface, batch (Variables: text/visibility variables bound to objects, an object may carry several, dataset rows applied in one undo step, panel and Window › Variables with dialogs; image/graph kinds and dataset import/export open) | 6–10 |
| UI chrome (panels, contextual Properties, workspaces, prefs) | 6 | 80% | macOS native menu dismissal fixed (M14.8); Variables, Links, Asset Export, CSS Properties, Attributes and the canvas context menu are done; remaining: SVG Interactivity, Properties per context, Consolidate All Windows, floating panels outside the app window and docked into any group, context menus beyond the canvas, about 15 Preferences options stored but not yet acted on (#394: Highlight Alternate Glyphs, Open Documents As Tabs, Real-time Drawing, hyphenation language, devices…) | 13–20 |
| UI chrome (panels, contextual Properties, workspaces, prefs) | 6 | 80% | macOS native menu dismissal fixed (M14.8); Variables, Links, Asset Export, CSS Properties, Attributes and the canvas context menu are done; remaining: SVG Interactivity, Properties per context, Consolidate All Windows, floating panels outside the app window and docked into any group, context menus beyond the canvas, about 14 Preferences options stored but not yet acted on (#394: Highlight Alternate Glyphs, Open Documents As Tabs, Real-time Drawing, hyphenation language, devices…) | 13–20 |
| Libraries, Links, Package | 2 | 82% | the Libraries panel is local only (no cloud by design: libraries are files on this machine) and has no shared libraries, linked graphics or gradients and patterns as items yet; its graphics, colours and text styles (#745) and its search, groups and library export/import as `.vclibrary` files (#926) are done; Links and Package are done; placed VectorCraft documents are done (linked by default, vectors in every output, previews, Break Link, Expand, Edit Original, refresh on save) | 2–3 |
| **Feature parity** | **103** | **~75%** | | **~210–330** |
| Interaction-fidelity pass (side by side with Illustrator: every tool modifier, cursor, dialog, Properties context) | | | | 60–90 |
@@ -406,6 +406,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- Native macOS save sheets offer a localised Format menu (#866), built from the request's existing format filters. It selects the suggested filename's format and updates the suffix when changed, including legacy `.drawcraft` names. `apps/vectorcraft/src/mac_save_format.rs` has filename regressions; an actual native SVG save, selection of PDF and cancellation without changing the saved document were verified.
- Save / Don't Save / Cancel before closing or quitting with unsaved documents (tabs, Close, Close All, Quit, window close).
- Four brightness themes, persistent preferences.
- Preferences that act (#394, in part): Show Tool Tips, Double Click To Isolate, Zoom with Mouse Wheel (the wheel zooms about the pointer, Shift-wheel scrolls up and down, Cmd/Ctrl-wheel sideways; Alt/Option-wheel and a trackpad pinch zoom about the pointer either way), Use Precise Cursors; Selection & Anchor Display › Tolerance (objects, anchors and handles), Object Selection by Path Only, Command Click to Select Objects Behind (cycling down the stack), Zoom to Selection, anchor Size, Highlight anchors on mouse over; Type › Number of Recent Fonts (up to 15); Guides & Grid › guide and grid Color and Style (lines or dots) and Grids In Back. Then Snap to Point (its distance, for moves, drawn points and reference points), Move Locked and Hidden Artwork with Artboard, Enable Rubber Band for the Pen and Curvature tools, Handles style, Show handles when multiple anchors are selected, Hide Corner Widget for angles greater than; General › Transform Pattern Tiles (the transforms' and dialogs' Transform Patterns, Align and Distribute, art moved or duplicated with an artboard, and objects moved whole with direct-selected anchors) and Select Same Tint %; Type › Size/Leading, Tracking and Baseline Shift increments (`type.step`, Cmd+Shift+. and Cmd+Shift+, for the size, the Type tool's Alt+arrows), Enable Missing Glyph Protection and Fill New Type Objects With Placeholder Text. Then Smart Guides › Color, Alignment Guides, Anchor/Path Labels, Measurement Labels, Transform Tools, Object Highlighting (with Smart Guides on) and Snapping Tolerance (4 px by default, for every smart guide snap), then Construction Guides and their Angles (#506: a drawn point near a line at one of those angles through the point it leaves from lands on it). Then Type › Type Object Selection by Path Only (type is picked on the baseline of any of its lines, area type's frame or the path of type on a path, no longer anywhere in its bounds; the Type tools and the Eyedropper still take a click among the characters). Then General › Anti-aliased Artwork (off, the canvas draws the art with hard edges). Then Guides & Grid › Show Pixel Grid (Above 600% Zoom), a line at every document pixel over the art in Pixel Preview, turning with a rotated view as the document grid now does too. Then General › Show The Home Screen When No Documents Are Open (off, closing the last document leaves an empty window; the Home button still opens the screen). Then Units › Numbers Without Units Are Points (on by default: a number typed with no unit into a field in picas is read in points, a typed unit still wins; dimmed unless a unit is Picas). Then User Interface › Auto-Collapse Iconic Panels (off by default: on, a click away from a panel popped out of the icon column puts it away). Then Performance › Animated Zoom (on, with GPU Performance, which it needs and which dims it when off: the Zoom tool dragged sideways zooms about the press, right in and left out, and held still zooms on, Alt out; off, a drag zooms to the area dragged across, now drawn as a marquee), User Interface › Large Tabs (taller document tabs) and Scale Cursor Proportional to UI (off, the tool cursors keep their size whatever the UI Scaling), and File Handling › Display Bitmaps as Anti-aliased Images in Pixel Preview (off by default: Pixel Preview draws images with each pixel taking its nearest image pixel). Type › Auto Size New Area Type works too (new area type gets Auto Size). They reach agents too (`prefs.set`, then `pointer_gesture` or `press_key`; `ui.wheel` on the control channel). Then Smart Guides › Spacing Guides (moves space the selection evenly with its row or column, see Drawing aids). Then Hyphenation › Exceptions (comma-separated words that never break, or break only at their hyphens; `prefs.set` and restore push them into the hyphenator, #394). Then Type › Show Font Names in English (off, font menus label a family by its native-language name when the font has one; documents still use the English family, #394). Then General › Display Print Size at 100% Zoom (off by default: View › Actual Size is one document point per screen point; on, one document inch fills 96 screen points, an inch at the reference density display scaling is set against; `view.actualSize` / `prefs.set`, #394). Then Guides & Grid › Gridline every and Subdivisions (changing either sets the grid of every open document; new and opened documents take that grid too, except a native file and the native document an SVG, PDF, `.ai` or EPS file carries, which open with the grid they were saved with; `file.new`, `document.open`, `file.newFromTemplate`, `file.revert`, #394). Then Appearance of Black › On Screen (Display All Blacks Accurately by default: pure CMYK black shows through the CMYK profile, a dark grey; Display All Blacks as Rich Black shows it on the canvas as RGB black, also in CMYK documents drawn by ink planes, while rich blacks, black tints and images show as before; `RendeLine truncated
- Preferences that act (#394, in part): Show Tool Tips, Double Click To Isolate, Zoom with Mouse Wheel (the wheel zooms about the pointer, Shift-wheel scrolls up and down, Cmd/Ctrl-wheel sideways; Alt/Option-wheel and a trackpad pinch zoom about the pointer either way), Use Precise Cursors; Selection & Anchor Display › Tolerance (objects, anchors and handles), Object Selection by Path Only, Command Click to Select Objects Behind (cycling down the stack), Zoom to Selection, anchor Size, Highlight anchors on mouse over; Type › Number of Recent Fonts (up to 15); Guides & Grid › guide and grid Color and Style (lines or dots) and Grids In Back. Then Snap to Point (its distance, for moves, drawn points and reference points), Move Locked and Hidden Artwork with Artboard, Enable Rubber Band for the Pen and Curvature tools, Handles style, Show handles when multiple anchors are selected, Hide Corner Widget for angles greater than; General › Transform Pattern Tiles (the transforms' and dialogs' Transform Patterns, Align and Distribute, art moved or duplicated with an artboard, and objects moved whole with direct-selected anchors) and Select Same Tint %; Type › Size/Leading, Tracking and Baseline Shift increments (`type.step`, Cmd+Shift+. and Cmd+Shift+, for the size, the Type tool's Alt+arrows), Enable Missing Glyph Protection and Fill New Type Objects With Placeholder Text. Then Smart Guides › Color, Alignment Guides, Anchor/Path Labels, Measurement Labels, Transform Tools, Object Highlighting (with Smart Guides on) and Snapping Tolerance (4 px by default, for every smart guide snap), then Construction Guides and their Angles (#506: a drawn point near a line at one of those angles through the point it leaves from lands on it). Then Type › Type Object Selection by Path Only (type is picked on the baseline of any of its lines, area type's frame or the path of type on a path, no longer anywhere in its bounds; the Type tools and the Eyedropper still take a click among the characters). Then General › Anti-aliased Artwork (off, the canvas draws the art with hard edges). Then Guides & Grid › Show Pixel Grid (Above 600% Zoom), a line at every document pixel over the art in Pixel Preview, turning with a rotated view as the document grid now does too. Then General › Show The Home Screen When No Documents Are Open (off, closing the last document leaves an empty window; the Home button still opens the screen). Then Units › Numbers Without Units Are Points (on by default: a number typed with no unit into a field in picas is read in points, a typed unit still wins; dimmed unless a unit is Picas). Then User Interface › Auto-Collapse Iconic Panels (off by default: on, a click away from a panel popped out of the icon column puts it away). Then Performance › Animated Zoom (on, with GPU Performance, which it needs and which dims it when off: the Zoom tool dragged sideways zooms about the press, right in and left out, and held still zooms on, Alt out; off, a drag zooms to the area dragged across, now drawn as a marquee), User Interface › Large Tabs (taller document tabs) and Scale Cursor Proportional to UI (off, the tool cursors keep their size whatever the UI Scaling), and File Handling › Display Bitmaps as Anti-aliased Images in Pixel Preview (off by default: Pixel Preview draws images with each pixel taking its nearest image pixel). Type › Auto Size New Area Type works too (new area type gets Auto Size). They reach agents too (`prefs.set`, then `pointer_gesture` or `press_key`; `ui.wheel` on the control channel). Then Smart Guides › Spacing Guides (moves space the selection evenly with its row or column, see Drawing aids). Then Hyphenation › Exceptions (comma-separated words that never break, or break only at their hyphens; `prefs.set` and restore push them into the hyphenator, #394). Then Type › Show Font Names in English (off, font menus label a family by its native-language name when the font has one; documents still use the English family, #394). Then General › Display Print Size at 100% Zoom (off by default: View › Actual Size is one document point per screen point; on, one document inch fills 96 screen points, an inch at the reference density display scaling is set against; `view.actualSize` / `prefs.set`, #394). Then Guides & Grid › Gridline every and Subdivisions (changing either sets the grid of every open document; new and opened documents take that grid too, except a native file and the native document an SVG, PDF, `.ai` or EPS file carries, which open with the grid they were saved with; `file.new`, `document.open`, `file.newFromTemplate`, `file.revert`, #394). Then Appearance of Black › On Screen (Display All Blacks Accurately by default: pure CMYK black shows through the CMYK profile, a dark grey; Display All Blacks as Rich Black shows it on the canvas as RGB black, also in CMYK documents drawn by ink planes, while rich blacks, black tints and images show as before; `RendeLine truncated
- Dialogs move by dragging their heading (the band down from the top edge, as a title bar), stay on screen and keep where they were moved for the session; the form dialogs' choices are dropdowns (Offset Path's Joins, Average's and Shear's and Reflect's axis), and Offset Path lists Offset, Joins, Miter Limit and previews (#423).
- Dialogs open with their first field focused and its value selected: type a value and press Enter (Move, Rotate, Scale…). A dialog opened from a menu takes the keyboard focus every time it opens, not only the first (#1367), and one whose first field starts greyed out (Blend Options on Smooth Color, Layers Panel Options) gives the focus to that field once it is enabled (#1395). Number fields in panels, the Control bar and dialogs (preferences too) step with Up/Down, or with the mouse wheel over the focused field (#485: a notch a step; the panel under it then stays put, and an unfocused field leaves the wheel to scrolling) (Shift: ten, Ctrl/Cmd: a tenth of their unit), applied at once.