Files
Artstr-Studio/docs/PEN_TOOL_FEATURE.md
thePR0M3TH3AN 5b0eec1a1a docs: log the Illustrator parity arc + follow-ups
PEN_TOOL_FEATURE.md gets a 2026-06-02 status block documenting
everything that landed on top of Phase D — Direct Selection split,
auto-add/auto-delete, click-vs-drag convert, Alt-click curved ↔
pointed, multi-anchor select / marquee / arrow nudge, drag-segment
bow, continue-endpoint, spacebar reposition, Alt mid-draw, handle
break, Cmd+J / Cmd+Alt+J, Path Actions side panel — plus the
adjacent design-tool features that shipped in the same arc
(Eyedropper, Align & distribute, Free Transform rotation,
Pathfinder, marquee box-select, dive-into-edit on dbl-click).

TODO.md gets a "Design-tool follow-ups" section listing the gaps:
Smart Guides, Gradients + shadows, Scissors / Outline text /
Command palette, Flip H / V (needs renderer scaleX/scaleY),
Pathfinder rotation support, pixel-art frame timeline.
2026-06-02 09:53:06 -04:00

525 lines
25 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Pen & Pencil Tools
## Goals
Add freehand and Bézier path drawing directly on the canvas, plus an editor
tool-mode system with flanking toolbars to host it.
### Product goals
1. A **Pencil** tool for freehand strokes — markup, scribbles, hand-drawn
accents, tracing over placed artwork.
2. A **Pen** tool for precise Bézier paths — clean cut lines, custom
silhouettes, editable curves.
3. Pen/pencil output reuses the existing `shape` layer system, so a drawn
path is a first-class vector layer: solid / gradient fill, stroke,
opacity, transform, clip, undo, JSON round-trip, Nostr publish, and
PDF / JPEG export — all free.
4. A canvas tool-mode system with a left tool palette and a right
contextual options toolbar, styled to match the existing bottom zoom
toolbar.
### Implementation status — 2026-05-19
- **Phase A** — done. `_editorTool` UI state, left tool palette, right
contextual options toolbar, pen overlay element, V/P/B + Esc shortcuts.
- **Phase B** — done. Pencil freehand capture, raw polyline, open-path
shape-layer commit.
- **Phase C** — done. Pen click/drag anchors, rubber-band preview, close /
Enter / dbl-click / Esc, closed-fill-on-close, `shape.pen.nodes` stored.
- **Phase D** — done (close-out 2026-06-01). On top of the existing
drag-anchor / drag-handle path: click on a segment between anchors
inserts a new anchor via a de Casteljau cubic split (curve shape
preserved exactly); right-click or Backspace on a selected anchor
deletes it; Alt-click on an anchor toggles between **corner**
(handles move independently, square anchor glyph) and **smooth**
(handles stay mirrored across the anchor, circle anchor glyph);
dragging a node or handle past the original 0–100 viewBox refits
the layer's bounding rect + remaps coordinates so the path stays
visually anchored without clipping. The currently-selected anchor
renders with an amber stroke so Backspace-to-delete is unambiguous.
Rotated paths are still not node-editable in v1.
- **Phase E** — done. `layer.hidden` flag, eye toggle in the layer list, all
six renderers + Canvas2D export skip hidden layers.
- **Unified vector editing** — done. With the Pen tool active, the options
panel edits the *selected* shape's fill/stroke (any kind, not just pen
paths). A **"Make editable path"** button converts a selected primitive
(rect / rounded-rect / circle / ellipse / triangle / polygon / star /
line) or single-path uploaded SVG into a `tool:'pen'` editable path, after
which Phase D anchor editing applies. Primitives convert via exact node
generators; SVG `d` strings are parsed (M/L/H/V/C/S/Q/T/Z + elliptical
arcs, abs & rel). *Not convertible:* multi-element SVGs and compound
(multi-subpath) paths — the panel shows a hint instead.
### Implementation status — 2026-06-02 (Illustrator parity arc)
A subsequent arc brought the pen / select tools much closer to
Adobe Illustrator's feel. The tool-mode system from Phases A–E is
unchanged; the new work plugs into it:
- **Direct Selection (A)** as a separate tool from Select (V),
matching Illustrator's V/A split. Cmd / Ctrl held from any
non-pen tool temporarily flips the effective tool to Direct
Selection so a quick anchor tweak doesn't require dropping the
current tool.
- **Hover cursor cues**: − over an anchor in Pen mode (auto-delete),
+ over a segment (auto-add), the "continue" pointer over an
endpoint of an open path, grab over a segment in Direct Select
(bow the curve).
- **Pen auto-add / auto-delete**: clicking a segment with no
modifier inserts an anchor at the click point; clicking an
existing anchor deletes it. Drag (instead of click) on an anchor
converts it on the fly — pulls symmetric handles out. Alt-drag
pulls an asymmetric corner.
- **Convert anchor** (Alt-click in Pen or Direct Select): flips
curved ↔ pointed completely. Curved → pointed drops both handles
for a real sharp corner; pointed → curved grows symmetric handles
along the chord through the anchor (1/3 of chord length).
- **Shift constrains** pen segments to 0 / 45 / 90° while drawing
and previewing the next click.
- **Multi-anchor selection**: Shift-click anchors to add / remove
them from a selection Set. Drag a marquee inside the editable
path to box-select anchors. Arrow keys nudge every selected
anchor (1 px, 10 px with Shift) instead of the whole layer.
Backspace deletes the whole anchor selection at once.
- **Drag a segment** to bow the curve (Direct Select). Equal-split
delta across the two inner control points so the cursor tracks
the grabbed point; smooth anchors on either end keep their
handles mirrored.
- **Continue an open path**: with the Pen tool, click the start /
end anchor of an open path to resume drawing from there. The
source layer is hidden during the draft; on commit (Enter / close
/ Escape) the new path takes its place, preserving z-order,
target, fill and stroke.
- **Spacebar reposition** during pen create-drag: slides the anchor
itself instead of growing handles; release Space to resume
handle-pulling from the new position.
- **Alt mid-draw** during pen create-drag: only the outgoing handle
is pulled, incoming side stays at the anchor — asymmetric corner
for tangent-then-corner transitions without lifting the pen.
- **Alt-drag handle** in Direct Selection: breaks symmetry, the
opposite handle stays put.
- **Path keyboard ops**: Cmd+J closes an open path when both
endpoints are selected; Cmd+Alt+J averages selected anchors to
their centroid.
- **Path Actions side panel**: when Pen or Direct Selection is
active and a pen path is selected, a sidebar block surfaces the
same ops as buttons — Pointed corner, Curved smooth, Delete
anchor, Average, Join endpoints, Close / Open path, Reverse
direction. Buttons auto-disable when the action would be a no-op.
### Non-goals (v1)
- A raster paint layer (true pixel buffers). Explicitly out of scope — the
app's design-as-JSON model and Nostr-friendliness depend on storing
instructions, not bitmaps. A `paint` layer type could be a separate future
feature; this doc does not pursue it.
- Pressure / tilt / velocity-sensitive brushes.
- Moving the existing shape-primitive menu into the left toolbar. The
sidebar "+ Shape" menu stays; pen/pencil are additive.
### Adjacent design-tool features (shipped in the same arc)
- **Eyedropper (I)** — native EyeDropper picker, samples any pixel
on screen to the selected layer's fill (or stroke with Shift).
- **Align & distribute panel** — six align ops + horizontal /
vertical distribute on the layer selection (uses the selection
bbox for 2+, target bounds for a single layer).
- **Free Transform rotation handle** above both the single-layer
selection bbox and the multi-layer selection box; Shift snaps to
15°. Multi-layer rotates each layer's centre around the bbox
centre AND advances each layer's own rotation.
- **Pathfinder** — Unite / Subtract / Intersect / Exclude on 2+
selected shape layers, backed by `vendor/polygon-clipping.min.js`.
Curves are flattened to polygon approximations. Rotated layers
are not supported yet.
- **Marquee box-select** on empty canvas with the Select (V) tool —
marching-ants rectangle, Shift adds to the existing selection.
- **Double-click dive-into-edit**: a vector shape under the Select
tool flips to Direct Selection on dbl-click and auto-converts
primitives to editable pen paths so anchors show immediately.
---
## Why this fits the repo
The vector-shapes feature already shipped these, all of which a pen path
inherits with zero new renderer code:
- `shape.kind: 'path'` with a `d` string + `viewBox`, rendered on the editor
canvas, all five preview surfaces, and Canvas2D export.
- `fill`: `none` / `solid` / `linear-gradient` / `radial-gradient`.
- `stroke`: solid, width, dash.
- `clip`: `inline-shape` and `layer-ref` (one shape layer masks another).
- Layer transform (x/y/w/h/rotate/opacity), aspect-lock, lock toggle,
clipboard, and undo/redo.
So the pen tool is **a path-authoring UI feeding the existing shape model**.
The render/save/export side is essentially done. The new work is: the tool
mode system, the canvas pointer interaction, and an optional editable-node
representation.
### Current-state facts the plan is built on
- Schema version is **5**; this feature bumps it to **6** (additive only).
- Layer types today: `image`, `text`, `color`, `shape`, `qr`.
- Shape kinds: `rect`, `rounded-rect`, `circle`, `ellipse`, `triangle`,
`polygon`, `star`, `line`, `path`, `svg`.
- There is **no per-layer visibility flag** — only `locked`. This feature
adds `hidden` (needed for mask-source shapes).
- `shape.kind: 'path'` `d` data lives in its own `viewBox`; the renderer
already maps that viewBox to the layer box via a `<g transform>`. The pen
tool exploits this — it can emit `d` in any coordinate space as long as
`viewBox` matches.
- Layer drag is `startLayerDrag` on a layer wrapper's `pointerdown`. It
already does the screen-px → sheet-inch math the pen tool needs.
---
## Data model
### Pen/pencil output is a `shape` path layer
```js
{
id: 'layer-…',
type: 'shape',
name: 'Pen path',
target: 'cover-trim' | 'canvas' | …,
x, y, w, h, rotate, opacity, z, locked,
hidden: false, // NEW — see "Layer visibility" below
shape: {
kind: 'path',
d: 'M … C … Z', // canonical render/export path string
viewBox: { x, y, w, h }, // the d-string's coordinate space
pen: { // NEW — optional editor metadata
closed: true, // pen-closed path vs open pencil stroke
tool: 'pen' | 'pencil', // which tool authored it
nodes: [ // anchors with optional Bézier handles
{ x, y, hIn: { x, y } | null, hOut: { x, y } | null },
…
]
}
},
fill: { type: 'solid', color: '#…' }, // pen-closed default
// or fill: { type: 'none' } for pencil strokes
stroke: { type: 'solid', color: '#…', width, dash }
}
```
- `d` is the **canonical** value — render, export, and old clients use only
`d` + `viewBox`. Everything else is editor convenience.
- `shape.pen` is optional metadata. It lets the pen tool reconstruct
editable anchors later. A path imported via SVG upload won't have it, and
that's fine — it just isn't node-editable (consistent with today).
- When the user drags an anchor or handle, regenerate `d` from `nodes`.
### Why store `nodes` separately instead of parsing `d`
Parsing an arbitrary SVG path `d` string back into editable Bézier anchors
is fiddly and lossy (arcs, relative commands, shorthand). Keeping `nodes` as
the source of truth for pen-authored paths — and `d` as the derived render
string — sidesteps all of it. SVG-uploaded paths simply have no `nodes` and
stay non-node-editable.
### Layer visibility (`hidden`)
New optional boolean `layer.hidden`. When `true`:
- All six visual renderers and the Canvas2D export **skip** the layer.
- `clip: { kind: 'layer-ref' }` still resolves the hidden layer's geometry —
so a pen shape drawn purely as a mask can be hidden while still masking.
- The layer list shows an eye toggle.
- `hidden` is in the undo snapshot and the JSON payload (additive — old
clients ignore it and render the layer, which is acceptable).
This is small but touches every renderer; it ships as its own phase so it
can be verified independently.
---
## Tool-mode system + toolbars
### Editor tool state
New `state.editorTool: 'select' | 'pen' | 'pencil'`, default `'select'`.
- **select** — today's behavior exactly: click/drag layers, click to
select. Nothing changes.
- **pen** / **pencil** — canvas pointer events route to the drawing handler;
layer drag/select is suppressed while drawing.
- `editorTool` is **UI state, not design state** — it is NOT part of the
undo snapshot and NOT saved to JSON.
- `Esc` always returns to `select` (and cancels an in-progress path).
### Left toolbar — tool palette
A thin vertical strip floating on the **left edge** of the preview area
(absolute-positioned inside `previewShell`, same visual language as the
sticky bottom zoom toolbar). Visible in template modes **and** Disc
Designer mode.
```
┌──┐
│ ▸│ Select (V)
│ ✎│ Pen (P)
│ ✐│ Pencil (B)
└──┘
```
Each button sets `state.editorTool`. The active tool is highlighted.
Single-key shortcuts (V / P / B) when no input is focused.
### Right toolbar — contextual tool options
A floating panel on the **right edge** of the preview area, **visible only
when `editorTool !== 'select'`**. Shows the active tool's configuration:
- **Pencil options**: stroke color and stroke width. No smoothing control —
pencil strokes are intentionally raw polylines. A Done / Cancel pair.
- **Pen options**: fill on/off + fill color, stroke color + width,
"close path" toggle, and Finish / Cancel. While editing an existing
path's nodes: Add point / Delete point / toggle corner-smooth hints.
Defaults persist across drawings in a session (so you don't re-pick red
every stroke) but are NOT design state.
### Why floating toolbars, not sidebar panels
The user specifically wants Figma/Illustrator-style flanking toolbars, and
the bottom zoom toolbar already established the floating-over-canvas
pattern. The left/right bars reuse that CSS treatment (translucent panel,
rounded, subtle shadow). Keeps the sidebar focused on layer/project
controls.
---
## Pointer interaction
A dedicated **pen overlay** — an absolutely-positioned, full-bleed SVG layer
on top of the active canvas host — captures pointer events while a drawing
tool is active. It renders the in-progress path, anchors, and handles, and
on finish commits a real shape layer. When `editorTool === 'select'` the
overlay has `pointer-events: none` and is invisible.
The overlay attaches to whichever canvas host is active: `#sheet` in
template modes (cover / disc / jewel / customart) or `#designerDiscWrap` in
Disc Designer mode. The Disc Designer canvas is circular only as visual
framing — paths are still drawn in the disc's square bounding box, so the
pen tool works there with no special-casing beyond the coordinate scale.
All coordinates are captured in **canvas-inch space** by reusing the
screen-px → inch math from `startLayerDrag`, which already branches
`isDesignerLayer ? discTemplate.discD : sheetW()/sheetH()` and accounts for
the current zoom scale. Paths committed in Disc Designer mode get
`target: 'designer-disc'`; template-mode paths get the active sub-mode's
target.
### Pencil (freehand)
1. `pointerdown` on the overlay → start a stroke, push the first point.
2. `pointermove` → append points (minimum-distance filtered so a single
stroke doesn't capture thousands of near-identical points).
3. `pointerup` → finish:
- **No smoothing.** The stroke is a raw polyline through the captured
points — straight `L` segments, deliberately a little jaggy (the
"early-web" look the project wants). Light distance-decimation only,
to keep the `d` string a sane length; no Bézier fitting, no
Catmull-Rom, no smoothing slider.
- Compute the bounding box → that's the new layer's `x/y/w/h` (inches)
and the shape `viewBox`.
- Build the `d` string (open path: `M` + `L` segments, no `Z`).
- Commit a `shape` layer: `fill: { type: 'none' }`,
`stroke: { type: 'solid', … }`, `shape.pen = { closed: false,
tool: 'pencil', nodes }`.
- One `pushHistory('draw pencil stroke')` snapshot for the whole stroke.
4. Tool stays `pencil` so the user can keep drawing strokes.
### Pen (Bézier)
1. `pointerdown` → place an anchor. If it's a quick click → corner anchor
(no handles). If the user `pointermove`s before `pointerup` → drag out
symmetric Bézier handles (smooth anchor).
2. The overlay live-previews the path through the placed anchors plus a
"rubber-band" segment to the cursor.
3. Finish the path by any of: clicking the first anchor (closes it),
pressing `Enter` (open path) or double-click, or `Esc` (cancel).
4. On finish:
- Build `d` from `nodes` (`M` + `C`/`L` segments, `Z` if closed).
- bbox → layer `x/y/w/h` + `viewBox`.
- Commit a `shape` layer: closed → `fill: { type: 'solid', … }`;
open → `fill: { type: 'none' }` + stroke. `shape.pen = { closed,
tool: 'pen', nodes }`.
- One `pushHistory('draw pen path')` snapshot.
5. Tool stays `pen` for the next path.
### Editing an existing pen path
With the Pen tool active and a `shape` layer that has `shape.pen.nodes`
selected, the overlay shows the editable anchors + handles:
- Drag an anchor → move it; drag a handle → reshape the curve.
- Click an anchor to select; `Delete` removes it.
- Click a segment to insert an anchor.
- Alt-click an anchor toggles corner ↔ smooth.
- Every committed edit regenerates `d` and pushes one undo snapshot.
SVG-uploaded paths (no `nodes`) are not node-editable — selecting one with
the Pen tool shows a hint to that effect.
---
## Masking with pen shapes
Already supported by `clip: { kind: 'layer-ref', layerId }` — any layer can
point at a shape layer (including a pen path) and use its geometry as a
mask. The pen tool needs **no new mask code**; it just produces shape layers
that the existing clip system can reference.
What this feature adds for a clean mask workflow:
1. The `hidden` flag (above), so a pen shape used purely as a mask can be
hidden while still masking.
2. A convenience action — "Use as clip for selected layer" — in the pen
path's controls or a right-click affordance. Optional polish.
**Known caveat** (carried over from the vector-shapes design): a `layer-ref`
clip uses the referenced shape's path geometry normalized to the *clipped
layer's* bounding box — it is not yet a Photoshop-style absolute-canvas
mask. For v1 that's fine. A future `clip.mode: 'absolute'` could compute the
mask layer's transform relative to the clipped layer at render time; out of
scope here.
---
## Rendering & export
Essentially free. A committed pen/pencil layer is a `shape.kind: 'path'`
layer, so:
- `buildShapeSvgInner` already renders path kinds on all six surfaces.
- `drawShapeLayerToCanvas` already draws path kinds via `Path2D` for
PDF / JPEG export.
- Fill / gradient / stroke / clip / opacity / rotate all already apply.
The only renderer change in the whole feature is the `hidden` skip, which is
one early-`continue` per renderer.
---
## Phased delivery
### Phase A — Tool-mode system + toolbars
- `state.editorTool` + Esc-to-select.
- Left tool palette (Select / Pen / Pencil) floating on the canvas, with
V / P / B shortcuts.
- Right contextual options toolbar shell, shown only for non-select tools.
- The pen overlay element (inert for now).
- No drawing yet — just mode plumbing and the UI shell.
**Ship gate**: switching tools highlights the palette and shows/hides the
right toolbar; Esc returns to Select; nothing else regresses.
### Phase B — Pencil tool
- Freehand capture on the overlay; raw polyline, distance-decimation only,
no smoothing.
- Commit an open-path `shape` layer with stroke, no fill.
- Right toolbar: stroke color / width.
- One undo snapshot per stroke.
**Ship gate**: draw a stroke, it becomes a selectable shape layer that
round-trips through save/load and exports to PDF.
### Phase C — Pen tool (create)
- Click = corner anchor, click-drag = smooth anchor.
- Live rubber-band preview overlay.
- Close via first-anchor click; finish via Enter / double-click; cancel
via Esc.
- Commit a closed (fillable) or open `shape` layer; store `shape.pen.nodes`.
- Right toolbar: fill on/off + color, stroke, close-path.
**Ship gate**: draw a closed Bézier shape, fill it with a gradient using the
existing fill controls, export it.
### Phase D — Pen path editing
- Editable anchors/handles overlay for layers with `shape.pen.nodes`.
- Move / add / delete anchors; corner ↔ smooth toggle; regenerate `d`.
- One undo snapshot per committed edit.
**Ship gate**: re-open a pen path, drag an anchor, the path updates and
re-exports correctly.
### Phase E — Layer visibility + mask polish
- `layer.hidden` flag; eye toggle in the layer list; all renderers + export
skip hidden layers; `layer-ref` clips still resolve hidden geometry.
- "Use as clip for selected layer" convenience action.
**Ship gate**: a hidden pen shape masks an image and doesn't render itself;
toggling the eye shows it again.
---
## Risks & tradeoffs
| Risk | Mitigation |
|---|---|
| Tool-mode changes the canvas pointer contract — could regress layer drag/select | `select` mode keeps the exact current code path; pen/pencil only intercept when active. Heavy regression testing on Phase A. |
| Coordinate mapping under zoom / scroll | Reuse the proven screen-px → inch math from `startLayerDrag`; capture in inch space throughout. |
| Freehand strokes produce huge `d` strings | RDP simplification on `pointerup`; tune epsilon so visual fidelity holds while node count stays modest. |
| Pen path `d` ↔ `nodes` drift | `nodes` is the source of truth for pen-authored paths; `d` is always regenerated from `nodes`, never hand-edited. |
| `hidden` flag touches all 6 renderers + export | Isolated as Phase E; one-line skip per renderer; explicit acceptance test. |
| Old clients ignore `hidden` and render a mask-only shape | Acceptable — the design still reads; a future schemaVersion bump can formalize it. |
| Disc Designer uses a different canvas host + coordinate scale | Overlay attaches to `#designerDiscWrap`; coordinate capture branches on `isDesignerLayer` exactly like `startLayerDrag` already does. |
| Mask geometry is normalized-to-clipped-layer, not absolute | Documented caveat; `clip.mode: 'absolute'` is future work. |
| Two new floating toolbars crowd the canvas on small screens | Collapse/auto-hide on narrow viewports; the right toolbar is already conditional on tool mode. |
---
## Open questions
1. **Pencil smoothing** — DECIDED: no smoothing. Pencil strokes are raw
polylines (straight segments through captured points). Deliberately a
bit jaggy — matches the project's early-web aesthetic. No smoothing
slider, no Bézier fitting on freehand strokes.
2. **Pencil node-editability** — DECIDED: no. Only pen paths are
node-editable. A pencil stroke commits as a plain `kind: 'path'` layer
(no `shape.pen`); "Make editable path" still works on it by parsing the
`d` string. The pencil path is stored in a **pixel-space viewBox**
(1 unit = 1px) rather than a 0–100 one, so its `stroke-width` is a true
on-screen thickness — constant regardless of stroke length or direction.
3. **Closed pen path default fill** — DECIDED: yes, fill it on close with a
solid accent color, so a finished closed path is immediately visible.
The user can switch to none / gradient via the existing fill controls.
4. **Disc Designer support** — DECIDED: yes, pen + pencil work in Disc
Designer mode too. The overlay attaches to `#designerDiscWrap`; the
circular canvas is just visual framing.
5. **Touch / stylus** — capture works via Pointer Events (already used for
layer drag), so basic touch drawing is free. Pressure is a non-goal.
6. **Schema bump** — set `schemaVersion` to 6 when this lands. Old payloads
(v5) still load; `shape.pen` and `hidden` are additive.
7. **Where do the shape primitives ultimately live?** Long-term they could
move from the sidebar "+ Shape" menu into the left tool palette. Out of
scope for v1.
### Noted for later (out of scope here)
The project owner is considering merging the **Custom Art** layout into the
**Disc Designer** tab and renaming the tab to just **"Designer"**, with
*disc* becoming one of several artboard layout options inside it — so the
designer tab becomes a fully-featured surface rather than a disc-only one.
The pen tool does not depend on or block this: it produces plain `shape`
layers keyed by `target`, so it works regardless of how the tabs are
ultimately organized. Flagged here only so the eventual restructure keeps
the pen tool's per-mode overlay-host logic in mind.
---
## Acceptance summary
### Phase A
- [ ] Left tool palette switches `state.editorTool`; active tool highlighted.
- [ ] V / P / B shortcuts work when no input is focused; Esc → Select.
- [ ] Right options toolbar appears only for Pen/Pencil.
- [ ] Select mode behaves exactly as before — no drag/select regression.
### Phase B
- [ ] Freehand pencil stroke commits an open-path shape layer.
- [ ] Stroke round-trips through save/load and Nostr publish/fork.
- [ ] Exports correctly to PDF / JPEG.
- [ ] One undo entry per stroke.
### Phase C
- [ ] Pen places corner + smooth anchors; live preview is accurate.
- [ ] Closing / finishing / cancelling all work.
- [ ] Closed path is fillable with the existing solid/gradient controls.
- [ ] `shape.pen.nodes` persists through save/load.
### Phase D
- [ ] A pen path re-opens with editable anchors/handles.
- [ ] Move / add / delete anchors regenerates `d` and re-exports.
- [ ] SVG-uploaded paths show a "not node-editable" hint.
### Phase E
- [ ] `hidden` layers are skipped by every renderer and the export.
- [ ] A hidden pen shape still works as a `layer-ref` clip.
- [ ] Eye toggle in the layer list flips `hidden`.