mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 13:47:21 -06:00
Merge pull request #5004 from storytold/feat/pen-ctrl-edit-4980
Fix #4980: Pen with Cmd/Ctrl drags the path, not hidden bounding-box handles
This commit is contained in:
@@ -2579,6 +2579,60 @@ fn pen_with_cmd_borrows_direct_selection_and_goes_on_drawing() {
|
||||
assert_eq!(s.tool_id(), "rectangle");
|
||||
}
|
||||
|
||||
/// #4980: Cmd/Ctrl held with the Pen drags as Direct Selection: an anchor of the path being drawn
|
||||
/// moves on its own and the Pen goes on drawing that path from it; another path's straight segment
|
||||
/// moves both its ends and a curved one bends; each drag is one undo step and the pointer is the
|
||||
/// Pen's again once Cmd is let go. With the Selection tool used last, a Cmd drag on the path being
|
||||
/// drawn moves it: the bounding box isn't shown, so its handles don't scale it to nothing.
|
||||
#[test]
|
||||
fn pen_cmd_drag_edits_anchors_and_segments_and_goes_on_drawing() {
|
||||
let (none, cmd) = (Mods::default(), Mods { cmd: true, ..Mods::default() });
|
||||
let v = view();
|
||||
let mut s = session();
|
||||
let straight = line(&mut s, 100.0, 500.0, 300.0, 500.0);
|
||||
let anchors = json!([{"x": 400, "y": 500, "out": [450, 400]}, {"x": 600, "y": 500, "in": [550, 400]}]);
|
||||
let curve = NodeId(s.execute("path.create", &json!({"anchors": anchors})).unwrap()["id"].as_u64().unwrap());
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
s.select_tool("pen", v).unwrap();
|
||||
for x in [100.0, 200.0, 300.0] {
|
||||
gesture(&mut s, &[(x, 100.0)], none);
|
||||
}
|
||||
let id = s.doc().unwrap().selection.objects[0];
|
||||
let points = |s: &Session, id| path(s, id).subpaths[0].anchors.iter().map(|a| (a.p.x, a.p.y)).collect::<Vec<_>>();
|
||||
assert_eq!(gesture(&mut s, &[(200.0, 100.0), (205.0, 120.0), (210.0, 140.0)], cmd), 1);
|
||||
assert_eq!(points(&s, id), [(100.0, 100.0), (210.0, 140.0), (300.0, 100.0)]);
|
||||
assert_eq!(s.tool_id(), "pen");
|
||||
let pen = s.cursor(Point::new(700.0, 300.0), none, v);
|
||||
assert!(matches!(pen, vectorcraft_tools::Cursor::Pen | vectorcraft_tools::Cursor::PenDrawing), "{pen:?}");
|
||||
// The next click goes on drawing the same path from its end.
|
||||
gesture(&mut s, &[(400.0, 200.0)], none);
|
||||
assert_eq!(paths(&s).len(), 3, "no new path");
|
||||
assert_eq!(path(&s, id).subpaths[0].anchors.len(), 4);
|
||||
assert_eq!(gesture(&mut s, &[(200.0, 500.0), (200.0, 510.0), (200.0, 530.0)], cmd), 1);
|
||||
assert_eq!(points(&s, straight), [(100.0, 530.0), (300.0, 530.0)]);
|
||||
let before = path(&s, curve).subpaths[0].anchors.clone();
|
||||
assert_eq!(gesture(&mut s, &[(500.0, 425.0), (500.0, 410.0), (500.0, 380.0)], cmd), 1);
|
||||
let after = &path(&s, curve).subpaths[0].anchors;
|
||||
assert_eq!((after[0].p, after[1].p), (before[0].p, before[1].p), "the ends stay");
|
||||
assert!(after[0].h_out.y < before[0].h_out.y && after[1].h_in.y < before[1].h_in.y, "{after:?}");
|
||||
// A Cmd-click away from the art deselects: the path is done, the next click starts another.
|
||||
gesture(&mut s, &[(700.0, 300.0)], cmd);
|
||||
assert!(s.doc().unwrap().selection.is_empty());
|
||||
gesture(&mut s, &[(700.0, 50.0)], none);
|
||||
assert_eq!(paths(&s).len(), 4);
|
||||
// Selection used last, a flat path being drawn: a Cmd drag where its box's top handle would be moves it.
|
||||
s.select_tool("selection", v).unwrap();
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
s.select_tool("pen", v).unwrap();
|
||||
gesture(&mut s, &[(100.0, 300.0)], none);
|
||||
gesture(&mut s, &[(300.0, 300.0)], none);
|
||||
let flat = s.doc().unwrap().selection.objects[0];
|
||||
assert_ne!(s.cursor(Point::new(200.0, 300.0), cmd, v), vectorcraft_tools::Cursor::ResizeV);
|
||||
assert_eq!(gesture(&mut s, &[(200.0, 300.0), (200.0, 320.0), (200.0, 340.0)], cmd), 1);
|
||||
assert_eq!(points(&s, flat), [(100.0, 340.0), (300.0, 340.0)]);
|
||||
assert_eq!(s.tool_id(), "pen");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmd_alt_lends_group_selection_while_direct_selection_is_active() {
|
||||
let (cmd, cmd_alt) = (Mods { cmd: true, ..Mods::default() }, Mods { cmd: true, alt: true, ..Mods::default() });
|
||||
|
||||
@@ -427,6 +427,13 @@ pub fn selection_box(cx: &ToolContext) -> Option<OrientedBox> {
|
||||
remembered(cx, |m| &mut m.oriented, || vectorcraft_effects::preview::oriented_bounds(cx.doc, &cx.selection.objects, cx.preview_bounds))
|
||||
}
|
||||
|
||||
/// [`selection_box`] while the canvas shows it (View › Show Bounding Box, and no anchors selected
|
||||
/// on their own): its handles take a press only then. A path the Pen is drawing keeps its end
|
||||
/// anchor selected, so Cmd/Ctrl lending the Selection tool doesn't scale it by hidden handles (#4980).
|
||||
fn shown_box(cx: &ToolContext) -> Option<OrientedBox> {
|
||||
(cx.show_bbox && cx.selection.anchors.is_empty()).then(|| selection_box(cx)).flatten()
|
||||
}
|
||||
|
||||
/// The bounds of object `id`, measured as [`selection_bounds`] measures the selection.
|
||||
fn object_bounds(cx: &ToolContext, id: NodeId) -> Option<Rect> {
|
||||
remembered(cx, |m| m.by_object.entry(id).or_insert(None), || vectorcraft_effects::preview::bounds_of(cx.doc, &[id], cx.preview_bounds))
|
||||
@@ -460,7 +467,7 @@ fn box_hit(cx: &ToolContext, b: &OrientedBox, p: Point) -> Option<BoxHit> {
|
||||
/// Whether the current selection's bounding-box handle is under `p`, using the same geometry as
|
||||
/// the Selection tool's press handling.
|
||||
pub(crate) fn bbox_handle_at(cx: &ToolContext, p: Point) -> bool {
|
||||
cx.show_bbox && selection_box(cx).is_some_and(|b| matches!(box_hit(cx, &b, p), Some(BoxHit::Handle(_))))
|
||||
shown_box(cx).is_some_and(|b| matches!(box_hit(cx, &b, p), Some(BoxHit::Handle(_))))
|
||||
}
|
||||
|
||||
/// How near (screen points) the pointer must come to an object's centre or anchor for Smart Guides
|
||||
@@ -933,9 +940,7 @@ impl Tool for SelectionTool {
|
||||
if typewidget::over_widget(cx, p) {
|
||||
return Cursor::TypeWidget;
|
||||
}
|
||||
if cx.show_bbox
|
||||
&& let Some(bx) = selection_box(cx)
|
||||
{
|
||||
if let Some(bx) = shown_box(cx) {
|
||||
match box_hit(cx, &bx, p) {
|
||||
Some(BoxHit::Handle(h)) => return handle_cursor(h, bx.angle - cx.view_angle()),
|
||||
Some(BoxHit::Rotate) => return Cursor::rotate_about_in_view(bx.center(), p, cx.view_angle()),
|
||||
@@ -1205,9 +1210,7 @@ impl SelectionTool {
|
||||
self.state = State::Idle;
|
||||
return Some(vec![]);
|
||||
}
|
||||
if cx.show_bbox
|
||||
&& let Some(bx) = selection_box(cx)
|
||||
{
|
||||
if let Some(bx) = shown_box(cx) {
|
||||
match box_hit(cx, &bx, p) {
|
||||
Some(BoxHit::Handle(handle)) => {
|
||||
let is_area = |id: &NodeId| cx.doc.node(*id).is_some_and(is_area_type);
|
||||
|
||||
@@ -571,6 +571,28 @@ fn turned_cursors_step_with_the_view() {
|
||||
assert_eq!(Cursor::rotate_about_in_view(Point::ORIGIN, Point::ORIGIN, 90.0), Cursor::Rotate, "on the center");
|
||||
}
|
||||
|
||||
/// #4980: with anchors selected on their own (the end anchor of a path the Pen is drawing, when
|
||||
/// Cmd/Ctrl lends the Selection tool) the canvas shows no bounding box, so its handles neither
|
||||
/// offer a resize pointer nor scale on a press: the press takes the path, as away from the box.
|
||||
#[test]
|
||||
fn hidden_box_handles_take_no_press_while_anchors_are_selected() {
|
||||
let (d, id) = doc_with_rect();
|
||||
let p = paint();
|
||||
let top_middle = Point::new(150.0, 100.0);
|
||||
let mut s = Selection::default();
|
||||
s.add(id);
|
||||
let mut t = SelectionTool::default();
|
||||
assert_eq!(t.cursor(&cx(&d, &s, &p), top_middle, Mods::default()), Cursor::ResizeV, "the shown box's handle");
|
||||
assert_eq!(t.pointer(&cx(&d, &s, &p), &ev(PointerKind::Down, 150.0, 100.0)), vec![Action::Begin("Scale".into())]);
|
||||
s.anchors.entry(id).or_default().insert((0, 1));
|
||||
let c = cx(&d, &s, &p);
|
||||
let mut t = SelectionTool::default();
|
||||
assert_eq!(t.cursor(&c, top_middle, Mods::default()), Cursor::Move);
|
||||
let down = t.pointer(&c, &ev(PointerKind::Down, 150.0, 100.0));
|
||||
assert!(!down.contains(&Action::Begin("Scale".into())), "{down:?}");
|
||||
assert!(!crate::select::bbox_handle_at(&c, top_middle));
|
||||
}
|
||||
|
||||
/// In a rotated view the resize and rotate cursors turn with the view.
|
||||
#[test]
|
||||
fn box_cursors_turn_with_the_view() {
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
# UI and interaction parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (dynamic symbols, #4938) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Pen: Cmd/Ctrl drags with the lent selection tool, not hidden box handles, #4980) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
## Feature submission evidence
|
||||
|
||||
@@ -78,7 +78,7 @@ tools, never checked side by side) isn't captured row by row.
|
||||
| Click the first anchor to close; the drag shapes the closing curve | done | |
|
||||
| Click the last anchor to retract its outgoing handle; drag from it to pull a new one | done | |
|
||||
| Alt over a handle, anchor or segment acts as the Anchor Point tool | done | |
|
||||
| Cmd/Ctrl lends the last-used selection tool | done | the path being drawn continues afterwards |
|
||||
| Cmd/Ctrl lends the last-used selection tool | done | the path being drawn continues afterwards; with Direct Selection (the default until a selection tool is chosen) a Cmd drag moves an anchor of the path being drawn, moves a straight segment or bends a curved one, one undo step each, and a Cmd-click away from art ends the path; with Selection the hidden bounding box of the path being drawn takes no press, so the drag moves the path (#4980) |
|
||||
| Click an open path's end to continue it; click another path's end to join | done | #776 |
|
||||
| Rubber Band preview | done | Preferences option |
|
||||
| Auto Add/Delete on a selected path; Shift or the preference disables it | done | |
|
||||
@@ -387,6 +387,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-01 | minor | Pen with Cmd/Ctrl held: the lent Selection tool no longer scales the path being drawn by bounding-box handles the canvas hides while its end anchor is selected (a flat path did nothing under a resize pointer); a Cmd drag moves the path instead, and with Direct Selection moves anchors and segments, one undo step each (#4980); scores and estimates unchanged |
|
||||
| 2026-11-01 | minor | Dynamic symbols (#4938): Symbol Options › Symbol Type; per-instance appearance for objects of a dynamic symbol's art; Reset in the Properties panel and the Symbols panel menu; on-canvas Direct Selection of the parts remains |
|
||||
| 2026-11-01 | minor | Export for Screens has Include Artboard Fill beside Include Bleed (on by default, remembered; `includeArtboardFill`): the files draw each artboard's background color behind its art, as exports and print now do, and the Print dialog's preview shows it (#4937); scores and estimates unchanged |
|
||||
| 2026-11-01 | minor | Select › Start Global Edit on a group takes the groups like it (the same objects at the same places, moved or scaled; Appearance, Size and the artboard options apply) and gives an edit made to an object inside one to the object at its place in each of the others, in proportion, as one undo step (#4939). Scores and estimates unchanged |
|
||||
|
||||
Reference in New Issue
Block a user