Merge branch 'pen-tool-phase-d': Illustrator-parity pen / select tools, Pathfinder, Align, Eyedropper, Free Transform, Marquee

Bundles the design-tool polish arc:

- Pen tool: Direct Selection (A) as a separate tool, Cmd/Ctrl temp
  toggle, hover cursor cues, Shift constrain, auto-add / auto-delete
  on segment / anchor, click-vs-drag convert, Alt-drag asymmetric
  pull, Alt-click curved ↔ pointed (drops / grows handles), multi-
  anchor select via Shift-click + marquee, arrow-nudge anchors,
  drag-segment to bow the curve, continue-an-open-path from its
  endpoint, Spacebar reposition + Alt mid-draw asymmetric during
  create-drag, Alt-drag handle breaks symmetry in DSel, Cmd+J join,
  Cmd+Alt+J average, Path Actions side panel with batch buttons.
- Eyedropper (I) via native window.EyeDropper.
- Align / distribute panel for layer selection.
- Free Transform rotation handle on single + multi-layer selection
  bounding boxes, Shift snaps to 15°.
- Pathfinder ops (Unite / Subtract / Intersect / Exclude), backed
  by vendor/polygon-clipping.min.js.
- Marquee box-select on the V tool with marching-ants animation.
- Double-click a vector shape with V to dive into Direct Selection;
  DSel empty-click deselects the layer entirely.
- Misc: pen draft stroke width now matches the panel value, anchor
  + handle glyphs shrunk closer to Illustrator size, Direct Select
  arrow tinted light blue.
- Docs: PEN_TOOL_FEATURE.md + TODO.md updated.
This commit is contained in:
thePR0M3TH3AN committed 2026-06-02 09:53:28 -04:00
commit e25180346b
4 files changed
+1964 -68

No files matched your search

+23
View File
@@ -8,6 +8,29 @@ Running list of deferred work. Detailed specs for the larger items live in
_(Empty — see "Shipped (recent)" below for the items that just
landed. Drop new items in here as they come up.)_
## Design-tool follow-ups (Illustrator parity arc)
The pen / select / pathfinder bundle (`docs/PEN_TOOL_FEATURE.md`)
landed with these intentional gaps:
- **Smart Guides.** Snap-to-anchor and snap-to-edge alignment with
on-canvas distance hints while dragging. Biggest "feels professional"
upgrade still on the table; deserves its own arc.
- **Gradients + drop shadows.** Linear / radial gradient fills and a
basic drop-shadow / glow effect on the appearance panel. Mostly an
SVG `<defs>` + `filter` addition.
- **Scissors (C) + Outline text + Command palette (Cmd+K).** Three
small wins: split a path at a click, convert text to editable pen
paths (font-independent for Nostr export), searchable action list.
- **Flip H / V.** Prototyped during the Align panel arc but pulled
out — needs the renderer to honor per-layer scaleX / scaleY.
- **Pathfinder limitations to lift.** Today: refuses rotated shapes,
flattens curves, only the first polygon's outer ring stays
pen-editable post-op. Rotation support is the most user-facing of
these; curve fidelity would need a Bézier-aware clipper.
- **Pixel-art frame timeline.** Onion skin + frame list + FPS control.
Largest open arc; transformative if pixel art is a focus.
## QR code layer — remaining phases
Phases A and B shipped (the `qr` layer type: data, error-correction level,
+89 -7
View File
@@ -24,12 +24,18 @@ tool-mode system with flanking toolbars to host it.
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** — partial. Editable anchors + Bézier handles for selected
`tool:'pen'` paths; drag-to-move anchor (carries handles) and drag-to-move
handle, regenerates `d`, one undo per drag. *Deferred:* add/delete anchor,
corner↔smooth toggle, click-segment-to-insert, and bbox-refit when an
anchor is dragged past the original 0–100 viewBox (path clips at the box
edge until refit lands). Rotated paths are not node-editable in v1.
- **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
@@ -42,16 +48,92 @@ tool-mode system with flanking toolbars to host it.
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.
- Boolean path operations (union / subtract / intersect).
- 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
+1828 -61
View File
File diff suppressed because it is too large. Load diff
+24
View File
@@ -0,0 +1,24 @@
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).polygonClipping=e()}(this,(function(){"use strict";
/**
* splaytree v3.1.2
* Fast Splay tree for Node and browser
*
* @author Alexander Milevski <[email protected]>
* @license MIT
* @preserve
*/
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */function t(t,e){var n,r,i,o,s={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return o={next:l(0),throw:l(1),return:l(2)},"function"==typeof Symbol&&(o[Symbol.iterator]=function(){return this}),o;function l(o){return function(l){return function(o){if(n)throw new TypeError("Generator is already executing.");for(;s;)try{if(n=1,r&&(i=2&o[0]?r.return:o[0]?r.throw||((i=r.return)&&i.call(r),0):r.next)&&!(i=i.call(r,o[1])).done)return i;switch(r=0,i&&(o=[2&o[0],i.value]),o[0]){case 0:case 1:i=o;break;case 4:return s.label++,{value:o[1],done:!1};case 5:s.label++,r=o[1],o=[0];continue;case 7:o=s.ops.pop(),s.trys.pop();continue;default:if(!(i=s.trys,(i=i.length>0&&i[i.length-1])||6!==o[0]&&2!==o[0])){s=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]<i[3])){s.label=o[1];break}if(6===o[0]&&s.label<i[1]){s.label=i[1],i=o;break}if(i&&s.label<i[2]){s.label=i[2],s.ops.push(o);break}i[2]&&s.ops.pop(),s.trys.pop();continue}o=e.call(t,s)}catch(t){o=[6,t],r=0}finally{n=i=0}if(5&o[0])throw o[1];return{value:o[0]?o[1]:void 0,done:!0}}([o,l])}}}var e=function(t,e){this.next=null,this.key=t,this.data=e,this.left=null,this.right=null};function n(t,e){return t>e?1:t<e?-1:0}function r(t,n,r){for(var i=new e(null,null),o=i,s=i;;){var l=r(t,n.key);if(l<0){if(null===n.left)break;if(r(t,n.left.key)<0){var h=n.left;if(n.left=h.right,h.right=n,null===(n=h).left)break}s.left=n,s=n,n=n.left}else{if(!(l>0))break;if(null===n.right)break;if(r(t,n.right.key)>0){h=n.right;if(n.right=h.left,h.left=n,null===(n=h).right)break}o.right=n,o=n,n=n.right}}return o.right=n.left,s.left=n.right,n.left=i.right,n.right=i.left,n}function i(t,n,i,o){var s=new e(t,n);if(null===i)return s.left=s.right=null,s;var l=o(t,(i=r(t,i,o)).key);return l<0?(s.left=i.left,s.right=i,i.left=null):l>=0&&(s.right=i.right,s.left=i,i.right=null),s}function o(t,e,n){var i=null,o=null;if(e){var s=n((e=r(t,e,n)).key,t);0===s?(i=e.left,o=e.right):s<0?(o=e.right,e.right=null,i=e):(i=e.left,e.left=null,o=e)}return{left:i,right:o}}function s(t,e,n,r,i){if(t){r(e+(n?"└── ":"├── ")+i(t)+"\n");var o=e+(n?" ":"│ ");t.left&&s(t.left,o,!1,r,i),t.right&&s(t.right,o,!0,r,i)}}var l=function(){function l(t){void 0===t&&(t=n),this._root=null,this._size=0,this._comparator=t}return l.prototype.insert=function(t,e){return this._size++,this._root=i(t,e,this._root,this._comparator)},l.prototype.add=function(t,n){var i=new e(t,n);null===this._root&&(i.left=i.right=null,this._size++,this._root=i);var o=this._comparator,s=r(t,this._root,o),l=o(t,s.key);return 0===l?this._root=s:(l<0?(i.left=s.left,i.right=s,s.left=null):l>0&&(i.right=s.right,i.left=s,s.right=null),this._size++,this._root=i),this._root},l.prototype.remove=function(t){this._root=this._remove(t,this._root,this._comparator)},l.prototype._remove=function(t,e,n){var i;return null===e?null:0===n(t,(e=r(t,e,n)).key)?(null===e.left?i=e.right:(i=r(t,e.left,n)).right=e.right,this._size--,i):e},l.prototype.pop=function(){var t=this._root;if(t){for(;t.left;)t=t.left;return this._root=r(t.key,this._root,this._comparator),this._root=this._remove(t.key,this._root,this._comparator),{key:t.key,data:t.data}}return null},l.prototype.findStatic=function(t){for(var e=this._root,n=this._comparator;e;){var r=n(t,e.key);if(0===r)return e;e=r<0?e.left:e.right}return null},l.prototype.find=function(t){return this._root&&(this._root=r(t,this._root,this._comparator),0!==this._comparator(t,this._root.key))?null:this._root},l.prototype.contains=function(t){for(var e=this._root,n=this._comparator;e;){var r=n(t,e.key);if(0===r)return!0;e=r<0?e.left:e.right}return!1},l.prototype.forEach=function(t,e){for(var n=this._root,r=[],i=!1;!i;)null!==n?(r.push(n),n=n.left):0!==r.length?(n=r.pop(),t.call(e,n),n=n.right):i=!0;return this},l.prototype.range=function(t,e,n,r){for(var i=[],o=this._comparator,s=this._root;0!==i.length||s;)if(s)i.push(s),s=s.left;else{if(o((s=i.pop()).key,e)>0)break;if(o(s.key,t)>=0&&n.call(r,s))return this;s=s.right}return this},l.prototype.keys=function(){var t=[];return this.forEach((function(e){var n=e.key;return t.push(n)})),t},l.prototype.values=function(){var t=[];return this.forEach((function(e){var n=e.data;return t.push(n)})),t},l.prototype.min=function(){return this._root?this.minNode(this._root).key:null},l.prototype.max=function(){return this._root?this.maxNode(this._root).key:null},l.prototype.minNode=function(t){if(void 0===t&&(t=this._root),t)for(;t.left;)t=t.left;return t},l.prototype.maxNode=function(t){if(void 0===t&&(t=this._root),t)for(;t.right;)t=t.right;return t},l.prototype.at=function(t){for(var e=this._root,n=!1,r=0,i=[];!n;)if(e)i.push(e),e=e.left;else if(i.length>0){if(e=i.pop(),r===t)return e;r++,e=e.right}else n=!0;return null},l.prototype.next=function(t){var e=this._root,n=null;if(t.right){for(n=t.right;n.left;)n=n.left;return n}for(var r=this._comparator;e;){var i=r(t.key,e.key);if(0===i)break;i<Line truncated
//# sourceMappingURL=polygon-clipping.umd.min.js.map