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. |