From 5ecf7e7dc0ec764b7bbde163eacb23cacff97478 Mon Sep 17 00:00:00 2001 From: FractalEncrypt <59615752+FractalEncrypt@users.noreply.github.com> Date: Fri, 6 Nov 2026 10:25:38 -0500 Subject: [PATCH] Transform fields: the W/H link stays a tab stop, after the fields A review found the link lost its keyboard focus when it stopped being a tab stop. It is focusable again, and the Control bar now keeps the link's place between W and H but makes the button after the H field, so Tab still goes from W straight to H and finds the link after it (as the Transform and Properties panels already draw it). Tests: Shift+Tab goes back one field too, and a click on the Control bar's link still toggles the preference. The Blob Brush Tool Options test that expected two Tabs between the Roundness field and the tip preview now expects one, as every field does. Docs: the Tab entry in target-app-parity.md, both revision histories and the M1 milestone. --- crates/ui-egui/src/chrome/control_bar.rs | 10 ++++- crates/ui-egui/src/dialogs/tests.rs | 8 ++-- crates/ui-egui/src/panels/tests_constrain.rs | 46 +++++++++++++++++++- crates/ui-egui/src/panels/transform.rs | 20 ++++++--- docs/roadmap.md | 2 +- docs/target-app-parity.md | 3 +- docs/ui-parity.md | 3 +- 7 files changed, 77 insertions(+), 15 deletions(-) diff --git a/crates/ui-egui/src/chrome/control_bar.rs b/crates/ui-egui/src/chrome/control_bar.rs index 9afbe838e..70c6fe272 100644 --- a/crates/ui-egui/src/chrome/control_bar.rs +++ b/crates/ui-egui/src/chrome/control_bar.rs @@ -381,10 +381,12 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) { let link = crate::panels::transform::proportional(app, ui); let copy = crate::panels::transform::alt_enter_copies(app, ui); let values = [(c.x, origin.x), (c.y, origin.y), (b.rect.width(), 0.0), (b.rect.height(), 0.0)]; + // The W/H link sits between W and H, but is made after H so Tab goes from W + // straight to H and finds the link after it. + let mut link_room = None; for ((k, lbl), (v, offset)) in INLINE_FIELDS.into_iter().zip(values) { - // The W/H link sits between W and H. if k == "height" { - crate::panels::transform::constrain_link(app, ui); + link_room = Some(crate::panels::transform::constrain_link_room(ui)); } widgets::field_label(ui, inline_label(&t, lbl)); let (id, shown) = (("cb", k), Some(v - offset)); @@ -397,6 +399,10 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) { app.run("object.setBounds", json!({k: nv + offset, "reference": 4, "proportional": link, "copy": copy})).ok(); } } + if let Some(room) = link_room { + let id = ui.scope_id().with("cb-constrain-link"); + crate::panels::transform::constrain_link_at(app, ui, ui.interact(room, id, Sense::click())); + } } }); }); diff --git a/crates/ui-egui/src/dialogs/tests.rs b/crates/ui-egui/src/dialogs/tests.rs index c2a7aa2b1..4421888c7 100644 --- a/crates/ui-egui/src/dialogs/tests.rs +++ b/crates/ui-egui/src/dialogs/tests.rs @@ -2386,8 +2386,8 @@ fn the_flattener_and_3d_sliders_step_with_the_arrow_keys() { /// The Blob Brush Tool Options' tip preview is the Angle slider for screen readers, with the Angle /// as its value. A click in it points the Angle at the pointer and gives it the focus, its arrow /// keys then step the Angle by one degree (ten with Shift) within -180° to 180°, Shift+Tab leaves -/// it for the Roundness field and Tab comes back to it, and a drag still sets the Angle and gives -/// it the focus. +/// it for the Roundness field and Tab comes back to it (one press: the Roundness field's label is +/// not a tab stop of its own), and a drag still sets the Angle and gives it the focus. #[test] fn the_blob_brush_tip_preview_is_a_named_slider_with_arrow_keys() { use egui::Key::{ArrowDown, ArrowLeft, ArrowRight, ArrowUp, End, Home, Tab}; @@ -2428,8 +2428,8 @@ fn the_blob_brush_tip_preview_is_a_named_slider_with_arrow_keys() { f.key(&mut app, Tab, M::SHIFT); f.key(&mut app, ArrowRight, M::NONE); assert_eq!(angle(&app), -179.0, "the Roundness field has the focus"); - // Tab goes from the Roundness field to the scrub area on its label, then to the preview. - f.key(&mut app, Tab, M::NONE); + // Tab goes from the Roundness field straight to the preview: a field's scrub label is not a + // tab stop of its own. f.key(&mut app, Tab, M::NONE); f.key(&mut app, ArrowRight, M::NONE); assert_eq!(angle(&app), -178.0, "Tab comes back to the preview"); diff --git a/crates/ui-egui/src/panels/tests_constrain.rs b/crates/ui-egui/src/panels/tests_constrain.rs index 44ede9641..390438084 100644 --- a/crates/ui-egui/src/panels/tests_constrain.rs +++ b/crates/ui-egui/src/panels/tests_constrain.rs @@ -321,11 +321,55 @@ fn one_tab_moves_from_a_field_to_the_next() { let mut app = rect_app(); tab_then_type(&mut app, crate::chrome::control_bar, "100 pt", "200"); assert_eq!(size(&app), (100.0, 200.0), "Control bar: Tab went from W to H"); + // Shift+Tab goes back one field, one press too: from W to X in the panels. + for (name, draw) in [("Transform panel", transform::show as Draw), ("Properties", properties::transform_section as Draw)] { + let mut app = rect_app(); + shift_tab_then_type(&mut app, draw, "100 pt", "200"); + let st = app.session.active().unwrap(); + let b = st.doc.bounds_of(&st.selection.objects, false).unwrap(); + assert_eq!((b.width().round(), b.x0.round()), (100.0, 150.0), "{name}: Shift+Tab went from W to X"); + } +} + +/// A click on the Control bar's W/H link toggles Constrain Width and Height Proportions, and the +/// link comes after the fields in the tab order, so Tab still moves from W to H. +#[test] +fn the_control_bar_link_toggles_by_click() { + let mut app = rect_app(); + assert!(!app.session.prefs.constrain_proportions); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let mut time = 0.0; + let mut frame = |app: &mut VectorcraftApp, events: Vec| { + time += 1.0; + frame_raw(&ctx, app, egui::RawInput { events, time: Some(time), ..Default::default() }, crate::chrome::control_bar) + }; + frame(&mut app, vec![]); + let shown = frame(&mut app, vec![]); + let at = |text: &str| shown.iter().find(|(t, _)| t == text).map(|(_, r)| *r).unwrap_or_else(|| panic!("no `{text}` in {shown:?}")); + let (w, h) = (at("100 pt"), at("50 pt")); + // The link's button fills the gap between the W and H fields. + let link = egui::pos2((w.right() + h.left()) / 2.0, w.center().y); + let button = |pressed| Event::PointerButton { pos: link, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE }; + for events in [vec![Event::PointerMoved(link), button(true), button(false)], vec![]] { + frame(&mut app, events); + } + assert!(app.session.prefs.constrain_proportions, "the click toggled the link"); } /// Type `value` into the field of what `draw` shows that shows `field` after one Tab, and press /// Enter. The field the Tab moved to takes the value; the one pressed keeps its own. fn tab_then_type(app: &mut VectorcraftApp, draw: Draw, field: &str, value: &str) { + tab_then_type_with(app, draw, field, value, Modifiers::NONE); +} + +/// [`tab_then_type`] with Shift+Tab, which goes back one field. +fn shift_tab_then_type(app: &mut VectorcraftApp, draw: Draw, field: &str, value: &str) { + tab_then_type_with(app, draw, field, value, Modifiers::SHIFT); +} + +/// [`tab_then_type`], the Tab key held with `modifiers`. +fn tab_then_type_with(app: &mut VectorcraftApp, draw: Draw, field: &str, value: &str, modifiers: Modifiers) { let ctx = egui::Context::default(); crate::theme::install_fonts(&ctx); let mut time = 0.0; @@ -337,7 +381,7 @@ fn tab_then_type(app: &mut VectorcraftApp, draw: Draw, field: &str, value: &str) let shown = frame(app, vec![]); let at = shown.iter().filter(|(s, _)| s == field).map(|(_, r)| r.center()).next().unwrap_or_else(|| panic!("no {field} field in {shown:?}")); let button = |pos, pressed| Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE }; - let tab = Event::Key { key: Key::Tab, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE }; + let tab = Event::Key { key: Key::Tab, physical_key: None, pressed: true, repeat: false, modifiers }; for events in [vec![Event::PointerMoved(at), button(at, true), button(at, false)], vec![], vec![tab], vec![]] { frame(app, events); } diff --git a/crates/ui-egui/src/panels/transform.rs b/crates/ui-egui/src/panels/transform.rs index caf69ec86..815b5c4a8 100644 --- a/crates/ui-egui/src/panels/transform.rs +++ b/crates/ui-egui/src/panels/transform.rs @@ -484,18 +484,28 @@ pub fn link_width(ui: &Ui) -> f32 { pub const CONSTRAIN_LINK_SIZE: f32 = 22.0; /// The link between W and H (Transform panel, Properties panel, Control bar): one toggle, the -/// `constrainProportions` preference, which the size fields pass on as `proportional`. It is not a -/// tab stop, so Tab goes from W straight to H (it is the same preference as Preferences › General -/// › Constrain Width and Height Proportions for the keyboard). +/// `constrainProportions` preference, which the size fields pass on as `proportional`. It is drawn +/// after the fields it sits between, so Tab reaches the fields first and the link after them. pub fn constrain_link(app: &mut VectorcraftApp, ui: &mut Ui) { + let (_, resp) = ui.allocate_exact_size(egui::Vec2::splat(CONSTRAIN_LINK_SIZE), egui::Sense::click()); + constrain_link_at(app, ui, resp); +} + +/// [`constrain_link`]'s toggle, made over `at`: the Control bar keeps the link's place between W +/// and H but makes it after H, so Tab goes from W straight to H and finds the link after it. +pub fn constrain_link_at(app: &mut VectorcraftApp, ui: &mut Ui, at: egui::Response) { let on = app.session.prefs.constrain_proportions; let tip = tl!("Constrain Width and Height Proportions"); - let (_, resp) = ui.allocate_exact_size(egui::Vec2::splat(CONSTRAIN_LINK_SIZE), egui::Sense::CLICK); - if widgets::paint_icon_button(ui, resp, if on { "link" } else { "link-2-off" }, tip, on).clicked() { + if widgets::paint_icon_button(ui, at, if on { "link" } else { "link-2-off" }, tip, on).clicked() { set_pref(app, "constrainProportions", !on); } } +/// The room the Control bar keeps for [`constrain_link_at`] between its W and H fields. +pub fn constrain_link_room(ui: &mut Ui) -> egui::Rect { + ui.allocate_exact_size(egui::Vec2::splat(CONSTRAIN_LINK_SIZE), egui::Sense::hover()).0 +} + /// The `copy` param of a command that a field of the Transform panel, the Properties panel's /// Transform section or the Control bar runs: true when Alt+Enter (Option+Return) applied the value /// and no anchors are selected with Direct Selection. diff --git a/docs/roadmap.md b/docs/roadmap.md index f3ad64942..dd5504b0d 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -117,7 +117,7 @@ native wording review, layout checks and a web Devanagari font remain. [Notes](l | # | Milestone | Status | Est. remaining, one agent (h) | |---|---|---|---| | M0 | Skeleton + vertical slice | ✅ done | — | -| M1 | Selection, transform, layers, MCP (common command tools, strict keys, session resources and version-aware result hints) | ✅ mostly done (document-tab artwork drag with lossless resource transfer and one target Undo; malformed MCP app replies discard the connection without replay, #1761; bounded control socket waits and connection-slot recovery, #1726; bounded synthetic control input, #1709; no-op anchor Align keeps Undo, Redo and saved state (#1180 follow-up); transform reference point snaps to anchors/centres; rotated objects keep a rotated bounding box and their angle; Free Transform, Scale and Reflect along a rotated box pending) | 4–6 | +| M1 | Selection, transform, layers, MCP (common command tools, strict keys, session resources and version-aware result hints) | ✅ mostly done (document-tab artwork drag with lossless resource transfer and one target Undo; malformed MCP app replies discard the connection without replay, #1761; bounded control socket waits and connection-slot recovery, #1726; bounded synthetic control input, #1709; no-op anchor Align keeps Undo, Redo and saved state (#1180 follow-up); transform reference point snaps to anchors/centres; rotated objects keep a rotated bounding box and their angle; Free Transform, Scale and Reflect along a rotated box pending; the Transform fields take one Tab each, the W/H link after them) | 4–6 | | M2 | Drawing tools + smart guides | ✅ mostly done (Polygon/Star exact-size count persistence, with repeated-drag, tool-switch and Cancel regression coverage; Curvature point markers without direction handles (#1236, in part); Direct Selection segment click/Delete preserves neighboring edges, #1278; Flare, Reshape, Live Corners widget dragging and per-corner radii and kinds landed, on any path's corners; Shaper rough-shape recognition and orientation steps, the remaining Pen modifier nuances; Shaper Groups with editable sources and separate fill/stroke edits landed; custom ruler origin with drag preview, active-artboard reset, relative X/Y fields and native round-trip (#1212); construction widget/refinements pending); Tangent and straight-segment perpendicular snapping use saved Smart Guides Geometric Guides preferences and Control bar quick access (#2537); no View submenu | 12–18 | | M3 | Paint & appearance (swatches, color, gradient, stroke, appearance, transparency, styles) | ✅ done (M3.7–M3.98): swatches, groups, libraries, tints, Lab spots and Registration; Eyedropper styles highlighted text only (atomic transparency rejection); Eyedropper reads visible locked and outside-isolation sources (#1251); Color Picker, Color panel, Color Guide, Color Themes, Edit Colors and Recolor Artwork (screen-bounded Edit chip list, #3416); gradients that follow every transform, the annotator, linked stops, freeform gradients, focal points, gradients on strokes and Expand; visible strokes when paint is added to inherited None/zero-width art (#1253); stroke geometry, arrowheads, dashes fitted to corners, stroke on type, width profiles and the Width tool (Shift-double-click enables adjoining adjustment in Width Point Edit; finite-arithmetic follow-up regressions and full CI passed ([Validation evidence](target-app-parity.md#evidence-what-ships-today-by-area))), Scale Strokes and preview bounds; Appearance targeting, container appearance, target circles, Expand Appearance; knockout, isolation, blend accuracy, CMYK blending, overprint, Attributes, masks and clipping, Flatten Transparency; graphic styles, links and libraries; CSS library export preserves fractional linear and radial gradient stop positions. Left: freeform gradients in SVG/PDF, mixed spot/process gradients in SVG and PDF gradients mixing a spot color with RGB or Lab stops export as stops or process colors; PDF export writes dithered gradient fills with spot stops, and Perceptual spot gradients with RGB or Lab stops other than white or more than 4,096 stops on their curve, as images in process colors; confirm the Scale Strokes & Effects default; Recolor model selection is available through the engine and MCP only (#2520); the dialog retains its existing controls | 4–8 | | M2 | Drawing tools + smart guides | ✅ mostly done (Curvature point markers without direction handles (#1236, in part); Direct Selection segment click/Delete preserves neighboring edges, #1278; Flare, Reshape, Live Corners widget dragging and per-corner radii and kinds landed, on any path's corners; Shaper rough-shape recognition and orientation steps, the remaining Pen modifier nuances; Shaper Groups with editable sources and separate fill/stroke edits landed; custom ruler origin with drag preview, active-artboard reset, relative X/Y fields and native round-trip (#1212); construction widget/refinements pending) | 12–18 | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 2031e5714..b8fe1a68f 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -727,7 +727,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - The Control bar's underlined Transform link opens the Transform panel in a popover under it (reference point, X/Y/W/H, Rotate and Shear with their icons and angle presets, flips, shape properties, Scale Corners and Scale Strokes & Effects), as its Stroke link does the Stroke panel; X/Y/W/H also show inline while the bar has room. The reference point locator is nine squares, the outer eight joined around the box. - In isolation mode the Control bar has an Exit Isolation Mode button (`object.exitIsolation`), except while a pattern is edited. Outside isolation mode, with one group, compound path, path, image, mesh or placed file selected, it has an Isolate Selected Object button (`object.isolate`), which also isolates a path inside a group that Direct Selection selected. - While an opacity mask is edited, Exit Isolation Mode (Esc, the isolation bar's back arrow, the Control bar's button, and the Layers panel menu's and canvas context menu's Exit Isolation Mode) leaves mask editing as Stop Editing Opacity Mask does, in one undo step: the mask keeps the edits, and the masked object is selected. While a pattern is edited, the Exit Isolation Mode item of the Layers panel menu and of the canvas context menu saves the pattern and leaves pattern editing, as Done does. - - Tab shows and hides the panels (in the Type tool it types a tab) without moving the keyboard focus onto a field, so the next shortcut or letter still works; in dialogs and panel fields it moves between fields. + - Tab shows and hides the panels (in the Type tool it types a tab) without moving the keyboard focus onto a field, so the next shortcut or letter still works; in dialogs and panel fields it moves between fields, one press each — a numeric field's scrub label is not a tab stop of its own, and the Transform panel's, Properties panel's and Control bar's W/H link comes after the fields it sits between (`one_tab_moves_from_a_field_to_the_next`). - Shift+Tab (`window.panels`) shows and hides the dock and the floating panels and leaves the toolbar and the Control bar as they are; hiding the panels with Tab or Shift+Tab also closes a panel popped out of the icon column. - The second Tab (`window.dock`) shows again only the bars the first Tab hid, after a restart too: a Control bar that was off stays off, a toolbar hidden with Window › Tools stays hidden, and panels shown with Shift+Tab in between stay shown. - The function key shortcuts for Brushes, Color, Layers, Info, Gradient, Stroke, Attributes, Graphic Styles, Appearance, Align, Transform, Pathfinder, Transparency, and Symbols activate a background tab on the first press, hide its expanded dock or floating group when that tab is in front, and restore the group with all its tabs together on the next press; in an icon column they open and close the flyout while keeping the icons. A reassigned chord keeps that behavior, and Window > a hidden sibling restores the group. @@ -1721,6 +1721,7 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | 2026-10-30 | minor | Effects: Color Halftone's Max Radius is 4–127 px of the Document Raster Effects resolution, shown in px whatever the units; older documents open converted, and files saved for older app versions hold points (#3260); scores unchanged. | | 2026-11-05 | minor | Compound Shapes add-on (beyond Illustrator): Object › Plug-ins › Compound Shapes lists Make, Release and Expand Compound Shape, and holds a setting, off by default, that makes the Pathfinder panel's Shape Mode buttons make a compound shape on a click and give the permanent result on Alt-click (the default is the reverse). Selected members of a compound shape always take the clicked mode. The setting is `window.pathfinder.compoundShapes`; no existing behavior changes while it is off. Scores and estimates unchanged. | | 2026-11-01 | minor | Batch Trace Folder (`imageTrace.batchFolder`, #4921); evidence added; scores unchanged. | +| 2026-11-06 | minor | Transform fields: one Tab moves from one numeric field to the next in the Transform panel, the Properties panel's Transform section and the Control bar (each field's scrub label was a second, invisible tab stop, and so was the W/H link between W and H in the Control bar, which now comes after the fields); evidence: `one_tab_moves_from_a_field_to_the_next` and `the_control_bar_link_toggles_by_click` in `crates/ui-egui/src/panels/tests_constrain.rs`; scores and estimates unchanged | | 2026-11-01 | minor | Copy and paste ruler guides (#3226); evidence added; scores unchanged. | | 2026-11-04 | minor | Area type vertical alignment buttons in the Properties panel's Paragraph section and the Control panel, off for columns, rows and threaded frames (#3759); evidence added; scores unchanged. | | 2026-11-03 | minor | Enclosing Lasso is an add-on with no default shortcut or Basic toolbar slot. Scores and estimates remain unchanged. | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 3bdb39930..a62d8588b 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-11-06 · **Change:** minor (opening a slow file shows a progress window with Cancel and Continue in Background, part of #5930; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Layers: Rename (`ui.rename`) opens the name field of the selected layer, group or object, #5324; Properties in Edit Artboards: Transform section above the Artboards section, #5219; scrubby labels: the Control bar's Stroke: and Opacity: links and the Properties panel's Stroke link scrub their fields, #4088; Properties: Quick Actions has Repeat Options for one selected repeat, #5181; Direct Selection: Shift-click picks several blend spine points, which drag together, #5285; Direct Selection: Cmd/Ctrl held lends the Selection tool and shows its bounding box, #3901; Join tool trims two crossing open paths and joins them at the crossing, #5844; Control bar: Paragraph: link and Align left/center/right while type is selected, #5372; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Layers: Rename (`ui.rename`) opens the name field of the selected layer, group or object, #5324; Properties in Edit Artboards: Transform section above the Artboards section, #5219; scrubby labels: the Control bar's Stroke: and Opacity: links and the Properties panel's Stroke link scrub their fields, #4088; Properties: Quick Actions has Repeat Options for one selected repeat, #5181; Direct Selection: Shift-click picks several blend spine points, which drag together, #5285; Direct Selection: Cmd/Ctrl held lends the Selection tool and shows its bounding box, #3901; Join tool trims two crossing open paths and joins them at the crossing, #5844; Control bar: Paragraph: link and Align left/center/right while type is selected, #5372; Transform fields: one Tab per field, the W/H link after them; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged) ## Feature submission evidence @@ -45,6 +45,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | 2026-11-06 | minor | Drop Shadow, Outer Glow and Inner Glow: spinner arrows beside Opacity, the offsets, Blur and Darkness (#5367, #5368); scores and estimates unchanged | | 2026-11-06 | minor | Rename (`ui.rename`, no default key) opens the Layers panel's name field on the selected layer, group or object's row, as double-clicking its name does; a key can be assigned in Keyboard Shortcuts (#5324, a VectorCraft addition); scores and estimates unchanged | | 2026-11-06 | minor | Properties panel in Edit Artboards (Shift+O): a Transform section with the shared reference point, X, Y, W and H, the preset and orientation, and Move and Scale Artwork with Artboard, above the Artboards section with the name, Artboard Options and Fill (#5219); scores and estimates unchanged | +| 2026-11-06 | minor | Transform fields: one Tab moves from one numeric field to the next in the Transform panel, the Properties panel's Transform section and the Control bar — a field's scrub label was a second, invisible tab stop, and the W/H link between W and H is now made after the fields, so it stays a tab stop after them; scores and estimates unchanged | | 2026-11-06 | minor | Scrubby labels: the Control bar's Stroke: and Opacity: and the Properties panel's Stroke, links to their panels, scrub the field after them as other labels do; a click still opens the panel (#4088); scores and estimates unchanged | | 2026-11-05 | minor | Direct Selection: Shift-click picks several points of a blend's spine and dragging one moves them all, with the key objects on them, in one undo step (#5285); scores and estimates unchanged | | 2026-11-05 | minor | Direct Selection: Cmd/Ctrl held lends the Selection tool, whose bounding box shows while the key is down over objects selected whole; its handles scale and rotate, and a Cmd drag moves the whole object with its direct-selected anchors kept (#3901); scores and estimates unchanged |