diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index f6491d757..dfd50ae72 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -930,7 +930,9 @@ fn without_inner(doc: &Document, placed: Vec<(NodeId, Option>)>) -> i /// The direct-selection flag and resolved root nodes for Copy and Cut operations. fn copy_cut_targets(s: &Session) -> Result<(bool, Vec)> { let st = s.doc()?; - let in_drawing_tool = matches!(s.tool_id(), "pen" | "curvature"); + // The Pen and the Curvature tool leave the anchor they placed or edited selected: Copy takes + // the whole path then (#2122), unless a selection tool Cmd lent them picked the anchors (#6123). + let in_drawing_tool = matches!(s.tool_id(), "pen" | "curvature") && s.lent_selection.as_ref() != Some(&st.selection); let direct = !in_drawing_tool && (!st.selection.anchors.is_empty() || !st.selection.segments.is_empty()); let in_isolated_compound = st.isolation.is_some_and(|iso| st.doc.node(iso).is_some_and(|n| matches!(n.kind, NodeKind::Compound { .. }))); let roots = roots_within(&st.doc, st.selection.in_paint_order(&st.doc), |_compound, member| { diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index dc19b9edb..2200b7990 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -62,6 +62,10 @@ pub struct Session { /// While Cmd lends `tool` (a selection tool) for a drag: the tool it was lent to, which comes /// back as it was at the release ([`Session::pointer`]). pub(crate) lender: Option>, + /// The selection the lent tool's gesture left as it gave `lender` back: while it is still the + /// selection, the anchors in it are ones picked, not the Pen's last anchor, and Copy and Cut take + /// them with the Pen or Curvature tool back too (#6123). + pub(crate) lent_selection: Option, /// The selection tool chosen last (Selection, Direct Selection or Group Selection): the one Cmd /// lends the other tools. None until one is chosen. pub(crate) last_selection_tool: Option<&'static str>, diff --git a/crates/engine/src/session/documents.rs b/crates/engine/src/session/documents.rs index ab258887f..cee5040f4 100644 --- a/crates/engine/src/session/documents.rs +++ b/crates/engine/src/session/documents.rs @@ -42,6 +42,7 @@ impl Session { journal: Journal::default(), tool: vectorcraft_tools::create("selection"), lender: None, + lent_selection: None, last_selection_tool: None, last_view: ViewInfo::default(), preview_memo: Default::default(), @@ -162,6 +163,7 @@ impl Session { } self.keep_tool_settings(); self.tool = self.make_tool(self.tool.id()); + self.lent_selection = None; self.tool_resets = self.tool_resets.wrapping_add(1); self.tool_set_since_reset = false; } diff --git a/crates/engine/src/tests/draw2.rs b/crates/engine/src/tests/draw2.rs index 1af4ac40f..13cdefbdf 100644 --- a/crates/engine/src/tests/draw2.rs +++ b/crates/engine/src/tests/draw2.rs @@ -2673,6 +2673,44 @@ fn pen_with_cmd_borrows_direct_selection_and_goes_on_drawing() { assert_eq!(s.tool_id(), "rectangle"); } +/// #6123: anchors that Cmd (Ctrl) held with the Pen selects with the Direct Selection it lends are +/// what Copy and Cut take, as with Direct Selection chosen; the last anchor the Pen leaves selected +/// while drawing is not, Copy taking the whole path (#2122). +#[test] +fn pen_cmd_selected_anchors_are_what_copy_and_cut_take() { + let (none, cmd) = (Mods::default(), Mods { cmd: true, ..Mods::default() }); + let v = view(); + let mut s = session(); + s.select_tool("pen", v).unwrap(); + for x in [100.0, 200.0, 300.0, 400.0] { + gesture(&mut s, &[(x, 300.0)], none); + } + let id = s.doc().unwrap().selection.objects[0]; + let pasted = |s: &mut Session| { + let ids = s.execute("edit.paste", &json!({})).unwrap()["ids"].clone(); + let ids: Vec = ids.as_array().unwrap().iter().map(|i| NodeId(i.as_u64().unwrap())).collect(); + ids.iter().map(|i| path(s, *i).subpaths[0].anchors.len()).collect::>() + }; + // A Cmd marquee around the middle two anchors: Copy takes them and the segment between. + gesture(&mut s, &[(150.0, 250.0), (350.0, 350.0)], cmd); + assert_eq!(s.tool_id(), "pen"); + assert_eq!(s.doc().unwrap().selection.anchors[&id], [(0, 1), (0, 2)].into()); + s.execute("edit.copy", &json!({})).unwrap(); + assert_eq!(pasted(&mut s), [2]); + // Cut takes the segment between them out of the path, as with Direct Selection chosen. + s.execute("edit.undo", &json!({})).unwrap(); + s.execute("edit.cut", &json!({})).unwrap(); + let left = path(&s, id).subpaths.iter().map(|sp| sp.anchors.len()).collect::>(); + assert_eq!((s.clipboard.nodes.len(), left), (1, vec![2, 2])); + // Drawing on: the last anchor the Pen leaves selected doesn't make Copy take that anchor. + s.execute("edit.undo", &json!({})).unwrap(); + s.execute("select.set", &json!({"ids": [id.0]})).unwrap(); + gesture(&mut s, &[(500.0, 300.0)], none); + assert_eq!(s.doc().unwrap().selection.anchors[&id], [(0, 4)].into()); + s.execute("edit.copy", &json!({})).unwrap(); + assert_eq!(pasted(&mut s), [5]); +} + /// #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 diff --git a/crates/engine/src/tooling.rs b/crates/engine/src/tooling.rs index 8ecc738f2..9df450e1b 100644 --- a/crates/engine/src/tooling.rs +++ b/crates/engine/src/tooling.rs @@ -232,6 +232,7 @@ impl Session { let r = self.apply_actions(acts); self.keep_tool_settings(); self.tool = lender; + self.lent_selection = self.active().map(|d| d.selection.clone()); self.after_command(None); r.map(drop) } diff --git a/docs/ui-parity.md b/docs/ui-parity.md index bc19d68c7..30ed2ed66 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-07 · **Change:** minor (Live Corners: with every corner selected, a Shift-click on a corner widget picks that corner, #5188; 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; shape tools: a press on the selection's centre point moves it and one in the rotate zone outside a box corner turns it, #5477; 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-07 · **Change:** minor (Pen with Cmd/Ctrl held: Copy and Cut take the anchors the lent Direct Selection picked, #6123; 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; shape tools: a press on the selection's centre point moves it and one in the rotate zone outside a box corner turns it, #5477; 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 @@ -131,7 +131,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; 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) | +| 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); anchors or segments the lent Direct Selection picks are what Copy and Cut take, as with Direct Selection chosen, while Copy with the Pen's own last anchor selected takes the whole path (#6123) | | 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 | | @@ -480,6 +480,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development | Date | Change | Summary | |---|---|---| +| 2026-11-07 | minor | Pen and Curvature tool with Cmd/Ctrl held: Edit › Copy and Cut take the anchors and segments the lent Direct Selection picked, as with Direct Selection chosen, not the whole path; with the Pen's own last anchor selected Copy still takes the whole path (#6123); scores and estimates unchanged | | 2026-11-07 | minor | Live Corners: with Direct Selection, while every corner of a path is selected, a Shift-click on a corner widget picks that corner alone instead of deselecting it (#5188); adding a hidden corner's widget by Shift-click stays open (#1232); scores and estimates unchanged | | 2026-11-06 | minor | Keyboard shortcuts: Cmd/Ctrl+Alt+. and Cmd/Ctrl+Alt+, (Stroke Weight Up and Down) step each selected stroke's weight, or the next object drawn's (#5797); scores and estimates unchanged | | 2026-11-06 | minor | Properties: Quick Actions has Repeat Options for one selected radial, grid or mirror repeat, opening Object › Repeat › Options with its values (#5181); scores and estimates unchanged |