@weasel-js/core 1.8.1 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/README.md +9 -5
- package/dist/{builtins-CMEh6-Oc.d.ts → builtins-CQcvcLXh.d.ts} +98 -31
- package/dist/canvas/DrawCanvas.js +33 -33
- package/dist/canvas/DrawCanvas.js.map +1 -1
- package/dist/canvas/MinimapCanvas.js +29 -29
- package/dist/canvas/MinimapCanvas.js.map +1 -1
- package/dist/canvas/NodeShape.js +63 -54
- package/dist/canvas/NodeShape.js.map +1 -1
- package/dist/canvas/SceneCanvas/features.js +26 -5
- package/dist/canvas/SceneCanvas/features.js.map +1 -1
- package/dist/canvas/SceneCanvas/useSceneSelectTool.js +3 -2
- package/dist/canvas/SceneCanvas/useSceneSelectTool.js.map +1 -1
- package/dist/canvas/SceneCanvas.js +526 -529
- package/dist/canvas/SceneCanvas.js.map +1 -1
- package/dist/canvas/SceneViewCanvas.js +35 -35
- package/dist/canvas/SceneViewCanvas.js.map +1 -1
- package/dist/canvas/defaultDrawOne.js +6 -1
- package/dist/canvas/defaultDrawOne.js.map +1 -1
- package/dist/canvas/pickWalk.js.map +1 -1
- package/dist/canvas/sceneAdapter.js.map +1 -1
- package/dist/canvas/useHitObserver.js +23 -0
- package/dist/canvas/useHitObserver.js.map +1 -0
- package/dist/canvas/useHostAnchor.js.map +1 -1
- package/dist/core/device/useDeviceProfile.js.map +1 -1
- package/dist/core/scene/derivedPath.js.map +1 -1
- package/dist/core/scene/effectivePose.js.map +1 -1
- package/dist/core/scene/kitRegistry.js.map +1 -1
- package/dist/core/scene/scene.js.map +1 -1
- package/dist/core/scene/types.js.map +1 -1
- package/dist/core/scene/useScene.js.map +1 -1
- package/dist/core/viewport/fitViewToBounds.js +3 -3
- package/dist/core/viewport/fitViewToBounds.js.map +1 -1
- package/dist/core/viewport/screenSlop.js +8 -2
- package/dist/core/viewport/screenSlop.js.map +1 -1
- package/dist/features/minimap/actions.js.map +1 -1
- package/dist/features/paths/markerCommands.js +95 -63
- package/dist/features/paths/markerCommands.js.map +1 -1
- package/dist/features/paths/tessellate/dash.js +52 -0
- package/dist/features/paths/tessellate/dash.js.map +1 -0
- package/dist/features/paths/tessellate/insets.js +32 -0
- package/dist/features/paths/tessellate/insets.js.map +1 -0
- package/dist/features/paths/tessellate/metric.js +72 -0
- package/dist/features/paths/tessellate/metric.js.map +1 -0
- package/dist/features/paths/tessellate/stroke.js +74 -134
- package/dist/features/paths/tessellate/stroke.js.map +1 -1
- package/dist/features/paths/tessellate/vertexWidths.js +32 -0
- package/dist/features/paths/tessellate/vertexWidths.js.map +1 -0
- package/dist/features/pointer/PointerContext.js +2 -2
- package/dist/features/pointer/PointerContext.js.map +1 -1
- package/dist/features/poseRun/usePoseRun.js.map +1 -1
- package/dist/features/text/useTextEdit.js +86 -84
- package/dist/features/text/useTextEdit.js.map +1 -1
- package/dist/index.d.ts +78 -39
- package/dist/index.js +148 -148
- package/dist/interactions/actions/defaults/align.js.map +1 -1
- package/dist/interactions/actions/defaults/anchorEditing.js.map +1 -1
- package/dist/interactions/actions/defaults/areaSelect.js.map +1 -1
- package/dist/interactions/actions/defaults/booleans.js.map +1 -1
- package/dist/interactions/actions/defaults/cancelGesture.js.map +1 -1
- package/dist/interactions/actions/defaults/clearSelection.js.map +1 -1
- package/dist/interactions/actions/defaults/clipboard.js.map +1 -1
- package/dist/interactions/actions/defaults/clone.js.map +1 -1
- package/dist/interactions/actions/defaults/createOutlines.js +1 -1
- package/dist/interactions/actions/defaults/createOutlines.js.map +1 -1
- package/dist/interactions/actions/defaults/createPaintAction.js.map +1 -1
- package/dist/interactions/actions/defaults/delete.js.map +1 -1
- package/dist/interactions/actions/defaults/distribute.js.map +1 -1
- package/dist/interactions/actions/defaults/duplicate.js.map +1 -1
- package/dist/interactions/actions/defaults/editAnchors.js.map +1 -1
- package/dist/interactions/actions/defaults/enterPathEdit.js.map +1 -1
- package/dist/interactions/actions/defaults/enterTextEdit.js.map +1 -1
- package/dist/interactions/actions/defaults/escape.js +1 -1
- package/dist/interactions/actions/defaults/escape.js.map +1 -1
- package/dist/interactions/actions/defaults/exitPathEdit.js.map +1 -1
- package/dist/interactions/actions/defaults/flip.js.map +1 -1
- package/dist/interactions/actions/defaults/group.js.map +1 -1
- package/dist/interactions/actions/defaults/ingest.js.map +1 -1
- package/dist/interactions/actions/defaults/insert.js.map +1 -1
- package/dist/interactions/actions/defaults/insertPathAnchor.js +1 -1
- package/dist/interactions/actions/defaults/insertPathAnchor.js.map +1 -1
- package/dist/interactions/actions/defaults/lassoSelect.js.map +1 -1
- package/dist/interactions/actions/defaults/move.js.map +1 -1
- package/dist/interactions/actions/defaults/nudge.js.map +1 -1
- package/dist/interactions/actions/defaults/pinchZoom.js.map +1 -1
- package/dist/interactions/actions/defaults/reorder.js.map +1 -1
- package/dist/interactions/actions/defaults/requiresSelection.js.map +1 -1
- package/dist/interactions/actions/defaults/resize.js.map +1 -1
- package/dist/interactions/actions/defaults/rotate.js.map +1 -1
- package/dist/interactions/actions/defaults/selectAll.js.map +1 -1
- package/dist/interactions/actions/defaults/slice.js.map +1 -1
- package/dist/interactions/actions/defaults/toolActivate.js.map +1 -1
- package/dist/interactions/actions/defaults/toolResetToDefault.js.map +1 -1
- package/dist/interactions/actions/defaults/undoRedo.js +1 -1
- package/dist/interactions/actions/defaults/undoRedo.js.map +1 -1
- package/dist/interactions/actions/defaults/viewportDragPan.js.map +1 -1
- package/dist/interactions/actions/defaults/viewportWheelPan.js.map +1 -1
- package/dist/interactions/actions/defaults/viewportZoom.js.map +1 -1
- package/dist/interactions/actions/planeInput.js.map +1 -1
- package/dist/interactions/gestures/shared/screenTolerance.js.map +1 -1
- package/dist/{registerProgram-ateHDGjw.d.ts → registerProgram-BMZRDgrh.d.ts} +4 -0
- package/dist/renderer/cache/markerCommandCache.js +46 -42
- package/dist/renderer/cache/markerCommandCache.js.map +1 -1
- package/dist/renderer/cache/outlineStrokeMeshCache.js +30 -25
- package/dist/renderer/cache/outlineStrokeMeshCache.js.map +1 -1
- package/dist/renderer/cache/strokeMeshCache.js +42 -31
- package/dist/renderer/cache/strokeMeshCache.js.map +1 -1
- package/dist/renderer/cullDrawCommands.js +32 -31
- package/dist/renderer/cullDrawCommands.js.map +1 -1
- package/dist/renderer/draw.js +315 -362
- package/dist/renderer/draw.js.map +1 -1
- package/dist/renderer/glyphOutlineMesh.js +67 -0
- package/dist/renderer/glyphOutlineMesh.js.map +1 -0
- package/dist/renderer/math/mat3.js +8 -3
- package/dist/renderer/math/mat3.js.map +1 -1
- package/dist/renderer/strokeLengths.js +50 -0
- package/dist/renderer/strokeLengths.js.map +1 -0
- package/dist/renderer.d.ts +2 -2
- package/dist/{test-seams-fYf6m5pu.d.ts → test-seams-CuIxe0xY.d.ts} +1 -1
- package/dist/test-seams.d.ts +2 -2
- package/dist/tools/builtin/eyedropper/useEyedropperTool.js.map +1 -1
- package/dist/tools/builtin/pen/usePenTool.js +1 -1
- package/dist/tools/builtin/pen/usePenTool.js.map +1 -1
- package/dist/tools/builtin/select/selectionContributions.js +43 -23
- package/dist/tools/builtin/select/selectionContributions.js.map +1 -1
- package/dist/tools/builtin/select/useSelectTool.js.map +1 -1
- package/dist/tools/builtin/slice/useSliceTool.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +17 -17
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { strokeSpaceOf as e } from "../features/paths/tessellate/metric.js";
|
|
2
|
+
import { quantizeStrokeScale as t } from "./cache/strokeMeshCache.js";
|
|
3
|
+
import { outlineMesh as n } from "./cache/outlineMeshCache.js";
|
|
4
|
+
import { outlineStrokeMesh as r, quantizeEmWidth as i } from "./cache/outlineStrokeMeshCache.js";
|
|
5
|
+
import { SYNTHETIC_ITALIC_RADIANS as a } from "./syntheticItalic.js";
|
|
6
|
+
//#region src/renderer/glyphOutlineMesh.ts
|
|
7
|
+
function o(e, t, r) {
|
|
8
|
+
return l(e, t, r, (e) => n(e.key, e.d));
|
|
9
|
+
}
|
|
10
|
+
function s(n, a, o, s) {
|
|
11
|
+
let u = n.stroke;
|
|
12
|
+
if (!u) return null;
|
|
13
|
+
let d = u.width ?? 1;
|
|
14
|
+
if (typeof d == "object") {
|
|
15
|
+
if (!(d.px > 0)) return null;
|
|
16
|
+
let i = c(n), [f, p, m, h] = [
|
|
17
|
+
s[0],
|
|
18
|
+
s[1],
|
|
19
|
+
s[3],
|
|
20
|
+
s[4]
|
|
21
|
+
];
|
|
22
|
+
return l(n, a, o, (n) => {
|
|
23
|
+
if (!(n.scale > 0)) return null;
|
|
24
|
+
let a = n.scale, o = e(a * f, a * p, a * (m - f * i), a * (h - p * i)), s = t(d.px, o.scale);
|
|
25
|
+
if (!(s > 0)) return null;
|
|
26
|
+
let c = u.dash?.map((e) => e / s);
|
|
27
|
+
return r(n.key, n.d, d.px / s, {
|
|
28
|
+
...u,
|
|
29
|
+
dash: c
|
|
30
|
+
}, o.metric ?? void 0);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return d > 0 ? l(n, a, o, (e) => e.scale > 0 ? r(e.key, e.d, i(d / e.scale), {
|
|
34
|
+
...u,
|
|
35
|
+
dash: u.dash?.map((t) => t / e.scale)
|
|
36
|
+
}) : null) : null;
|
|
37
|
+
}
|
|
38
|
+
var c = (e) => e.synthetic.italic ? Math.tan(a) : 0;
|
|
39
|
+
function l(e, t, n, r) {
|
|
40
|
+
let i = [], a = 0, o = 0;
|
|
41
|
+
for (let t of e.glyphs) {
|
|
42
|
+
let e = r(t);
|
|
43
|
+
e && e.indices.length !== 0 && (i.push({
|
|
44
|
+
mesh: e,
|
|
45
|
+
glyph: t
|
|
46
|
+
}), a += e.vertices.length, o += e.indices.length);
|
|
47
|
+
}
|
|
48
|
+
if (i.length === 0) return null;
|
|
49
|
+
let s = c(e), l = new Float32Array(a), u = new Uint32Array(o), d = 0, f = 0, p = 0;
|
|
50
|
+
for (let { mesh: e, glyph: r } of i) {
|
|
51
|
+
let { x: i, baselineY: a, scale: o } = r;
|
|
52
|
+
for (let r = 0; r < e.vertices.length; r += 2) {
|
|
53
|
+
let c = e.vertices[r], u = e.vertices[r + 1];
|
|
54
|
+
l[d++] = i + t + (c - u * s) * o, l[d++] = a + n + u * o;
|
|
55
|
+
}
|
|
56
|
+
for (let t = 0; t < e.indices.length; t++) u[f++] = p + e.indices[t];
|
|
57
|
+
p += e.vertices.length / 2;
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
vertices: l,
|
|
61
|
+
indices: u
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
export { o as outlineGroupMesh, s as outlineGroupStrokeMesh };
|
|
66
|
+
|
|
67
|
+
//# sourceMappingURL=glyphOutlineMesh.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glyphOutlineMesh.js","names":["outlineGroupMesh","mergeGlyphMeshes","group","dx","dy","outlineMesh","glyph","outlineGroupStrokeMesh","stroke","width","shear","shearOf","a","b","c","d","transform","s","space","strokeSpaceOf","pxPerEm","quantizeStrokeScale","dash","v","outlineStrokeMesh","quantizeEmWidth","SYNTHETIC_ITALIC_RADIANS","parts","vertexFloats","indexCount","mesh","meshFor","vertices","indices","vi","ii","base","k","ex","ey","x","scale","baselineY"],"sources":["../../src/renderer/glyphOutlineMesh.ts"],"sourcesContent":["/**\n * Glyph outlines as world-space meshes: each glyph's em-space fill or stroke\n * mesh, placed and concatenated so a run of outlined text is one draw.\n */\nimport type { Mesh } from '@weasel-js/geom/tessellate';\nimport type { LaidOutGroup, LaidOutOutlineGlyph } from '@weasel-js/text';\nimport { strokeSpaceOf } from 'features/paths/tessellate/metric';\nimport type { GlMat3 } from './math/mat3';\nimport { quantizeStrokeScale } from './cache/strokeMeshCache';\nimport { outlineMesh } from './cache/outlineMeshCache';\nimport { outlineStrokeMesh, quantizeEmWidth } from './cache/outlineStrokeMeshCache';\nimport { SYNTHETIC_ITALIC_RADIANS } from './syntheticItalic';\n\n/**\n * Merge a group's glyphs into one world-space mesh, or `null` when nothing in\n * it has area.\n *\n * Every glyph mesh here is `'nonzero'` (that is what `pathFromD` produces and\n * what a font outline means), so none of them sets `requiresStencil` and\n * concatenating their triangles is sound — an even-odd mesh would be a naive\n * per-contour fan that only resolves correctly through a stencil pass, and\n * merging one into a batch would fill its counters solid.\n */\nexport function outlineGroupMesh(group: LaidOutGroup, dx: number, dy: number): Mesh | null {\n return mergeGlyphMeshes(group, dx, dy, (glyph) => outlineMesh(glyph.key, glyph.d));\n}\n\n/**\n * The same merge for the group's stroke: one ribbon per glyph, tessellated in\n * em space and batched into one buffer, so a stroked paragraph is one extra\n * draw call rather than one per glyph.\n *\n * A world width crosses into em space by dividing by the glyph's `scale`,\n * which is world units per em — so `stroke.width` stays a world-unit measure\n * and does not grow with `fontSize`, matching every other stroke in the kit.\n * Dashes cross with it.\n *\n * A synthetic oblique shears a world-width ribbon along with the glyph. That\n * is a real (small) distortion — a sheared circle is an ellipse, so the\n * outline is marginally thicker across the lean than along it — and it is\n * deliberate: shearing the finished ribbon is what keeps the outline glued to\n * the glyph it outlines, which re-tessellating in sheared space would not.\n *\n * A `{ px }` ribbon is built in the stretch of the whole em-to-screen map,\n * shear included, as a path's is in its transform's (see `metric.ts`), so it\n * is that many pixels wide in every direction on every glyph.\n */\nexport function outlineGroupStrokeMesh(\n group: LaidOutGroup,\n dx: number,\n dy: number,\n transform: GlMat3,\n): Mesh | null {\n const stroke = group.stroke;\n if (!stroke) return null;\n const width = stroke.width ?? 1;\n if (typeof width === 'object') {\n if (!(width.px > 0)) return null;\n const shear = shearOf(group);\n const [a, b, c, d] = [transform[0], transform[1], transform[3], transform[4]];\n return mergeGlyphMeshes(group, dx, dy, (glyph) => {\n if (!(glyph.scale > 0)) return null;\n const s = glyph.scale;\n const space = strokeSpaceOf(s * a, s * b, s * (c - a * shear), s * (d - b * shear));\n const pxPerEm = quantizeStrokeScale(width.px, space.scale);\n if (!(pxPerEm > 0)) return null;\n const dash = stroke.dash?.map((v) => v / pxPerEm);\n return outlineStrokeMesh(\n glyph.key, glyph.d, width.px / pxPerEm, { ...stroke, dash }, space.metric ?? undefined,\n );\n });\n }\n if (!(width > 0)) return null;\n return mergeGlyphMeshes(group, dx, dy, (glyph) =>\n glyph.scale > 0\n ? outlineStrokeMesh(\n glyph.key, glyph.d, quantizeEmWidth(width / glyph.scale),\n { ...stroke, dash: stroke.dash?.map((v) => v / glyph.scale) },\n )\n : null);\n}\n\nconst shearOf = (group: LaidOutGroup): number =>\n group.synthetic.italic ? Math.tan(SYNTHETIC_ITALIC_RADIANS) : 0;\n\n/**\n * Transform each glyph's em-space mesh into world space and concatenate.\n * Shared by the fill and stroke paths, which differ only in which mesh they\n * ask for per glyph — the placement math must not fork, or an outline would\n * drift off the glyph it outlines.\n */\nfunction mergeGlyphMeshes(\n group: LaidOutGroup,\n dx: number,\n dy: number,\n meshFor: (glyph: LaidOutOutlineGlyph) => Mesh | null,\n): Mesh | null {\n const parts: { mesh: Mesh; glyph: LaidOutOutlineGlyph }[] = [];\n let vertexFloats = 0;\n let indexCount = 0;\n for (const glyph of group.glyphs) {\n const mesh = meshFor(glyph);\n if (!mesh || mesh.indices.length === 0) continue;\n parts.push({ mesh, glyph });\n vertexFloats += mesh.vertices.length;\n indexCount += mesh.indices.length;\n }\n if (parts.length === 0) return null;\n\n // Matches the SDF vertex shader's skew exactly: x moves by\n // `(baselineY - y) * tan(angle)`, and in em space `baselineY - y` is\n // `-ey * scale`, so above-baseline vertices (negative ey) lean right.\n const shear = shearOf(group);\n\n const vertices = new Float32Array(vertexFloats);\n const indices = new Uint32Array(indexCount);\n let vi = 0;\n let ii = 0;\n let base = 0;\n for (const { mesh, glyph } of parts) {\n const { x, baselineY, scale } = glyph;\n for (let k = 0; k < mesh.vertices.length; k += 2) {\n const ex = mesh.vertices[k];\n const ey = mesh.vertices[k + 1];\n vertices[vi++] = x + dx + (ex - ey * shear) * scale;\n vertices[vi++] = baselineY + dy + ey * scale;\n }\n for (let k = 0; k < mesh.indices.length; k++) indices[ii++] = base + mesh.indices[k];\n base += mesh.vertices.length / 2;\n }\n return { vertices, indices };\n}\n"],"mappings":";;;;;;AAuBA,SAAgBA,EAAiB,GAAqB,GAAY,GAAyB;CACzF,OAAOC,EAAiBC,GAAOC,GAAIC,IAAK,MAAUC,EAAYC,EAAM,KAAKA,EAAM,CAAC,CAAC;AACnF;AAsBA,SAAgBC,EACd,GACA,GACA,GACA,GACa;CACb,IAAMC,IAASN,EAAM;CACrB,IAAI,CAACM,GAAQ,OAAO;CACpB,IAAMC,IAAQD,EAAO,SAAS;CAC9B,IAAI,OAAOC,KAAU,UAAU;EAC7B,IAAI,EAAEA,EAAM,KAAK,IAAI,OAAO;EAC5B,IAAMC,IAAQC,EAAQT,CAAK,GACrB,CAACU,GAAGC,GAAGC,GAAGC,KAAK;GAACC,EAAU;GAAIA,EAAU;GAAIA,EAAU;GAAIA,EAAU;EAAE;EAC5E,OAAOf,EAAiBC,GAAOC,GAAIC,IAAK,MAAU;GAChD,IAAI,EAAEE,EAAM,QAAQ,IAAI,OAAO;GAC/B,IAAMW,IAAIX,EAAM,OACVY,IAAQC,EAAcF,IAAIL,GAAGK,IAAIJ,GAAGI,KAAKH,IAAIF,IAAIF,IAAQO,KAAKF,IAAIF,IAAIH,EAAM,GAC5EU,IAAUC,EAAoBZ,EAAM,IAAIS,EAAM,KAAK;GACzD,IAAI,EAAEE,IAAU,IAAI,OAAO;GAC3B,IAAME,IAAOd,EAAO,MAAM,KAAK,MAAMe,IAAIH,CAAO;GAChD,OAAOI,EACLlB,EAAM,KAAKA,EAAM,GAAGG,EAAM,KAAKW,GAAS;IAAE,GAAGZ;IAAQ;GAAK,GAAGU,EAAM,UAAU,KAAA,CAC/E;EACF,CAAC;CACH;CAEA,OADMT,IAAQ,IACPR,EAAiBC,GAAOC,GAAIC,IAAK,MACtCE,EAAM,QAAQ,IACVkB,EACAlB,EAAM,KAAKA,EAAM,GAAGmB,EAAgBhB,IAAQH,EAAM,KAAK,GACvD;EAAE,GAAGE;EAAQ,MAAMA,EAAO,MAAM,KAAK,MAAMe,IAAIjB,EAAM,KAAK;CAAE,CAC9D,IACE,IAAI,IAPe;AAQ3B;AAEA,IAAMK,KAAW,MACfT,EAAM,UAAU,SAAS,KAAK,IAAIwB,CAAwB,IAAI;AAQhE,SAASzB,EACP,GACA,GACA,GACA,GACa;CACb,IAAM0B,IAAsD,CAAC,GACzDC,IAAe,GACfC,IAAa;CACjB,KAAK,IAAMvB,KAASJ,EAAM,QAAQ;EAChC,IAAM4B,IAAOC,EAAQzB,CAAK;EAC1B,AAAKwB,KAAQA,EAAK,QAAQ,WAAW,MACrCH,EAAM,KAAK;GAAE;GAAM;EAAM,CAAC,GAC1B,KAAgBG,EAAK,SAAS,QAC9B,KAAcA,EAAK,QAAQ;CAC7B;CACA,IAAIH,EAAM,WAAW,GAAG,OAAO;CAK/B,IAAMjB,IAAQC,EAAQT,CAAK,GAErB8B,IAAW,IAAI,aAAaJ,CAAY,GACxCK,IAAU,IAAI,YAAYJ,CAAU,GACtCK,IAAK,GACLC,IAAK,GACLC,IAAO;CACX,KAAK,IAAM,EAAE,SAAM,cAAWT,GAAO;EACnC,IAAM,EAAE,MAAG,cAAW,aAAUrB;EAChC,KAAK,IAAI+B,IAAI,GAAGA,IAAIP,EAAK,SAAS,QAAQ,KAAK,GAAG;GAChD,IAAMQ,IAAKR,EAAK,SAASO,IACnBE,IAAKT,EAAK,SAASO,IAAI;GAE7B,AADA,EAAS,OAAQG,IAAIrC,KAAMmC,IAAKC,IAAK7B,KAAS+B,GAC9C,EAAS,OAAQC,IAAYtC,IAAKmC,IAAKE;EACzC;EACA,KAAK,IAAIJ,IAAI,GAAGA,IAAIP,EAAK,QAAQ,QAAQ,KAAK,EAAQ,OAAQM,IAAON,EAAK,QAAQO;EAClF,KAAQP,EAAK,SAAS,SAAS;CACjC;CACA,OAAO;EAAE;EAAU;CAAQ;AAC7B"}
|
|
@@ -53,7 +53,11 @@ function u(e, n) {
|
|
|
53
53
|
function d(e) {
|
|
54
54
|
return Math.sqrt(Math.abs(e[0] * e[4] - e[1] * e[3]));
|
|
55
55
|
}
|
|
56
|
-
|
|
56
|
+
function f(e) {
|
|
57
|
+
let t = e[0] * e[0] + e[1] * e[1] + e[3] * e[3] + e[4] * e[4], n = e[0] * e[4] - e[1] * e[3];
|
|
58
|
+
return Math.sqrt(Math.max(0, (t - Math.sqrt(Math.max(0, t * t - 4 * n * n))) / 2));
|
|
59
|
+
}
|
|
60
|
+
var p = {
|
|
57
61
|
identity: n,
|
|
58
62
|
toAffine: o,
|
|
59
63
|
fromAffine: s,
|
|
@@ -63,9 +67,10 @@ var f = {
|
|
|
63
67
|
invert: c,
|
|
64
68
|
apply: l,
|
|
65
69
|
screenToClip: u,
|
|
66
|
-
meanScaleOf: d
|
|
70
|
+
meanScaleOf: d,
|
|
71
|
+
minScaleOf: f
|
|
67
72
|
};
|
|
68
73
|
//#endregion
|
|
69
|
-
export {
|
|
74
|
+
export { p as mat3 };
|
|
70
75
|
|
|
71
76
|
//# sourceMappingURL=mat3.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mat3.js","names":["multiply","invert","invertAffine","create","a","b","c","d","tx","ty","identity","multiply$1","out","ma","m","mb","mc","md","mtx","mty","translated","scaled","sx","sy","toAffine","fromAffine","invert$1","inv","invert","apply","x","y","screenToClip","width","height","meanScaleOf","mat3"],"sources":["../../../src/renderer/math/mat3.ts"],"sourcesContent":["/**\n * 2D affine matrix utilities. Column-major 9-element Float32Array, matching\n * `WebGL2RenderingContext.uniformMatrix3fv` byte order so we can pass the\n * array directly without a transpose flag.\n *\n * Layout (column-major):\n * [m00, m10, 0,\n * m01, m11, 0,\n * tx, ty, 1]\n *\n * `apply(m, x, y)` returns `[m * (x, y, 1)] = [m00*x + m01*y + tx,\n * m10*x + m11*y + ty]`.\n */\n\nimport { invert as invertAffine, type Mat3 as Affine } from '@weasel-js/geom';\n\n/** A 2D affine transform as a column-major 9-element `Float32Array`, laid out as above. */\nexport type GlMat3 = Float32Array;\n\nfunction create(a: number, b: number, c: number, d: number, tx: number, ty: number): GlMat3 {\n // Column-major:\n // col 0: (a, b, 0)\n // col 1: (c, d, 0)\n // col 2: (tx, ty, 1)\n return new Float32Array([a, b, 0, c, d, 0, tx, ty, 1]);\n}\n\nfunction identity(): GlMat3 {\n return create(1, 0, 0, 1, 0, 0);\n}\n\nfunction multiply(out: GlMat3, m: GlMat3): GlMat3 {\n // out := out · m (right-multiply by m).\n const a = out[0], b = out[1];\n const c = out[3], d = out[4];\n const tx = out[6], ty = out[7];\n const ma = m[0], mb = m[1];\n const mc = m[3], md = m[4];\n const mtx = m[6], mty = m[7];\n\n return create(\n a * ma + c * mb, // new a\n b * ma + d * mb, // new b\n a * mc + c * md, // new c\n b * mc + d * md, // new d\n a * mtx + c * mty + tx, // new tx\n b * mtx + d * mty + ty, // new ty\n );\n}\n\n/** `m · T(tx, ty)`: composes onto `m`, where `@weasel-js/geom`'s `translate`\n * constructs a fresh matrix. */\nfunction translated(m: GlMat3, tx: number, ty: number): GlMat3 {\n const t = create(1, 0, 0, 1, tx, ty);\n return multiply(m, t);\n}\n\n/** `m · S(sx, sy)`: composes onto `m`, where geom's `scale` constructs one. */\nfunction scaled(m: GlMat3, sx: number, sy: number): GlMat3 {\n const s = create(sx, 0, 0, sy, 0, 0);\n return multiply(m, s);\n}\n\n/** This layout read as the kernel's 6-element affine. The two carry the same\n * logical order, so the conversion is a repack and nothing else — it exists\n * so the repack has a name instead of appearing inline at each crossing. */\nfunction toAffine(m: GlMat3): Affine {\n return [m[0], m[1], m[3], m[4], m[6], m[7]];\n}\n\n/** The kernel's affine in this layout. The inverse of {@link toAffine}, and\n * what an `SvgGroupNode.transform` (also a kernel affine) becomes on its way\n * to a `GroupDrawCommand`. */\nfunction fromAffine(a: Affine): GlMat3 {\n return create(a[0], a[1], a[2], a[3], a[4], a[5]);\n}\n\n/** Inverse of an affine matrix, or `null` when `@weasel-js/geom`'s `invert`\n * finds it singular — the same rule, read through this layout. */\nfunction invert(m: GlMat3): GlMat3 | null {\n const inv = invertAffine(toAffine(m));\n return inv && fromAffine(inv);\n}\n\nfunction apply(m: GlMat3, x: number, y: number): [number, number] {\n const a = m[0], b = m[1];\n const c = m[3], d = m[4];\n const tx = m[6], ty = m[7];\n return [a * x + c * y + tx, b * x + d * y + ty];\n}\n\n/**\n * Map screen pixel coords (0..width on X, 0..height on Y, top-left origin)\n * into clip space (-1..1 on X, 1..-1 on Y — note Y flip so screen-down\n * matches clip-down).\n */\nfunction screenToClip(width: number, height: number): GlMat3 {\n return create(\n 2 / width, // a\n 0, // b\n 0, // c\n -2 / height, // d\n -1, // tx\n 1, // ty\n );\n}\n\n/**\n * Uniform-equivalent scale factor: the square root of the absolute\n * determinant of the linear part, i.e. the geometric mean of the two axis\n * scales. Rotation-invariant. Under non-uniform scale it is between the two\n * axes and exact on neither — the same compromise `meanScale` documents.\n */\nfunction meanScaleOf(m: GlMat3): number {\n return Math.sqrt(Math.abs(m[0] * m[4] - m[1] * m[3]));\n}\n\n/** The renderer's 3x3 matrix operations, as one namespace. These work on the\n * 9-element `Float32Array` form the GL uniform upload wants — distinct from\n * `@weasel-js/geom`'s 6-element affine `GlMat3`, though the logical element\n * order is the same. */\nexport const mat3 = {\n identity,\n toAffine,\n fromAffine,\n multiply,\n translated,\n scaled,\n invert,\n apply,\n screenToClip,\n meanScaleOf,\n};\n"],"mappings":";;AAmBA,SAASG,EAAO,GAAW,GAAW,GAAW,GAAW,GAAY,GAAoB;CAK1F,OAAO,IAAI,aAAa;EAACC;EAAGC;EAAG;EAAGC;EAAGC;EAAG;EAAGC;EAAIC;EAAI;CAAC,CAAC;AACvD;AAEA,SAASC,IAAmB;CAC1B,OAAOP,EAAO,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;AAChC;AAEA,SAASH,EAAS,GAAa,GAAmB;CAEhD,IAAMI,IAAIQ,EAAI,IAAIP,IAAIO,EAAI,IACpBN,IAAIM,EAAI,IAAIL,IAAIK,EAAI,IACpBJ,IAAKI,EAAI,IAAIH,IAAKG,EAAI,IACtBC,IAAKC,EAAE,IAAIC,IAAKD,EAAE,IAClBE,IAAKF,EAAE,IAAIG,IAAKH,EAAE,IAClBI,IAAMJ,EAAE,IAAIK,IAAML,EAAE;CAE1B,OAAOX,EACLC,IAAIS,IAAKP,IAAIS,GACbV,IAAIQ,IAAKN,IAAIQ,GACbX,IAAIY,IAAKV,IAAIW,GACbZ,IAAIW,IAAKT,IAAIU,GACbb,IAAIc,IAAMZ,IAAIa,IAAMX,GACpBH,IAAIa,IAAMX,IAAIY,IAAMV,CACtB;AACF;AAIA,SAASW,EAAW,GAAW,GAAY,GAAoB;CAE7D,OAAOpB,EAASc,GADNX,EAAO,GAAG,GAAG,GAAG,GAAGK,GAAIC,CACd,CAAC;AACtB;AAGA,SAASY,EAAO,GAAW,GAAY,GAAoB;CAEzD,OAAOrB,EAASc,GADNX,EAAOmB,GAAI,GAAG,GAAGC,GAAI,GAAG,CACf,CAAC;AACtB;AAKA,SAASC,EAAS,GAAmB;CACnC,OAAO;EAACV,EAAE;EAAIA,EAAE;EAAIA,EAAE;EAAIA,EAAE;EAAIA,EAAE;EAAIA,EAAE;CAAE;AAC5C;AAKA,SAASW,EAAW,GAAmB;CACrC,OAAOtB,EAAOC,EAAE,IAAIA,EAAE,IAAIA,EAAE,IAAIA,EAAE,IAAIA,EAAE,IAAIA,EAAE,EAAE;AAClD;AAIA,SAASH,EAAO,GAA0B;CACxC,IAAM0B,IAAMzB,EAAasB,EAASV,CAAC,CAAC;CACpC,OAAOa,KAAOF,EAAWE,CAAG;AAC9B;AAEA,SAASE,EAAM,GAAW,GAAW,GAA6B;CAChE,IAAMzB,IAAIU,EAAE,IAAIT,IAAIS,EAAE,IAChBR,IAAIQ,EAAE,IAAIP,IAAIO,EAAE,IAChBN,IAAKM,EAAE,IAAIL,IAAKK,EAAE;CACxB,OAAO,CAACV,IAAI0B,IAAIxB,IAAIyB,IAAIvB,GAAIH,IAAIyB,IAAIvB,IAAIwB,IAAItB,CAAE;AAChD;AAOA,SAASuB,EAAa,GAAe,GAAwB;CAC3D,OAAO7B,EACL,IAAI8B,GACJ,GACA,GACA,KAAKC,GACL,IACA,CACF;AACF;AAQA,SAASC,EAAY,GAAmB;CACtC,OAAO,KAAK,KAAK,KAAK,IAAIrB,EAAE,KAAKA,EAAE,KAAKA,EAAE,KAAKA,EAAE,EAAE,CAAC;AACtD;AAMA,
|
|
1
|
+
{"version":3,"file":"mat3.js","names":["multiply","invert","invertAffine","create","a","b","c","d","tx","ty","identity","multiply$1","out","ma","m","mb","mc","md","mtx","mty","translated","scaled","sx","sy","toAffine","fromAffine","invert$1","inv","invert","apply","x","y","screenToClip","width","height","meanScaleOf","minScaleOf","sum","det","mat3"],"sources":["../../../src/renderer/math/mat3.ts"],"sourcesContent":["/**\n * 2D affine matrix utilities. Column-major 9-element Float32Array, matching\n * `WebGL2RenderingContext.uniformMatrix3fv` byte order so we can pass the\n * array directly without a transpose flag.\n *\n * Layout (column-major):\n * [m00, m10, 0,\n * m01, m11, 0,\n * tx, ty, 1]\n *\n * `apply(m, x, y)` returns `[m * (x, y, 1)] = [m00*x + m01*y + tx,\n * m10*x + m11*y + ty]`.\n */\n\nimport { invert as invertAffine, type Mat3 as Affine } from '@weasel-js/geom';\n\n/** A 2D affine transform as a column-major 9-element `Float32Array`, laid out as above. */\nexport type GlMat3 = Float32Array;\n\nfunction create(a: number, b: number, c: number, d: number, tx: number, ty: number): GlMat3 {\n // Column-major:\n // col 0: (a, b, 0)\n // col 1: (c, d, 0)\n // col 2: (tx, ty, 1)\n return new Float32Array([a, b, 0, c, d, 0, tx, ty, 1]);\n}\n\nfunction identity(): GlMat3 {\n return create(1, 0, 0, 1, 0, 0);\n}\n\nfunction multiply(out: GlMat3, m: GlMat3): GlMat3 {\n // out := out · m (right-multiply by m).\n const a = out[0], b = out[1];\n const c = out[3], d = out[4];\n const tx = out[6], ty = out[7];\n const ma = m[0], mb = m[1];\n const mc = m[3], md = m[4];\n const mtx = m[6], mty = m[7];\n\n return create(\n a * ma + c * mb, // new a\n b * ma + d * mb, // new b\n a * mc + c * md, // new c\n b * mc + d * md, // new d\n a * mtx + c * mty + tx, // new tx\n b * mtx + d * mty + ty, // new ty\n );\n}\n\n/** `m · T(tx, ty)`: composes onto `m`, where `@weasel-js/geom`'s `translate`\n * constructs a fresh matrix. */\nfunction translated(m: GlMat3, tx: number, ty: number): GlMat3 {\n const t = create(1, 0, 0, 1, tx, ty);\n return multiply(m, t);\n}\n\n/** `m · S(sx, sy)`: composes onto `m`, where geom's `scale` constructs one. */\nfunction scaled(m: GlMat3, sx: number, sy: number): GlMat3 {\n const s = create(sx, 0, 0, sy, 0, 0);\n return multiply(m, s);\n}\n\n/** This layout read as the kernel's 6-element affine. The two carry the same\n * logical order, so the conversion is a repack and nothing else — it exists\n * so the repack has a name instead of appearing inline at each crossing. */\nfunction toAffine(m: GlMat3): Affine {\n return [m[0], m[1], m[3], m[4], m[6], m[7]];\n}\n\n/** The kernel's affine in this layout. The inverse of {@link toAffine}, and\n * what an `SvgGroupNode.transform` (also a kernel affine) becomes on its way\n * to a `GroupDrawCommand`. */\nfunction fromAffine(a: Affine): GlMat3 {\n return create(a[0], a[1], a[2], a[3], a[4], a[5]);\n}\n\n/** Inverse of an affine matrix, or `null` when `@weasel-js/geom`'s `invert`\n * finds it singular — the same rule, read through this layout. */\nfunction invert(m: GlMat3): GlMat3 | null {\n const inv = invertAffine(toAffine(m));\n return inv && fromAffine(inv);\n}\n\nfunction apply(m: GlMat3, x: number, y: number): [number, number] {\n const a = m[0], b = m[1];\n const c = m[3], d = m[4];\n const tx = m[6], ty = m[7];\n return [a * x + c * y + tx, b * x + d * y + ty];\n}\n\n/**\n * Map screen pixel coords (0..width on X, 0..height on Y, top-left origin)\n * into clip space (-1..1 on X, 1..-1 on Y — note Y flip so screen-down\n * matches clip-down).\n */\nfunction screenToClip(width: number, height: number): GlMat3 {\n return create(\n 2 / width, // a\n 0, // b\n 0, // c\n -2 / height, // d\n -1, // tx\n 1, // ty\n );\n}\n\n/**\n * Uniform-equivalent scale factor: the square root of the absolute\n * determinant of the linear part, i.e. the geometric mean of the two axis\n * scales. Rotation-invariant. Under non-uniform scale it is between the two\n * axes and exact on neither — the same compromise `meanScale` documents.\n */\nfunction meanScaleOf(m: GlMat3): number {\n return Math.sqrt(Math.abs(m[0] * m[4] - m[1] * m[3]));\n}\n\n/** The least any direction is scaled by the linear part — its smaller singular\n * value. What a screen length is worth in world units, at most. */\nfunction minScaleOf(m: GlMat3): number {\n const sum = m[0] * m[0] + m[1] * m[1] + m[3] * m[3] + m[4] * m[4];\n const det = m[0] * m[4] - m[1] * m[3];\n return Math.sqrt(Math.max(0, (sum - Math.sqrt(Math.max(0, sum * sum - 4 * det * det))) / 2));\n}\n\n/** The renderer's 3x3 matrix operations, as one namespace. These work on the\n * 9-element `Float32Array` form the GL uniform upload wants — distinct from\n * `@weasel-js/geom`'s 6-element affine `GlMat3`, though the logical element\n * order is the same. */\nexport const mat3 = {\n identity,\n toAffine,\n fromAffine,\n multiply,\n translated,\n scaled,\n invert,\n apply,\n screenToClip,\n meanScaleOf,\n minScaleOf,\n};\n"],"mappings":";;AAmBA,SAASG,EAAO,GAAW,GAAW,GAAW,GAAW,GAAY,GAAoB;CAK1F,OAAO,IAAI,aAAa;EAACC;EAAGC;EAAG;EAAGC;EAAGC;EAAG;EAAGC;EAAIC;EAAI;CAAC,CAAC;AACvD;AAEA,SAASC,IAAmB;CAC1B,OAAOP,EAAO,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;AAChC;AAEA,SAASH,EAAS,GAAa,GAAmB;CAEhD,IAAMI,IAAIQ,EAAI,IAAIP,IAAIO,EAAI,IACpBN,IAAIM,EAAI,IAAIL,IAAIK,EAAI,IACpBJ,IAAKI,EAAI,IAAIH,IAAKG,EAAI,IACtBC,IAAKC,EAAE,IAAIC,IAAKD,EAAE,IAClBE,IAAKF,EAAE,IAAIG,IAAKH,EAAE,IAClBI,IAAMJ,EAAE,IAAIK,IAAML,EAAE;CAE1B,OAAOX,EACLC,IAAIS,IAAKP,IAAIS,GACbV,IAAIQ,IAAKN,IAAIQ,GACbX,IAAIY,IAAKV,IAAIW,GACbZ,IAAIW,IAAKT,IAAIU,GACbb,IAAIc,IAAMZ,IAAIa,IAAMX,GACpBH,IAAIa,IAAMX,IAAIY,IAAMV,CACtB;AACF;AAIA,SAASW,EAAW,GAAW,GAAY,GAAoB;CAE7D,OAAOpB,EAASc,GADNX,EAAO,GAAG,GAAG,GAAG,GAAGK,GAAIC,CACd,CAAC;AACtB;AAGA,SAASY,EAAO,GAAW,GAAY,GAAoB;CAEzD,OAAOrB,EAASc,GADNX,EAAOmB,GAAI,GAAG,GAAGC,GAAI,GAAG,CACf,CAAC;AACtB;AAKA,SAASC,EAAS,GAAmB;CACnC,OAAO;EAACV,EAAE;EAAIA,EAAE;EAAIA,EAAE;EAAIA,EAAE;EAAIA,EAAE;EAAIA,EAAE;CAAE;AAC5C;AAKA,SAASW,EAAW,GAAmB;CACrC,OAAOtB,EAAOC,EAAE,IAAIA,EAAE,IAAIA,EAAE,IAAIA,EAAE,IAAIA,EAAE,IAAIA,EAAE,EAAE;AAClD;AAIA,SAASH,EAAO,GAA0B;CACxC,IAAM0B,IAAMzB,EAAasB,EAASV,CAAC,CAAC;CACpC,OAAOa,KAAOF,EAAWE,CAAG;AAC9B;AAEA,SAASE,EAAM,GAAW,GAAW,GAA6B;CAChE,IAAMzB,IAAIU,EAAE,IAAIT,IAAIS,EAAE,IAChBR,IAAIQ,EAAE,IAAIP,IAAIO,EAAE,IAChBN,IAAKM,EAAE,IAAIL,IAAKK,EAAE;CACxB,OAAO,CAACV,IAAI0B,IAAIxB,IAAIyB,IAAIvB,GAAIH,IAAIyB,IAAIvB,IAAIwB,IAAItB,CAAE;AAChD;AAOA,SAASuB,EAAa,GAAe,GAAwB;CAC3D,OAAO7B,EACL,IAAI8B,GACJ,GACA,GACA,KAAKC,GACL,IACA,CACF;AACF;AAQA,SAASC,EAAY,GAAmB;CACtC,OAAO,KAAK,KAAK,KAAK,IAAIrB,EAAE,KAAKA,EAAE,KAAKA,EAAE,KAAKA,EAAE,EAAE,CAAC;AACtD;AAIA,SAASsB,EAAW,GAAmB;CACrC,IAAMC,IAAMvB,EAAE,KAAKA,EAAE,KAAKA,EAAE,KAAKA,EAAE,KAAKA,EAAE,KAAKA,EAAE,KAAKA,EAAE,KAAKA,EAAE,IACzDwB,IAAMxB,EAAE,KAAKA,EAAE,KAAKA,EAAE,KAAKA,EAAE;CACnC,OAAO,KAAK,KAAK,KAAK,IAAI,IAAIuB,IAAM,KAAK,KAAK,KAAK,IAAI,GAAGA,IAAMA,IAAM,IAAIC,IAAMA,CAAG,CAAC,KAAK,CAAC,CAAC;AAC7F;AAMA,IAAaC,IAAO;CAClB;CACA;CACA;CACA,UAAA5B;CACA;CACA;CACA,QAAAe;CACA;CACA;CACA;CACA;AACF"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { strokeSpaceOf as e } from "../features/paths/tessellate/metric.js";
|
|
2
|
+
import { resolveStrokeWidth as t } from "../features/paths/tessellate/stroke.js";
|
|
3
|
+
import { resolveMarkerSize as n } from "../core/markerInset.js";
|
|
4
|
+
import { quantizeStrokeScale as r } from "./cache/strokeMeshCache.js";
|
|
5
|
+
//#region src/renderer/strokeLengths.ts
|
|
6
|
+
var i = {};
|
|
7
|
+
function a(n, a) {
|
|
8
|
+
let c = a.stroke, { markerStart: l, markerMid: u, markerEnd: d } = c;
|
|
9
|
+
if (typeof c.width != "object" && !o(l) && !o(u) && !o(d)) return {
|
|
10
|
+
cmd: a,
|
|
11
|
+
spaces: i
|
|
12
|
+
};
|
|
13
|
+
let f = { ...c }, { scale: p, metric: m } = e(n[0], n[1], n[3], n[4]), h = (e) => m === null ? void 0 : {
|
|
14
|
+
metric: m,
|
|
15
|
+
pxPerUnit: e
|
|
16
|
+
}, g;
|
|
17
|
+
if (typeof c.width == "object") {
|
|
18
|
+
let e = r(c.width.px, p);
|
|
19
|
+
f.width = t(c.width, e), c.dash && e > 0 && (f.dash = c.dash.map((t) => t / e)), g = h(e);
|
|
20
|
+
}
|
|
21
|
+
let _ = (e) => {
|
|
22
|
+
if (e === void 0 || typeof e == "string" || e.size === void 0) return g;
|
|
23
|
+
if (typeof e.size != "number") return h(r(e.size.px, p));
|
|
24
|
+
}, v = {
|
|
25
|
+
ribbon: g,
|
|
26
|
+
start: _(l),
|
|
27
|
+
mid: _(u),
|
|
28
|
+
end: _(d)
|
|
29
|
+
};
|
|
30
|
+
return o(l) && (f.markerStart = s(l, p)), o(u) && (f.markerMid = s(u, p)), o(d) && (f.markerEnd = s(d, p)), {
|
|
31
|
+
cmd: {
|
|
32
|
+
...a,
|
|
33
|
+
stroke: f
|
|
34
|
+
},
|
|
35
|
+
spaces: v
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function o(e) {
|
|
39
|
+
return typeof e == "object" && typeof e.size == "object";
|
|
40
|
+
}
|
|
41
|
+
function s(e, t) {
|
|
42
|
+
return {
|
|
43
|
+
...e,
|
|
44
|
+
size: n(e, 0, r(e.size.px, t))
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
export { a as resolveStrokeLengths };
|
|
49
|
+
|
|
50
|
+
//# sourceMappingURL=strokeLengths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"strokeLengths.js","names":["WORLD","resolveStrokeLengths","stroke","cmd","isPxMarker","markerStart","markerMid","markerEnd","resolved","strokeSpaceOf","transform","screen","metric","ribbon","pxPerUnit","quantizeStrokeScale","scale","resolveStrokeWidth","d","head","ref","spaces","resolvedMarker","resolveMarkerSize"],"sources":["../../src/renderer/strokeLengths.ts"],"sourcesContent":["import type { FillStyle, MarkerRef, Stroke } from '@weasel-js/paint';\nimport type { PathDrawCommand } from './DrawCommand';\nimport type { GlMat3 } from './math/mat3';\nimport { resolveStrokeWidth } from 'features/paths/tessellate/stroke';\nimport {\n strokeSpaceOf, type ScreenSpace, type StrokeSpaces,\n} from 'features/paths/tessellate/metric';\nimport { quantizeStrokeScale } from './cache/strokeMeshCache';\nimport { resolveMarkerSize } from '../core/markerInset';\n\n/** A path command whose stroke paints. */\nexport type StrokedPathCommand = PathDrawCommand & { stroke: Stroke & { paint: FillStyle } };\n\n/** A stroke command ready to tessellate, and the spaces to tessellate it in. */\nexport interface ResolvedStrokeCommand {\n cmd: StrokedPathCommand;\n /** Where the ribbon and each head are built; every length in `cmd` is\n * measured in the space of the part it belongs to. */\n spaces: StrokeSpaces;\n}\n\nconst WORLD: StrokeSpaces = {};\n\n/**\n * `cmd` with its `{ px }` lengths — the stroke width, any marker size, and on a\n * `{ px }` stroke its dashes — resolved against the accumulated transform's\n * quantized scale, so everything downstream (the ribbon cache key, the insets,\n * the heads) sees plain numbers.\n *\n * Under a transform that stretches one direction more than another, each\n * screen-sized part is also built in the transform's stretch, where lengths\n * measure as they do on screen, so it is exact across, down and on every\n * diagonal rather than right only on average. World-sized parts stay in world.\n */\nexport function resolveStrokeLengths(transform: GlMat3, cmd: StrokedPathCommand): ResolvedStrokeCommand {\n const stroke = cmd.stroke;\n const { markerStart, markerMid, markerEnd } = stroke;\n if (typeof stroke.width !== 'object' && !isPxMarker(markerStart)\n && !isPxMarker(markerMid) && !isPxMarker(markerEnd)) {\n return { cmd, spaces: WORLD };\n }\n const resolved: Stroke & { paint: FillStyle } = { ...stroke };\n const { scale, metric } = strokeSpaceOf(transform[0], transform[1], transform[3], transform[4]);\n const screen = (pxPerUnit: number): ScreenSpace | undefined =>\n metric === null ? undefined : { metric, pxPerUnit };\n let ribbon: ScreenSpace | undefined;\n if (typeof stroke.width === 'object') {\n const pxPerUnit = quantizeStrokeScale(stroke.width.px, scale);\n resolved.width = resolveStrokeWidth(stroke.width, pxPerUnit);\n if (stroke.dash && pxPerUnit > 0) resolved.dash = stroke.dash.map((d) => d / pxPerUnit);\n ribbon = screen(pxPerUnit);\n }\n const head = (ref: MarkerRef | undefined): ScreenSpace | undefined => {\n if (ref === undefined || typeof ref === 'string' || ref.size === undefined) return ribbon;\n if (typeof ref.size === 'number') return undefined;\n return screen(quantizeStrokeScale(ref.size.px, scale));\n };\n const spaces = { ribbon, start: head(markerStart), mid: head(markerMid), end: head(markerEnd) };\n if (isPxMarker(markerStart)) resolved.markerStart = resolvedMarker(markerStart, scale);\n if (isPxMarker(markerMid)) resolved.markerMid = resolvedMarker(markerMid, scale);\n if (isPxMarker(markerEnd)) resolved.markerEnd = resolvedMarker(markerEnd, scale);\n return { cmd: { ...cmd, stroke: resolved }, spaces };\n}\n\ntype PxMarker = Exclude<MarkerRef, string> & { size: { px: number } };\n\nfunction isPxMarker(ref: MarkerRef | undefined): ref is PxMarker {\n return typeof ref === 'object' && typeof ref.size === 'object';\n}\n\nfunction resolvedMarker(ref: PxMarker, scale: number): MarkerRef {\n return { ...ref, size: resolveMarkerSize(ref, 0, quantizeStrokeScale(ref.size.px, scale)) };\n}\n"],"mappings":";;;;;AAqBA,IAAMA,IAAsB,CAAC;AAa7B,SAAgBC,EAAqB,GAAmB,GAAgD;CACtG,IAAMC,IAASC,EAAI,QACb,EAAE,gBAAa,cAAW,iBAAcD;CAC9C,IAAI,OAAOA,EAAO,SAAU,YAAY,CAACE,EAAWC,CAAW,KAC1D,CAACD,EAAWE,CAAS,KAAK,CAACF,EAAWG,CAAS,GAClD,OAAO;EAAE;EAAK,QAAQP;CAAM;CAE9B,IAAMQ,IAA0C,EAAE,GAAGN,EAAO,GACtD,EAAE,UAAO,cAAWO,EAAcC,EAAU,IAAIA,EAAU,IAAIA,EAAU,IAAIA,EAAU,EAAE,GACxFC,KAAU,MACdC,MAAW,OAAO,KAAA,IAAY;EAAE;EAAQ;CAAU,GAChDC;CACJ,IAAI,OAAOX,EAAO,SAAU,UAAU;EACpC,IAAMY,IAAYC,EAAoBb,EAAO,MAAM,IAAIc,CAAK;EAG5D,AAFA,EAAS,QAAQC,EAAmBf,EAAO,OAAOY,CAAS,GACvDZ,EAAO,QAAQY,IAAY,MAAG,EAAS,OAAOZ,EAAO,KAAK,KAAK,MAAMgB,IAAIJ,CAAS,IACtF,IAASH,EAAOG,CAAS;CAC3B;CACA,IAAMK,KAAQ,MAAwD;EACpE,IAAIC,MAAQ,KAAA,KAAa,OAAOA,KAAQ,YAAYA,EAAI,SAAS,KAAA,GAAW,OAAOP;EACnF,IAAI,OAAOO,EAAI,QAAS,UACxB,OAAOT,EAAOI,EAAoBK,EAAI,KAAK,IAAIJ,CAAK,CAAC;CACvD,GACMK,IAAS;EAAE;EAAQ,OAAOF,EAAKd,CAAW;EAAG,KAAKc,EAAKb,CAAS;EAAG,KAAKa,EAAKZ,CAAS;CAAE;CAI9F,OAHIH,EAAWC,CAAW,MAAG,EAAS,cAAciB,EAAejB,GAAaW,CAAK,IACjFZ,EAAWE,CAAS,MAAG,EAAS,YAAYgB,EAAehB,GAAWU,CAAK,IAC3EZ,EAAWG,CAAS,MAAG,EAAS,YAAYe,EAAef,GAAWS,CAAK,IACxE;EAAE,KAAK;GAAE,GAAGb;GAAK,QAAQK;EAAS;EAAG;CAAO;AACrD;AAIA,SAASJ,EAAW,GAA6C;CAC/D,OAAO,OAAOgB,KAAQ,YAAY,OAAOA,EAAI,QAAS;AACxD;AAEA,SAASE,EAAe,GAAe,GAA0B;CAC/D,OAAO;EAAE,GAAGF;EAAK,MAAMG,EAAkBH,GAAK,GAAGL,EAAoBK,EAAI,KAAK,IAAIJ,CAAK,CAAC;CAAE;AAC5F"}
|
package/dist/renderer.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { C as CullRect, D as DrawCommand, E as Effect, G as GroupDrawCommand, I as IDENTITY_COLOR_MATRIX, a as ImageDrawCommand, b as ImageMinification, P as PathDrawCommand, R as RenderOptions, c as RenderSpan, d as RenderSpanStats, e as RenderStats, f as RenderTarget, S as SPRITE_STRIDE, g as ShaderDrawCommand, h as SolidPaint, i as SpriteSheet, j as SpritesDrawCommand, k as StrokeOptions, T as TextDrawCommand, W as WeaselRenderer, l as WeaselRendererOptions, m as blur, n as buildGradientRamp, o as cullDrawCommands, p as frameRect, r as registerEffect, t as tessellateStroke, v as viewToMat3, q as vignette } from './builtins-
|
|
2
|
-
export { G as GlMat3, P as ProgramSource, S as ShaderCompileError, a as ShaderProgramHandle, b as ShaderUniform, m as mat3, p as programSourceRegistry, r as registerProgram } from './registerProgram-
|
|
1
|
+
export { C as CullRect, D as DrawCommand, E as Effect, G as GroupDrawCommand, I as IDENTITY_COLOR_MATRIX, a as ImageDrawCommand, b as ImageMinification, P as PathDrawCommand, R as RenderOptions, c as RenderSpan, d as RenderSpanStats, e as RenderStats, f as RenderTarget, S as SPRITE_STRIDE, g as ShaderDrawCommand, h as SolidPaint, i as SpriteSheet, j as SpritesDrawCommand, k as StrokeOptions, T as TextDrawCommand, W as WeaselRenderer, l as WeaselRendererOptions, m as blur, n as buildGradientRamp, o as cullDrawCommands, p as frameRect, r as registerEffect, t as tessellateStroke, v as viewToMat3, q as vignette } from './builtins-CQcvcLXh.js';
|
|
2
|
+
export { G as GlMat3, P as ProgramSource, S as ShaderCompileError, a as ShaderProgramHandle, b as ShaderUniform, m as mat3, p as programSourceRegistry, r as registerProgram } from './registerProgram-BMZRDgrh.js';
|
|
3
3
|
export { Mesh, TessellateOptions, tessellate } from '@weasel-js/geom/tessellate';
|
|
4
4
|
export { RegisterFontOptions, canQueryLocalFonts, cssFontFamily, cssFontFamilyLoading, enableLocalFontOutlines, fontOutlineRegistry, fontRegistry, hasFontOutlines, isCanvasFont, listFontOutlines, loadFontOutlines, outlineStatus, registerCanvasFont, registerFont, registerFontOutlines, subscribeGlyphReady, unregisterCanvasFont, unregisterFontOutlines } from '@weasel-js/font';
|
|
5
5
|
import { TextureHandle } from '@weasel-js/paint';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FillStyle, GradientUnits, GradStop, ColorSpace, MarkerKey, Stroke } from '@weasel-js/paint';
|
|
2
2
|
import { ComponentType } from 'react';
|
|
3
3
|
import { Reflection } from '@weasel-js/registry';
|
|
4
|
-
import { c as ShaderProgram, G as GlMat3, P as ProgramSource } from './registerProgram-
|
|
4
|
+
import { c as ShaderProgram, G as GlMat3, P as ProgramSource } from './registerProgram-BMZRDgrh.js';
|
|
5
5
|
import { Path } from '@weasel-js/geom';
|
|
6
6
|
|
|
7
7
|
/**
|
package/dist/test-seams.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { _ as _resetMarkersForTests, z as _resetPaintKindsForTests } from './test-seams-
|
|
1
|
+
export { _ as _resetMarkersForTests, z as _resetPaintKindsForTests } from './test-seams-CuIxe0xY.js';
|
|
2
2
|
import '@weasel-js/paint';
|
|
3
3
|
import 'react';
|
|
4
4
|
import '@weasel-js/registry';
|
|
5
|
-
import './registerProgram-
|
|
5
|
+
import './registerProgram-BMZRDgrh.js';
|
|
6
6
|
import '@weasel-js/geom';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useEyedropperTool.js","names":["EyedropperIcon","useEyedropperTool","onPickRef","useLatest","opts","colorOfRef","pickAction","useMemo","p","params","nodeAtPoint","deps","id","keybinding","hotkey","defineTool","cursorFor","createElement"],"sources":["../../../../src/tools/builtin/eyedropper/useEyedropperTool.ts"],"sourcesContent":["import { useMemo, createElement } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport { defineTool } from '../../overlayBinding';\nimport type { Tool } from '../../overlayBinding';\nimport type { HotkeyTrigger } from '@weasel-js/routing';\nimport type { ToolKeybinding } from '@weasel-js/routing';\nimport type { Action } from '@weasel-js/routing';\nimport
|
|
1
|
+
{"version":3,"file":"useEyedropperTool.js","names":["EyedropperIcon","useEyedropperTool","onPickRef","useLatest","opts","colorOfRef","pickAction","useMemo","p","params","nodeAtPoint","deps","id","keybinding","hotkey","defineTool","cursorFor","createElement"],"sources":["../../../../src/tools/builtin/eyedropper/useEyedropperTool.ts"],"sourcesContent":["import { useMemo, createElement } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport { defineTool } from '../../overlayBinding';\nimport type { Tool } from '../../overlayBinding';\nimport type { HotkeyTrigger } from '@weasel-js/routing';\nimport type { ToolKeybinding } from '@weasel-js/routing';\nimport type { Action } from '@weasel-js/routing';\nimport { EyedropperIcon } from '../../../icons';\nimport { cursorFor } from '@weasel-js/cursor';\n\n/** Options for `useEyedropperTool`. */\nexport interface UseEyedropperToolOptions {\n /** Called when the user picks a color. `null` means \"no node was hit\n * with a color\" — currently only reachable when `colorOf` returns null\n * on a real hit; empty-click is a no-op in v1. */\n onPick: (color: string | null) => void;\n\n /** Map a node id to a color string, or `null` if the node has no color\n * to sample. Called on click with the id of the topmost node under the\n * click point. */\n colorOf: (id: string) => string | null;\n\n /** Override the default `{ key: 'I' }` keybinding. Pass `null` to omit\n * the keybinding entirely (palette-only or hotkey-only wiring). */\n keybinding?: ToolKeybinding | null;\n\n /** Override the default `'alt'` hotkey trigger. Pass `null` to omit. */\n hotkey?: HotkeyTrigger | null;\n}\n\n/**\n * Eyedropper. Click a node to sample its color; the consumer's `onPick`\n * callback decides where the color goes. Engages as a sticky active-slot\n * tool (`I` keybinding) and as a momentary hotkey-slot tool while Alt\n * is held — consumers can override either trigger to `null`.\n *\n * Pure-read tool — does NOT mutate the scene. v1 is click-only; drag is\n * unbound (a future drag-to-sample option is additive).\n *\n * The node under the click comes from the `nodeAtPoint` dep, so an\n * eyedropper in a consumer that never registered that dep samples nothing.\n * `<SceneCanvas>` sources it from the same picker the rest of the kit hits.\n */\nexport function useEyedropperTool(opts: UseEyedropperToolOptions): Tool<null> {\n const onPickRef = useLatest(opts.onPick);\n const colorOfRef = useLatest(opts.colorOf);\n\n // Registered by `useToolActions` from inside the ActionsProvider — see\n // `ToolDef.actions`. Closing over the option refs here is what lets the\n // consumer's `onPick` stay a plain callback instead of a dep.\n const pickAction = useMemo<Action>(\n () => ({\n id: 'eyedropper.pick',\n label: 'Eyedropper — sample color',\n // Same tag the tool declares, so a mode that disallows color sampling\n // can't be reached through the gesture either. The tool's own\n // eligibility already gates activation; this closes the gesture side.\n eligible: { capability: 'samples-color' },\n requires: ['nodeAtPoint'],\n invoker: {\n timing: 'immediate' as const,\n run: (deps, params) => {\n const p = params as { pressX?: number; pressY?: number } | undefined;\n if (p?.pressX === undefined || p.pressY === undefined) return;\n const nodeAtPoint = deps.nodeAtPoint;\n const id = nodeAtPoint?.({ x: p.pressX, y: p.pressY }) ?? null;\n // Empty click is a no-op in v1 — `onPick(null)` is reserved for\n // \"hit a node that has no color to sample.\"\n if (id === null) return;\n onPickRef.current(colorOfRef.current(id));\n },\n },\n }),\n [onPickRef, colorOfRef],\n );\n\n return useMemo(() => {\n // Resolve overrides. The spec says `null` => omit; defineTool reads\n // these directly into the returned Tool, and the registry treats\n // `undefined` as \"no trigger.\" Convert null → undefined here.\n const keybinding =\n opts.keybinding === null ? undefined : (opts.keybinding ?? { key: 'I' });\n const hotkey =\n opts.hotkey === null ? undefined : (opts.hotkey ?? 'alt');\n\n return defineTool<null>({\n id: 'eyedropper',\n capabilities: ['samples-color'],\n hookName: 'useEyedropperTool',\n keybinding,\n hotkey,\n cursor: cursorFor('eyedropper', { fallback: 'crosshair' }),\n presentation: {\n label: 'Eyedropper',\n icon: createElement(EyedropperIcon),\n group: 'view',\n },\n actions: [pickAction],\n // One binding, no target filter: the action resolves the node itself\n // and no-ops on empty. Sampling deliberately reads the PRESS point\n // rather than the release — a 4px drift between the two can land on a\n // different node, and the color you meant is the one you pressed.\n //\n // The eyedropper used to carry a `pointerDown` route that existed only\n // to claim the press before `useSelectTool`'s own always-claiming\n // pointerdown route could. That was an artifact of two dispatchers\n // racing; hotkey scope now outranks active scope in the one dispatcher\n // that remains, so holding Alt wins without a gate.\n bindings: [{ spec: { kind: 'click' as const }, actionId: 'eyedropper.pick' }],\n });\n }, [opts.keybinding, opts.hotkey, pickAction]);\n}\n"],"mappings":";;;;;;AA2CA,SAAgBC,EAAkB,GAA4C;CAC5E,IAAMC,IAAYC,EAAUC,EAAK,MAAM,GACjCC,IAAaF,EAAUC,EAAK,OAAO,GAKnCE,IAAaC,SACV;EACL,IAAI;EACJ,OAAO;EAIP,UAAU,EAAE,YAAY,gBAAgB;EACxC,UAAU,CAAC,aAAa;EACxB,SAAS;GACP,QAAQ;GACR,MAAM,GAAM,MAAW;IACrB,IAAMC,IAAIC;IACV,IAAID,GAAG,WAAW,KAAA,KAAaA,EAAE,WAAW,KAAA,GAAW;IACvD,IAAME,IAAcC,EAAK,aACnBC,IAAKF,IAAc;KAAE,GAAGF,EAAE;KAAQ,GAAGA,EAAE;IAAO,CAAC,KAAK;IAG1D,AAAII,MAAO,QACXV,EAAU,QAAQG,EAAW,QAAQO,CAAE,CAAC;GAC1C;EACF;CACF,IACA,CAACV,GAAWG,CAAU,CACxB;CAEA,OAAOE,QAAc;EAInB,IAAMM,IACJT,EAAK,eAAe,OAAO,KAAA,IAAaA,EAAK,cAAc,EAAE,KAAK,IAAI,GAClEU,IACJV,EAAK,WAAW,OAAO,KAAA,IAAaA,EAAK,UAAU;EAErD,OAAOW,EAAiB;GACtB,IAAI;GACJ,cAAc,CAAC,eAAe;GAC9B,UAAU;GACV;GACA;GACA,QAAQC,EAAU,cAAc,EAAE,UAAU,YAAY,CAAC;GACzD,cAAc;IACZ,OAAO;IACP,MAAMC,EAAcjB,CAAc;IAClC,OAAO;GACT;GACA,SAAS,CAACM,CAAU;GAWpB,UAAU,CAAC;IAAE,MAAM,EAAE,MAAM,QAAiB;IAAG,UAAU;GAAkB,CAAC;EAC9E,CAAC;CACH,GAAG;EAACF,EAAK;EAAYA,EAAK;EAAQE;CAAU,CAAC;AAC/C"}
|
|
@@ -69,7 +69,7 @@ function w(e) {
|
|
|
69
69
|
let r = e.continuing;
|
|
70
70
|
if (r && n.id === r.id && n.sub === r.sub) return !1;
|
|
71
71
|
let i = t.scene;
|
|
72
|
-
return !i && !t.applyOps || !(r ? t.editAnchors
|
|
72
|
+
return !i && !t.applyOps || !(r ? t.editAnchors?.editOps : F.current.adapter.makeNode) ? !1 : r?.id === n.id || i?.get(n.id) !== void 0;
|
|
73
73
|
}, [F]), B = p((e, t, s) => {
|
|
74
74
|
let c = t.editAnchors, l = t.scene, d = e.current, { anchors: f, closed: p } = o(C([...e.finishedSubpaths, d], null)), m = c.getEditablePath(s.id), h = o(m), g = s.end === "last" ? u(h.anchors[s.sub]) : h.anchors[s.sub], _ = f[f.length - 1], v = _[_.length - 1], y = d.anchors[d.anchors.length - 1].outHandle ?? g[0].outHandle;
|
|
75
75
|
y && (v.outHandle = { ...y }), _.push(...g.slice(1));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usePenTool.js","names":["p","PenIcon","freshScratch","resetScratch","anchorCount","s","n","sp","buildPath","all","subpaths","trailing","anchorsToPath","usePenTool","options","scratchRef","useRef","optsRef","useLatest","forceRender","useReducer","x","commitContinuation","useCallback","cont","drawn","edit","added","addedClosed","pathToAnchors","reverseAnchors","set","next","samePath","commit","deps","allClosed","path","pose","id","joinable","hit","scene","commitJoin","current","other","otherSet","tail","joined","joint","out","sameNode","rest","i","label","ops","keptId","node","createInsertOp","createDeleteOp","applyOps","defaultCommitAdapter","pickUpEndpoint","scratch","radius","scale","viewScale","nearestPathAnchor","pathsNear","wx","wy","h","original","picked","reversed","snap","fn","placeAt","y","withinCloseRadius","withinPxRadius","ax","ay","actions","useMemo","p","params","first","join","ActionDisabledReason","origin","ctx","pickedUp","joinHit","linked","applyOutHandle","sm","moveCtx","se","reason","endCtx","defineTool","cursorFor","createElement","anchor","to","target","dx","dy","shift","c","constrainTo45","area","view","e","pxExtent","px","ids","a","b"],"sources":["../../../../src/tools/builtin/pen/usePenTool.ts"],"sourcesContent":["import { useCallback, useMemo, useReducer, useRef, createElement } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport { defineTool } from '../../overlayBinding';\nimport type { Tool } from '../../overlayBinding';\nimport type { ToolPrefGroup } from '../../prefs';\nimport type { Action } from '@weasel-js/routing';\nimport { ActionDisabledReason } from '@weasel-js/routing';\nimport type { ActionDeps, InvocationCtx } from '@weasel-js/routing';\nimport type { AreaSelectDep, EditAnchorsDep, ViewApi } from 'interactions/actions/depSchema';\nimport { pxExtent, withinPxRadius } from 'core/viewport/pxExtent';\nimport { PenIcon } from '../../../icons';\nimport type { PolygonPath } from '@weasel-js/geom';\nimport { anchorsToPath, pathToAnchors } from 'features/paths/anchors';\nimport { nearestPathAnchor, reverseAnchors, type PathAnchorHit } from 'features/paths/nearestAnchor';\nimport type { Op } from 'core/ops/types';\nimport { createInsertOp } from 'core/ops/create';\nimport { createDeleteOp } from 'core/ops/delete';\nimport type { Scene } from 'core/scene/types';\nimport { defaultCommitAdapter } from 'interactions/actions/defaultCommitAdapter';\nimport { constrainTo45 } from '../../../util/constrainTo45';\nimport { cursorFor } from '@weasel-js/cursor';\n\n/**\n * In-progress pen anchor. A click places a corner, with no handles. A drag\n * sets `outHandle` to the drag point and `inHandle` to its mirror through\n * the anchor, which shapes the segment coming into it.\n */\nexport interface PenAnchor {\n x: number;\n y: number;\n outHandle?: { x: number; y: number };\n inHandle?: { x: number; y: number };\n /** True once Alt is held during the placement drag: from then on the\n * in-handle stays where it is, and an anchor dragged with Alt down from\n * the start gets none. */\n altBroken?: boolean;\n}\n\n/** One in-progress subpath of the pen tool: its anchors so far, and whether\n * the user has closed it. */\nexport interface PenSubpath {\n anchors: PenAnchor[]; // LOCAL PenAnchor — in-progress\n closed: boolean;\n}\n\n/**\n * Mutable scratch shared across pen-tool gestures. The hook keeps a stable\n * reference to a single instance and `initScratch` returns it on every call,\n * so click-by-click state survives gesture boundaries and the preview layer\n * can read the same object.\n *\n * ## The pen draws paths; it does not reshape them\n *\n * It draws new nodes, and extends an existing node's open subpath from one\n * of its ends (see `continuing`). Reshaping an existing path is anchor\n * editing: double-click a path to\n * enter edit mode, then use the `editAnchors` / `insertPathAnchor` /\n * `nudgeAnchors` / `deleteAnchors` / `marqueeAnchors` / `cutPathAtAnchor`\n * Actions.\n *\n * The pen used to carry a second, private implementation of all of that,\n * with its own scratch mode, hit-test override, overlay, and undo\n * plumbing. It was reachable only through a `getPathObj` option whose\n * contract required `pose.kind` to be `'polygon'` or `'rect'` — which no\n * kit-created node has, including the ones the pen itself creates. So it\n * was dead in every consumer while shadowing the live implementation, and\n * the two had drifted. Its geometry now lives in\n * `features/paths/anchorEdits.ts`, driven by those Actions.\n */\nexport interface PenScratch {\n finishedSubpaths: PenSubpath[];\n current: PenSubpath | null;\n cursor: { x: number; y: number } | null;\n draggingHandleAt: number | null;\n closeHintActive: boolean;\n /** Set while the pen is extending an existing node's open subpath rather\n * than drawing a new node. `current` then starts as that subpath (turned\n * around when it was picked up by its first anchor), and the commit\n * writes back into the node. */\n continuing: PenContinuation | null;\n}\n\n/** Which existing subpath the pen picked up, and the node's path as it was. */\nexport interface PenContinuation {\n id: string;\n /** Index of the picked-up subpath within `original`. */\n sub: number;\n /** True when it was picked up by its first anchor, so `current` runs\n * backwards and is turned around again on commit. */\n reversed: boolean;\n /** The node's path in world coords at pick-up. */\n original: PolygonPath;\n}\n\n/** Options for `usePenTool`: how a finished path becomes a pose, where it is\n * inserted, and when a closed subpath commits. */\nexport interface UsePenToolOptions<TPose> {\n /** Wrap a finished PolygonPath in the consumer's pose type. */\n wrapPath: (path: PolygonPath, opts: { closed: boolean }) => TPose;\n /** Insert + select adapter. */\n adapter: {\n addNode: (pose: TPose) => string;\n setSelection: (ids: string[]) => void;\n /** Build, without inserting, the node `addNode` would insert for `pose`,\n * so the pen can put it in a larger op batch — a new path finished on\n * another open path's endpoint is inserted and the other path deleted\n * in one undo step. Without it, that click only places an anchor. */\n makeNode?: (pose: TPose) => { id: string };\n };\n /** Auto-select the new object after commit. Default `true`. */\n autoSelect?: boolean;\n /** When true (default), clicking the first anchor to close a subpath\n * commits immediately — the closed region renders with its fill right\n * away. When false, the closed subpath stays in scratch so the user can\n * start another subpath; commit then fires on Enter / tool-switch /\n * ⌘-click / double-click, producing a compound path. */\n autoCommitOnClose?: boolean;\n /** Screen-px hit radius for \"click first anchor to close\". Default `8`.\n * Measured as a screen-space circle, so it stays round under non-uniform\n * zoom; aligns with `useSelectTool.handleHitRadius`. */\n closeHitRadius?: number;\n /** Screen-px radius within which a placed anchor lands exactly on an\n * existing path's anchor. Default `8`; `0` turns it off. Takes precedence\n * over `snapPoint`, which applies only when no anchor is in range. */\n anchorSnapRadius?: number;\n /** Optional point snapper applied to every world-space coordinate the\n * pen records or previews — anchor positions (corner clicks, smooth-\n * drag base point), the rubber-band cursor, and the outgoing-handle\n * target. Receives raw world coords; returns snapped world coords.\n * Wire to `gridSnapStrategy`-style spacing via the consumer:\n *\n * snapPoint: (p) => ({\n * x: Math.round(p.x / SPACING) * SPACING,\n * y: Math.round(p.y / SPACING) * SPACING,\n * })\n *\n * Pen state (anchor handles, etc.) is computed AFTER snapping so the\n * visible geometry stays grid-aligned. */\n snapPoint?: (p: { x: number; y: number }) => { x: number; y: number };\n}\n\nfunction freshScratch(): PenScratch {\n return {\n finishedSubpaths: [],\n current: null,\n cursor: null,\n draggingHandleAt: null,\n closeHintActive: false,\n continuing: null,\n };\n}\n\nfunction resetScratch(s: PenScratch): void {\n s.finishedSubpaths = [];\n s.current = null;\n s.cursor = null;\n s.draggingHandleAt = null;\n s.closeHintActive = false;\n s.continuing = null;\n}\n\n/** Total anchors across finished subpaths plus the in-progress one. */\nfunction anchorCount(s: PenScratch): number {\n return (s.current ? s.current.anchors.length : 0)\n + s.finishedSubpaths.reduce((n, sp) => n + sp.anchors.length, 0);\n}\n\n/** Build a PolygonPath from the pen's accumulated subpaths, by the same\n * rule as any anchor set: segment A→B is a cubic through A's out-handle and\n * B's in-handle when either exists, else a line. */\nfunction buildPath(\n subpaths: PenSubpath[],\n trailing: PenSubpath | null,\n): PolygonPath {\n const all = [...subpaths];\n if (trailing && trailing.anchors.length > 0) all.push(trailing);\n return anchorsToPath(all.map((sp) => sp.anchors), all.map((sp) => sp.closed));\n}\n\n/**\n * Active-slot Tool: click + drag to build a `PolygonPath` Illustrator-style.\n *\n * State machine: Idle / Drawing / BetweenSubpaths (see design doc).\n * Click places a corner anchor; click-drag places an anchor with an\n * outgoing bezier handle; click-the-first-anchor closes the subpath; Enter\n * open-finishes; Esc discards; tool-switch commits if ≥2 anchors else\n * discards. Shift constrains the placement-drag handle to 0/45/90/135°;\n * Alt during drag breaks the handle mirror for the next segment.\n *\n * Reshaping an existing path is not this tool's job — see {@link PenScratch}.\n */\nexport function usePenTool<TPose>(\n options: UsePenToolOptions<TPose>,\n): Tool<PenScratch> {\n const { wrapPath, adapter, autoSelect = true, autoCommitOnClose = true, closeHitRadius = 8, anchorSnapRadius = 8, snapPoint } = options;\n\n // Persistent scratch: single ref reused across gestures so multi-click\n // state survives the dispatcher's per-gesture initScratch contract.\n const scratchRef = useRef<PenScratch | null>(null);\n if (scratchRef.current === null) scratchRef.current = freshScratch();\n\n // Latest options stashed so handlers see fresh values without rebuilding\n // the Tool record (which would lose scratch identity in the dispatcher).\n const optsRef = useLatest({ wrapPath, adapter, autoSelect, autoCommitOnClose, closeHitRadius, anchorSnapRadius, snapPoint });\n\n // Scratch is a mutable ref (so click-by-click state survives the\n // dispatcher's per-gesture initScratch contract). Mutations alone don't\n // trigger React re-renders, so Canvas never re-paints and the preview\n // layer stays invisible until something else (e.g. commit) bumps host\n // state. Force a render after every scratch mutation so the host's\n // <Canvas layers={{...}}> literal gets a new identity and the paint\n // useEffect fires. Pull the trigger via ref so the memoized Tool record\n // doesn't need to rebuild.\n const [, forceRender] = useReducer((x: number) => x + 1, 0);\n\n // Geometry helpers shared by the actions below. They read `optsRef` so a\n // re-render with new options is visible without rebuilding the actions\n // (which would lose the scratch identity `penPreviewLayer` reads through\n // `Tool.initScratch`).\n\n /** Write the pen's subpaths back into the node it picked up: the first\n * replaces the subpath it continued, any later ones are appended. Skips\n * the write when nothing changed, so an empty pick-up leaves no undo\n * entry. */\n const commitContinuation = useCallback(\n (s: PenScratch, trailing: PenSubpath | null, edit: EditAnchorsDep | undefined): void => {\n const cont = s.continuing!;\n const drawn = [...s.finishedSubpaths, ...(trailing ? [trailing] : [])];\n if (!edit || drawn.length === 0) return;\n const { anchors: added, closed: addedClosed } = pathToAnchors(buildPath(drawn, null));\n if (cont.reversed) added[0] = reverseAnchors(added[0]);\n const set = pathToAnchors(cont.original);\n set.anchors.splice(cont.sub, 1, added[0]);\n set.closed.splice(cont.sub, 1, addedClosed[0]);\n set.anchors.push(...added.slice(1));\n set.closed.push(...addedClosed.slice(1));\n const next: PolygonPath = { ...anchorsToPath(set.anchors, set.closed), fillRule: cont.original.fillRule };\n if (samePath(next, cont.original)) return;\n edit.applyEdit(cont.id, next, 'Continue path');\n if (optsRef.current.autoSelect) optsRef.current.adapter.setSelection([cont.id]);\n },\n [optsRef],\n );\n\n const commit = useCallback((s: PenScratch, deps: ActionDeps): void => {\n const trailing = s.current && s.current.anchors.length > 0 ? s.current : null;\n if (s.continuing) {\n commitContinuation(s, trailing, deps.editAnchors as EditAnchorsDep | undefined);\n resetScratch(s);\n return;\n }\n if (s.finishedSubpaths.length === 0 && !trailing) return;\n const allClosed =\n s.finishedSubpaths.every((sp) => sp.closed) &&\n (trailing === null || trailing.closed);\n const path = buildPath(s.finishedSubpaths, trailing);\n const pose = optsRef.current.wrapPath(path, { closed: allClosed });\n const id = optsRef.current.adapter.addNode(pose);\n if (optsRef.current.autoSelect) optsRef.current.adapter.setSelection([id]);\n resetScratch(s);\n }, [commitContinuation, optsRef]);\n\n /** Whether an anchor about to land on `hit` finishes the path by joining\n * onto another open subpath. Its own continued subpath never counts —\n * reaching that one's far end is a close. Declines when the deps can't\n * commit the join as one op batch. */\n const joinable = useCallback((s: PenScratch, deps: ActionDeps, hit: PathAnchorHit | null): hit is PathAnchorHit => {\n if (!hit || hit.end === null || !s.current || s.current.anchors.length === 0) return false;\n const cont = s.continuing;\n if (cont && hit.id === cont.id && hit.sub === cont.sub) return false;\n const scene = deps.scene as Scene<unknown, string, unknown> | undefined;\n if (!scene && !deps.applyOps) return false;\n if (!(cont ? (deps.editAnchors as EditAnchorsDep).editOps : optsRef.current.adapter.makeNode)) return false;\n return cont?.id === hit.id || scene?.get(hit.id as never) !== undefined;\n }, [optsRef]);\n\n /** Finish the path onto `hit`, the endpoint its last anchor was just placed\n * on: the other subpath's anchors follow on from it, turned around when\n * `hit` is its last anchor, and its node is deleted. The path being drawn\n * keeps its identity — a continued node keeps its id and style, and a new\n * path is minted the way any pen path is. One op batch, so one undo. */\n const commitJoin = useCallback((s: PenScratch, deps: ActionDeps, hit: PathAnchorHit): void => {\n const edit = deps.editAnchors as EditAnchorsDep;\n const scene = deps.scene as Scene<unknown, string, unknown> | undefined;\n const current = s.current!;\n const { anchors: added, closed: addedClosed } = pathToAnchors(buildPath([...s.finishedSubpaths, current], null));\n\n const other = edit.getEditablePath(hit.id) as PolygonPath;\n const otherSet = pathToAnchors(other);\n const tail = hit.end === 'last' ? reverseAnchors(otherSet.anchors[hit.sub]) : otherSet.anchors[hit.sub];\n const joined = added[added.length - 1];\n const joint = joined[joined.length - 1];\n const out = current.anchors[current.anchors.length - 1].outHandle ?? tail[0].outHandle;\n if (out) joint.outHandle = { ...out };\n joined.push(...tail.slice(1));\n\n const cont = s.continuing;\n const sameNode = cont?.id === hit.id;\n const rest = sameNode ? { anchors: [], closed: [] } : {\n anchors: otherSet.anchors.filter((_, i) => i !== hit.sub),\n closed: otherSet.closed.filter((_, i) => i !== hit.sub),\n };\n const label = 'Join paths';\n const ops: Op[] = [];\n let keptId: string;\n if (cont) {\n if (cont.reversed) added[0] = reverseAnchors(added[0]);\n const set = pathToAnchors(cont.original);\n set.anchors.splice(cont.sub, 1, added[0]);\n set.closed.splice(cont.sub, 1, addedClosed[0]);\n set.anchors.push(...added.slice(1), ...rest.anchors);\n set.closed.push(...addedClosed.slice(1), ...rest.closed);\n if (sameNode) {\n set.anchors.splice(hit.sub, 1);\n set.closed.splice(hit.sub, 1);\n }\n const next: PolygonPath = { ...anchorsToPath(set.anchors, set.closed), fillRule: cont.original.fillRule };\n ops.push(...edit.editOps!(cont.id, next, label));\n keptId = cont.id;\n } else {\n const path = anchorsToPath([...added, ...rest.anchors], [...addedClosed, ...rest.closed]);\n const node = optsRef.current.adapter.makeNode!(optsRef.current.wrapPath(path, { closed: false }));\n ops.push(createInsertOp({ node }));\n keptId = node.id;\n }\n if (!sameNode) ops.push(createDeleteOp({ node: scene!.get(hit.id as never) as { id: string } }));\n\n const applyOps = deps.applyOps as ((ops: Op[], label: string) => void) | undefined;\n if (applyOps) applyOps(ops, label);\n else scene!.applyBatch(ops, label, defaultCommitAdapter(scene!));\n if (optsRef.current.autoSelect) optsRef.current.adapter.setSelection([keptId]);\n resetScratch(s);\n }, [optsRef]);\n\n /** From idle, a press on an open subpath's end anchor picks that subpath\n * up: it becomes `current`, turned so the pressed end is last. Returns\n * whether it did. */\n const pickUpEndpoint = useCallback((deps: ActionDeps, wx: number, wy: number): boolean => {\n const scratch = scratchRef.current!;\n if (scratch.current || scratch.finishedSubpaths.length > 0) return false;\n const radius = optsRef.current.closeHitRadius;\n const scale = viewScale(deps);\n const hit = nearestPathAnchor(pathsNear(deps, wx, wy, radius), { x: wx, y: wy }, radius, scale, (h) => h.end !== null);\n if (!hit) return false;\n const original = (deps.editAnchors as EditAnchorsDep).getEditablePath(hit.id) as PolygonPath;\n const picked = pathToAnchors(original).anchors[hit.sub];\n const reversed = hit.end === 'first' && picked.length > 1;\n scratch.current = { anchors: reversed ? reverseAnchors(picked) : picked, closed: false };\n scratch.continuing = { id: hit.id, sub: hit.sub, reversed, original };\n return true;\n }, [optsRef]);\n\n const snap = useCallback((x: number, y: number): { x: number; y: number } => {\n const fn = optsRef.current.snapPoint;\n return fn ? fn({ x, y }) : { x, y };\n }, [optsRef]);\n\n /** Where an anchor pressed at `(x, y)` lands: on an existing path's anchor\n * when one is within `anchorSnapRadius`, else wherever `snapPoint` puts it.\n * `hit` names the anchor it snapped to. */\n const placeAt = useCallback((deps: ActionDeps, x: number, y: number): { x: number; y: number; hit: PathAnchorHit | null } => {\n const radius = optsRef.current.anchorSnapRadius;\n if (radius > 0) {\n const hit = nearestPathAnchor(pathsNear(deps, x, y, radius), { x, y }, radius, viewScale(deps));\n if (hit) return { x: hit.x, y: hit.y, hit };\n }\n return { ...snap(x, y), hit: null };\n }, [snap, optsRef]);\n\n /** Is a world point within the close-hit radius of `(ax, ay)`? Measured as a\n * screen-space circle, so the zone stays round under non-uniform zoom. */\n const withinCloseRadius = useCallback(\n (deps: ActionDeps, ax: number, ay: number, wx: number, wy: number): boolean => {\n return withinPxRadius(ax - wx, ay - wy, optsRef.current.closeHitRadius, viewScale(deps));\n },\n [optsRef],\n );\n\n /**\n * Actions the pen owns, registered by `useToolActions` from inside the\n * ActionsProvider — see `ToolDef.actions`.\n *\n * They close over `scratchRef`, which is why the pen needs no `Selector`\n * exposing tool scratch to the action layer: the state never leaves the\n * hook. `useMemo([])` keeps their identity stable for the same reason\n * `scratchRef` is a ref — the preview layer reads that one object.\n */\n const actions = useMemo<Action[]>(() => {\n const s = () => scratchRef.current!;\n\n return [\n {\n id: 'pen.placeAnchor',\n label: 'Pen — place anchor',\n eligible: { capability: 'creates-paths' },\n requires: ['view', 'areaSelect', 'editAnchors', 'scene', 'applyOps'],\n invoker: {\n timing: 'immediate' as const,\n run: (deps, params) => {\n const p = params as { pressX?: number; pressY?: number } | undefined;\n if (p?.pressX === undefined || p.pressY === undefined) return;\n // The press point, not the release: a click may drift up to the\n // drag threshold, and an anchor should land where you put the\n // pointer down. Snapping happens here rather than at press time\n // so the snapped value is what the geometry records.\n if (pickUpEndpoint(deps, p.pressX, p.pressY)) {\n forceRender();\n return;\n }\n const { x: wx, y: wy, hit } = placeAt(deps, p.pressX, p.pressY);\n const scratch = s();\n\n // Close-on-first-anchor (>= 3 anchors). With `autoCommitOnClose`\n // on (default) this commits right away so the closed region\n // renders with its fill — Illustrator-style. With it off the\n // closed subpath stays in scratch for compound-path builds and\n // commits on Enter / tool switch.\n if (scratch.current && scratch.current.anchors.length >= 3) {\n const first = scratch.current.anchors[0];\n if (withinCloseRadius(deps, first.x, first.y, wx, wy)) {\n scratch.current.closed = true;\n scratch.finishedSubpaths.push(scratch.current);\n scratch.current = null;\n scratch.closeHintActive = false;\n if (optsRef.current.autoCommitOnClose) commit(scratch, deps);\n forceRender();\n return;\n }\n }\n\n const join = joinable(scratch, deps, hit);\n // Otherwise: append a corner anchor, starting a subpath if needed.\n if (!scratch.current) scratch.current = { anchors: [], closed: false };\n scratch.current.anchors.push({ x: wx, y: wy });\n if (join) commitJoin(scratch, deps, hit);\n forceRender();\n },\n },\n },\n\n {\n id: 'pen.finishOpen',\n label: 'Pen — finish open path',\n eligible: { capability: 'creates-paths' },\n requires: ['editAnchors'],\n // Needs an actual path to commit. Declining also lets the gesture\n // fall through: a ⌘-click with fewer than two anchors reaches the\n // plain-click binding and places an anchor instead.\n enabled: () => (anchorCount(s()) >= 2 ? true : ActionDisabledReason.NotApplicable),\n invoker: {\n timing: 'immediate' as const,\n run: (deps, params) => {\n const scratch = s();\n const p = params as { viaDoubleClick?: boolean } | undefined;\n\n // A double click arrives AFTER both of its clicks, so the second\n // one has already appended an anchor. Undo exactly that before\n // committing: pop it, and drop the subpath entirely if it was the\n // only anchor in it (which happens when the FIRST click closed a\n // subpath and the second started a fresh one). The net effect\n // matches the pen's old private 300ms detector — one anchor\n // placed, path finished — while using the dispatcher's single\n // definition of a double click.\n //\n // No position check is needed: the anchor to drop is by\n // construction the one the second click just appended.\n if (p?.viaDoubleClick && scratch.current) {\n scratch.current.anchors.pop();\n if (scratch.current.anchors.length === 0) scratch.current = null;\n }\n\n commit(scratch, deps);\n forceRender();\n },\n },\n },\n\n {\n id: 'pen.dragHandle',\n label: 'Pen — drag out a bezier handle',\n eligible: { capability: 'creates-paths' },\n requires: ['view', 'areaSelect', 'editAnchors', 'scene', 'applyOps'],\n invoker: {\n timing: 'ongoing' as const,\n start: (ctx: InvocationCtx) => {\n const scratch = s();\n // `drag.start` is the pointerdown position — the dispatcher\n // buffers the press and releases it at the drag threshold, so the\n // anchor lands where the user pressed rather than where the\n // threshold happened to be crossed.\n const origin = ctx.drag?.start ?? ctx.world;\n // Dragging from an open path's end picks the path up and pulls\n // that end's own handle rather than placing a new anchor on it.\n const pickedUp = pickUpEndpoint(ctx.deps, origin.x, origin.y);\n // Dragging on another open path's endpoint joins onto it once\n // the handle is set.\n let joinHit: PathAnchorHit | null = null;\n if (!pickedUp) {\n const { x: ax, y: ay, hit } = placeAt(ctx.deps, origin.x, origin.y);\n if (joinable(scratch, ctx.deps, hit)) joinHit = hit;\n if (!scratch.current) scratch.current = { anchors: [], closed: false };\n scratch.current.anchors.push({ x: ax, y: ay });\n }\n scratch.draggingHandleAt = scratch.current!.anchors.length - 1;\n if (ctx.modifiers.alt) {\n scratch.current!.anchors[scratch.draggingHandleAt].altBroken = true;\n }\n // A picked-up endpoint's in-handle belongs to the existing\n // segment into it, which the pen does not reshape.\n const linked = !pickedUp;\n applyOutHandle(scratch, ctx.world, ctx.modifiers.shift, linked, optsRef.current.snapPoint);\n forceRender();\n\n return {\n onMove: (moveCtx: InvocationCtx) => {\n const sm = s();\n if (sm.draggingHandleAt === null) return;\n if (moveCtx.modifiers.alt && sm.current) {\n sm.current.anchors[sm.draggingHandleAt].altBroken = true;\n }\n applyOutHandle(sm, moveCtx.world, moveCtx.modifiers.shift, linked, optsRef.current.snapPoint);\n forceRender();\n },\n onEnd: (endCtx: InvocationCtx, reason: 'commit' | 'cancel') => {\n const se = s();\n if (reason === 'cancel') {\n // `start` appended the anchor (or picked a path up); a\n // cancel (pointercancel is the reachable one — Escape and a\n // tool switch both reset the whole scratch first) has to\n // take it back out.\n if (pickedUp) {\n resetScratch(se);\n } else if (se.current && se.draggingHandleAt === se.current.anchors.length - 1) {\n se.current.anchors.pop();\n if (se.current.anchors.length === 0) se.current = null;\n }\n se.draggingHandleAt = null;\n forceRender();\n return;\n }\n if (se.draggingHandleAt !== null) {\n if (endCtx.modifiers.alt && se.current) {\n se.current.anchors[se.draggingHandleAt].altBroken = true;\n }\n applyOutHandle(se, endCtx.world, endCtx.modifiers.shift, linked, optsRef.current.snapPoint);\n se.draggingHandleAt = null;\n }\n if (joinHit && se.current) commitJoin(se, ctx.deps, joinHit);\n forceRender();\n },\n };\n },\n },\n },\n\n {\n id: 'pen.finish',\n label: 'Pen — finish path',\n eligible: { capability: 'creates-paths' },\n requires: ['editAnchors'],\n enabled: () => (anchorCount(s()) > 0 ? true : ActionDisabledReason.NotApplicable),\n invoker: {\n timing: 'immediate' as const,\n run: (deps) => {\n commit(s(), deps);\n forceRender();\n },\n },\n },\n\n {\n id: 'pen.cancel',\n label: 'Pen — discard path',\n eligible: { capability: 'creates-paths' },\n // Declining with nothing drawn is what keeps Escape's first-match\n // ladder intact: an empty pen passes the key to `escape`, which goes\n // on to clear the selection / return to the default tool.\n enabled: () => {\n const scratch = s();\n return scratch.current !== null || scratch.finishedSubpaths.length > 0\n ? true\n : ActionDisabledReason.NotApplicable;\n },\n invoker: {\n timing: 'immediate' as const,\n run: () => {\n resetScratch(s());\n forceRender();\n },\n },\n },\n ];\n }, [commit, placeAt, withinCloseRadius, pickUpEndpoint, joinable, commitJoin, optsRef]);\n\n return useMemo(() => {\n return defineTool<PenScratch>({\n id: 'pen',\n capabilities: ['creates-paths'],\n hookName: 'usePenTool',\n // Reads the tool's own ref rather than a scratch handed in by a\n // dispatcher — the pen owns this state and nothing else needs it.\n cursor: () =>\n scratchRef.current?.closeHintActive\n ? 'pointer'\n : cursorFor('pen', { fallback: 'crosshair' }),\n presentation: {\n label: 'Pen',\n icon: createElement(PenIcon),\n group: 'draw',\n },\n // Persistent-ref scratch. `penPreviewLayer` reads the in-progress path\n // by calling `Tool.initScratch()` and getting this same object back, so\n // the identity is load-bearing.\n initScratch: () => scratchRef.current!,\n actions,\n\n onDeactivate: () => {\n // Anything still in scratch is by definition incomplete — the user\n // hasn't closed it (close-on-first-anchor), open-finished it\n // (cmd-click), or pressed Enter. Switching tools mid-path should\n // discard, not auto-commit a stub polyline that the user didn't\n // ask for. Mirrors Escape's behavior so \"stop drawing\" is\n // consistent across exits.\n resetScratch(scratchRef.current!);\n forceRender();\n },\n\n bindings: [\n // Plain click places an anchor; ⌘/Ctrl-click open-finishes\n // (Illustrator). Strict modifier matching keeps the two apart, and\n // `mod` resolves to meta on mac / ctrl elsewhere — the tool-route\n // grammar's modifier matcher accepted either key on either platform.\n { spec: { kind: 'click' as const, mods: { shift: 'optional' as const, alt: 'optional' as const } }, actionId: 'pen.placeAnchor' },\n { spec: { kind: 'click' as const, mods: { mod: true, shift: 'optional' as const, alt: 'optional' as const } }, actionId: 'pen.finishOpen' },\n // Double-click on the last anchor also open-finishes. This used to be\n // a private 300ms/radius check inside the click route — a fourth\n // double-click detector in a codebase that had just collapsed three\n // into one (audit 3.3).\n {\n spec: { kind: 'doubleClick' as const, mods: { shift: 'optional' as const, alt: 'optional' as const } },\n actionId: 'pen.finishOpen',\n opts: { params: { viaDoubleClick: true } },\n },\n { spec: { kind: 'drag' as const, mods: { shift: 'optional' as const, alt: 'optional' as const } }, actionId: 'pen.dragHandle' },\n { spec: { kind: 'key' as const, key: 'Enter' }, actionId: 'pen.finish' },\n { spec: { kind: 'key' as const, key: 'Escape' }, actionId: 'pen.cancel' },\n ],\n });\n }, [actions]);\n}\n\nusePenTool.prefs = {\n name: 'Pen',\n description: 'Pen-tool behavior.',\n children: {\n autoCommitOnClose: {\n kind: 'boolean',\n name: 'Auto-commit pen on close',\n description: \"When you click the pen tool's first anchor to close a region, commit immediately so it renders with its fill. Off: keep the path in preview until you press Enter (lets you build a compound path from multiple closed subpaths).\",\n default: true,\n },\n },\n} satisfies ToolPrefGroup;\n\n/** Point the anchor's outgoing handle at `target`, optionally snapped and\n * optionally constrained to 45° steps. When `linked` and not Alt-broken,\n * the in-handle follows as its mirror through the anchor. */\nfunction applyOutHandle<S extends PenScratch>(\n s: S,\n target: { x: number; y: number },\n shift: boolean,\n linked: boolean,\n snap?: (p: { x: number; y: number }) => { x: number; y: number },\n): void {\n if (s.current === null || s.draggingHandleAt === null) return;\n const anchor = s.current.anchors[s.draggingHandleAt];\n const to = snap ? snap(target) : target;\n let dx = to.x - anchor.x;\n let dy = to.y - anchor.y;\n if (shift) {\n const c = constrainTo45(dx, dy);\n dx = c.dx;\n dy = c.dy;\n }\n anchor.outHandle = { x: anchor.x + dx, y: anchor.y + dy };\n if (linked && !anchor.altBroken) anchor.inHandle = { x: anchor.x - dx, y: anchor.y - dy };\n}\n\nfunction viewScale(deps: ActionDeps): { x: number; y: number } {\n return (deps.view as ViewApi | undefined)?.get().scale ?? { x: 1, y: 1 };\n}\n\n/** Existing editable paths, world coords, with ink within `px` screen\n * pixels of `(wx, wy)` — front-to-back, as the area query returns them.\n * Empty when the host publishes no `areaSelect` or `editAnchors` dep. */\nfunction pathsNear(\n deps: ActionDeps,\n wx: number,\n wy: number,\n px: number,\n): Array<{ id: string; path: PolygonPath }> {\n const area = deps.areaSelect as AreaSelectDep | undefined;\n const edit = deps.editAnchors as EditAnchorsDep | undefined;\n if (!area || !edit) return [];\n const view = deps.view as ViewApi | undefined;\n const e = pxExtent(px, viewScale(deps));\n const ids = area.hitTestArea({ x: wx - e.x, y: wy - e.y, width: 2 * e.x, height: 2 * e.y }, view);\n const out: Array<{ id: string; path: PolygonPath }> = [];\n for (const id of ids) {\n const path = edit.getEditablePath(id) as PolygonPath | null;\n if (path?.kind === 'polygon') out.push({ id, path });\n }\n return out;\n}\n\nfunction samePath(a: PolygonPath, b: PolygonPath): boolean {\n if (a.commands.length !== b.commands.length || a.coords.length !== b.coords.length) return false;\n for (let i = 0; i < a.commands.length; i++) if (a.commands[i] !== b.commands[i]) return false;\n for (let i = 0; i < a.coords.length; i++) if (a.coords[i] !== b.coords[i]) return false;\n return true;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"usePenTool.js","names":["p","PenIcon","freshScratch","resetScratch","anchorCount","s","n","sp","buildPath","all","subpaths","trailing","anchorsToPath","usePenTool","options","scratchRef","useRef","optsRef","useLatest","forceRender","useReducer","x","commitContinuation","useCallback","cont","drawn","edit","added","addedClosed","pathToAnchors","reverseAnchors","set","next","samePath","commit","deps","allClosed","path","pose","id","joinable","hit","scene","commitJoin","current","other","otherSet","tail","joined","joint","out","sameNode","rest","i","label","ops","keptId","node","createInsertOp","createDeleteOp","applyOps","defaultCommitAdapter","pickUpEndpoint","scratch","radius","scale","viewScale","nearestPathAnchor","pathsNear","wx","wy","h","original","picked","reversed","snap","fn","placeAt","y","withinCloseRadius","withinPxRadius","ax","ay","actions","useMemo","p","params","first","join","ActionDisabledReason","origin","ctx","pickedUp","joinHit","linked","applyOutHandle","sm","moveCtx","se","reason","endCtx","defineTool","cursorFor","createElement","anchor","to","target","dx","dy","shift","c","constrainTo45","area","view","e","pxExtent","px","ids","a","b"],"sources":["../../../../src/tools/builtin/pen/usePenTool.ts"],"sourcesContent":["import { useCallback, useMemo, useReducer, useRef, createElement } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport { defineTool } from '../../overlayBinding';\nimport type { Tool } from '../../overlayBinding';\nimport type { ToolPrefGroup } from '../../prefs';\nimport type { Action } from '@weasel-js/routing';\nimport { ActionDisabledReason } from '@weasel-js/routing';\nimport type { ActionDeps, InvocationCtx } from '@weasel-js/routing';\nimport type { EditAnchorsDep } from 'interactions/actions/depSchema';\nimport { pxExtent, withinPxRadius } from 'core/viewport/pxExtent';\nimport { PenIcon } from '../../../icons';\nimport type { PolygonPath } from '@weasel-js/geom';\nimport { anchorsToPath, pathToAnchors } from 'features/paths/anchors';\nimport { nearestPathAnchor, reverseAnchors, type PathAnchorHit } from 'features/paths/nearestAnchor';\nimport type { Op } from 'core/ops/types';\nimport { createInsertOp } from 'core/ops/create';\nimport { createDeleteOp } from 'core/ops/delete';\nimport { defaultCommitAdapter } from 'interactions/actions/defaultCommitAdapter';\nimport { constrainTo45 } from '../../../util/constrainTo45';\nimport { cursorFor } from '@weasel-js/cursor';\n\n/**\n * In-progress pen anchor. A click places a corner, with no handles. A drag\n * sets `outHandle` to the drag point and `inHandle` to its mirror through\n * the anchor, which shapes the segment coming into it.\n */\nexport interface PenAnchor {\n x: number;\n y: number;\n outHandle?: { x: number; y: number };\n inHandle?: { x: number; y: number };\n /** True once Alt is held during the placement drag: from then on the\n * in-handle stays where it is, and an anchor dragged with Alt down from\n * the start gets none. */\n altBroken?: boolean;\n}\n\n/** One in-progress subpath of the pen tool: its anchors so far, and whether\n * the user has closed it. */\nexport interface PenSubpath {\n anchors: PenAnchor[]; // LOCAL PenAnchor — in-progress\n closed: boolean;\n}\n\n/**\n * Mutable scratch shared across pen-tool gestures. The hook keeps a stable\n * reference to a single instance and `initScratch` returns it on every call,\n * so click-by-click state survives gesture boundaries and the preview layer\n * can read the same object.\n *\n * ## The pen draws paths; it does not reshape them\n *\n * It draws new nodes, and extends an existing node's open subpath from one\n * of its ends (see `continuing`). Reshaping an existing path is anchor\n * editing: double-click a path to\n * enter edit mode, then use the `editAnchors` / `insertPathAnchor` /\n * `nudgeAnchors` / `deleteAnchors` / `marqueeAnchors` / `cutPathAtAnchor`\n * Actions.\n *\n * The pen used to carry a second, private implementation of all of that,\n * with its own scratch mode, hit-test override, overlay, and undo\n * plumbing. It was reachable only through a `getPathObj` option whose\n * contract required `pose.kind` to be `'polygon'` or `'rect'` — which no\n * kit-created node has, including the ones the pen itself creates. So it\n * was dead in every consumer while shadowing the live implementation, and\n * the two had drifted. Its geometry now lives in\n * `features/paths/anchorEdits.ts`, driven by those Actions.\n */\nexport interface PenScratch {\n finishedSubpaths: PenSubpath[];\n current: PenSubpath | null;\n cursor: { x: number; y: number } | null;\n draggingHandleAt: number | null;\n closeHintActive: boolean;\n /** Set while the pen is extending an existing node's open subpath rather\n * than drawing a new node. `current` then starts as that subpath (turned\n * around when it was picked up by its first anchor), and the commit\n * writes back into the node. */\n continuing: PenContinuation | null;\n}\n\n/** Which existing subpath the pen picked up, and the node's path as it was. */\nexport interface PenContinuation {\n id: string;\n /** Index of the picked-up subpath within `original`. */\n sub: number;\n /** True when it was picked up by its first anchor, so `current` runs\n * backwards and is turned around again on commit. */\n reversed: boolean;\n /** The node's path in world coords at pick-up. */\n original: PolygonPath;\n}\n\n/** Options for `usePenTool`: how a finished path becomes a pose, where it is\n * inserted, and when a closed subpath commits. */\nexport interface UsePenToolOptions<TPose> {\n /** Wrap a finished PolygonPath in the consumer's pose type. */\n wrapPath: (path: PolygonPath, opts: { closed: boolean }) => TPose;\n /** Insert + select adapter. */\n adapter: {\n addNode: (pose: TPose) => string;\n setSelection: (ids: string[]) => void;\n /** Build, without inserting, the node `addNode` would insert for `pose`,\n * so the pen can put it in a larger op batch — a new path finished on\n * another open path's endpoint is inserted and the other path deleted\n * in one undo step. Without it, that click only places an anchor. */\n makeNode?: (pose: TPose) => { id: string };\n };\n /** Auto-select the new object after commit. Default `true`. */\n autoSelect?: boolean;\n /** When true (default), clicking the first anchor to close a subpath\n * commits immediately — the closed region renders with its fill right\n * away. When false, the closed subpath stays in scratch so the user can\n * start another subpath; commit then fires on Enter / tool-switch /\n * ⌘-click / double-click, producing a compound path. */\n autoCommitOnClose?: boolean;\n /** Screen-px hit radius for \"click first anchor to close\". Default `8`.\n * Measured as a screen-space circle, so it stays round under non-uniform\n * zoom; aligns with `useSelectTool.handleHitRadius`. */\n closeHitRadius?: number;\n /** Screen-px radius within which a placed anchor lands exactly on an\n * existing path's anchor. Default `8`; `0` turns it off. Takes precedence\n * over `snapPoint`, which applies only when no anchor is in range. */\n anchorSnapRadius?: number;\n /** Optional point snapper applied to every world-space coordinate the\n * pen records or previews — anchor positions (corner clicks, smooth-\n * drag base point), the rubber-band cursor, and the outgoing-handle\n * target. Receives raw world coords; returns snapped world coords.\n * Wire to `gridSnapStrategy`-style spacing via the consumer:\n *\n * snapPoint: (p) => ({\n * x: Math.round(p.x / SPACING) * SPACING,\n * y: Math.round(p.y / SPACING) * SPACING,\n * })\n *\n * Pen state (anchor handles, etc.) is computed AFTER snapping so the\n * visible geometry stays grid-aligned. */\n snapPoint?: (p: { x: number; y: number }) => { x: number; y: number };\n}\n\nfunction freshScratch(): PenScratch {\n return {\n finishedSubpaths: [],\n current: null,\n cursor: null,\n draggingHandleAt: null,\n closeHintActive: false,\n continuing: null,\n };\n}\n\nfunction resetScratch(s: PenScratch): void {\n s.finishedSubpaths = [];\n s.current = null;\n s.cursor = null;\n s.draggingHandleAt = null;\n s.closeHintActive = false;\n s.continuing = null;\n}\n\n/** Total anchors across finished subpaths plus the in-progress one. */\nfunction anchorCount(s: PenScratch): number {\n return (s.current ? s.current.anchors.length : 0)\n + s.finishedSubpaths.reduce((n, sp) => n + sp.anchors.length, 0);\n}\n\n/** Build a PolygonPath from the pen's accumulated subpaths, by the same\n * rule as any anchor set: segment A→B is a cubic through A's out-handle and\n * B's in-handle when either exists, else a line. */\nfunction buildPath(\n subpaths: PenSubpath[],\n trailing: PenSubpath | null,\n): PolygonPath {\n const all = [...subpaths];\n if (trailing && trailing.anchors.length > 0) all.push(trailing);\n return anchorsToPath(all.map((sp) => sp.anchors), all.map((sp) => sp.closed));\n}\n\n/**\n * Active-slot Tool: click + drag to build a `PolygonPath` Illustrator-style.\n *\n * State machine: Idle / Drawing / BetweenSubpaths (see design doc).\n * Click places a corner anchor; click-drag places an anchor with an\n * outgoing bezier handle; click-the-first-anchor closes the subpath; Enter\n * open-finishes; Esc discards; tool-switch commits if ≥2 anchors else\n * discards. Shift constrains the placement-drag handle to 0/45/90/135°;\n * Alt during drag breaks the handle mirror for the next segment.\n *\n * Reshaping an existing path is not this tool's job — see {@link PenScratch}.\n */\nexport function usePenTool<TPose>(\n options: UsePenToolOptions<TPose>,\n): Tool<PenScratch> {\n const { wrapPath, adapter, autoSelect = true, autoCommitOnClose = true, closeHitRadius = 8, anchorSnapRadius = 8, snapPoint } = options;\n\n // Persistent scratch: single ref reused across gestures so multi-click\n // state survives the dispatcher's per-gesture initScratch contract.\n const scratchRef = useRef<PenScratch | null>(null);\n if (scratchRef.current === null) scratchRef.current = freshScratch();\n\n // Latest options stashed so handlers see fresh values without rebuilding\n // the Tool record (which would lose scratch identity in the dispatcher).\n const optsRef = useLatest({ wrapPath, adapter, autoSelect, autoCommitOnClose, closeHitRadius, anchorSnapRadius, snapPoint });\n\n // Scratch is a mutable ref (so click-by-click state survives the\n // dispatcher's per-gesture initScratch contract). Mutations alone don't\n // trigger React re-renders, so Canvas never re-paints and the preview\n // layer stays invisible until something else (e.g. commit) bumps host\n // state. Force a render after every scratch mutation so the host's\n // <Canvas layers={{...}}> literal gets a new identity and the paint\n // useEffect fires. Pull the trigger via ref so the memoized Tool record\n // doesn't need to rebuild.\n const [, forceRender] = useReducer((x: number) => x + 1, 0);\n\n // Geometry helpers shared by the actions below. They read `optsRef` so a\n // re-render with new options is visible without rebuilding the actions\n // (which would lose the scratch identity `penPreviewLayer` reads through\n // `Tool.initScratch`).\n\n /** Write the pen's subpaths back into the node it picked up: the first\n * replaces the subpath it continued, any later ones are appended. Skips\n * the write when nothing changed, so an empty pick-up leaves no undo\n * entry. */\n const commitContinuation = useCallback(\n (s: PenScratch, trailing: PenSubpath | null, edit: EditAnchorsDep | undefined): void => {\n const cont = s.continuing!;\n const drawn = [...s.finishedSubpaths, ...(trailing ? [trailing] : [])];\n if (!edit || drawn.length === 0) return;\n const { anchors: added, closed: addedClosed } = pathToAnchors(buildPath(drawn, null));\n if (cont.reversed) added[0] = reverseAnchors(added[0]);\n const set = pathToAnchors(cont.original);\n set.anchors.splice(cont.sub, 1, added[0]);\n set.closed.splice(cont.sub, 1, addedClosed[0]);\n set.anchors.push(...added.slice(1));\n set.closed.push(...addedClosed.slice(1));\n const next: PolygonPath = { ...anchorsToPath(set.anchors, set.closed), fillRule: cont.original.fillRule };\n if (samePath(next, cont.original)) return;\n edit.applyEdit(cont.id, next, 'Continue path');\n if (optsRef.current.autoSelect) optsRef.current.adapter.setSelection([cont.id]);\n },\n [optsRef],\n );\n\n const commit = useCallback((s: PenScratch, deps: ActionDeps): void => {\n const trailing = s.current && s.current.anchors.length > 0 ? s.current : null;\n if (s.continuing) {\n commitContinuation(s, trailing, deps.editAnchors);\n resetScratch(s);\n return;\n }\n if (s.finishedSubpaths.length === 0 && !trailing) return;\n const allClosed =\n s.finishedSubpaths.every((sp) => sp.closed) &&\n (trailing === null || trailing.closed);\n const path = buildPath(s.finishedSubpaths, trailing);\n const pose = optsRef.current.wrapPath(path, { closed: allClosed });\n const id = optsRef.current.adapter.addNode(pose);\n if (optsRef.current.autoSelect) optsRef.current.adapter.setSelection([id]);\n resetScratch(s);\n }, [commitContinuation, optsRef]);\n\n /** Whether an anchor about to land on `hit` finishes the path by joining\n * onto another open subpath. Its own continued subpath never counts —\n * reaching that one's far end is a close. Declines when the deps can't\n * commit the join as one op batch. */\n const joinable = useCallback((s: PenScratch, deps: ActionDeps, hit: PathAnchorHit | null): hit is PathAnchorHit => {\n if (!hit || hit.end === null || !s.current || s.current.anchors.length === 0) return false;\n const cont = s.continuing;\n if (cont && hit.id === cont.id && hit.sub === cont.sub) return false;\n const scene = deps.scene;\n if (!scene && !deps.applyOps) return false;\n if (!(cont ? deps.editAnchors?.editOps : optsRef.current.adapter.makeNode)) return false;\n return cont?.id === hit.id || scene?.get(hit.id as never) !== undefined;\n }, [optsRef]);\n\n /** Finish the path onto `hit`, the endpoint its last anchor was just placed\n * on: the other subpath's anchors follow on from it, turned around when\n * `hit` is its last anchor, and its node is deleted. The path being drawn\n * keeps its identity — a continued node keeps its id and style, and a new\n * path is minted the way any pen path is. One op batch, so one undo. */\n const commitJoin = useCallback((s: PenScratch, deps: ActionDeps, hit: PathAnchorHit): void => {\n const edit = deps.editAnchors!;\n const scene = deps.scene;\n const current = s.current!;\n const { anchors: added, closed: addedClosed } = pathToAnchors(buildPath([...s.finishedSubpaths, current], null));\n\n const other = edit.getEditablePath(hit.id) as PolygonPath;\n const otherSet = pathToAnchors(other);\n const tail = hit.end === 'last' ? reverseAnchors(otherSet.anchors[hit.sub]) : otherSet.anchors[hit.sub];\n const joined = added[added.length - 1];\n const joint = joined[joined.length - 1];\n const out = current.anchors[current.anchors.length - 1].outHandle ?? tail[0].outHandle;\n if (out) joint.outHandle = { ...out };\n joined.push(...tail.slice(1));\n\n const cont = s.continuing;\n const sameNode = cont?.id === hit.id;\n const rest = sameNode ? { anchors: [], closed: [] } : {\n anchors: otherSet.anchors.filter((_, i) => i !== hit.sub),\n closed: otherSet.closed.filter((_, i) => i !== hit.sub),\n };\n const label = 'Join paths';\n const ops: Op[] = [];\n let keptId: string;\n if (cont) {\n if (cont.reversed) added[0] = reverseAnchors(added[0]);\n const set = pathToAnchors(cont.original);\n set.anchors.splice(cont.sub, 1, added[0]);\n set.closed.splice(cont.sub, 1, addedClosed[0]);\n set.anchors.push(...added.slice(1), ...rest.anchors);\n set.closed.push(...addedClosed.slice(1), ...rest.closed);\n if (sameNode) {\n set.anchors.splice(hit.sub, 1);\n set.closed.splice(hit.sub, 1);\n }\n const next: PolygonPath = { ...anchorsToPath(set.anchors, set.closed), fillRule: cont.original.fillRule };\n ops.push(...edit.editOps!(cont.id, next, label));\n keptId = cont.id;\n } else {\n const path = anchorsToPath([...added, ...rest.anchors], [...addedClosed, ...rest.closed]);\n const node = optsRef.current.adapter.makeNode!(optsRef.current.wrapPath(path, { closed: false }));\n ops.push(createInsertOp({ node }));\n keptId = node.id;\n }\n if (!sameNode) ops.push(createDeleteOp({ node: scene!.get(hit.id as never) as { id: string } }));\n\n const applyOps = deps.applyOps;\n if (applyOps) applyOps(ops, label);\n else scene!.applyBatch(ops, label, defaultCommitAdapter(scene!));\n if (optsRef.current.autoSelect) optsRef.current.adapter.setSelection([keptId]);\n resetScratch(s);\n }, [optsRef]);\n\n /** From idle, a press on an open subpath's end anchor picks that subpath\n * up: it becomes `current`, turned so the pressed end is last. Returns\n * whether it did. */\n const pickUpEndpoint = useCallback((deps: ActionDeps, wx: number, wy: number): boolean => {\n const scratch = scratchRef.current!;\n if (scratch.current || scratch.finishedSubpaths.length > 0) return false;\n const radius = optsRef.current.closeHitRadius;\n const scale = viewScale(deps);\n const hit = nearestPathAnchor(pathsNear(deps, wx, wy, radius), { x: wx, y: wy }, radius, scale, (h) => h.end !== null);\n if (!hit) return false;\n const original = deps.editAnchors!.getEditablePath(hit.id) as PolygonPath;\n const picked = pathToAnchors(original).anchors[hit.sub];\n const reversed = hit.end === 'first' && picked.length > 1;\n scratch.current = { anchors: reversed ? reverseAnchors(picked) : picked, closed: false };\n scratch.continuing = { id: hit.id, sub: hit.sub, reversed, original };\n return true;\n }, [optsRef]);\n\n const snap = useCallback((x: number, y: number): { x: number; y: number } => {\n const fn = optsRef.current.snapPoint;\n return fn ? fn({ x, y }) : { x, y };\n }, [optsRef]);\n\n /** Where an anchor pressed at `(x, y)` lands: on an existing path's anchor\n * when one is within `anchorSnapRadius`, else wherever `snapPoint` puts it.\n * `hit` names the anchor it snapped to. */\n const placeAt = useCallback((deps: ActionDeps, x: number, y: number): { x: number; y: number; hit: PathAnchorHit | null } => {\n const radius = optsRef.current.anchorSnapRadius;\n if (radius > 0) {\n const hit = nearestPathAnchor(pathsNear(deps, x, y, radius), { x, y }, radius, viewScale(deps));\n if (hit) return { x: hit.x, y: hit.y, hit };\n }\n return { ...snap(x, y), hit: null };\n }, [snap, optsRef]);\n\n /** Is a world point within the close-hit radius of `(ax, ay)`? Measured as a\n * screen-space circle, so the zone stays round under non-uniform zoom. */\n const withinCloseRadius = useCallback(\n (deps: ActionDeps, ax: number, ay: number, wx: number, wy: number): boolean => {\n return withinPxRadius(ax - wx, ay - wy, optsRef.current.closeHitRadius, viewScale(deps));\n },\n [optsRef],\n );\n\n /**\n * Actions the pen owns, registered by `useToolActions` from inside the\n * ActionsProvider — see `ToolDef.actions`.\n *\n * They close over `scratchRef`, which is why the pen needs no `Selector`\n * exposing tool scratch to the action layer: the state never leaves the\n * hook. `useMemo([])` keeps their identity stable for the same reason\n * `scratchRef` is a ref — the preview layer reads that one object.\n */\n const actions = useMemo<Action[]>(() => {\n const s = () => scratchRef.current!;\n\n return [\n {\n id: 'pen.placeAnchor',\n label: 'Pen — place anchor',\n eligible: { capability: 'creates-paths' },\n requires: ['view', 'areaSelect', 'editAnchors', 'scene', 'applyOps'],\n invoker: {\n timing: 'immediate' as const,\n run: (deps, params) => {\n const p = params as { pressX?: number; pressY?: number } | undefined;\n if (p?.pressX === undefined || p.pressY === undefined) return;\n // The press point, not the release: a click may drift up to the\n // drag threshold, and an anchor should land where you put the\n // pointer down. Snapping happens here rather than at press time\n // so the snapped value is what the geometry records.\n if (pickUpEndpoint(deps, p.pressX, p.pressY)) {\n forceRender();\n return;\n }\n const { x: wx, y: wy, hit } = placeAt(deps, p.pressX, p.pressY);\n const scratch = s();\n\n // Close-on-first-anchor (>= 3 anchors). With `autoCommitOnClose`\n // on (default) this commits right away so the closed region\n // renders with its fill — Illustrator-style. With it off the\n // closed subpath stays in scratch for compound-path builds and\n // commits on Enter / tool switch.\n if (scratch.current && scratch.current.anchors.length >= 3) {\n const first = scratch.current.anchors[0];\n if (withinCloseRadius(deps, first.x, first.y, wx, wy)) {\n scratch.current.closed = true;\n scratch.finishedSubpaths.push(scratch.current);\n scratch.current = null;\n scratch.closeHintActive = false;\n if (optsRef.current.autoCommitOnClose) commit(scratch, deps);\n forceRender();\n return;\n }\n }\n\n const join = joinable(scratch, deps, hit);\n // Otherwise: append a corner anchor, starting a subpath if needed.\n if (!scratch.current) scratch.current = { anchors: [], closed: false };\n scratch.current.anchors.push({ x: wx, y: wy });\n if (join) commitJoin(scratch, deps, hit);\n forceRender();\n },\n },\n },\n\n {\n id: 'pen.finishOpen',\n label: 'Pen — finish open path',\n eligible: { capability: 'creates-paths' },\n requires: ['editAnchors'],\n // Needs an actual path to commit. Declining also lets the gesture\n // fall through: a ⌘-click with fewer than two anchors reaches the\n // plain-click binding and places an anchor instead.\n enabled: () => (anchorCount(s()) >= 2 ? true : ActionDisabledReason.NotApplicable),\n invoker: {\n timing: 'immediate' as const,\n run: (deps, params) => {\n const scratch = s();\n const p = params as { viaDoubleClick?: boolean } | undefined;\n\n // A double click arrives AFTER both of its clicks, so the second\n // one has already appended an anchor. Undo exactly that before\n // committing: pop it, and drop the subpath entirely if it was the\n // only anchor in it (which happens when the FIRST click closed a\n // subpath and the second started a fresh one). The net effect\n // matches the pen's old private 300ms detector — one anchor\n // placed, path finished — while using the dispatcher's single\n // definition of a double click.\n //\n // No position check is needed: the anchor to drop is by\n // construction the one the second click just appended.\n if (p?.viaDoubleClick && scratch.current) {\n scratch.current.anchors.pop();\n if (scratch.current.anchors.length === 0) scratch.current = null;\n }\n\n commit(scratch, deps);\n forceRender();\n },\n },\n },\n\n {\n id: 'pen.dragHandle',\n label: 'Pen — drag out a bezier handle',\n eligible: { capability: 'creates-paths' },\n requires: ['view', 'areaSelect', 'editAnchors', 'scene', 'applyOps'],\n invoker: {\n timing: 'ongoing' as const,\n start: (ctx: InvocationCtx) => {\n const scratch = s();\n // `drag.start` is the pointerdown position — the dispatcher\n // buffers the press and releases it at the drag threshold, so the\n // anchor lands where the user pressed rather than where the\n // threshold happened to be crossed.\n const origin = ctx.drag?.start ?? ctx.world;\n // Dragging from an open path's end picks the path up and pulls\n // that end's own handle rather than placing a new anchor on it.\n const pickedUp = pickUpEndpoint(ctx.deps, origin.x, origin.y);\n // Dragging on another open path's endpoint joins onto it once\n // the handle is set.\n let joinHit: PathAnchorHit | null = null;\n if (!pickedUp) {\n const { x: ax, y: ay, hit } = placeAt(ctx.deps, origin.x, origin.y);\n if (joinable(scratch, ctx.deps, hit)) joinHit = hit;\n if (!scratch.current) scratch.current = { anchors: [], closed: false };\n scratch.current.anchors.push({ x: ax, y: ay });\n }\n scratch.draggingHandleAt = scratch.current!.anchors.length - 1;\n if (ctx.modifiers.alt) {\n scratch.current!.anchors[scratch.draggingHandleAt].altBroken = true;\n }\n // A picked-up endpoint's in-handle belongs to the existing\n // segment into it, which the pen does not reshape.\n const linked = !pickedUp;\n applyOutHandle(scratch, ctx.world, ctx.modifiers.shift, linked, optsRef.current.snapPoint);\n forceRender();\n\n return {\n onMove: (moveCtx: InvocationCtx) => {\n const sm = s();\n if (sm.draggingHandleAt === null) return;\n if (moveCtx.modifiers.alt && sm.current) {\n sm.current.anchors[sm.draggingHandleAt].altBroken = true;\n }\n applyOutHandle(sm, moveCtx.world, moveCtx.modifiers.shift, linked, optsRef.current.snapPoint);\n forceRender();\n },\n onEnd: (endCtx: InvocationCtx, reason: 'commit' | 'cancel') => {\n const se = s();\n if (reason === 'cancel') {\n // `start` appended the anchor (or picked a path up); a\n // cancel (pointercancel is the reachable one — Escape and a\n // tool switch both reset the whole scratch first) has to\n // take it back out.\n if (pickedUp) {\n resetScratch(se);\n } else if (se.current && se.draggingHandleAt === se.current.anchors.length - 1) {\n se.current.anchors.pop();\n if (se.current.anchors.length === 0) se.current = null;\n }\n se.draggingHandleAt = null;\n forceRender();\n return;\n }\n if (se.draggingHandleAt !== null) {\n if (endCtx.modifiers.alt && se.current) {\n se.current.anchors[se.draggingHandleAt].altBroken = true;\n }\n applyOutHandle(se, endCtx.world, endCtx.modifiers.shift, linked, optsRef.current.snapPoint);\n se.draggingHandleAt = null;\n }\n if (joinHit && se.current) commitJoin(se, ctx.deps, joinHit);\n forceRender();\n },\n };\n },\n },\n },\n\n {\n id: 'pen.finish',\n label: 'Pen — finish path',\n eligible: { capability: 'creates-paths' },\n requires: ['editAnchors'],\n enabled: () => (anchorCount(s()) > 0 ? true : ActionDisabledReason.NotApplicable),\n invoker: {\n timing: 'immediate' as const,\n run: (deps) => {\n commit(s(), deps);\n forceRender();\n },\n },\n },\n\n {\n id: 'pen.cancel',\n label: 'Pen — discard path',\n eligible: { capability: 'creates-paths' },\n // Declining with nothing drawn is what keeps Escape's first-match\n // ladder intact: an empty pen passes the key to `escape`, which goes\n // on to clear the selection / return to the default tool.\n enabled: () => {\n const scratch = s();\n return scratch.current !== null || scratch.finishedSubpaths.length > 0\n ? true\n : ActionDisabledReason.NotApplicable;\n },\n invoker: {\n timing: 'immediate' as const,\n run: () => {\n resetScratch(s());\n forceRender();\n },\n },\n },\n ];\n }, [commit, placeAt, withinCloseRadius, pickUpEndpoint, joinable, commitJoin, optsRef]);\n\n return useMemo(() => {\n return defineTool<PenScratch>({\n id: 'pen',\n capabilities: ['creates-paths'],\n hookName: 'usePenTool',\n // Reads the tool's own ref rather than a scratch handed in by a\n // dispatcher — the pen owns this state and nothing else needs it.\n cursor: () =>\n scratchRef.current?.closeHintActive\n ? 'pointer'\n : cursorFor('pen', { fallback: 'crosshair' }),\n presentation: {\n label: 'Pen',\n icon: createElement(PenIcon),\n group: 'draw',\n },\n // Persistent-ref scratch. `penPreviewLayer` reads the in-progress path\n // by calling `Tool.initScratch()` and getting this same object back, so\n // the identity is load-bearing.\n initScratch: () => scratchRef.current!,\n actions,\n\n onDeactivate: () => {\n // Anything still in scratch is by definition incomplete — the user\n // hasn't closed it (close-on-first-anchor), open-finished it\n // (cmd-click), or pressed Enter. Switching tools mid-path should\n // discard, not auto-commit a stub polyline that the user didn't\n // ask for. Mirrors Escape's behavior so \"stop drawing\" is\n // consistent across exits.\n resetScratch(scratchRef.current!);\n forceRender();\n },\n\n bindings: [\n // Plain click places an anchor; ⌘/Ctrl-click open-finishes\n // (Illustrator). Strict modifier matching keeps the two apart, and\n // `mod` resolves to meta on mac / ctrl elsewhere — the tool-route\n // grammar's modifier matcher accepted either key on either platform.\n { spec: { kind: 'click' as const, mods: { shift: 'optional' as const, alt: 'optional' as const } }, actionId: 'pen.placeAnchor' },\n { spec: { kind: 'click' as const, mods: { mod: true, shift: 'optional' as const, alt: 'optional' as const } }, actionId: 'pen.finishOpen' },\n // Double-click on the last anchor also open-finishes. This used to be\n // a private 300ms/radius check inside the click route — a fourth\n // double-click detector in a codebase that had just collapsed three\n // into one (audit 3.3).\n {\n spec: { kind: 'doubleClick' as const, mods: { shift: 'optional' as const, alt: 'optional' as const } },\n actionId: 'pen.finishOpen',\n opts: { params: { viaDoubleClick: true } },\n },\n { spec: { kind: 'drag' as const, mods: { shift: 'optional' as const, alt: 'optional' as const } }, actionId: 'pen.dragHandle' },\n { spec: { kind: 'key' as const, key: 'Enter' }, actionId: 'pen.finish' },\n { spec: { kind: 'key' as const, key: 'Escape' }, actionId: 'pen.cancel' },\n ],\n });\n }, [actions]);\n}\n\nusePenTool.prefs = {\n name: 'Pen',\n description: 'Pen-tool behavior.',\n children: {\n autoCommitOnClose: {\n kind: 'boolean',\n name: 'Auto-commit pen on close',\n description: \"When you click the pen tool's first anchor to close a region, commit immediately so it renders with its fill. Off: keep the path in preview until you press Enter (lets you build a compound path from multiple closed subpaths).\",\n default: true,\n },\n },\n} satisfies ToolPrefGroup;\n\n/** Point the anchor's outgoing handle at `target`, optionally snapped and\n * optionally constrained to 45° steps. When `linked` and not Alt-broken,\n * the in-handle follows as its mirror through the anchor. */\nfunction applyOutHandle<S extends PenScratch>(\n s: S,\n target: { x: number; y: number },\n shift: boolean,\n linked: boolean,\n snap?: (p: { x: number; y: number }) => { x: number; y: number },\n): void {\n if (s.current === null || s.draggingHandleAt === null) return;\n const anchor = s.current.anchors[s.draggingHandleAt];\n const to = snap ? snap(target) : target;\n let dx = to.x - anchor.x;\n let dy = to.y - anchor.y;\n if (shift) {\n const c = constrainTo45(dx, dy);\n dx = c.dx;\n dy = c.dy;\n }\n anchor.outHandle = { x: anchor.x + dx, y: anchor.y + dy };\n if (linked && !anchor.altBroken) anchor.inHandle = { x: anchor.x - dx, y: anchor.y - dy };\n}\n\nfunction viewScale(deps: ActionDeps): { x: number; y: number } {\n return deps.view?.get().scale ?? { x: 1, y: 1 };\n}\n\n/** Existing editable paths, world coords, with ink within `px` screen\n * pixels of `(wx, wy)` — front-to-back, as the area query returns them.\n * Empty when the host publishes no `areaSelect` or `editAnchors` dep. */\nfunction pathsNear(\n deps: ActionDeps,\n wx: number,\n wy: number,\n px: number,\n): Array<{ id: string; path: PolygonPath }> {\n const area = deps.areaSelect;\n const edit = deps.editAnchors;\n if (!area || !edit) return [];\n const view = deps.view;\n const e = pxExtent(px, viewScale(deps));\n const ids = area.hitTestArea({ x: wx - e.x, y: wy - e.y, width: 2 * e.x, height: 2 * e.y }, view);\n const out: Array<{ id: string; path: PolygonPath }> = [];\n for (const id of ids) {\n const path = edit.getEditablePath(id) as PolygonPath | null;\n if (path?.kind === 'polygon') out.push({ id, path });\n }\n return out;\n}\n\nfunction samePath(a: PolygonPath, b: PolygonPath): boolean {\n if (a.commands.length !== b.commands.length || a.coords.length !== b.coords.length) return false;\n for (let i = 0; i < a.commands.length; i++) if (a.commands[i] !== b.commands[i]) return false;\n for (let i = 0; i < a.coords.length; i++) if (a.coords[i] !== b.coords[i]) return false;\n return true;\n}\n"],"mappings":"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"}
|