Skip to content

feat(image-editor): Pen tool — vector paths with anchors and bezier handles#46

Merged
lyfuci merged 1 commit intomainfrom
feat/image-editor-pen-tool
Apr 25, 2026
Merged

feat(image-editor): Pen tool — vector paths with anchors and bezier handles#46
lyfuci merged 1 commit intomainfrom
feat/image-editor-pen-tool

Conversation

@lyfuci
Copy link
Copy Markdown
Owner

@lyfuci lyfuci commented Apr 25, 2026

Summary

PS-aligned Pen tool, end-to-end:

  • Click adds a corner anchor (no handles → straight segment).
  • Click + drag turns the new anchor into a smooth one with symmetric handles (the drag delta from the anchor sets `hout`; `hin` mirrors).
  • Click near the first anchor (within 8 px, ≥2 anchors total) closes the path.
  • Enter commits the current path open.
  • Esc cancels the in-progress path.

Result is a new annotation layer with a `path` shape — open paths render as a stroked curve; closed paths additionally support fill (the type carries `fill?`; the v1 OptionsBar exposes stroke + colour only — the fill swatch is a follow-up).

State + render layer

  • types.ts: new `PathAnchor` (with relative `hin` / `hout` offsets) and `PathShape` join the Shape union. Persists through .json round-trip via the existing tagged-variant serialization.
  • drawing.ts: `drawPath` walks anchors emitting `bezierCurveTo` / `quadraticCurveTo` / `lineTo` segments based on which handles each segment has on either side; closed paths get a final last→first segment.
  • hit.ts: bbox covers anchor positions plus handle endpoints (rough bound — exact bezier extents are non-trivial; this is fine for layer picking). No corner handles in v1 — selection chrome shows the bbox.
  • transform.ts: translate shifts anchor positions; relative handle offsets are unaffected.
  • LayersPanel.tsx: `layerLabelKey` gains a `path` branch.

Canvas integration

  • New `pen-drawing` interaction kind: `{ anchors, pressed, cursor }`.
  • `mousedown` handles close-vs-append and sets `pressed=true`.
  • `mousemove` with `pressed` updates the last anchor's symmetric handles from the drag delta. Without press, just tracks cursor for the rubber-band preview.
  • `mouseup` releases `pressed`; anchors stay until close / Enter / Esc.
  • `CanvasHandle` gains `hasPendingPen` / `commitPendingPen` / `cancelPendingPen`, mirroring the existing crop pair so Enter / Esc in the parent's keyboard handler route to the right action.
  • `drawPenPreview` overlay: orange first-anchor square (close target), white squares for the rest; blue handle tethers + dots; dashed rubber-band line / curve from the last anchor to the cursor.

Other wiring

  • ToolsPalette: pen entry no longer carries `stub: true`.
  • tool-meta: `pen` removed from STUB_TOOLS — only the three sample-pixel tools (`spotHeal` / `stamp` / `historyBrush`) remain.
  • OptionsBar: pen variant — stroke width + colour + hint text.
  • i18n: `penHint` (en + zh-CN) and `annoLabel.path`.

Built on top of PR #45

This branch was cut from `feat/image-editor-note-frame-pathsel` (PR #45) since both tools touch the same files. Once #45 merges, this PR will be a clean fast-forward; if any conflicts arise on rebase, they'll be in the shape switch + STUB_TOOLS — easy to resolve.

Test plan

  • P key → palette shows Pen active, cursor → crosshair.
  • Click 3+ corners → straight-line path appears live; press Enter → committed as open Path layer.
  • Click + drag → smooth anchor with visible blue handles; subsequent clicks continue.
  • Click near the first orange-square anchor → path closes, layer named "Closed Path".
  • Esc mid-path → in-progress curve disappears; no layer committed.
  • Save .json → reload — open / closed path round-trips correctly.
  • Move tool: drag a path layer — anchors translate together; handles stay relative.
  • Crop applied → draw a path on the cropped image — coords align (no offset bug).

Stub palette status after this PR

Before: spotHeal, stamp, historyBrush, pen
After:  spotHeal, stamp, historyBrush

Final PR D (sample-pixel family — Spot Heal + Clone Stamp + History Brush) closes out the palette.

🤖 Generated with Claude Code

…ier handles

PS-aligned Pen tool, end-to-end:

- **Click** adds a corner anchor (no handles → straight segment to it).
- **Click + drag** turns the new anchor into a smooth one with symmetric
  handles (the drag delta from the anchor sets `hout`; `hin` mirrors).
- **Click near the first anchor** (within 8 px, ≥2 anchors total)
  closes the path.
- **Enter** commits the current path open.
- **Esc** cancels the in-progress path.

The committed result is a new annotation layer with a `path` shape,
move-only (per-anchor editing arrives in a follow-up). Both open and
closed paths render via `bezierCurveTo` / `quadraticCurveTo` /
`lineTo` based on which handles each segment has on either side; closed
paths can be filled (the type supports it; v1 UI exposes stroke only —
fill is a future OptionsBar bump).

State + render layer:

- types.ts: new `PathAnchor` (with relative `hin` / `hout` offsets) and
  `PathShape` join the Shape union. Persists through .json round-trip
  via the existing tagged-variant serialization.
- drawing.ts: `drawPath` walks anchors emitting bezier / quadratic /
  line segments; closed paths get an additional last→first segment.
- hit.ts: bbox covers anchor positions plus handle endpoints (rough
  bound — exact bezier extents are non-trivial; this is fine for layer
  picking). No corner handles in v1 — selection chrome shows the bbox.
- transform.ts: translate shifts anchor positions; relative handle
  offsets are unaffected.
- LayersPanel.tsx: layerLabelKey gains a `path` branch.

Canvas integration:

- New `pen-drawing` interaction kind (anchors / pressed / cursor).
- mousedown handles close-vs-append; sets `pressed=true`.
- mousemove with `pressed` updates the last anchor's symmetric handles
  from the drag delta. Without press, just tracks cursor for the
  rubber-band preview.
- mouseup releases `pressed`; anchors stay until close / Enter / Esc.
- `CanvasHandle` gains `hasPendingPen` / `commitPendingPen` /
  `cancelPendingPen`, mirroring the existing crop pair so Enter / Esc
  in the parent's keyboard handler route to the right action.
- `drawPenPreview` overlay: orange first-anchor square (close target),
  white squares for the rest; blue handle tethers + dots; dashed
  rubber-band line / curve from the last anchor to the cursor.

Other wiring:

- ToolsPalette: pen entry no longer carries `stub: true`.
- tool-meta: `pen` removed from STUB_TOOLS; only the three sample-
  pixel tools (spotHeal / stamp / historyBrush) remain.
- OptionsBar: pen variant — stroke width + colour + the same hint text
  the keyboard handler honours.
- i18n: `penHint` (en + zh-CN) and `annoLabel.path`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lyfuci lyfuci merged commit dab9c31 into main Apr 25, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant