From 7852f363a1cac4c8c2b490035d993ce024ab5b8e Mon Sep 17 00:00:00 2001 From: Dan Green Date: Mon, 19 Oct 2026 14:48:23 -0700 Subject: [PATCH] Fix #2143: selected ruler guides align to a key object A lone object selected with ruler guides can be the key (the Selection tool's click on it), and Align to the key moves the other selected guides to its edge or centre: vertical guides to left, centre or right, horizontal ones to top, centre or bottom. Parallel guides line up on a key guide. Aligning to the selection or the artboard leaves guides alone. --- crates/doc/src/selection.rs | 9 ++--- crates/engine/src/cmd/object.rs | 56 ++++++++++++++++++++++++----- crates/engine/src/cmd/select.rs | 2 +- crates/engine/src/lib.rs | 4 +++ crates/engine/src/tests_keyguide.rs | 56 +++++++++++++++++++++++++++++ crates/tools/src/select.rs | 16 +++++---- docs/mcp.md | 6 ++-- docs/ui-parity.md | 5 +-- 8 files changed, 130 insertions(+), 24 deletions(-) diff --git a/crates/doc/src/selection.rs b/crates/doc/src/selection.rs index 3181aa572..1d45f5d73 100644 --- a/crates/doc/src/selection.rs +++ b/crates/doc/src/selection.rs @@ -175,13 +175,16 @@ impl Selection { refs.retain(|&(si, seg)| path.subpaths.get(si).is_some_and(|sp| seg < sp.segment_count())); !refs.is_empty() && self.objects.contains(id) && self.anchors.contains_key(id) }); - // The key object stays while another selected object lies outside it. + // Guides on a hidden or locked layer can't stay selected. + self.guides.retain(|i| doc.guides.get(*i).is_some_and(|g| doc.guide_editable(g))); + // The key object stays while another selected object lies outside it, or while ruler + // guides are selected for it to line up (#2143). let keep = self.key.and_then(|k| found.get(&k)).and_then(|p| doc.node_at(p)).is_some_and(|n| { let mut inside = BTreeSet::new(); n.walk(&mut |c| { inside.insert(c.id); }); - self.objects.iter().any(|o| !inside.contains(o)) + self.objects.iter().any(|o| !inside.contains(o)) || (self.objects.contains(&n.id) && !self.guides.is_empty()) }); if !keep { self.key = None; @@ -190,8 +193,6 @@ impl Selection { self.target = None; } self.slices.retain(|id| doc.is_slice(*id)); - // Guides on a hidden or locked layer can't stay selected. - self.guides.retain(|i| doc.guides.get(*i).is_some_and(|g| doc.guide_editable(g))); // A key guide needs objects to line up on it, and stays selected. if self.key.is_some() || self.objects.is_empty() || self.key_guide.is_some_and(|k| !self.guides.contains(&k)) { self.key_guide = None; diff --git a/crates/engine/src/cmd/object.rs b/crates/engine/src/cmd/object.rs index 231cafd7c..186630a88 100644 --- a/crates/engine/src/cmd/object.rs +++ b/crates/engine/src/cmd/object.rs @@ -243,7 +243,7 @@ pub fn specs() -> Vec { "Align", ["Window", "Align"], None, - "{horizontal?: \"left\"|\"center\"|\"right\", vertical?: \"top\"|\"center\"|\"bottom\", to?: \"selection\"|\"artboard\"|\"key\" (default: the key object when the selection has one, select.key, else the selection; a key guide, select.key {guide}, stays put and the objects line up on its line: a vertical guide sets where left, center and right go and vertical alignment leaves them, a horizontal one the other way round), artboard?: index (0-based; with to: \"artboard\", the artboard to align to; the app passes the active one; default: the artboard under the center of the selection, else the first), bounds?: \"preview\"|\"geometric\" (default: the Use Preview Bounds preference; preview bounds take in strokes), patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles)} With anchor points selected (Direct Selection), the points align instead: to their own bounds, or to the artboard (#1180)", + "{horizontal?: \"left\"|\"center\"|\"right\", vertical?: \"top\"|\"center\"|\"bottom\", to?: \"selection\"|\"artboard\"|\"key\" (default: the key object when the selection has one, select.key, else the selection; a key guide, select.key {guide}, stays put and the objects line up on its line: a vertical guide sets where left, center and right go and vertical alignment leaves them, a horizontal one the other way round; aligning to the key, the other selected ruler guides move too: a vertical guide to the key's left edge, center or right edge, a horizontal one to its top, center or bottom), artboard?: index (0-based; with to: \"artboard\", the artboard to align to; the app passes the active one; default: the artboard under the center of the selection, else the first), bounds?: \"preview\"|\"geometric\" (default: the Use Preview Bounds preference; preview bounds take in strokes), patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles)} With anchor points selected (Direct Selection), the points align instead: to their own bounds, or to the artboard (#1180)", has_selection, align ), @@ -1251,12 +1251,17 @@ fn items_bounds(d: &Document, ids: &[NodeId], preview: bool) -> Vec<(NodeId, Rec ids.iter().filter_map(|id| Some((*id, d.bounds_of(&[*id], preview)?))).collect() } -/// Move each object of `moves` by its offset, as undo step `label`; pattern tiles move too when -/// [`transform_patterns`] says so for `ids`. When every move is a rounding error (1e-9 pt or less -/// on both axes), the document and its undo history stay as they are. -fn apply_moves(s: &mut Session, label: &str, p: &Value, ids: &[NodeId], moves: &[(NodeId, Vec2)]) -> Result { +/// Move each object of `moves` by its offset and each ruler guide of `guides` (index, position) +/// to its position, as undo step `label`; pattern tiles move too when [`transform_patterns`] says +/// so for `ids`. When every move is a rounding error (1e-9 pt or less on both axes), the document +/// and its undo history stay as they are. +fn apply_moves(s: &mut Session, label: &str, p: &Value, ids: &[NodeId], moves: &[(NodeId, Vec2)], guides: &[(usize, f64)]) -> Result { let sc = Scaling { patterns: transform_patterns(s, p, ids)?, ..Scaling::default() }; - if moves.iter().all(|(_, dv)| dv.x.abs() <= 1e-9 && dv.y.abs() <= 1e-9) { + let guides: Vec<(usize, f64)> = { + let d = &s.doc()?.doc; + guides.iter().copied().filter(|&(i, pos)| d.guides.get(i).is_some_and(|g| (g.pos - pos).abs() > 1e-9)).collect() + }; + if moves.iter().all(|(_, dv)| dv.x.abs() <= 1e-9 && dv.y.abs() <= 1e-9) && guides.is_empty() { return ok(); } s.edit(label, |d, _| { @@ -1265,6 +1270,11 @@ fn apply_moves(s: &mut Session, label: &str, p: &Value, ids: &[NodeId], moves: & n.transform(Affine::translate(*dv), sc); } } + for &(i, pos) in &guides { + if let Some(g) = d.guides.get_mut(i) { + g.pos = pos; + } + } Ok(()) })?; ok() @@ -1375,7 +1385,35 @@ fn align(s: &mut Session, p: &Value) -> Result { }) .collect() }; - apply_moves(s, "Align", p, &ids, &moves) + let guides = guide_moves(s, p, r, h, v)?; + apply_moves(s, "Align", p, &ids, &moves, &guides) +} + +/// The selected ruler guides that Align to the key object (or key guide) moves, and where: a +/// vertical guide goes to the left edge, centre or right edge of `r` that `h` names, a horizontal +/// one to the top, centre or bottom that `v` names. The key guide stays put, and so do guides +/// when aligning to the selection or the artboard. +fn guide_moves(s: &Session, p: &Value, r: Rect, h: Option<&str>, v: Option<&str>) -> Result> { + if align_to(s, p) != Some("key") { + return Ok(vec![]); + } + let st = s.doc()?; + let at = |vertical: bool| match (vertical, h, v) { + (true, Some("left"), _) => Some(r.x0), + (true, Some("center"), _) => Some(r.center().x), + (true, Some("right"), _) => Some(r.x1), + (false, _, Some("top")) => Some(r.y0), + (false, _, Some("center")) => Some(r.center().y), + (false, _, Some("bottom")) => Some(r.y1), + _ => None, + }; + Ok(st + .selection + .guides + .iter() + .filter(|&&i| Some(i) != st.selection.key_guide) + .filter_map(|&i| Some((i, at(st.doc.guides.get(i)?.vertical)?))) + .collect()) } /// [`align`] of the selected anchor points (whole paths among them give all theirs): each point @@ -1540,7 +1578,7 @@ fn distribute(s: &mut Session, p: &Value) -> Result { (*id, if horiz { Vec2::new(delta, 0.0) } else { Vec2::new(0.0, delta) }) }) .collect(); - apply_moves(s, "Distribute", p, &ids, &moves) + apply_moves(s, "Distribute", p, &ids, &moves, &[]) } fn distribute_spacing(s: &mut Session, p: &Value) -> Result { @@ -1585,7 +1623,7 @@ fn distribute_spacing(s: &mut Session, p: &Value) -> Result { let fixed = spacing.and_then(|_| deltas.iter().find(|(id, _)| Some(*id) == key)).map_or(0.0, |k| k.1); let moves: Vec<(NodeId, Vec2)> = deltas.into_iter().map(|(id, d)| (id, if horiz { Vec2::new(d - fixed, 0.0) } else { Vec2::new(0.0, d - fixed) })).collect(); - apply_moves(s, "Distribute Spacing", p, &ids, &moves) + apply_moves(s, "Distribute Spacing", p, &ids, &moves, &[]) } fn set_bounds(s: &mut Session, p: &Value) -> Result { diff --git a/crates/engine/src/cmd/select.rs b/crates/engine/src/cmd/select.rs index cb7d06c02..2197877e3 100644 --- a/crates/engine/src/cmd/select.rs +++ b/crates/engine/src/cmd/select.rs @@ -66,7 +66,7 @@ pub fn specs() -> Vec { "Cancel Key Object", [], None, - "{id?: a selected top-level object (not a layer, a compound-path member or an object inside another selected object); one of two or more (the Selection tool: a click on one object of a selection of several; on the key again: no id), guide?: index (a selected ruler guide, guide.list, with one or more objects selected: the Selection tool's click on a selected guide; Align lines the objects up on it and the guide stays put)} the key object Align aligns to and Distribute Spacing spaces from; without `id` or `guide` it cancels the key, as does a selection that holds only the key and objects inside it (one object, or a group key and its own members), or a key guide with no object selected or taken out of the selection; any other id or guide is an error that leaves the key as it was → {key, guide?} (the resulting key object, or null; guide: the key guide, left out without one)", + "{id?: a selected top-level object (not a layer, a compound-path member or an object inside another selected object); one of two or more or the only one with ruler guides selected (the Selection tool: a click on one object of a selection of several, or of one with guides; on the key again: no id), guide?: index (a selected ruler guide, guide.list, with one or more objects selected: the Selection tool's click on a selected guide; Align lines the objects up on it and the guide stays put)} the key object Align aligns to and Distribute Spacing spaces from; without `id` or `guide` it cancels the key, as does a selection that holds only the key and objects inside it (one object, or a group key and its own members) and no ruler guide, or a key guide with no object selected or taken out of the selection; any other id or guide is an error that leaves the key as it was → {key, guide?} (the resulting key object, or null; guide: the key guide, left out without one)", has_doc, key ), diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 52839080e..4c848ecbc 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -775,6 +775,10 @@ pub(crate) fn deselect_guides(d: &Document, sel: &mut Selection) -> bool { } sel.guides.clear(); sel.key_guide = None; + // A key object kept only for the guides to line up on goes with them. + if sel.key.is_some_and(|k| sel.objects.iter().all(|o| *o == k)) { + sel.key = None; + } any } diff --git a/crates/engine/src/tests_keyguide.rs b/crates/engine/src/tests_keyguide.rs index abcd4e160..5c908709e 100644 --- a/crates/engine/src/tests_keyguide.rs +++ b/crates/engine/src/tests_keyguide.rs @@ -127,3 +127,59 @@ fn selected_anchor_points_align_to_a_key_guide() { assert_eq!(p.x, 400.0); assert_eq!(path.subpaths[0].anchors[1].p.x, 140.0, "the other points stay"); } + +#[test] +fn selected_guides_align_to_a_clicked_key_object() { + let mut s = session(); + let a = rect(&mut s, 100.0, 100.0); + s.execute("guide.add", &json!({"vertical": true, "pos": 500})).unwrap(); + s.execute("guide.add", &json!({"vertical": false, "pos": 400})).unwrap(); + s.select_tool("selection", ViewInfo::default()).unwrap(); + // Click the object, Shift-click both guides, then click the object again: one object is + // enough to be the key while guides are selected. + click(&mut s, 110.0, 110.0, false); + click(&mut s, 500.0, 300.0, true); + click(&mut s, 300.0, 400.0, true); + assert_eq!(s.doc().unwrap().selection.guides, vec![0, 1]); + click(&mut s, 110.0, 110.0, false); + assert_eq!(s.doc().unwrap().selection.key, Some(a), "the object is the key"); + let before = bounds(&s, a); + s.execute("object.align", &json!({"horizontal": "center", "vertical": "center"})).unwrap(); + let guides = |s: &Session| s.doc().unwrap().doc.guides.iter().map(|g| g.pos).collect::>(); + assert_eq!(guides(&s), [120.0, 110.0], "the vertical guide on the centre's x, the horizontal one on its y"); + assert_eq!(bounds(&s, a), before, "the key stays put"); + s.execute("object.align", &json!({"horizontal": "right"})).unwrap(); + assert_eq!(guides(&s), [140.0, 110.0], "horizontal alignment moves only the vertical guide"); + s.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(guides(&s), [120.0, 110.0], "one undo step"); + // Aligning to the artboard leaves the guides alone. + s.execute("object.align", &json!({"horizontal": "left", "to": "artboard"})).unwrap(); + assert_eq!(guides(&s), [120.0, 110.0]); + // Deselecting the guides lets the lone key go. + s.execute("guide.select", &json!({"indexes": [0, 1], "toggle": true})).unwrap(); + assert_eq!(s.doc().unwrap().selection.objects, vec![a]); + assert_eq!(s.doc().unwrap().selection.key, None); +} + +#[test] +fn guides_align_to_a_key_object_among_others_and_parallel_ones_to_a_key_guide() { + let mut s = session(); + let (a, b) = (rect(&mut s, 100.0, 100.0), rect(&mut s, 300.0, 250.0)); + for pos in [500, 600] { + s.execute("guide.add", &json!({"vertical": true, "pos": pos})).unwrap(); + } + s.execute("select.set", &json!({"ids": [a.0, b.0]})).unwrap(); + s.execute("guide.select", &json!({"indexes": [0, 1], "toggle": true})).unwrap(); + s.execute("select.key", &json!({"id": b.0})).unwrap(); + s.execute("object.align", &json!({"horizontal": "left"})).unwrap(); + assert_eq!(bounds(&s, a).x0, 300.0); + assert!(s.doc().unwrap().doc.guides.iter().all(|g| g.pos == 300.0)); + // Aligning to the selection leaves guides alone. + s.execute("object.align", &json!({"horizontal": "right", "to": "selection"})).unwrap(); + assert!(s.doc().unwrap().doc.guides.iter().all(|g| g.pos == 300.0)); + // A key guide stays put, and the other vertical guide lines up on it. + s.execute("guide.move", &json!({"index": 1, "pos": 700})).unwrap(); + s.execute("select.key", &json!({"guide": 1})).unwrap(); + s.execute("object.align", &json!({"horizontal": "center"})).unwrap(); + assert_eq!(s.doc().unwrap().doc.guides.iter().map(|g| g.pos).collect::>(), [700.0, 700.0]); +} diff --git a/crates/tools/src/select.rs b/crates/tools/src/select.rs index 5a88ab104..be3f08da1 100644 --- a/crates/tools/src/select.rs +++ b/crates/tools/src/select.rs @@ -48,9 +48,9 @@ enum State { /// Shift-pressed on this selected object: released without a drag, it leaves the /// selection. deselect: Option, - /// Pressed (no modifier) on this object of a selection of several: released without a - /// drag, it becomes the key object that Align aligns to if `can_be_key` allows it (the key - /// again: no key). + /// Pressed (no modifier) on this object of a selection of several, or of one with ruler + /// guides: released without a drag, it becomes the key object that Align aligns to if + /// `can_be_key` allows it (the key again: no key). key: Option, }, Scaling { @@ -582,7 +582,7 @@ impl Tool for SelectionTool { } } else if !cx.selection.contains(top) { out.push(Action::Exec("select.set".into(), json!({ "ids": [top.0] }))); - } else if cx.selection.objects.len() > 1 && !m.cmd && !m.alt { + } else if (cx.selection.objects.len() > 1 || !cx.selection.guides.is_empty()) && !m.cmd && !m.alt { key = Some(top); } self.state = State::Moving { start: grab, began: false, deselect, key }; @@ -1442,7 +1442,8 @@ mod tests { } /// #541: a click on one object of a selection of several makes it the key object; a click on - /// the key again lets it go. A drag moves them all, a lone object has no key. + /// the key again lets it go. A drag moves them all, a lone object has no key unless ruler + /// guides are selected with it. #[test] fn a_click_on_a_selected_object_makes_it_the_key() { let (d, id) = doc_with_area_type(); @@ -1466,10 +1467,13 @@ mod tests { t.pointer(&c, &ev(PointerKind::Down, 150.0, 150.0)); t.pointer(&c, &ev(PointerKind::Drag, 170.0, 150.0)); assert_eq!(t.pointer(&c, &ev(PointerKind::Up, 170.0, 150.0)), vec![Action::Commit]); - // One object selected: a click on it does nothing. + // One object selected: a click on it does nothing, unless ruler guides are selected + // with it for it to be the key of (#2143). let mut one = Selection::default(); one.add(rect); assert!(click(&one, 150.0, 150.0).is_empty()); + one.guides = vec![0]; + assert_eq!(click(&one, 150.0, 150.0), vec![Action::Exec("select.key".into(), json!({"id": rect.0}))]); } /// In isolation mode a click makes only a selected top-level object the key, as `select.key` diff --git a/docs/mcp.md b/docs/mcp.md index bced3e6a3..824babebf 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1082,8 +1082,10 @@ The id must name a selected top-level object. Layers, compound-path members and objects inside another selected object are not top-level objects. Any other id is an error naming that value, and the key is left unchanged. A null `id` is an error too. -A click with the Selection tool on a selected top-level object, while two or more objects are selected, runs `select.key {id}`, and a click on the key object again runs `select.key {}`. -On a single selected object, or on a group selected with only its own members, `select.key` sets no key. +A click with the Selection tool on a selected top-level object, while two or more objects are selected or ruler guides are selected with it, runs `select.key {id}`, and a click on the key object again runs `select.key {}`. +On a single selected object with no ruler guide selected, or on a group selected with only its own members, `select.key` sets no key. +With a key object (or a key guide), `object.align` moves the other selected ruler guides too: a vertical guide goes to the key's left edge, centre or right edge, a horizontal one to its top, centre or bottom (#2143). +Aligning to the selection or the artboard leaves guides where they are. `select.key {guide}` makes a selected ruler guide (its `guide.list` index) the key instead, while one or more objects are selected (#2143). `object.align` then lines the objects up on the guide and the guide stays put: a vertical guide sets where their left edges, centres or right edges go and vertical alignment leaves them where they are, a horizontal guide the other way round. Selected anchor points align to it the same way. Distribute Spacing doesn't space from a key guide. diff --git a/docs/ui-parity.md b/docs/ui-parity.md index ba96d7b46..2bc5339c7 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI and interaction parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-19 · **Change:** minor (a ruler guide can be the key object Align aligns to, #2143; Pencil: Alt key toggles to Smooth Tool, #2001; status-readout sample widths cached by language, document unit and pixels per point; status messages and readouts use stable responsive allocations sized for localized labels and ordinary coordinates; cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission; cached canvas reprojection preserves scale during resize, rotation and DPI changes; on-canvas Mirror Repeat axis, #2877; Background Export progress dialog with Cancel, #2564; 3D lighting: one to ten colored lights, smooth colored Inflate, legacy white-light appearance preserved; surface batching and restored seam minimum; scores and estimates unchanged; Blend spine Anchor Point conversions, selection/stacking priority and Shift regression coverage (#2544); scores and hours unchanged; Artboard Options' X and Y with a reference point, #1876; scores unchanged; library panels: one per library, dockable and tabbable, with Persistent, #2603; the Selection tool threads text from the out port, #2572 in part; scores unchanged; Group Selection picks and moves one path of a compound path, #2441 in part; the Move dialog opens on the last move and its last checkboxes; scores unchanged; transparency grid visibility saved in native documents, #1968; Live Corners radius up to the neighbouring cuts, #2332; Image Trace quick presets: wording corrected to the shipped behaviour, the Image Trace button opens the controls; batch exporting, #1935; document window scroll bars, #2322; Anchor Point tool: a click on a handle end retracts that handle only, #2267; scores unchanged; Smart Guides: the dragged end of a straight segment keeps to its extension at any angle, #2120; Warp Options controls verified by focused checks and original-image review; estimates unchanged; existing-guide Escape cancellation validated 2026-10-14; scores and hours unchanged; open-path anchor Delete keeps surviving runs separate; estimates unchanged; Artboard tab-switch follow-up historically validated on `a0671777`; scores and hours unchanged; Artboard clean active-tab close/Delete follow-up validated 2026-10-15; Curvature curve geometry, #1236 in part; Width-point finite-arithmetic follow-up validated; scores and hours unchanged; Image Trace adjustment discovery; later 2026-10-14 merges: macOS save sheet Format menu back to missing, #1855, #866 reopened; anchor X/Y/W/H in the Transform panel, Properties and Control bar, #1824; Snap to Grid over Snap to Pixel, #1816; toolbar labels, columns and the All Tools drawer, #1751; Actions panel selection, #1629 in part; scores unchanged; 2026-10-14 merges: anchor Align and Distribute, #1636; Delete on closed paths and whole objects, #1674; selection inside isolation, #1666; Escape in a Direct Selection drag and while typing, #1743, #1686; Pen clicks under 3 px, #1663; [ and ] on more tools, #1661 in part; fields commit on click away, #1787; command search keys, #1757; a guide's position in the Transform panel is a convenience with no parity claim, #1659; scores unchanged; intersect Smart Guides for drawn points and dragged anchors, #1602 in part; a ruler guide's position in the Transform panel, #1597 in part; Symbol Options before New Symbol, #1560; tool flyout widths, #1616; Pathfinder Trim, Merge and Crop strokes, #1570; scores unchanged; G1/M11: Artboard deletion and history selection normalization; history regressions and full workspace CI pass; scores and hours unchanged; Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged; per-corner radius fields with a link toggle, split from #1594; Join merges anchors lying on one another, #1533; correction: Average and Join with a corner or smooth choice is missing, not done; guide objects take the guide colour and style and Lock Guides, #1534; Keyboard Shortcuts lists commands outside the menus, #1539; round corners next to curves and the Smooth tool's reach, #1512; Make Compound Path on a group, #1537; dialog slider rails, #1483; template layers stay in Preview in Outline view, #1486; Direct Selection takes the selected object's anchor where anchors coincide, #1508; Tabs panel stop drags, #1458; Split Into Grid dialog, #1462; Transform Objects and Reflect's angle, #1448 and #1460; Pathfinder panel on a group, #1450; value fields: readable selection, #1465 in part, and a focused field follows its value, #1457; scores unchanged; Transform dialogs have a live Preview and step with the arrow keys, #1412; transform tools, dialogs and Move act on direct-selected anchors, #1408; the bounding box hides during a corner drag, #1229; scores unchanged; dialogs focus their first field on every open, #1367, and a first field that starts greyed out once it is enabled, #1395; scores unchanged; Rectangular Grid and Polar Grid click dialogs take the skew, frame, compound path and Fill Grid options, #1377 in part, score unchanged; Live Corners: a Direct Selection click on a widget selects its corner, #1239; custom ruler origin, #1212; Artboard tool clicks fit the topmost editable object in one undo step (#1227); scores unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; 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; Simplify preview/counts and valid rewritten-path selection (#1272); parity remains partial; Texture-effect drag redraws optimized (#1257); idle Windows interaction still unverified; scores unchanged; Curvature point display improved (#1236, in part); scores unchanged; Direct Selection: no-op anchor Align preserves Undo, Redo and saved state (#1180 follow-up); Eyedropper cursor distinguished from precise crosshair (#1269); scores unchanged; Eyedropper can read visible locked and outside-isolation sources (#1251); scores unchanged; Direct Selection: independent segment picks and Delete (#1278); scores unchanged; Adding paint to an unpainted zero-width stroke restores the 1 pt default (#1253); scores unchanged; macOS save sheets offer a Format menu, #866; Attributes fill-rule controls edit the compound when a member is selected, #1197; a marquee needs to touch a path, not just lie inside its fill, #1103; earlier: Live Corners round corners between curved sides, #1144; interactive Mirror Repeat axis; Background Export progress dialog with Cancel, #2564; scores unchanged; 3D lighting: one to ten colored lights, smooth colored Inflate, legacy white-light appearance preserved; surface batching and restored seam minimum; scores and estimates unchanged; Blend spine Anchor Point conversions, selection/stacking priority and Shift regression coverage (#2544); scores and hours unchanged; Artboard Options' X and Y with a reference point, #1876; scores unchanged; library panels: one per library, dockable and tabbable, with Persistent, #2603; the Selection tool threads text from the out port, #2572 in part; scores unchanged; Group Selection picks and moves one path of a compound path, #2441 in part; the Move dialog opens on the last move and its last checkboxes; scores unchanged; transparency grid visibility saved in native documents, #1968; Live Corners radius up to the neighbouring cuts, #2332; Image Trace quick presets: wording corrected to the shipped behaviour, the Image Trace button opens the controls; batch exporting, #1935; document window scroll bars, #2322; Anchor Point tool: a click on a handle end retracts that handle only, #2267; scores unchanged; Smart Guides: the dragged end of a straight segment keeps to its extension at any angle, #2120; Warp Options controls verified by focused checks and original-image review; estimates unchanged; existing-guide Escape cancellation validated 2026-10-14; scores and hours unchanged; open-path anchor Delete keeps surviving runs separate; estimates unchanged; Artboard tab-switch follow-up historically validated on `a0671777`; scores and hours unchanged; Artboard clean active-tab close/Delete follow-up validated 2026-10-15; Curvature curve geometry, #1236 in part; Width-point finite-arithmetic follow-up validated; scores and hours unchanged; Image Trace adjustment discovery; later 2026-10-14 merges: macOS save sheet Format menu back to missing, #1855, #866 reopened; anchor X/Y/W/H in the Transform panel, Properties and Control bar, #1824; Snap to Grid over Snap to Pixel, #1816; toolbar labels, columns and the All Tools drawer, #1751; Actions panel selection, #1629 in part; scores unchanged; 2026-10-14 merges: anchor Align and Distribute, #1636; Delete on closed paths and whole objects, #1674; selection inside isolation, #1666; Escape in a Direct Selection drag and while typing, #1743, #1686; Pen clicks under 3 px, #1663; [ and ] on more tools, #1661 in part; fields commit on click away, #1787; command search keys, #1757; a guide's position in the Transform panel is a convenience with no parity claim, #1659; scores unchanged; intersect Smart Guides for drawn points and dragged anchors, #1602 in part; a ruler guide's position in the Transform panel, #1597 in part; Symbol Options before New Symbol, #1560; tool flyout widths, #1616; Pathfinder Trim, Merge and Crop strokes, #1570; scores unchanged; G1/M11: Artboard deletion and history selection normalization; history regressions and full workspace CI pass; scores and hours unchanged; Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged; per-corner radius fields with a link toggle, split from #1594; Join merges anchors lying on one another, #1533; correction: Average and Join with a corner or smooth choice is missing, not done; guide objects take the guide colour and style and Lock Guides, #1534; Keyboard Shortcuts lists commands outside the menus, #1539; round corners next to curves and the Smooth tool's reach, #1512; Make Compound Path on a group, #1537; dialog slider rails, #1483; template layers stay in Preview in Outline view, #1486; Direct Selection takes the selected object's anchor where anchors coincide, #1508; Tabs panel stop drags, #1458; Split Into Grid dialog, #1462; Transform Objects and Reflect's angle, #1448 and #1460; Pathfinder panel on a group, #1450; value fields: readable selection, #1465 in part, and a focused field follows its value, #1457; scores unchanged; Transform dialogs have a live Preview and step with the arrow keys, #1412; transform tools, dialogs and Move act on direct-selected anchors, #1408; the bounding box hides during a corner drag, #1229; scores unchanged; dialogs focus their first field on every open, #1367, and a first field that starts greyed out once it is enabled, #1395; scores unchanged; Rectangular Grid and Polar Grid click dialogs take the skew, frame, compound path and Fill Grid options, #1377 in part, score unchanged; Live Corners: a Direct Selection click on a widget selects its corner, #1239; custom ruler origin, #1212; Artboard tool clicks fit the topmost editable object in one undo step (#1227); scores unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; 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; Simplify preview/counts and valid rewritten-path selection (#1272); parity remains partial; Texture-effect drag redraws optimized (#1257); idle Windows interaction still unverified; scores unchanged; Curvature point display improved (#1236, in part); scores unchanged; Direct Selection: no-op anchor Align preserves Undo, Redo and saved state (#1180 follow-up); Eyedropper cursor distinguished from precise crosshair (#1269); scores unchanged; Eyedropper can read visible locked and outside-isolation sources (#1251); scores unchanged; Direct Selection: independent segment picks and Delete (#1278); scores unchanged; Adding paint to an unpainted zero-width stroke restores the 1 pt default (#1253); scores unchanged; macOS save sheets offer a Format menu, #866; Attributes fill-rule controls edit the compound when a member is selected, #1197; a marquee needs to touch a path, not just lie inside its fill, #1103; earlier: Live Corners round corners between curved sides, #1144) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-19 · **Change:** minor (a ruler guide can be the key object Align aligns to, and selected guides align to a key object, #2143; Pencil: Alt key toggles to Smooth Tool, #2001; status-readout sample widths cached by language, document unit and pixels per point; status messages and readouts use stable responsive allocations sized for localized labels and ordinary coordinates; cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission; cached canvas reprojection preserves scale during resize, rotation and DPI changes; on-canvas Mirror Repeat axis, #2877; Background Export progress dialog with Cancel, #2564; 3D lighting: one to ten colored lights, smooth colored Inflate, legacy white-light appearance preserved; surface batching and restored seam minimum; scores and estimates unchanged; Blend spine Anchor Point conversions, selection/stacking priority and Shift regression coverage (#2544); scores and hours unchanged; Artboard Options' X and Y with a reference point, #1876; scores unchanged; library panels: one per library, dockable and tabbable, with Persistent, #2603; the Selection tool threads text from the out port, #2572 in part; scores unchanged; Group Selection picks and moves one path of a compound path, #2441 in part; the Move dialog opens on the last move and its last checkboxes; scores unchanged; transparency grid visibility saved in native documents, #1968; Live Corners radius up to the neighbouring cuts, #2332; Image Trace quick presets: wording corrected to the shipped behaviour, the Image Trace button opens the controls; batch exporting, #1935; document window scroll bars, #2322; Anchor Point tool: a click on a handle end retracts that handle only, #2267; scores unchanged; Smart Guides: the dragged end of a straight segment keeps to its extension at any angle, #2120; Warp Options controls verified by focused checks and original-image review; estimates unchanged; existing-guide Escape cancellation validated 2026-10-14; scores and hours unchanged; open-path anchor Delete keeps surviving runs separate; estimates unchanged; Artboard tab-switch follow-up historically validated on `a0671777`; scores and hours unchanged; Artboard clean active-tab close/Delete follow-up validated 2026-10-15; Curvature curve geometry, #1236 in part; Width-point finite-arithmetic follow-up validated; scores and hours unchanged; Image Trace adjustment discovery; later 2026-10-14 merges: macOS save sheet Format menu back to missing, #1855, #866 reopened; anchor X/Y/W/H in the Transform panel, Properties and Control bar, #1824; Snap to Grid over Snap to Pixel, #1816; toolbar labels, columns and the All Tools drawer, #1751; Actions panel selection, #1629 in part; scores unchanged; 2026-10-14 merges: anchor Align and Distribute, #1636; Delete on closed paths and whole objects, #1674; selection inside isolation, #1666; Escape in a Direct Selection drag and while typing, #1743, #1686; Pen clicks under 3 px, #1663; [ and ] on more tools, #1661 in part; fields commit on click away, #1787; command search keys, #1757; a guide's position in the Transform panel is a convenience with no parity claim, #1659; scores unchanged; intersect Smart Guides for drawn points and dragged anchors, #1602 in part; a ruler guide's position in the Transform panel, #1597 in part; Symbol Options before New Symbol, #1560; tool flyout widths, #1616; Pathfinder Trim, Merge and Crop strokes, #1570; scores unchanged; G1/M11: Artboard deletion and history selection normalization; history regressions and full workspace CI pass; scores and hours unchanged; Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged; per-corner radius fields with a link toggle, split from #1594; Join merges anchors lying on one another, #1533; correction: Average and Join with a corner or smooth choice is missing, not done; guide objects take the guide colour and style and Lock Guides, #1534; Keyboard Shortcuts lists commands outside the menus, #1539; round corners next to curves and the Smooth tool's reach, #1512; Make Compound Path on a group, #1537; dialog slider rails, #1483; template layers stay in Preview in Outline view, #1486; Direct Selection takes the selected object's anchor where anchors coincide, #1508; Tabs panel stop drags, #1458; Split Into Grid dialog, #1462; Transform Objects and Reflect's angle, #1448 and #1460; Pathfinder panel on a group, #1450; value fields: readable selection, #1465 in part, and a focused field follows its value, #1457; scores unchanged; Transform dialogs have a live Preview and step with the arrow keys, #1412; transform tools, dialogs and Move act on direct-selected anchors, #1408; the bounding box hides during a corner drag, #1229; scores unchanged; dialogs focus their first field on every open, #1367, and a first field that starts greyed out once it is enabled, #1395; scores unchanged; Rectangular Grid and Polar Grid click dialogs take the skew, frame, compound path and Fill Grid options, #1377 in part, score unchanged; Live Corners: a Direct Selection click on a widget selects its corner, #1239; custom ruler origin, #1212; Artboard tool clicks fit the topmost editable object in one undo step (#1227); scores unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; 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; Simplify preview/counts and valid rewritten-path selection (#1272); parity remains partial; Texture-effect drag redraws optimized (#1257); idle Windows interaction still unverified; scores unchanged; Curvature point display improved (#1236, in part); scores unchanged; Direct Selection: no-op anchor Align preserves Undo, Redo and saved state (#1180 follow-up); Eyedropper cursor distinguished from precise crosshair (#1269); scores unchanged; Eyedropper can read visible locked and outside-isolation sources (#1251); scores unchanged; Direct Selection: independent segment picks and Delete (#1278); scores unchanged; Adding paint to an unpainted zero-width stroke restores the 1 pt default (#1253); scores unchanged; macOS save sheets offer a Format menu, #866; Attributes fill-rule controls edit the compound when a member is selected, #1197; a marquee needs to touch a path, not just lie inside its fill, #1103; earlier: Live Corners round corners between curved sides, #1144; interactive Mirror Repeat axis; Background Export progress dialog with Cancel, #2564; scores unchanged; 3D lighting: one to ten colored lights, smooth colored Inflate, legacy white-light appearance preserved; surface batching and restored seam minimum; scores and estimates unchanged; Blend spine Anchor Point conversions, selection/stacking priority and Shift regression coverage (#2544); scores and hours unchanged; Artboard Options' X and Y with a reference point, #1876; scores unchanged; library panels: one per library, dockable and tabbable, with Persistent, #2603; the Selection tool threads text from the out port, #2572 in part; scores unchanged; Group Selection picks and moves one path of a compound path, #2441 in part; the Move dialog opens on the last move and its last checkboxes; scores unchanged; transparency grid visibility saved in native documents, #1968; Live Corners radius up to the neighbouring cuts, #2332; Image Trace quick presets: wording corrected to the shipped behaviour, the Image Trace button opens the controls; batch exporting, #1935; document window scroll bars, #2322; Anchor Point tool: a click on a handle end retracts that handle only, #2267; scores unchanged; Smart Guides: the dragged end of a straight segment keeps to its extension at any angle, #2120; Warp Options controls verified by focused checks and original-image review; estimates unchanged; existing-guide Escape cancellation validated 2026-10-14; scores and hours unchanged; open-path anchor Delete keeps surviving runs separate; estimates unchanged; Artboard tab-switch follow-up historically validated on `a0671777`; scores and hours unchanged; Artboard clean active-tab close/Delete follow-up validated 2026-10-15; Curvature curve geometry, #1236 in part; Width-point finite-arithmetic follow-up validated; scores and hours unchanged; Image Trace adjustment discovery; later 2026-10-14 merges: macOS save sheet Format menu back to missing, #1855, #866 reopened; anchor X/Y/W/H in the Transform panel, Properties and Control bar, #1824; Snap to Grid over Snap to Pixel, #1816; toolbar labels, columns and the All Tools drawer, #1751; Actions panel selection, #1629 in part; scores unchanged; 2026-10-14 merges: anchor Align and Distribute, #1636; Delete on closed paths and whole objects, #1674; selection inside isolation, #1666; Escape in a Direct Selection drag and while typing, #1743, #1686; Pen clicks under 3 px, #1663; [ and ] on more tools, #1661 in part; fields commit on click away, #1787; command search keys, #1757; a guide's position in the Transform panel is a convenience with no parity claim, #1659; scores unchanged; intersect Smart Guides for drawn points and dragged anchors, #1602 in part; a ruler guide's position in the Transform panel, #1597 in part; Symbol Options before New Symbol, #1560; tool flyout widths, #1616; Pathfinder Trim, Merge and Crop strokes, #1570; scores unchanged; G1/M11: Artboard deletion and history selection normalization; history regressions and full workspace CI pass; scores and hours unchanged; Width Point Edit: Shift-double-click enables adjoining adjustment; plain double-click keeps it off; scores and hours unchanged; per-corner radius fields with a link toggle, split from #1594; Join merges anchors lying on one another, #1533; correction: Average and Join with a corner or smooth choice is missing, not done; guide objects take the guide colour and style and Lock Guides, #1534; Keyboard Shortcuts lists commands outside the menus, #1539; round corners next to curves and the Smooth tool's reach, #1512; Make Compound Path on a group, #1537; dialog slider rails, #1483; template layers stay in Preview in Outline view, #1486; Direct Selection takes the selected object's anchor where anchors coincide, #1508; Tabs panel stop drags, #1458; Split Into Grid dialog, #1462; Transform Objects and Reflect's angle, #1448 and #1460; Pathfinder panel on a group, #1450; value fields: readable selection, #1465 in part, and a focused field follows its value, #1457; scores unchanged; Transform dialogs have a live Preview and step with the arrow keys, #1412; transform tools, dialogs and Move act on direct-selected anchors, #1408; the bounding box hides during a corner drag, #1229; scores unchanged; dialogs focus their first field on every open, #1367, and a first field that starts greyed out once it is enabled, #1395; scores unchanged; Rectangular Grid and Polar Grid click dialogs take the skew, frame, compound path and Fill Grid options, #1377 in part, score unchanged; Live Corners: a Direct Selection click on a widget selects its corner, #1239; custom ruler origin, #1212; Artboard tool clicks fit the topmost editable object in one undo step (#1227); scores unchanged; Pathfinder single-result source names and SVG IDs retained, #1293; DXF control-point SPLINE closure read, #1299 follow-up; 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; Simplify preview/counts and valid rewritten-path selection (#1272); parity remains partial; Texture-effect drag redraws optimized (#1257); idle Windows interaction still unverified; scores unchanged; Curvature point display improved (#1236, in part); scores unchanged; Direct Selection: no-op anchor Align preserves Undo, Redo and saved state (#1180 follow-up); Eyedropper cursor distinguished from precise crosshair (#1269); scores unchanged; Eyedropper can read visible locked and outside-isolation sources (#1251); scores unchanged; Direct Selection: independent segment picks and Delete (#1278); scores unchanged; Adding paint to an unpainted zero-width stroke restores the 1 pt default (#1253); scores unchanged; macOS save sheets offer a Format menu, #866; Attributes fill-rule controls edit the compound when a member is selected, #1197; a marquee needs to touch a path, not just lie inside its fill, #1103; earlier: Live Corners round corners between curved sides, #1144) · **Target:** Adobe Illustrator 2026 (30.x) How VectorCraft's tools, handles, snapping and panels behave next to Illustrator's. A vector illustrator's trust is in the details: where a handle lands, what Shift and Alt do mid-drag, what a smart guide snaps to, @@ -75,6 +75,7 @@ tools, never checked side by side) isn't captured row by row. | Align selected anchors to their bounds or the artboard; repeating an alignment that moves nothing keeps Undo, Redo and saved state | done | #1180 follow-up; engine regressions include rounding-only moves and unchanged handles; #1641 (fixes #1636): Align and Distribute act on the anchors selected with Direct Selection, the points of one path aligning to each other and Distribute spacing them | | Align selected anchors to a key anchor | missing | #1636 | | A ruler guide as the key object: a click on a guide selected with objects (or Align to Key Object with one object and a guide) makes it the key, drawn thicker; Align lines the objects (or selected anchors) up on it and the guide stays put | done | #2143; `select.key {guide}`; a vertical guide sets x only, a horizontal one y; Distribute Spacing doesn't space from a key guide | +| Ruler guides align to a key object: a click on an object selected with guides makes it the key (one object is enough); Align moves the selected guides to its edge or centre (vertical guides to left, centre or right, horizontal ones to top, centre or bottom) and the key stays put | done | #2143; aligning to the selection or the artboard leaves guides alone | | A marquee selects the objects whose paths it touches (anchors or segments): drawn inside a filled shape, over a background, it leaves that shape (Preview and Outline) | done | #1103 | | Click a segment independently of its endpoint anchors; only its direction handles show | done | #1278: hollow endpoint squares, selected edge emphasized; headless egui checked | | Drag a curved segment to bend it, a straight one to move its anchors | done | | @@ -256,7 +257,7 @@ re-measure of the panels added since. Slider rails show on Medium Dark (#1808, # | Date | Change | Summary | |---|---|---| -| 2026-10-19 | minor | A ruler guide selected with objects can be the key object (#2143): the Selection tool's click on it, or Align to Key Object with one object; drawn thicker; Align lines the objects up on it and the guide stays put; scores unchanged | +| 2026-10-19 | minor | A ruler guide selected with objects can be the key object (#2143): the Selection tool's click on it, or Align to Key Object with one object; drawn thicker; Align lines the objects up on it and the guide stays put; the other way round, selected guides align to a key object, which one object selected with guides can be; scores unchanged | | 2026-10-19 | minor | Cache status-readout sample widths per UI, language, document unit and pixels per point; regressions check reuse across live frames and refresh after display-setting changes. Scores and estimates unchanged. | | 2026-10-19 | minor | Status messages and readouts use stable responsive allocations sized for localized labels and ordinary coordinates. Contributor original-file acceptance confirmed; scores and estimates unchanged. | | 2026-10-19 | minor | Cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission. Regression coverage; scores and estimates unchanged. |