Fix #494: Pen + Cmd edits the path being drawn; Pen + Alt reshapes segments; handle drags fixed and fast

Reported: with the Pen there was no way to edit curves in place as Ctrl does in Illustrator on
Windows (Cmd on the Mac), segments couldn't be grabbed, and the curve-editing tools felt slow and
sometimes didn't take a handle.

- Cmd/Ctrl held at a press with any tool but a selection tool now borrows the selection tool in the
  engine (Session::pointer), not in the canvas: the selection tool chosen last, or Direct Selection
  for the Pen, Curvature and anchor tools until one is chosen. The tool lent to isn't deactivated,
  so the Pen goes on drawing its path after the release while that path stays selected; it no
  longer draws on another open path selected meanwhile. Agents reach it with pointer_gesture
  mods.cmd. With Cmd held the pointer is the borrowed tool's. Choosing a tool or switching
  documents mid-gesture gives the tool back first.
- Pen + Alt over a segment of a selected path reshapes it, as the Anchor Point tool does.
- The handles shown are the handles that drag (one helper for Direct Selection, the Anchor Point
  tool and Pen + Alt): Direct Selection now drags the handles of a path selected as a whole (as
  the Pen leaves it, so Cmd + drag of a handle while drawing works), the Anchor Point tool no
  longer grabs handles that Show handles when multiple anchors are selected hides, the nearest
  handle wins, and a handle shorter than the tolerance leaves its anchor to be picked. The anchor
  tools pick within Selection & Anchor Display > Tolerance instead of a fixed 4 px.
- path.reshapeSegment keeps a smooth anchor at either end smooth (its other handle turns with the
  moved one) instead of kinking it.
- A dragged handle gathered its smart guide targets from the whole document on every pointer move
  (21 ms per move on 20,000 paths): HandleSnap now gathers them once per drag (1 ms).
  vectorcraft-cli perf measures handle and segment drags on a 5,000-anchor path.
- catalog::is_selection_tool and catalog::edits_anchors replace the UI's own lists.
- Tests in tools, engine and MCP (headless pointer_gesture); docs/mcp.md, control-protocol.md,
  ROADMAP.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic
2026-10-08 22:00:13 +07:00
co-authored by Claude Opus 5.5
parent d12d5195da
commit aefd961bbd
21 changed files with 578 additions and 180 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ so only enable it while you use it. Transport: `apps/vectorcraft/src/control_ser
| `ui.menu.list` / `ui.menu.invoke` | `{command, params}` | the full menu tree / invoke an item |
| `ui.contextMenu.list` | | the canvas context menu for the current selection, flattened like `ui.menu.list` (`path` holds its submenus). `ui.click {x, y, button: "right"}` on the canvas opens it, after selecting the object there unless it is already selected |
| `ui.tool.select` / `ui.tool.list` | `{tool}` | |
| `ui.pointer` | `{events:[{kind: down|drag|up|move|doubleclick, x, y, space?: "doc"|"screen", mods?}]}` | drive the active tool exactly like the mouse |
| `ui.pointer` | `{events:[{kind: down|drag|up|move|doubleclick, x, y, space?: "doc"|"screen", mods?}]}` | drive the active tool exactly like the mouse: `mods.cmd` held at a press with any tool but a selection tool drags with the selection tool chosen last (Direct Selection with the Pen until one is), and the release gives the tool back as it was |
| `ui.key` / `ui.text` | `{key, shift?, alt?, cmd?}` / `{text}` | synthetic keyboard input |
| `ui.wheel` | `{x, y, dy?, dx?, unit?: "line"\|"point", shift?, alt?, cmd?}` | a mouse wheel turn over screen point (x, y): `dy` notches up (+) or down, `dx` sideways. Over the canvas the wheel scrolls and Cmd- or Alt-wheel (Option on the Mac) zooms about the pointer; with the `zoomWithMouseWheel` preference the wheel and Alt-wheel zoom about the pointer, Shift-wheel scrolls up and down and Cmd-wheel (Ctrl on Windows and Linux) sideways. Over a focused numeric field (click it first) each notch steps its value as Up/Down do (Shift: ten, Cmd/Ctrl: a tenth) and the panel stays put; over anything else in a panel the wheel scrolls it |
| `ui.set` | `{brightness?, panel?, rulers?, outline?, grid?, smartGuides?, boundingBox?, controlBar?}` | |
+20 -5
View File
@@ -1049,7 +1049,20 @@ on top of each other, and an open path becomes one path per piece. Each cut leav
so `path.moveAnchors {dx, dy}` (or a Direct Selection drag) pulls the path apart there; `path.join {}` (Connect
Selected End Points) joins the ends again. `path.convertAnchor {id, subpath?, anchor, to, x?, y?}` and
`path.split {id, subpath?, anchor}` do the same to one anchor (the Anchor Point and Scissors tools); the Pen with Alt
held over a selected path's handle or anchor works as the Anchor Point tool.
held over a selected path's handle, anchor or segment works as the Anchor Point tool. `path.reshapeSegment {id,
subpath?, segment, t, dx, dy}` (a segment dragged with Direct Selection or the Anchor Point tool) moves the segment's
point at `t` by `dx`, `dy`: a smooth anchor at either end stays smooth, its other handle turning with the moved one.
A `pointer_gesture` with `mods: {cmd: true}` held at the press, with any tool but the selection tools (Command on
macOS, Ctrl elsewhere), drags with the selection tool chosen last (`select_tool`), or Direct Selection with the Pen,
Curvature and anchor tools until one is chosen; the release gives the tool back as it was, so the Pen goes on drawing
the path it had begun while that stays selected. Its reply's `tool` is the tool given back.
```json
{"name":"pointer_gesture","arguments":{"tool":"pen","events":[{"kind":"down","x":100,"y":300},{"kind":"up","x":100,"y":300},{"kind":"down","x":200,"y":300},{"kind":"drag","x":250,"y":300},{"kind":"up","x":250,"y":300}]}}
{"name":"pointer_gesture","arguments":{"mods":{"cmd":true},"events":[{"kind":"down","x":250,"y":300},{"kind":"drag","x":250,"y":260},{"kind":"up","x":250,"y":260}]}}
{"name":"pointer_gesture","arguments":{"events":[{"kind":"down","x":300,"y":350},{"kind":"up","x":300,"y":350}]}}
```
```json
{"name":"run_command","arguments":{"command":"select.anchors","params":{"id":12,"anchors":[[0,2]]}}}
@@ -1153,8 +1166,9 @@ Effects off a 100 pt wide rectangle with a 10 pt stroke set to `width: 220` gets
The Selection & Anchor Display and General preferences apply to `pointer_gesture` as they do to the mouse:
- `selectionTolerance` (1–8 px, 3 by default): how near a click must be to a path to pick it, and to an anchor or
handle for Direct Selection.
- `selectionTolerance` (1–8 px, 3 by default): how near a click must be to a path to pick it, and to an anchor,
handle or segment for Direct Selection, the Anchor Point, Add and Delete Anchor Point and Scissors tools, and the
Pen with Alt. A handle shorter than that leaves its anchor to be picked when pressed nearer the anchor.
- `objectSelectionByPathOnly`: a click inside a filled path or compound path doesn't select it, one on its path does.
- `ctrlClickSelectsBehind` (on by default): a Selection tool click with `mods: {cmd: true}` (Command on macOS, Ctrl
elsewhere) selects the object under the selected one there, the next such click the one under that, then the
@@ -1168,8 +1182,9 @@ The Selection & Anchor Display and General preferences apply to `pointer_gesture
hidden art with the artboard too; off (the default) it stays where it is.
- `penRubberBand`, `curvatureRubberBand` (on by default): off, the Pen and Curvature tools draw no preview segment to
the pointer.
- `showHandlesMultipleAnchors` (on by default): off, Direct Selection shows and drags direction handles only while a
single anchor is selected. `handleStyle` (`solid`, `hollow`, `large`) draws their ends (desktop app).
- `showHandlesMultipleAnchors` (on by default): off, Direct Selection (and the Anchor Point tool, and the Pen with Alt)
shows and drags direction handles only while a single anchor is selected. On, the handles of the direct-selected
anchors, or of every anchor of a path selected as a whole, show and drag. `handleStyle` (`solid`, `hollow`, `large`) draws their ends (desktop app).
- `hideCornerWidgetAbove` (177° by default): corners wider than this show no Live Corners widget (a rectangle's right
angles hide below 90°).
- `transformPatternTiles` (off by default): the default of the transforms' `patterns` param (`object.transform`,