mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-07 13:23:22 +00:00
Merge pull request #5851 from mockydevs/feat/direct-selection-cmd-bounding-box
Direct Selection: Cmd/Ctrl held lends the Selection tool and shows its bounding box (#3901)
This commit is contained in:
@@ -2757,6 +2757,42 @@ fn cmd_alt_lends_group_selection_while_direct_selection_is_active() {
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, 80.0, 80.0).with_mods(cmd_alt), v).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmd_lends_the_selection_tool_and_its_bounding_box_to_direct_selection() {
|
||||
// #3901: with Direct Selection, Cmd held gives the Selection tool for the press: its cursor,
|
||||
// and its bounding box's handles scale the object; Direct Selection is back on the release.
|
||||
let (none, cmd) = (Mods::default(), Mods { cmd: true, ..Mods::default() });
|
||||
let v = view();
|
||||
let mut s = session();
|
||||
let anchors = json!([{"x": 100, "y": 100}, {"x": 200, "y": 100}, {"x": 200, "y": 200}, {"x": 100, "y": 200}]);
|
||||
let id = NodeId(s.execute("path.create", &json!({"anchors": anchors, "closed": true})).unwrap()["id"].as_u64().unwrap());
|
||||
s.select_tool("directSelection", v).unwrap();
|
||||
assert_eq!(s.lent_tool_id(&none), None);
|
||||
assert_eq!(s.lent_tool_id(&cmd), Some("selection"));
|
||||
// The path selected whole: the bottom-right handle scales it.
|
||||
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
|
||||
assert_eq!(s.cursor(Point::new(200.0, 200.0), cmd, v), vectorcraft_tools::Cursor::ResizeNwSe);
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 200.0, 200.0).with_mods(cmd), v).unwrap();
|
||||
assert_eq!(s.tool_id(), "selection");
|
||||
for at in [250.0, 300.0] {
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, at, at).with_mods(cmd), v).unwrap();
|
||||
}
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, 300.0, 300.0).with_mods(cmd), v).unwrap();
|
||||
assert_eq!(s.tool_id(), "directSelection");
|
||||
let b = s.doc().unwrap().doc.node(id).unwrap().geometric_bounds().unwrap();
|
||||
assert!((b.x0 - 100.0).abs() < 1e-6 && (b.x1 - 300.0).abs() < 1e-6 && (b.y1 - 300.0).abs() < 1e-6, "{b:?}");
|
||||
// An anchor selected: a Cmd drag inside moves the whole path and the anchor stays selected.
|
||||
gesture(&mut s, &[(300.0, 300.0)], none);
|
||||
let picked = s.doc().unwrap().selection.anchors.clone();
|
||||
assert!(!picked.is_empty(), "Direct Selection picked the anchor");
|
||||
let before: Vec<_> = path(&s, id).subpaths[0].anchors.iter().map(|a| a.p).collect();
|
||||
gesture(&mut s, &[(150.0, 150.0), (160.0, 160.0), (170.0, 150.0)], cmd);
|
||||
let after: Vec<_> = path(&s, id).subpaths[0].anchors.iter().map(|a| a.p).collect();
|
||||
assert_eq!(after, before.iter().map(|p| *p + kurbo::Vec2::new(20.0, 0.0)).collect::<Vec<_>>());
|
||||
assert_eq!(s.doc().unwrap().selection.anchors, picked, "the anchor is still selected");
|
||||
assert_eq!(s.tool_id(), "directSelection");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_pen_drops_the_new_path_asterisk_once_a_path_is_started() {
|
||||
// The Pen's asterisk means a click starts a new path: it shows until the first anchor is
|
||||
|
||||
@@ -154,6 +154,8 @@ impl Session {
|
||||
/// Direct Selection to the tools that edit anchors (the Pen) and Selection to the others.
|
||||
/// Cmd+Alt lends Group Selection while Direct Selection is active. Cmd still lends the last
|
||||
/// selection tool from other tools (Alt on the lent Direct Selection gesture stays copy/subtract).
|
||||
/// Cmd alone lends Direct Selection the Selection tool, whose bounding box transforms the
|
||||
/// object (#3901; <https://community.adobe.com/t5/illustrator-discussions/handles-disappar-on-shape-with-direct-selection-tool/m-p/14848662>).
|
||||
/// Perspective Selection keeps Cmd+Alt drags to copy objects on their perspective plane.
|
||||
/// None for the other selection tools and the view tools.
|
||||
fn lent_tool(&self, id: &str, mods: &Mods) -> Option<&'static str> {
|
||||
@@ -161,8 +163,8 @@ impl Session {
|
||||
if matches!(id, "hand" | "zoom" | "rotateView") {
|
||||
return None;
|
||||
}
|
||||
if id == "directSelection" && mods.cmd && mods.alt {
|
||||
return Some("groupSelection");
|
||||
if id == "directSelection" && mods.cmd {
|
||||
return Some(if mods.alt { "groupSelection" } else { "selection" });
|
||||
}
|
||||
if id == "perspectiveSelection" && mods.cmd && mods.alt {
|
||||
return None;
|
||||
@@ -173,6 +175,16 @@ impl Session {
|
||||
Some(self.last_selection_tool.unwrap_or(if edits_anchors(id) { "directSelection" } else { "selection" }))
|
||||
}
|
||||
|
||||
/// The selection tool a press with `mods` would borrow from the active tool
|
||||
/// ([`Self::lend_selection_tool`]), as [`Self::cursor`] shows it: the canvas also shows what
|
||||
/// that tool shows (Direct Selection with Cmd held shows the bounding box).
|
||||
pub fn lent_tool_id(&self, mods: &Mods) -> Option<&'static str> {
|
||||
if !mods.cmd || self.lender.is_some() {
|
||||
return None;
|
||||
}
|
||||
self.lent_tool(self.tool.id(), mods)
|
||||
}
|
||||
|
||||
/// A press with Cmd held (Ctrl elsewhere) and any other tool than a selection tool: the
|
||||
/// selection tool [`Self::lent_tool`] takes the gesture, pressing without Cmd (a plain click,
|
||||
/// not Select Behind). The tool lent to stays as it was, unaware, until the release.
|
||||
|
||||
@@ -100,7 +100,11 @@ pub(super) fn selection_overlay(app: &mut VectorcraftApp, p: &egui::Painter, xf:
|
||||
let shape_tool = vectorcraft_tools::shape::shows_shape_widgets(app.session.tool_id());
|
||||
// A shape tool drawing a shape shows none of this; adjusting a widget, it shows the widgets.
|
||||
let drawing = shape_tool && app.session.tool_busy() && !app.session.tool_transforming();
|
||||
let show_box = (app.session.tool_id() == "selection" || (shape_tool && !app.session.tool_busy()))
|
||||
// Cmd held lends the Selection tool to a press (Direct Selection's included, #3901): its box
|
||||
// shows while the key is down, where its handles take that press.
|
||||
let held = super::drag::mods(p.ctx().input(|i| i.modifiers), false);
|
||||
let lends_selection = !app.session.tool_busy() && app.session.lent_tool_id(&held) == Some("selection");
|
||||
let show_box = (app.session.tool_id() == "selection" || lends_selection || (shape_tool && !app.session.tool_busy()))
|
||||
&& (!app.session.tool_transforming() || moved.is_some())
|
||||
&& app.ui.view.bounding_box
|
||||
&& app.session.active().is_some_and(|st| !st.selection.is_empty() && st.selection.anchors.is_empty());
|
||||
|
||||
@@ -9268,3 +9268,37 @@ fn the_render_worker_waits_for_a_frame_only_as_long_as_asked() {
|
||||
assert!(w.wait(std::time::Duration::from_secs(5)).is_some(), "finished during the wait");
|
||||
sender.join().unwrap();
|
||||
}
|
||||
|
||||
/// #3901: with Direct Selection, the Selection tool's bounding box shows while Cmd/Ctrl is held
|
||||
/// (a press then takes its handles), and not otherwise.
|
||||
#[test]
|
||||
fn direct_selection_shows_the_bounding_box_while_cmd_is_held() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
|
||||
let id = app.run("shape.rectangle", json!({"x": 100, "y": 100, "width": 100, "height": 50})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("select.set", json!({"ids": [id]})).unwrap();
|
||||
app.select_tool("directSelection");
|
||||
let ctx = egui::Context::default();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let xf = Xf::new(app.canvas_rect.unwrap(), app.view().unwrap());
|
||||
let corner = xf.to_screen(Point::new(200.0, 150.0));
|
||||
// A frame with `modifiers` held → is a white box handle drawn at the bottom-right corner?
|
||||
let handle_drawn = |app: &mut VectorcraftApp, modifiers: egui::Modifiers| {
|
||||
let screen = Some(egui::Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0)));
|
||||
let raw = egui::RawInput { screen_rect: screen, events: vec![egui::Event::ModifiersChanged(modifiers)], ..Default::default() };
|
||||
let out = ctx.run_ui(raw, |ui| show(app, ui));
|
||||
fn flat(s: Shape, out: &mut Vec<Shape>) {
|
||||
match s {
|
||||
Shape::Vec(v) => v.into_iter().for_each(|s| flat(s, out)),
|
||||
s => out.push(s),
|
||||
}
|
||||
}
|
||||
let mut v = vec![];
|
||||
out.shapes.into_iter().for_each(|c| flat(c.shape, &mut v));
|
||||
v.iter().any(|s| matches!(s, Shape::Rect(r) if r.fill == Color32::WHITE && (r.rect.width() - 6.0).abs() < 1.0 && r.rect.center().distance(corner) < 0.5))
|
||||
};
|
||||
assert!(!handle_drawn(&mut app, egui::Modifiers::NONE), "Direct Selection alone shows no box");
|
||||
assert!(handle_drawn(&mut app, egui::Modifiers::COMMAND), "Cmd held shows the Selection tool's box");
|
||||
assert!(!handle_drawn(&mut app, egui::Modifiers::NONE), "gone once Cmd is released");
|
||||
assert!(app.ui.view.bounding_box, "View › Show Bounding Box is unchanged");
|
||||
}
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
# UI and interaction parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (Join tool: scrubbing the overhanging ends of two crossing paths trims them and joins the paths at the crossing, #5844; 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 (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)
|
||||
|
||||
## Feature submission evidence
|
||||
|
||||
@@ -38,6 +38,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 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 |
|
||||
| 2026-11-05 | minor | Join tool: scrubbing over the ends of two crossing open paths that run past the crossing trims those ends and joins the paths there with a corner, instead of drawing a segment between the ends (#5844); scores and estimates unchanged |
|
||||
| 2026-11-05 | minor | Effect dialogs: a swatch beside a hex colour field follows typed values, and a double-click opens the Color Picker over the dialog, whose OK fills the field (#5365); scores and estimates unchanged |
|
||||
| 2026-11-05 | minor | Shape tools: a shape just drawn keeps its bounding box and Live Shape widgets on show with the Rectangle, Rounded Rectangle, Ellipse, Polygon or Star tool, and its corner and pie widgets drag as with the Selection tool (#5477); scores and estimates unchanged |
|
||||
@@ -263,7 +264,7 @@ tools, never checked side by side) isn't captured row by row.
|
||||
|---|---|---|
|
||||
| Space: Hand; Cmd+Space / Cmd+Alt+Space: zoom in/out; middle button pans | done | Space pressed after a tool takes a drag keeps the view still and shows that tool's cursor. Shape and anchor tools may use Space to move the geometry being drawn. Cmd+Space pressed mid-drag likewise keeps the active tool cursor. Space held before a press starts a view gesture, and its view cursor returns after release. `space_during_a_pencil_drag_keeps_the_pencil_cursor`, `cmd_space_mid_tool_drag_keeps_the_active_tool_cursor` |
|
||||
| Document window scroll bars along the right and bottom edges (Normal and Full Screen Mode with Menu Bar; none in Full Screen and Presentation Mode): a thumb as long as the view against the art and the artboards; dragging it scrolls, a click on the track pages, the wheel over a bar scrolls along it; in a rotated view they follow the screen's axes | partial | #2322. The span is the art and the artboards with half a window around them, not Illustrator's 227 in canvas: VectorCraft has no bounded drawing area (#2284). A track click pages rather than jumping to the click; the page size and the rotated-view behaviour are our own choice (the public documentation gives neither) |
|
||||
| Cmd/Ctrl: last-used selection tool; Cmd+Alt: Group Selection inside Direct Selection | done | Cmd+Alt lends Group Selection only while Direct Selection is active (Cmd borrow from the Pen keeps Direct Selection so Alt stays copy/subtract); engine pointer regressions |
|
||||
| Cmd/Ctrl: last-used selection tool; Cmd+Alt: Group Selection inside Direct Selection | done | Cmd+Alt lends Group Selection only while Direct Selection is active (Cmd borrow from the Pen keeps Direct Selection so Alt stays copy/subtract); Cmd alone lends Direct Selection the Selection tool: its bounding box shows while Cmd is held over objects selected whole and its handles scale and rotate them, and a Cmd drag moves the whole object while its direct-selected anchors stay selected (#3901; no box over a selection with anchors picked yet); engine pointer and canvas regressions |
|
||||
| Eyedropper styles highlighted characters only and preserves ranges across Type switches | done | Font/paint, Shift color-only, Unicode, Undo/Redo and stale-range regression tests; enabled non-neutral source or target object transparency is refused atomically; disable Transparency to sample supported attributes |
|
||||
| Eyedropper reads visible sources while their layer/object is locked or outside isolation | done | Ordinary/Shift/Shift+Alt and gradient-stop reads preserve the source and selection; Alt reverse remains editable-only; hidden art stays excluded (`eyedropper_sources.rs`, #1251) |
|
||||
| Add a visible stroke after drawing with inherited None/zero-width paint | done | Paint and Swap Fill/Stroke restore the existing 1 pt default; positive widths and painted zero widths stay as set; Undo/Redo and targeted appearance items tested (`stroke_visibility.rs`, #1253) |
|
||||
|
||||
Reference in New Issue
Block a user