Merge pull request #1922 from sanyamsingh001/codex/r68-guide-escape

G1: Cancel existing-guide drags with Escape
This commit is contained in:
BFlat
2026-10-16 05:11:32 +07:00
committed by GitHub
9 changed files with 326 additions and 7 deletions
+3
View File
@@ -364,6 +364,9 @@ impl Tool for DirectSelectionTool {
/// Escape during a drag (anchors, handles, segments, objects, type areas, blend spines, a
/// marquee) cancels it: what it moved goes back and the tool waits for the next press (#1743).
fn key(&mut self, _cx: &ToolContext, key: ToolKey, _mods: Mods) -> Vec<Action> {
if key == ToolKey::Escape && self.guide.busy() {
return self.guide.cancel();
}
if key != ToolKey::Escape || matches!(self.state, State::Idle | State::Mesh | State::Corner(_) | State::Bracket(_)) {
return vec![];
}
+7
View File
@@ -208,6 +208,13 @@ impl GuideEdit {
self.drag.is_some()
}
/// Escape drops a pressed guide; a begun preview is rolled back without a history step.
pub fn cancel(&mut self) -> Vec<Action> {
let drag = self.drag.take();
self.snap = GuideSnap::default();
if drag.is_some_and(|d| d.began) { vec![Action::Cancel] } else { vec![] }
}
/// A press `ev` on a ruler guide: it gets selected and the drag starts. None (and nothing
/// done) when no guide is there.
pub fn press(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Option<Vec<Action>> {
+5 -1
View File
@@ -23,7 +23,7 @@ use crate::guides::Targets;
use crate::pathtype::{self, BracketDrag, over_bracket};
use crate::rulerguide::GuideEdit;
use crate::typewidget;
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, json_ids};
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey, json_ids};
#[derive(Clone, Debug, Default)]
enum State {
@@ -245,6 +245,10 @@ impl Tool for SelectionTool {
!matches!(self.state, State::Idle) || self.guide.busy()
}
fn key(&mut self, _cx: &ToolContext, key: ToolKey, _mods: Mods) -> Vec<Action> {
if key == ToolKey::Escape { self.guide.cancel() } else { vec![] }
}
fn transforming(&self) -> bool {
match &self.state {
State::Moving { began: true, .. } | State::Scaling { .. } | State::Rotating { .. } => true,
+2
View File
@@ -85,6 +85,8 @@ mod tests_font_menu;
#[cfg(test)]
mod tests_fonts;
#[cfg(test)]
mod tests_guide_escape;
#[cfg(test)]
mod tests_home;
#[cfg(test)]
mod tests_labels;
+293
View File
@@ -0,0 +1,293 @@
//! Escape cancels an existing ruler-guide gesture through the window's actual input path.
use std::sync::Arc;
use egui::{Event, Key, Modifiers, PointerButton, Pos2, Rect, vec2};
use serde_json::json;
use vectorcraft_doc::{Document, Selection};
use vectorcraft_engine::Session;
use vectorcraft_geom::Point;
use crate::{VectorcraftApp, canvas::Xf};
const SELECTION_TOOLS: [&str; 3] = ["selection", "directSelection", "groupSelection"];
struct GuideDrag {
app: VectorcraftApp,
ctx: egui::Context,
time: f64,
vertical: bool,
copy: bool,
tool: &'static str,
borrowed: bool,
mods: Modifiers,
from: Pos2,
to: Pos2,
end: Pos2,
before: Arc<Document>,
selection: Selection,
redo: Arc<Document>,
undo: usize,
tool_options: serde_json::Value,
}
impl GuideDrag {
fn new(tool: &'static str, vertical: bool, copy: bool, borrowed: bool) -> Self {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width":400,"height":300})).unwrap();
app.run("view.guides.lock", json!({"locked":false})).unwrap();
app.run("shape.rectangle", json!({"x":20,"y":20,"width":20,"height":20})).unwrap();
app.run("guide.add", json!({"vertical":vertical,"pos":100,"artboard":0})).unwrap();
app.run("guide.add", json!({"vertical":!vertical,"pos":220,"artboard":0})).unwrap();
app.run("guide.select", json!({"indexes":[0,1]})).unwrap();
app.run("shape.rectangle", json!({"x":300,"y":260,"width":10,"height":10})).unwrap();
let redo = app.session.active().unwrap().doc.clone();
app.run("edit.undo", json!({})).unwrap();
// Switch away first so even the initial Selection tool becomes the last chosen one.
// Cmd borrows that tool and returns the Pen on pointer Up.
app.select_tool("pen");
app.select_tool(tool);
if borrowed {
app.select_tool("pen");
}
let tool_options = app.session.tool_options();
app.ui.view.rulers = true;
app.ui.view.guides = true;
app.ui.view.smart_guides = false;
app.ui.view.snap_to_point = false;
app.ui.view.snap_to_grid = false;
app.ui.view.snap_to_pixel = false;
let st = app.session.active().unwrap();
let (before, selection, undo) = (st.doc.clone(), st.selection.clone(), st.history.undo.len());
let mut f = Self {
app,
ctx: egui::Context::default(),
time: 0.0,
vertical,
copy,
tool,
borrowed,
mods: Modifiers { command: borrowed, alt: copy, ..Default::default() },
from: Pos2::ZERO,
to: Pos2::ZERO,
end: Pos2::ZERO,
before,
selection,
redo,
undo,
tool_options,
};
for _ in 0..3 {
f.frame(vec![]);
}
let view = f.app.view_mut().unwrap();
view.center = Point::new(200.0, 150.0);
view.zoom = 1.0;
view.rotation = 0.0;
f.frame(vec![]);
let rect = f.app.canvas_rect.unwrap();
let xf = Xf::new(rect, f.app.view().unwrap());
let point = |at| if vertical { Point::new(at, 60.0) } else { Point::new(60.0, at) };
(f.from, f.to, f.end) = (xf.to_screen(point(100.0)), xf.to_screen(point(140.0)), xf.to_screen(point(160.0)));
assert!([f.from, f.to, f.end].into_iter().all(|p| rect.contains(p)));
assert_eq!(f.selection.guides, vec![0, 1]);
f.app.session.active_mut().unwrap().mark_saved();
f.unchanged();
f
}
fn frame(&mut self, events: Vec<Event>) {
self.time += 0.1;
let raw = egui::RawInput {
events,
time: Some(self.time),
screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(1600.0, 850.0))),
..Default::default()
};
let app = &mut self.app;
let mut out = self.ctx.run_ui(raw, |ui| {
app.logic(ui.ctx());
app.ui(ui);
});
out.textures_delta.clear();
assert!(!app.ui.status.starts_with("Internal error:"), "{}", app.ui.status);
}
fn button(&self, pos: Pos2, pressed: bool) -> Event {
Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: self.mods }
}
fn key(&mut self, key: Key) {
let events = [true, false].map(|pressed| Event::Key { key, physical_key: None, pressed, repeat: false, modifiers: self.mods }).to_vec();
self.frame(events);
}
fn press(&mut self) {
self.frame(vec![Event::ModifiersChanged(self.mods), Event::PointerMoved(self.from)]);
self.frame(vec![self.button(self.from, true)]);
assert!(self.ctx.input(|i| i.pointer.primary_down()));
assert!(self.app.session.tool_busy());
assert_eq!(self.app.session.tool_id(), self.tool);
}
fn start(&mut self) {
self.press();
self.frame(vec![Event::PointerMoved(self.to)]);
self.guides_at_target();
let st = self.app.session.active().unwrap();
assert_eq!(st.interaction.as_ref().unwrap().preview.as_ref().unwrap().0, "guide.move");
assert_eq!((st.history.undo.len(), st.history.redo.len()), (self.undo, 1));
}
fn guides_at_target(&self) {
let mut moved = self.before.guides.clone();
for guide in &mut moved {
if guide.vertical == self.vertical {
guide.pos += 40.0;
}
}
let expected = if self.copy { self.before.guides.iter().cloned().chain(moved).collect::<Vec<_>>() } else { moved };
let st = self.app.session.active().unwrap();
assert_eq!(st.doc.guides, expected);
assert_eq!(st.doc.layers, self.before.layers);
assert_eq!(st.selection.guides, if self.copy { vec![2, 3] } else { vec![0, 1] });
}
fn unchanged(&self) {
let st = self.app.session.active().unwrap();
assert_eq!(st.doc, self.before);
assert_eq!(st.selection, self.selection);
assert!(!self.app.session.in_interaction() && !self.app.session.tool_busy());
assert_eq!((st.history.undo.len(), st.history.redo.len(), st.is_dirty()), (self.undo, 1, false));
}
fn finish_cancel(&mut self) {
assert!(self.ctx.input(|i| i.pointer.primary_down()), "Escape does not release the pointer");
assert_eq!(self.app.session.tool_id(), self.tool, "the borrowed tool stays until Up");
self.unchanged();
// Further motion and modifier changes while still held cannot restart the canceled drag.
self.mods = Modifiers::NONE;
self.frame(vec![Event::ModifiersChanged(self.mods), Event::PointerMoved(self.end)]);
self.unchanged();
self.frame(vec![self.button(self.end, false)]);
self.unchanged();
assert!(!self.ctx.input(|i| i.pointer.primary_down()));
assert_eq!(self.app.session.tool_id(), if self.borrowed { "pen" } else { self.tool });
assert_eq!(self.app.session.tool_options(), self.tool_options);
self.app.run("edit.redo", json!({})).unwrap();
assert_eq!(self.app.session.active().unwrap().doc, self.redo, "Escape preserves the original Redo branch");
}
fn finish_commit(&mut self) {
self.frame(vec![self.button(self.to, false)]);
self.guides_at_target();
assert_eq!(self.app.session.tool_id(), if self.borrowed { "pen" } else { self.tool });
assert_eq!(self.app.session.tool_options(), self.tool_options);
let st = self.app.session.active().unwrap();
assert!(!self.app.session.in_interaction() && !self.app.session.tool_busy());
assert_eq!((st.history.undo.len(), st.history.redo.len(), st.is_dirty()), (self.undo + 1, 0, true));
assert_eq!(st.history.undo.last().unwrap().label, if self.copy { "Copy Guide" } else { "Move Guide" });
let (after, selected) = (st.doc.clone(), st.selection.clone());
self.app.run("edit.undo", json!({})).unwrap();
self.unchanged();
self.app.run("edit.redo", json!({})).unwrap();
let st = self.app.session.active().unwrap();
assert_eq!(st.doc, after);
assert_eq!(st.selection, selected);
}
}
fn cancel_cases(tool: &'static str, borrowed: bool) {
for vertical in [false, true] {
for copy in [false, true] {
let mut f = GuideDrag::new(tool, vertical, copy, borrowed);
f.start();
f.key(Key::Escape);
assert!(
!f.app.session.in_interaction(),
"Escape must cancel the existing-guide interaction: tool={tool} borrowed={borrowed} vertical={vertical} copy={copy}"
);
f.finish_cancel();
}
}
}
#[test]
fn escape_cancels_existing_guide_selection() {
cancel_cases("selection", false);
}
#[test]
fn escape_cancels_existing_guide_direct_selection() {
cancel_cases("directSelection", false);
}
#[test]
fn escape_cancels_existing_guide_group_selection() {
cancel_cases("groupSelection", false);
}
#[test]
fn escape_cancels_existing_guide_borrowed_selection() {
for tool in SELECTION_TOOLS {
cancel_cases(tool, true);
}
}
#[test]
fn escape_clears_existing_guide_shift_press() {
for tool in SELECTION_TOOLS {
for vertical in [false, true] {
let mut f = GuideDrag::new(tool, vertical, false, false);
f.mods = Modifiers::SHIFT;
f.press();
assert!(!f.app.session.in_interaction(), "no interaction before the drag threshold");
f.key(Key::Escape);
assert!(!f.app.session.tool_busy(), "Escape must clear the pressed guide before release: tool={tool}");
f.finish_cancel();
}
}
}
#[test]
fn existing_guide_release_without_escape_control() {
for tool in SELECTION_TOOLS {
for vertical in [false, true] {
for copy in [false, true] {
for borrowed in [false, true] {
let mut f = GuideDrag::new(tool, vertical, copy, borrowed);
f.start();
f.finish_commit();
}
}
}
}
}
#[test]
fn existing_guide_enter_keeps_drag_control() {
for tool in SELECTION_TOOLS {
for vertical in [false, true] {
for copy in [false, true] {
let mut f = GuideDrag::new(tool, vertical, copy, false);
f.start();
f.key(Key::Enter);
assert!(f.app.session.in_interaction() && f.app.session.tool_busy());
f.guides_at_target();
f.finish_commit();
}
}
}
}
#[test]
fn existing_guide_idle_escape_control() {
for tool in SELECTION_TOOLS {
let mut f = GuideDrag::new(tool, true, false, false);
f.key(Key::Escape);
f.unchanged();
assert_eq!(f.app.session.tool_id(), tool);
f.app.run("edit.redo", json!({})).unwrap();
assert_eq!(f.app.session.active().unwrap().doc, f.redo);
}
}
+4 -1
View File
File diff suppressed because one or more lines are too long
+3 -2
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3 -1
View File
@@ -1,6 +1,6 @@
# UI and interaction parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (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-14 · **Change:** minor (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,
@@ -101,6 +101,7 @@ tools, never checked side by side) isn't captured row by row.
| Snapping panel/popover consolidating the snap options (30.0) | missing | |
| Customisable rotation snap angle (#973) | missing | Illustrator: Constrain Angle and construction angles only |
| Ruler guides: drag out, select, move, delete, lock, Make/Release Guides | done | #414, #451; marquee selects unlocked ruler guides |
| Escape cancels an existing ruler-guide press, move or copy | done | Selection and Direct/Group Selection; app-input regressions cover preselected guides, document/interaction-start selection, Undo/Redo and saved state, later on-canvas motion/release and temporary borrowing from idle Pen. All eight checks and the Direct Selection Escape control passed in current full CI. [Validation evidence](target-app-parity.md#evidence-what-ships-today-by-area) |
| Ruler origin: corner drag with preview, double-click reset to the active artboard, relative X/Y fields, native save/reopen | done | #1212; `document.setRulerOrigin`, `document.resetRulerOrigin`; geometry and command coordinates stay absolute |
| Ruler guides as Layers rows: shown, hidden, locked and deleted with their layer; rows dragged to another layer | done | `guide.setLayer`; guides from older files go on a layer |
| Objects made into guides: drawn in the guide colour and style of Preferences; Lock Guides locks them too (no select, move or delete); a dragged selection snaps to ruler guides with Smart Guides on | done | #1534 (#1543, #1583) |
@@ -228,6 +229,7 @@ re-measure of the panels added since. Slider rails show on Medium Dark (#1808, #
| 2026-10-14 | minor | Curvature: the Curvature tool's curve: each stretch of three points follows the circle through them (handles along its tangent, chord/(3·cos²(α/2)) long, shorter in a turn tighter than 30°), and the ends of an open path curve in, so uneven spacing neither overshoots nor loops; the rubber band, dragged, inserted and converted points use the same rule; the Pencil's fitting unchanged (#1236, in part); scores unchanged (estimated) |
| 2026-10-14 | minor | Quick Image Trace presets reveal the adjustment panel with Advanced open; Properties reopens it. Sliders keep their preset row until release so the re-trace is committed. UI regressions added; scores and remaining estimates unchanged. |
| 2026-10-14 | minor | The macOS save sheet's Format menu goes back to missing: #1855 withdrew it to restore saving (#866 reopened). Rows added for the X/Y/W/H of direct-selected anchors (#1839, fixes #1824), the toolbar and All Tools drawer (#1751) and the Actions panel selection (#1853, part of #1629); the snapping row notes Snap to Grid over Snap to Pixel (#1840). Scores unchanged (estimated) |
| 2026-10-14 | minor | Existing-guide Escape cancellation: eight app-input regressions, the Direct Selection Escape control and current full CI passed; scores and hours unchanged. |
| 2026-10-15 | minor | Artboard tool: closing a clean active tab restores the displayed board before Properties Delete. Nine targeted checks passed; three original close-route captures reviewed; scores and hours unchanged. |
| 2026-10-14 | minor | Artboard tab changes align the tool with the displayed board; same-tab multiselection and Move/Scale Artwork options preserved. Three tab regressions and historical full CI on `a0671777` passed; scores and hours unchanged. |
| 2026-10-14 | minor | Width-point finite-arithmetic follow-up: atomicity, history and native round-trip regressions and current full CI passed; scores and hours unchanged. |