@pixerate/editor 0.112.0 → 0.114.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/THIRD_PARTY_NOTICES.md +11 -0
- package/dist/animation-editor/index.cjs +60 -54
- package/dist/animation-editor/index.cjs.map +1 -1
- package/dist/animation-editor/index.d.cts +3 -3
- package/dist/animation-editor/index.d.ts +3 -3
- package/dist/animation-editor/index.js +14 -8
- package/dist/animation-editor/index.js.map +1 -1
- package/dist/builtin-CY97ppKM.d.cts +354 -0
- package/dist/builtin-CY97ppKM.d.ts +354 -0
- package/dist/canvas/index.cjs +3 -3
- package/dist/canvas/index.cjs.map +1 -1
- package/dist/canvas/index.js +9 -9
- package/dist/chunk-26PGXHY7.cjs +753 -0
- package/dist/chunk-26PGXHY7.cjs.map +1 -0
- package/dist/chunk-2OJACTPR.cjs +929 -0
- package/dist/chunk-2OJACTPR.cjs.map +1 -0
- package/dist/chunk-3UFGNHSP.js +929 -0
- package/dist/chunk-3UFGNHSP.js.map +1 -0
- package/dist/{chunk-YGIMK4NX.cjs → chunk-5OVJHMZQ.cjs} +757 -825
- package/dist/chunk-5OVJHMZQ.cjs.map +1 -0
- package/dist/chunk-643B2HKC.cjs +534 -0
- package/dist/chunk-643B2HKC.cjs.map +1 -0
- package/dist/chunk-6FCKOS2B.js +534 -0
- package/dist/chunk-6FCKOS2B.js.map +1 -0
- package/dist/chunk-BRLYOPLT.js +2574 -0
- package/dist/chunk-BRLYOPLT.js.map +1 -0
- package/dist/{chunk-LQEEDHOP.js → chunk-C6C5PDKU.js} +759 -761
- package/dist/chunk-C6C5PDKU.js.map +1 -0
- package/dist/chunk-DFQEX3AM.cjs +781 -0
- package/dist/chunk-DFQEX3AM.cjs.map +1 -0
- package/dist/{chunk-CZOXTEMN.js → chunk-EUVSQ25B.js} +336 -404
- package/dist/chunk-EUVSQ25B.js.map +1 -0
- package/dist/chunk-G5NMLUVF.cjs +2574 -0
- package/dist/chunk-G5NMLUVF.cjs.map +1 -0
- package/dist/chunk-LKCYANMW.js +781 -0
- package/dist/chunk-LKCYANMW.js.map +1 -0
- package/dist/{chunk-W3GXSUSQ.cjs → chunk-LPLCWZTM.cjs} +5087 -1283
- package/dist/chunk-LPLCWZTM.cjs.map +1 -0
- package/dist/chunk-MMVHBKMM.js +753 -0
- package/dist/chunk-MMVHBKMM.js.map +1 -0
- package/dist/{chunk-POGBUYWE.cjs → chunk-ST7E3NEE.cjs} +857 -859
- package/dist/chunk-ST7E3NEE.cjs.map +1 -0
- package/dist/{chunk-7VOHOPLW.cjs → chunk-TCKIXI46.cjs} +59 -59
- package/dist/{chunk-7VOHOPLW.cjs.map → chunk-TCKIXI46.cjs.map} +1 -1
- package/dist/{chunk-XIOMMGPD.cjs → chunk-TFOPHZ3U.cjs} +1 -1
- package/dist/chunk-TFOPHZ3U.cjs.map +1 -0
- package/dist/{chunk-Z4BC6M74.js → chunk-UBIXVXPR.js} +2 -2
- package/dist/{chunk-BLWCAQCW.js → chunk-VXR7XWMF.js} +4574 -770
- package/dist/chunk-VXR7XWMF.js.map +1 -0
- package/dist/{chunk-HNS7UHCM.js → chunk-XK5LZKBD.js} +1 -1
- package/dist/chunk-XK5LZKBD.js.map +1 -0
- package/dist/cms/index.cjs +146 -0
- package/dist/cms/index.cjs.map +1 -0
- package/dist/cms/index.d.cts +51 -0
- package/dist/cms/index.d.ts +51 -0
- package/dist/cms/index.js +146 -0
- package/dist/{export-DNu_Jr8p.d.ts → export-_1dUKwpF.d.ts} +326 -195
- package/dist/{export-DBba-buV.d.cts → export-b6NjCZ5-.d.cts} +326 -195
- package/dist/image-editor/index.cjs +2377 -1291
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +149 -97
- package/dist/image-editor/index.d.ts +149 -97
- package/dist/image-editor/index.js +1343 -257
- package/dist/image-editor/index.js.map +1 -1
- package/dist/opentype-BZEOSOX3.d.ts +188 -0
- package/dist/opentype-Dpe75vL5.d.cts +188 -0
- package/dist/paint/index.cjs +63 -4
- package/dist/paint/index.cjs.map +1 -1
- package/dist/paint/index.d.cts +30 -3
- package/dist/paint/index.d.ts +30 -3
- package/dist/paint/index.js +62 -3
- package/dist/paint-editor/index.cjs +146 -145
- package/dist/paint-editor/index.cjs.map +1 -1
- package/dist/paint-editor/index.d.cts +9 -8
- package/dist/paint-editor/index.d.ts +9 -8
- package/dist/paint-editor/index.js +6 -5
- package/dist/paint-editor/index.js.map +1 -1
- package/dist/raster/index.cjs +53 -3
- package/dist/raster/index.cjs.map +1 -1
- package/dist/raster/index.d.cts +62 -91
- package/dist/raster/index.d.ts +62 -91
- package/dist/raster/index.js +59 -9
- package/dist/{render-BEriFuqK.d.cts → render-B1K_OoLy.d.cts} +87 -2
- package/dist/{render-tjzKaik-.d.ts → render-CrmHOgpo.d.ts} +87 -2
- package/dist/{blending-aOE6Y3UE.d.cts → retouch-Bxkuc0KH.d.cts} +219 -2
- package/dist/{blending-DXl06NSx.d.ts → retouch-Do6tBs2H.d.ts} +219 -2
- package/dist/{surface-C8RRg_Bh.d.ts → surface-Cn30xIV6.d.cts} +6 -1
- package/dist/{surface-C8RRg_Bh.d.cts → surface-Cn30xIV6.d.ts} +6 -1
- package/dist/{svg-D_cfkhlB.d.cts → svg-BnRj0q8i.d.cts} +16 -4
- package/dist/{svg-wIaKLrU0.d.ts → svg-CmmBFfGT.d.ts} +16 -4
- package/dist/{types-DtL_qLFz.d.ts → types-1SUQKDAw.d.ts} +273 -4
- package/dist/{types-DyQq-WPY.d.cts → types-yINF17Ta.d.cts} +273 -4
- package/dist/vector/index.cjs +12170 -7343
- package/dist/vector/index.cjs.map +1 -1
- package/dist/vector/index.d.cts +1579 -16
- package/dist/vector/index.d.ts +1579 -16
- package/dist/vector/index.js +6303 -1476
- package/dist/vector/index.js.map +1 -1
- package/dist/warp-B39VsOtd.d.cts +348 -0
- package/dist/warp-DMN30aY6.d.ts +348 -0
- package/dist/zlib-dxQdgHyN.d.cts +95 -0
- package/dist/zlib-dxQdgHyN.d.ts +95 -0
- package/package.json +6 -1
- package/skills/pixerate-image-editor/SKILL.md +34 -34
- package/skills/pixerate-image-editor/references/color-management.md +37 -0
- package/skills/pixerate-image-editor/references/photoshop-files.md +39 -0
- package/skills/pixerate-image-editor/references/retouching.md +36 -0
- package/skills/pixerate-vector/SKILL.md +154 -5
- package/dist/chunk-BLWCAQCW.js.map +0 -1
- package/dist/chunk-CZOXTEMN.js.map +0 -1
- package/dist/chunk-FTBZPI77.cjs +0 -1
- package/dist/chunk-FTBZPI77.cjs.map +0 -1
- package/dist/chunk-HNS7UHCM.js.map +0 -1
- package/dist/chunk-JLYX4V57.cjs +0 -2001
- package/dist/chunk-JLYX4V57.cjs.map +0 -1
- package/dist/chunk-KB2YBCVS.js +0 -2001
- package/dist/chunk-KB2YBCVS.js.map +0 -1
- package/dist/chunk-LQEEDHOP.js.map +0 -1
- package/dist/chunk-POGBUYWE.cjs.map +0 -1
- package/dist/chunk-VKK5VS2T.js +0 -1
- package/dist/chunk-W3GXSUSQ.cjs.map +0 -1
- package/dist/chunk-XIOMMGPD.cjs.map +0 -1
- package/dist/chunk-YGIMK4NX.cjs.map +0 -1
- package/dist/homography-CZ-tFjay.d.cts +0 -50
- package/dist/homography-Ctfjoc1b.d.ts +0 -50
- package/dist/warp-CWHZzE3x.d.ts +0 -118
- package/dist/warp-D5jz_tN7.d.cts +0 -118
- /package/dist/{chunk-Z4BC6M74.js.map → chunk-UBIXVXPR.js.map} +0 -0
- /package/dist/{chunk-VKK5VS2T.js.map → cms/index.js.map} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/editor/editor/packages/core/dist/vector/index.cjs","../../src/vector/gradientTool.ts","../../src/vector/blendGroup.ts","../../src/vector/distort.ts","../../src/vector/snap.ts","../../src/vector/pathEdit.ts","../../src/vector/ink.ts","../../src/vector/builder.ts","../../src/vector/controller.ts","../../src/vector/render.ts","../../src/vector/commands.ts"],"names":["lines","r","key","node","finish","stroke","fmt","corners","list","STROKE_KEYS"],"mappings":"AAAA;AACE;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B;AACE;AACF,yDAA8B;AAC9B;AACA;ACrVO,IAAM,yBAAA,EAA2B,EAAA;AACjC,IAAM,6BAAA,EAA+B,CAAA;AAErC,IAAM,wBAAA,EAA0B,EAAA;AACvC,IAAM,SAAA,EAAW,CAAA;AACjB,IAAM,aAAA,EAAe,CAAA;AACrB,IAAM,WAAA,EAAa,CAAA;AACnB,IAAM,QAAA,EAAU,CAAA;AA2BhB,IAAM,IAAA,EAAM,CAAC,CAAA,EAAA,GAAc,IAAA,CAAK,KAAA,CAAM,EAAA,EAAI,GAAG,EAAA,EAAI,GAAA;AACjD,IAAM,MAAA,EAAQ,CAAC,CAAA,EAAiB,CAAA,EAAA,GAAA,CAAmB,EAAE,CAAA,EAAG,CAAA,CAAE,CAAC,EAAA,EAAI,CAAA,CAAE,EAAA,EAAI,CAAA,CAAE,CAAC,EAAA,EAAI,CAAA,CAAE,EAAA,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A;AAEvC,MAAA;AAC+C,MAAA;AACtC,MAAA;AACqF,MAAA;AACzC,QAAA;AAC3B,QAAA;AACsB,QAAA;AACjD,MAAA;AACD,IAAA;AACA,IAAA;AACI,MAAA;AACU,MAAA;AACN,MAAA;AACuC,MAAA;AAE7C,MAAA;AACO,MAAA;AACD,QAAA;AACM,QAAA;AAC0B,UAAA;AACH,UAAA;AACa,UAAA;AACZ,UAAA;AACe,UAAA;AACF,UAAA;AACjD,QAAA;AACD,MAAA;AACqE,MAAA;AAC5B,QAAA;AACA,QAAA;AACb,QAAA;AACqB,QAAA;AACjD,MAAA;AACD,IAAA;AACA,IAAA;AACI,MAAA;AACU,MAAA;AACN,MAAA;AAC4C,MAAA;AACtC,MAAA;AACyC,MAAA;AACA,MAAA;AACvB,QAAA;AACH,QAAA;AAC5B,MAAA;AACD,IAAA;AACA,IAAA;AACI,MAAA;AACU,MAAA;AACN,MAAA;AACG,MAAA;AAC6B,MAAA;AAC1B,MAAA;AACwC,MAAA;AAC0C,MAAA;AAChG,IAAA;AACA,IAAA;AACI,MAAA;AACU,MAAA;AACN,MAAA;AACqC,MAAA;AAC/B,MAAA;AACwC,MAAA;AAC3C,MAAA;AAC0D,MAAA;AACf,QAAA;AACC,QAAA;AACtD,MAAA;AACD,IAAA;AACD,EAAA;AACD;AVg5P6D;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/editor/editor/packages/core/dist/vector/index.cjs","sourcesContent":[null,"/**\n * The Gradient tool's on-canvas annotator: a bar from the start (or centre) handle to the end (or\n * radius) handle with the colour stops under it and midpoint diamonds above it, plus a radial,\n * conic or diamond gradient's extent outline, aspect handle and (radial) focal point. All in\n * screen pixels, for hit-testing and the overlay.\n *\n * After vectorcraft `crates/tools/src/xform/gradient.rs` (`Annotator`: stop chips below the bar,\n * midpoints above it, a stop dragged off the bar is removed, a click on the bar adds one), ArtCraft,\n * MIT OR Apache-2.0 — see THIRD_PARTY_NOTICES.md.\n */\nimport { vectorTargetPaint } from './paintLayers.js';\nimport { collectPathNodes, findVectorNode, vectorWorldMatrix, IDENTITY_MATRIX, multiplyMatrices, type AffineMatrix } from './geometry.js';\nimport { vectorPathLocalBounds } from './svg.js';\nimport {\n\tisCentredVectorGradient,\n\tisVectorGradient,\n\tsortedVectorGradientStops,\n\tvectorGradientHandles,\n\tvectorGradientMatrix,\n\tvectorGradientMidpointPosition,\n\ttype VectorGradientHandle\n} from './gradient.js';\nimport type { Vec2, VectorBounds, VectorDocument, VectorGradient, VectorGradientStop, VectorPathNode, VectorViewport } from './types.js';\n\n/** Which paint of the selection the Gradient tool and the gradient editor work on. */\nexport type VectorGradientTarget = 'fill' | 'stroke';\n\n/** A part of the annotator under the pointer. */\nexport type VectorGradientPart =\n\t| { kind: 'handle'; handle: VectorGradientHandle }\n\t| { kind: 'stop'; index: number }\n\t| { kind: 'midpoint'; index: number }\n\t/** The bar at `offset` (0–1): a click adds a stop there, a drag moves the gradient. */\n\t| { kind: 'bar'; offset: number };\n\n/** Distance of the stop chips below the bar, the midpoint diamonds above it (screen pixels). */\nexport const VECTOR_GRADIENT_STOP_GAP = 12;\nexport const VECTOR_GRADIENT_MIDPOINT_GAP = 9;\n/** How far off the bar a dragged stop goes before it is removed (screen pixels). */\nexport const VECTOR_GRADIENT_OFF_BAR = 24;\nconst STOP_HIT = 6;\nconst MIDPOINT_HIT = 5;\nconst HANDLE_HIT = 6;\nconst BAR_HIT = 5;\n\nexport interface VectorGradientAnnotator {\n\tnodeId: string;\n\ttarget: VectorGradientTarget;\n\tgradient: VectorGradient;\n\t/** The path's box in its own space. */\n\tbounds: VectorBounds | null;\n\t/** The path's own space → document. */\n\tworld: AffineMatrix;\n\t/** Screen positions of the handles. */\n\tstart: Vec2;\n\tend: Vec2;\n\t/** Radial, conic and diamond gradients. */\n\taspect: Vec2 | null;\n\t/** Radial gradients. */\n\tfocal: Vec2 | null;\n\t/** Unit normal to the bar towards the stop chips (screen). */\n\tnormal: Vec2;\n\t/** Stops (sorted, as the gradient stores them) with their chips' screen positions. */\n\tstops: (VectorGradientStop & { index: number; x: number; y: number })[];\n\t/** The midpoint diamond after each stop but the last (screen). */\n\tmidpoints: { index: number; x: number; y: number; midpoint: number }[];\n\t/** Screen path of the extent: an ellipse (radial, conic) or a rhombus (diamond). */\n\textent: string | null;\n}\n\nconst fmt = (n: number) => Math.round(n * 100) / 100;\nconst apply = (m: AffineMatrix, p: Vec2): Vec2 => ({ x: m[0] * p.x + m[2] * p.y + m[4], y: m[1] * p.x + m[3] * p.y + m[5] });\n\n/** The path whose `target` paint the annotator shows: the first path of the selection with a gradient there. */\nexport function vectorGradientTargetPath(doc: VectorDocument, selection: readonly string[], target: VectorGradientTarget, layer: number | null = null): VectorPathNode | null {\n\tfor (const id of selection) {\n\t\tconst node = findVectorNode(doc.nodes, id);\n\t\tif (!node) continue;\n\t\tconst path = collectPathNodes(node).find((p) => isVectorGradient(vectorTargetPaint(p, target, layer)));\n\t\tif (path) return path;\n\t}\n\treturn null;\n}\n\n/** The annotator of the selection's gradient (`target` paint), in screen pixels, or null when it has none. */\nexport function getVectorGradientAnnotator(\n\tdoc: VectorDocument,\n\tselection: readonly string[],\n\tviewport: VectorViewport,\n\ttarget: VectorGradientTarget,\n\tlayer: number | null = null\n): VectorGradientAnnotator | null {\n\tconst path = vectorGradientTargetPath(doc, selection, target, layer);\n\tif (!path) return null;\n\tconst gradient = vectorTargetPaint(path, target, layer) as VectorGradient;\n\tconst world = vectorWorldMatrix(doc.nodes, path.id) ?? IDENTITY_MATRIX;\n\tconst screen = multiplyMatrices(world, [viewport.zoom, 0, 0, viewport.zoom, viewport.x, viewport.y]);\n\tconst bounds = vectorPathLocalBounds(path);\n\tconst h = vectorGradientHandles(gradient, bounds, screen);\n\tconst v = { x: h.end.x - h.start.x, y: h.end.y - h.start.y };\n\tconst len = Math.hypot(v.x, v.y);\n\tconst normal = len > 1e-9 ? { x: -v.y / len, y: v.x / len } : { x: 0, y: 1 };\n\tconst at = (t: number, gap: number) => ({ x: h.start.x + v.x * t + normal.x * gap, y: h.start.y + v.y * t + normal.y * gap });\n\tconst sorted = sortedVectorGradientStops(gradient);\n\tconst stops = sorted.map((s, index) => ({ ...s, index, ...at(s.offset, VECTOR_GRADIENT_STOP_GAP) }));\n\tconst midpoints = sorted.slice(0, -1).flatMap((s, index) => {\n\t\tconst pos = vectorGradientMidpointPosition(sorted, index);\n\t\treturn pos === null || sorted[index + 1].offset - s.offset < 1e-6 ? [] : [{ index, midpoint: s.midpoint ?? 0.5, ...at(pos, -VECTOR_GRADIENT_MIDPOINT_GAP) }];\n\t});\n\tlet extent: string | null = null;\n\tif (isCentredVectorGradient(gradient.type)) {\n\t\tconst frame = multiplyMatrices(vectorGradientMatrix(gradient, bounds), screen);\n\t\tconst pts =\n\t\t\tgradient.type === 'diamond'\n\t\t\t\t? [{ x: 1, y: 0 }, { x: 0, y: 1 }, { x: -1, y: 0 }, { x: 0, y: -1 }]\n\t\t\t\t: Array.from({ length: 48 }, (_, k) => ({ x: Math.cos((k * Math.PI) / 24), y: Math.sin((k * Math.PI) / 24) }));\n\t\textent = `M${pts.map((p) => apply(frame, p)).map((p) => `${fmt(p.x)} ${fmt(p.y)}`).join('L')}Z`;\n\t}\n\treturn { nodeId: path.id, target, gradient, bounds, world, start: h.start, end: h.end, aspect: h.aspect, focal: h.focal, normal, stops, midpoints, extent };\n}\n\n/** Where `p` lies relative to the bar: offset along it (0 at the start, 1 at the end) and distance towards the stop chips. */\nexport function vectorGradientBarPosition(a: Pick<VectorGradientAnnotator, 'start' | 'end' | 'normal'>, p: Vec2): { offset: number; across: number } {\n\tconst v = { x: a.end.x - a.start.x, y: a.end.y - a.start.y };\n\tconst l2 = v.x * v.x + v.y * v.y;\n\tconst d = { x: p.x - a.start.x, y: p.y - a.start.y };\n\tif (l2 < 1e-12) return { offset: 0, across: Math.hypot(d.x, d.y) };\n\treturn { offset: (d.x * v.x + d.y * v.y) / l2, across: d.x * a.normal.x + d.y * a.normal.y };\n}\n\n/** The annotator part under screen point `p`: stops, then midpoints, the handles and the bar. */\nexport function hitVectorGradientAnnotator(a: VectorGradientAnnotator, p: Vec2): VectorGradientPart | null {\n\tconst near = (q: Vec2 | null, r: number) => !!q && Math.hypot(q.x - p.x, q.y - p.y) <= r;\n\tfor (let i = a.stops.length - 1; i >= 0; i--) if (near(a.stops[i], STOP_HIT)) return { kind: 'stop', index: a.stops[i].index };\n\tfor (const m of a.midpoints) if (near(m, MIDPOINT_HIT)) return { kind: 'midpoint', index: m.index };\n\tconst centred = isCentredVectorGradient(a.gradient.type);\n\tif (near(a.end, HANDLE_HIT)) return { kind: 'handle', handle: centred ? 'radius' : 'end' };\n\t// The focal point sits on the centre until moved: grab it with Alt there (see the controller).\n\tif (a.focal && near(a.focal, HANDLE_HIT) && !near(a.start, 1)) return { kind: 'handle', handle: 'focal' };\n\tif (near(a.start, HANDLE_HIT)) return { kind: 'handle', handle: 'start' };\n\tif (near(a.aspect, HANDLE_HIT)) return { kind: 'handle', handle: 'aspect' };\n\tconst { offset, across } = vectorGradientBarPosition(a, p);\n\tif (offset >= 0 && offset <= 1 && across >= -BAR_HIT && across <= VECTOR_GRADIENT_STOP_GAP + STOP_HIT) return { kind: 'bar', offset };\n\treturn null;\n}\n","// Portions ported from vectorcraft-main/crates/doc/src/blend.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n/**\n * Live blend groups: a group whose `blend` is set keeps its key shapes as children and\n * regenerates the in-betweens (`blendStep` children, locked) whenever a key or the options\n * change. Steps follow the straight lines between the keys' centres, or a replaced spine.\n *\n * - **Spacing** (`vectorBlendStepCount`): a number of steps per pair of keys, a fixed distance\n * along the spine, or smooth colour — one step per two levels of the largest colour channel\n * difference, by distance when the colours match.\n * - **Spine**: with a replaced spine the keys sit along it, spread evenly by arc length; each\n * step is moved by how far the spine bends away from the chord between its keys, so a step\n * lands on the spine and the keys stay where they are. Orient to spine also turns the steps\n * by how much the spine's direction differs from the keys' (keys keep their orientation);\n * without a replaced spine the steps run straight and keep their orientation.\n */\nimport type { Point } from '../canvas/types.js';\nimport { hexToRgba } from '../tokens/color.js';\nimport { segmentPoint, segmentTangent } from '../canvas/pathops/bezier.js';\nimport { createVectorMorph, type VectorMorph } from './blend.js';\nimport {\n\tcollectPathNodes,\n\tIDENTITY_MATRIX,\n\trotateMatrix,\n\tsubpathSegment,\n\tsubpathSegmentCount,\n\ttransformVectorNode,\n\ttranslateMatrix,\n\tmultiplyMatrices,\n\tnodeMatrix,\n\tvectorNodeBounds,\n\ttype AffineMatrix\n} from './geometry.js';\nimport type { VectorBlend, VectorBlendOptions, VectorGroupNode, VectorNode, VectorPaint, VectorPathNode, VectorSubpath } from './types.js';\n\nexport const DEFAULT_VECTOR_BLEND_STEPS = 5;\nexport const DEFAULT_VECTOR_BLEND_DISTANCE = 20;\n/** Most steps between two keys. */\nexport const VECTOR_BLEND_MAX_STEPS = 1000;\n/** Most steps smooth colour makes between two keys. */\nexport const VECTOR_BLEND_MAX_SMOOTH_STEPS = 256;\n\n/** Whether a node is a live blend group. */\nexport function isVectorBlendGroup(node: VectorNode | null | undefined): node is VectorGroupNode & { blend: VectorBlend } {\n\treturn !!node && node.type === 'group' && !!node.blend;\n}\n\n/** The keys of a blend group (its children that are not generated steps), in stacking order. */\nexport function vectorBlendKeys(group: VectorGroupNode): VectorNode[] {\n\treturn group.children.filter((c) => !c.blendStep);\n}\n\n/** The full options of a blend, defaults filled in. */\nexport function resolveVectorBlendOptions(options: VectorBlendOptions | null | undefined): Required<VectorBlendOptions> {\n\treturn {\n\t\tspacing: options?.spacing ?? 'steps',\n\t\tsteps: options?.steps ?? DEFAULT_VECTOR_BLEND_STEPS,\n\t\tdistance: options?.distance ?? DEFAULT_VECTOR_BLEND_DISTANCE,\n\t\torientToSpine: options?.orientToSpine ?? false,\n\t\tlinear: options?.linear ?? false\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Step counts\n// ---------------------------------------------------------------------------\n\nfunction paintColors(p: VectorPaint | undefined): string[] {\n\tif (typeof p === 'string') return [p];\n\tif (p && typeof p === 'object') return p.stops.map((s) => s.color);\n\treturn [];\n}\n\n/** Largest channel difference (0–1) between the colours of two shapes' fills and strokes. */\nexport function vectorBlendColorDistance(a: VectorNode, b: VectorNode): number {\n\tconst pa = collectPathNodes(a)[0];\n\tconst pb = collectPathNodes(b)[0];\n\tif (!pa || !pb) return 0;\n\tlet d = 0;\n\tfor (const key of ['fill', 'stroke'] as const) {\n\t\tconst ca = paintColors(pa[key]);\n\t\tconst cb = paintColors(pb[key]);\n\t\tca.forEach((c, i) => {\n\t\t\tconst other = cb[i] ?? cb[0];\n\t\t\tif (other === undefined) return;\n\t\t\tconst x = hexToRgba(c);\n\t\t\tconst y = hexToRgba(other);\n\t\t\tif (!x || !y) return;\n\t\t\td = Math.max(d, Math.abs(x.r - y.r), Math.abs(x.g - y.g), Math.abs(x.b - y.b), Math.abs(x.a - y.a));\n\t\t});\n\t}\n\treturn d;\n}\n\nconst centreOf = (n: VectorNode): Point => {\n\tconst b = vectorNodeBounds(n);\n\treturn b ? [b.x + b.width / 2, b.y + b.height / 2] : [0, 0];\n};\n\n/** Number of in-betweens between two keys `length` apart along the spine. */\nexport function vectorBlendStepCount(a: VectorNode, b: VectorNode, options: VectorBlendOptions | null | undefined, length: number): number {\n\tconst o = resolveVectorBlendOptions(options);\n\tif (o.spacing === 'distance') {\n\t\tconst d = Math.max(0.01, o.distance);\n\t\treturn Math.min(VECTOR_BLEND_MAX_STEPS, Math.max(0, Math.round(length / d) - 1));\n\t}\n\tif (o.spacing === 'smoothColor') {\n\t\tconst cd = vectorBlendColorDistance(a, b);\n\t\tif (cd > 1 / 255) return Math.min(VECTOR_BLEND_MAX_SMOOTH_STEPS, Math.max(1, Math.ceil((cd * 255) / 2)));\n\t\t// Same colours: base the count on how far the shapes are apart.\n\t\tconst ba = vectorNodeBounds(a);\n\t\tconst bb = vectorNodeBounds(b);\n\t\tconst dist =\n\t\t\tba && bb\n\t\t\t\t? Math.max(Math.abs(ba.x - bb.x), Math.abs(ba.y - bb.y), Math.abs(ba.x + ba.width - bb.x - bb.width), Math.abs(ba.y + ba.height - bb.y - bb.height))\n\t\t\t\t: length;\n\t\treturn Math.min(VECTOR_BLEND_MAX_SMOOTH_STEPS, Math.max(1, Math.ceil(dist / 2)));\n\t}\n\treturn Math.min(VECTOR_BLEND_MAX_STEPS, Math.max(1, Math.round(Number.isFinite(o.steps) ? o.steps : DEFAULT_VECTOR_BLEND_STEPS)));\n}\n\n// ---------------------------------------------------------------------------\n// Spine\n// ---------------------------------------------------------------------------\n\n/** A subpath flattened to a polyline with cumulative arc lengths and tangent angles. */\nexport interface VectorBlendRail {\n\tpts: Point[];\n\tcum: number[];\n\tangles: number[];\n\ttotal: number;\n\tclosed: boolean;\n}\n\n/** Flattens a spine subpath (32 samples per curve). Null with fewer than two distinct points. */\nexport function vectorBlendRail(sp: VectorSubpath): VectorBlendRail | null {\n\tconst pts: Point[] = [];\n\tconst angles: number[] = [];\n\tconst push = (p: Point, tangent: Point) => {\n\t\tconst last = pts[pts.length - 1];\n\t\tif (last && Math.hypot(p[0] - last[0], p[1] - last[1]) < 1e-9) return;\n\t\tpts.push(p);\n\t\tangles.push(Math.atan2(tangent[1], tangent[0]));\n\t};\n\tconst count = subpathSegmentCount(sp);\n\tfor (let i = 0; i < count; i++) {\n\t\tconst seg = subpathSegment(sp, i);\n\t\tconst samples = seg.length === 2 ? 1 : 32;\n\t\tif (i === 0) push(seg[0], segmentTangent(seg, 0));\n\t\tfor (let k = 1; k <= samples; k++) push(segmentPoint(seg, k / samples), segmentTangent(seg, k / samples));\n\t}\n\tif (pts.length < 2) return null;\n\tconst cum = [0];\n\tfor (let i = 1; i < pts.length; i++) cum.push(cum[i - 1] + Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]));\n\treturn { pts, cum, angles, total: cum[cum.length - 1], closed: sp.closed };\n}\n\n/** Point and direction (radians) at arc length `s` (clamped; a closed rail wraps). */\nexport function vectorBlendRailAt(rail: VectorBlendRail, s: number): { point: Point; angle: number } {\n\tlet len = Number.isFinite(s) ? s : 0;\n\tif (rail.closed && rail.total > 0) len = ((len % rail.total) + rail.total) % rail.total;\n\tlen = Math.min(rail.total, Math.max(0, len));\n\tlet lo = 0;\n\tlet hi = rail.cum.length - 1;\n\twhile (hi - lo > 1) {\n\t\tconst mid = (lo + hi) >> 1;\n\t\tif (rail.cum[mid] <= len) lo = mid;\n\t\telse hi = mid;\n\t}\n\tconst span = rail.cum[hi] - rail.cum[lo];\n\tconst u = span > 1e-12 ? (len - rail.cum[lo]) / span : 0;\n\tconst a = rail.pts[lo];\n\tconst b = rail.pts[hi];\n\treturn { point: [a[0] + (b[0] - a[0]) * u, a[1] + (b[1] - a[1]) * u], angle: Math.atan2(b[1] - a[1], b[0] - a[0]) };\n}\n\n/**\n * The spine of a blend in the group's space and where along it each key sits: the replaced\n * spine (keys spread evenly by arc length) or the straight lines between the keys' centres.\n */\nexport function vectorBlendSpine(group: VectorGroupNode): { spine: VectorSubpath; rail: VectorBlendRail; stops: number[]; replaced: boolean } | null {\n\tconst keys = vectorBlendKeys(group);\n\tif (keys.length < 2) return null;\n\tconst stored = group.blend?.spine;\n\tif (stored && stored.anchors.length >= 2) {\n\t\tconst rail = vectorBlendRail(stored);\n\t\tif (rail) {\n\t\t\tconst end = rail.closed ? rail.total * (1 - 1 / keys.length) : rail.total;\n\t\t\treturn { spine: stored, rail, stops: keys.map((_, i) => (end * i) / (keys.length - 1)), replaced: true };\n\t\t}\n\t}\n\tconst centres = keys.map(centreOf);\n\tconst spine: VectorSubpath = { closed: false, anchors: centres.map((c) => ({ x: c[0], y: c[1] })) };\n\tconst stops = [0];\n\tfor (let i = 1; i < centres.length; i++) stops.push(stops[i - 1] + Math.hypot(centres[i][0] - centres[i - 1][0], centres[i][1] - centres[i - 1][1]));\n\tconst rail = vectorBlendRail(spine) ?? { pts: centres, cum: stops, angles: centres.map(() => 0), total: stops[stops.length - 1], closed: false };\n\treturn { spine, rail, stops, replaced: false };\n}\n\n/** Where key `i` of a blend with spine `spine` should sit: its stop on the spine. */\nexport function vectorBlendKeyTargets(group: VectorGroupNode): Point[] {\n\tconst s = vectorBlendSpine(group);\n\treturn s ? s.stops.map((stop) => vectorBlendRailAt(s.rail, stop).point) : [];\n}\n\n// ---------------------------------------------------------------------------\n// Generating steps\n// ---------------------------------------------------------------------------\n\nconst morphCache = new WeakMap<VectorNode, WeakMap<VectorNode, Map<string, VectorMorph>>>();\n\nfunction morphFor(a: VectorNode, b: VectorNode, blend: VectorBlend): VectorMorph {\n\tconst starts = [blend.starts?.[a.id] ?? null, blend.starts?.[b.id] ?? null] as const;\n\tconst key = JSON.stringify([!!blend.linear, starts]);\n\tlet inner = morphCache.get(a);\n\tif (!inner) morphCache.set(a, (inner = new WeakMap()));\n\tlet byKey = inner.get(b);\n\tif (!byKey) inner.set(b, (byKey = new Map()));\n\tlet morph = byKey.get(key);\n\tif (!morph) byKey.set(key, (morph = createVectorMorph(a, b, { linear: blend.linear, starts })));\n\treturn morph;\n}\n\nconst wrapAngle = (a: number) => Math.atan2(Math.sin(a), Math.cos(a));\n\n/** A step marked as generated: locked, flagged, ids derived from `id`. */\nfunction asStep(node: VectorNode, id: string): VectorNode {\n\tconst rename = (n: VectorNode, nid: string): VectorNode =>\n\t\tn.type === 'group' ? { ...n, id: nid, children: n.children.map((c, i) => rename(c, `${nid}.${i}`)) } : { ...n, id: nid };\n\treturn { ...rename(node, id), blendStep: true, locked: true };\n}\n\n/**\n * A blend group's children with fresh steps: each key followed by the in-betweens to the next,\n * bottom to top. Groups with fewer than two keys keep just their keys.\n */\nexport function generateVectorBlendChildren(group: VectorGroupNode): VectorNode[] {\n\tconst keys = vectorBlendKeys(group);\n\tconst blend = group.blend;\n\tif (!blend || keys.length < 2) return keys;\n\tconst spine = vectorBlendSpine(group);\n\tif (!spine) return keys;\n\tconst out: VectorNode[] = [];\n\tlet counter = 0;\n\tkeys.forEach((a, i) => {\n\t\tout.push(a);\n\t\tconst b = keys[i + 1];\n\t\tif (!b) return;\n\t\tconst s0 = spine.stops[i];\n\t\tconst s1 = spine.stops[i + 1];\n\t\tconst n = vectorBlendStepCount(a, b, blend, Math.abs(s1 - s0));\n\t\tconst morph = morphFor(a, b, blend);\n\t\tconst p0 = vectorBlendRailAt(spine.rail, s0);\n\t\tconst p1 = vectorBlendRailAt(spine.rail, s1);\n\t\tfor (let j = 1; j <= n; j++) {\n\t\t\tconst t = j / (n + 1);\n\t\t\tlet step = morph.at(t);\n\t\t\tif (spine.replaced) {\n\t\t\t\tconst p = vectorBlendRailAt(spine.rail, s0 + (s1 - s0) * t);\n\t\t\t\tconst dx = p.point[0] - (p0.point[0] + (p1.point[0] - p0.point[0]) * t);\n\t\t\t\tconst dy = p.point[1] - (p0.point[1] + (p1.point[1] - p0.point[1]) * t);\n\t\t\t\tlet m: AffineMatrix = Math.hypot(dx, dy) > 1e-9 ? translateMatrix(dx, dy) : IDENTITY_MATRIX;\n\t\t\t\tif (blend.orientToSpine) {\n\t\t\t\t\tconst turn = wrapAngle(p.angle - (p0.angle + wrapAngle(p1.angle - p0.angle) * t));\n\t\t\t\t\tif (Math.abs(turn) > 1e-9) {\n\t\t\t\t\t\tconst c = centreOf(step);\n\t\t\t\t\t\tconst origin = { x: c[0] + m[4], y: c[1] + m[5] };\n\t\t\t\t\t\tm = multiplyMatrices(m, rotateMatrix((turn * 180) / Math.PI, origin));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (m !== IDENTITY_MATRIX) step = transformVectorNode(step, m);\n\t\t\t}\n\t\t\tout.push(asStep(step, `${group.id}~${++counter}`));\n\t\t}\n\t});\n\treturn out;\n}\n\nconst childrenCache = new WeakMap<VectorNode, { sig: string; keys: VectorNode[]; children: VectorNode[] }>();\n\nconst sameList = (a: readonly VectorNode[], b: readonly VectorNode[]) => a.length === b.length && a.every((n, i) => n === b[i]);\n\nfunction blendChildren(group: VectorGroupNode): VectorNode[] {\n\tconst keys = vectorBlendKeys(group);\n\tconst sig = `${group.id}|${JSON.stringify(group.blend)}`;\n\tconst cached = keys[0] ? childrenCache.get(keys[0]) : undefined;\n\tlet children: VectorNode[];\n\tif (cached && cached.sig === sig && sameList(cached.keys, keys)) children = cached.children;\n\telse {\n\t\tchildren = generateVectorBlendChildren(group);\n\t\tif (keys[0]) childrenCache.set(keys[0], { sig, keys, children });\n\t}\n\treturn sameList(children, group.children) ? group.children : children;\n}\n\n/**\n * Nodes with every blend group's steps brought up to date (nested blends first). Returns the\n * same array when nothing changed; steps are cached per set of keys, so calling this after\n * every edit is cheap.\n */\nexport function refreshVectorBlends(nodes: readonly VectorNode[]): VectorNode[] {\n\tlet changed = false;\n\tconst out = nodes.map((n) => {\n\t\tif (n.type !== 'group') return n;\n\t\tconst inner = refreshVectorBlends(n.children);\n\t\tlet next: VectorGroupNode = inner === n.children ? n : { ...n, children: inner };\n\t\tif (next.blend) {\n\t\t\tconst children = blendChildren(next);\n\t\t\tif (children !== next.children) next = { ...next, children };\n\t\t}\n\t\tif (next !== n) changed = true;\n\t\treturn next;\n\t});\n\treturn changed ? out : (nodes as VectorNode[]);\n}\n\n/** A step or key with the generated flags dropped and fresh ids. */\nfunction plain(node: VectorNode, newId: () => string): VectorNode {\n\tconst { blendStep: _s, locked: _l, ...rest } = node;\n\tconst renamed = { ...rest, id: newId() } as VectorNode;\n\tif (renamed.type === 'group') renamed.children = renamed.children.map((c) => plain(c, newId));\n\treturn renamed;\n}\n\n/** Expand: the blend as a plain group of its keys and steps (steps get fresh ids). */\nexport function expandVectorBlend(group: VectorGroupNode, newId: () => string): VectorGroupNode {\n\tconst { blend: _b, ...rest } = group;\n\treturn { ...rest, children: group.children.map((c) => (c.blendStep ? plain(c, newId) : c)) };\n}\n\n/**\n * Release: the blend's keys in the group's parent space (the group's transform and opacity\n * applied), followed by the replaced spine as an unpainted path when there is one.\n */\nexport function releaseVectorBlend(group: VectorGroupNode, newId: () => string): VectorNode[] {\n\tconst m = nodeMatrix(group);\n\tconst opacity = group.opacity;\n\tconst keys = vectorBlendKeys(group)\n\t\t.map((c) => (group.transform ? transformVectorNode(c, m) : c))\n\t\t.map((c) => (opacity !== undefined && opacity < 1 ? { ...c, opacity: (c.opacity ?? 1) * opacity } : c));\n\tconst spine = group.blend?.spine;\n\tif (!spine || spine.anchors.length < 2) return keys;\n\tconst path: VectorPathNode = { id: newId(), type: 'path', name: 'Spine', fill: null, stroke: null, subpaths: [{ closed: spine.closed, anchors: spine.anchors.map((a) => ({ ...a })) }] };\n\treturn [...keys, group.transform ? transformVectorNode(path, m) : path];\n}\n","/**\n * Free Distort, perspective and skew: the selection box's corners or sides dragged, and nodes sent\n * through a map of document space while each keeps its own (rigid) transform.\n *\n * Ported from ArtCraft (MIT OR Apache-2.0, © ArtCraft Team and contributors; see\n * THIRD_PARTY_NOTICES.md): vectorcraft `crates/tools/src/xform/free.rs` (`distort_quad`: a corner\n * moved freely, or with its partner mirrored along the dominant axis for perspective, and a side's\n * two corners moved together; `shear_for_side`: a side handle shears about the opposite side, or\n * the centre with Alt).\n */\nimport { mapVectorSubpaths, type VectorPointMap } from './pathMap.js';\nimport { mapVectorPaint } from './gradient.js';\nimport { applyMatrix, invertMatrix, multiplyMatrices, nodeMatrix, type AffineMatrix } from './geometry.js';\nimport { vectorEffectInputBounds, vectorEnvelopeMap, vectorEnvelopeMesh } from './appearance.js';\nimport { mapVectorMeshPoints } from './warp.js';\nimport { matrixAbout, skewMatrix } from '../geom/affine.js';\nimport type { Vec2, VectorBounds, VectorEffect, VectorGroupNode, VectorNode, VectorPathNode, VectorTransformHandle } from './types.js';\n\n/** Default tolerance (document units) of Free Distort's adaptive path mapping. */\nexport const VECTOR_DISTORT_TOLERANCE = 0.05;\n\n/** How dragging a selection box handle distorts it: each corner freely, or in symmetric perspective. */\nexport type VectorDistortMode = 'distort' | 'perspective';\n\nconst CORNER_INDEX: Partial<Record<VectorTransformHandle, number>> = { nw: 0, ne: 1, se: 2, sw: 3 };\nconst SIDE_CORNERS: Partial<Record<VectorTransformHandle, [number, number]>> = { n: [0, 1], e: [1, 2], s: [2, 3], w: [3, 0] };\n\n/** A box's corners clockwise from the top left. */\nexport const vectorBoxCorners = (b: VectorBounds): Vec2[] => [\n\t{ x: b.x, y: b.y },\n\t{ x: b.x + b.width, y: b.y },\n\t{ x: b.x + b.width, y: b.y + b.height },\n\t{ x: b.x, y: b.y + b.height }\n];\n\n/**\n * The quad (clockwise from the top left) box `b` becomes when handle `handle` is dragged by `d`: a\n * corner moves by `d` (`distort`), or along the drag's dominant axis with the corner beside it on\n * that side moving the opposite way (`perspective`); a side moves both its corners.\n */\nexport function vectorDistortQuad(b: VectorBounds, handle: VectorTransformHandle, d: Vec2, mode: VectorDistortMode = 'distort'): Vec2[] {\n\tconst q = vectorBoxCorners(b);\n\tconst i = CORNER_INDEX[handle];\n\tif (i !== undefined) {\n\t\tif (mode === 'perspective') {\n\t\t\tif (Math.abs(d.x) >= Math.abs(d.y)) {\n\t\t\t\tconst partner = [1, 0, 3, 2][i];\n\t\t\t\tq[i].x += d.x;\n\t\t\t\tq[partner].x -= d.x;\n\t\t\t} else {\n\t\t\t\tconst partner = [3, 2, 1, 0][i];\n\t\t\t\tq[i].y += d.y;\n\t\t\t\tq[partner].y -= d.y;\n\t\t\t}\n\t\t} else {\n\t\t\tq[i].x += d.x;\n\t\t\tq[i].y += d.y;\n\t\t}\n\t\treturn q;\n\t}\n\tconst side = SIDE_CORNERS[handle];\n\tif (side) for (const k of side) q[k] = { x: q[k].x + d.x, y: q[k].y + d.y };\n\treturn q;\n}\n\nconst about = (o: Vec2, m: AffineMatrix): AffineMatrix => matrixAbout(m, o);\n\n/**\n * The skew for dragging side `handle` of box `b` by `d`: along that side, about the opposite side\n * (or the centre, `fromCenter`). Corners give the identity.\n */\nexport function vectorShearForSide(b: VectorBounds, handle: VectorTransformHandle, d: Vec2, fromCenter = false): AffineMatrix {\n\tconst center = { x: b.x + b.width / 2, y: b.y + b.height / 2 };\n\tif (handle === 'n' || handle === 's') {\n\t\tconst o = fromCenter ? center : { x: center.x, y: handle === 'n' ? b.y + b.height : b.y };\n\t\tconst lever = (handle === 'n' ? b.y : b.y + b.height) - o.y;\n\t\treturn about(o, [1, 0, Math.abs(lever) > 1e-9 ? d.x / lever : 0, 1, 0, 0]);\n\t}\n\tif (handle === 'e' || handle === 'w') {\n\t\tconst o = fromCenter ? center : { x: handle === 'w' ? b.x + b.width : b.x, y: center.y };\n\t\tconst lever = (handle === 'w' ? b.x : b.x + b.width) - o.x;\n\t\treturn about(o, [1, Math.abs(lever) > 1e-9 ? d.y / lever : 0, 0, 1, 0, 0]);\n\t}\n\treturn [1, 0, 0, 1, 0, 0];\n}\n\n/**\n * A skew by `ax` degrees horizontally (vertical lines lean right for positive angles: x' = x + tan(ax)·y)\n * and `ay` degrees vertically (y' = y + tan(ay)·x), about `origin`.\n */\nexport function vectorSkewMatrix(ax: number, ay: number, origin: Vec2 = { x: 0, y: 0 }): AffineMatrix {\n\treturn skewMatrix(ax, ay, origin);\n}\n\n/** Effects after their node's geometry went through a non-affine map: envelope meshes follow it (a preset becomes the mesh it stood for). */\nfunction mapEffects(node: VectorNode, f: VectorPointMap): VectorEffect[] | undefined {\n\tif (!node.effects?.length) return undefined;\n\treturn node.effects.map((e) => {\n\t\tif (e.type !== 'envelope') return e;\n\t\tif (e.params.style === 'mesh') return e.params.mesh ? { ...e, params: { ...e.params, mesh: mapVectorMeshPoints(e.params.mesh, f) } } : e;\n\t\tconst bounds = vectorEffectInputBounds(node, e.id);\n\t\tif (!bounds || !vectorEnvelopeMap(e.params, bounds)) return e;\n\t\treturn { ...e, params: { ...e.params, style: 'mesh', mesh: mapVectorMeshPoints(vectorEnvelopeMesh(e.params, bounds), f) } };\n\t});\n}\n\n/**\n * A node sent through `f`, a map of the space `parentToDoc` maps its parent to (the document):\n * every anchor and handle is mapped in the node's own space, adaptively so curves stay within\n * `tolerance`, and the node keeps its rigid `transform`. Gradients in the node's space have their\n * handles mapped (bounding-box ones follow the new bounds), envelope meshes and a blend's spine\n * are mapped too; stroke widths, live corner radii and effect parameters are kept.\n */\nexport function distortVectorNode<T extends VectorNode>(node: T, parentToDoc: AffineMatrix, f: VectorPointMap, tolerance = VECTOR_DISTORT_TOLERANCE): T {\n\tconst toDoc = multiplyMatrices(nodeMatrix(node), parentToDoc);\n\tconst inv = invertMatrix(toDoc);\n\tconst local: VectorPointMap = (p) => applyMatrix(inv, f(applyMatrix(toDoc, p)));\n\tconst effects = mapEffects(node, local);\n\tif (node.type === 'group') {\n\t\tconst group = node as VectorGroupNode;\n\t\tconst children = group.children.map((c) => distortVectorNode(c, toDoc, f, tolerance));\n\t\tconst spine = group.blend?.spine;\n\t\treturn {\n\t\t\t...group,\n\t\t\tchildren,\n\t\t\t...(effects ? { effects } : {}),\n\t\t\t...(spine ? { blend: { ...group.blend, spine: mapVectorSubpaths([spine], local, tolerance)[0] } } : {})\n\t\t} as T;\n\t}\n\tconst path = node as VectorPathNode;\n\treturn {\n\t\t...path,\n\t\t...(effects ? { effects } : {}),\n\t\t...(path.fill !== undefined ? { fill: mapVectorPaint(path.fill, local) } : {}),\n\t\t...(path.stroke !== undefined ? { stroke: mapVectorPaint(path.stroke, local) } : {}),\n\t\tsubpaths: mapVectorSubpaths(path.subpaths, local, tolerance)\n\t} as T;\n}\n","/**\n * Smart guides and object snapping for the vector editor. Targets — anchors, bounding-box\n * edges and centres, path segments and the artboard — are gathered once per gesture in\n * document space (rotated nodes included) and kept sorted by x and y, so each pointer move\n * finds the few near the pointer by bisection. A point snaps, in this order of priority: kept\n * to its Shift angle and slid into line with a target, onto an anchor or centre, onto an\n * intersection of two paths, onto the perpendicular foot or tangent point from where it\n * leaves, onto a path, onto a construction angle, then into line with targets on x and y. A\n * moved box lines its edges and centre up with targets, or spaces itself evenly in its row\n * and column; a resized box lands its moving edges on targets, or matches another object's\n * width or height. Every snap reports labelled guide lines in document units.\n *\n * Portions ported from vectorcraft-main/crates/tools/src/guides.rs (`Targets`, `within`,\n * `for_move`, `for_drawing`, `for_handle`, `for_anchor_drag`, `push_segments`, `snap_from`,\n * `slide`, `lined_up`, `on_segment`, `crossing`, `align_rects`, `snap_rects`, `even_spacing`,\n * `gap_marks`, `snap_scale`, `Leave`), figcraft-main/crates/engine/src/snap.rs (`spacings`,\n * `equal_spacing`, `spacing_guides`: equal-spacing cases and gap labels),\n * designcraft-main/crates/tools/src/snap.rs (`pass_dimensions`: matching other objects'\n * sizes) and cadcraft-main/crates/engine/src/snap/curves.rs (`Param::solve`: perpendicular and\n * tangent points by sign-change sampling and bisection) (MIT OR Apache-2.0, © ArtCraft Team\n * and contributors). See THIRD_PARTY_NOTICES.md.\n */\nimport type { Point } from '../canvas/types.js';\nimport { expandVectorAppearance, vectorAppearanceNodes } from './appearance.js';\nimport { isVectorMaskActive } from './mask.js';\nimport { segmentDerivative, segmentNearest, segmentPoint, type BezierSegment } from '../canvas/pathops/bezier.js';\nimport { segmentIntersections } from '../canvas/pathops/boolean.js';\nimport {\n\tapplyMatrix,\n\tIDENTITY_MATRIX,\n\tmatrixRotation,\n\tmultiplyMatrices,\n\tnodeMatrix,\n\trotateMatrix,\n\tsubpathSegment,\n\tsubpathSegmentCount,\n\tvectorNodeBounds,\n\ttype AffineMatrix\n} from './geometry.js';\nimport type { VectorAnchorRef, VectorBounds, VectorDocument, VectorNode, VectorSubpath, VectorTransformHandle, Vec2 } from './types.js';\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * What a guide line shows: `align` lines run from a target through the snapped point or box,\n * `construction` lines from where a drawn point leaves, `spacing` marks equal gaps and `size`\n * the side whose length matches another object's.\n */\nexport type VectorGuideKind = 'align' | 'construction' | 'spacing' | 'size';\n\n/** A guide line in document units. */\nexport interface VectorGuideLine {\n\tkind: VectorGuideKind;\n\tx1: number;\n\ty1: number;\n\tx2: number;\n\ty2: number;\n}\n\n/** What a point or box snapped to. */\nexport type VectorSnapKind = 'anchor' | 'centre' | 'intersect' | 'perpendicular' | 'tangent' | 'path' | 'construction' | 'align' | 'spacing' | 'size';\n\n/** A guide label in document units: the snap kind's name, or a distance for gaps and sizes. */\nexport interface VectorGuideLabel {\n\tkind: VectorSnapKind;\n\tx: number;\n\ty: number;\n\ttext: string;\n}\n\nexport interface VectorSnapGuides {\n\tlines: VectorGuideLine[];\n\tlabels: VectorGuideLabel[];\n}\n\nexport interface VectorPointSnap extends VectorSnapGuides {\n\tpoint: Vec2;\n\t/** The rule that placed the point, or null when nothing was in reach. */\n\tkind: VectorSnapKind | null;\n\t/** Whether x / y were set by a target (for falling back to the grid on the other axis). */\n\tx: boolean;\n\ty: boolean;\n}\n\nexport interface VectorRectSnap extends VectorSnapGuides {\n\t/** Correction to add to the move. */\n\tdx: number;\n\tdy: number;\n\tx: boolean;\n\ty: boolean;\n}\n\nexport interface VectorScaleSnap extends VectorSnapGuides {\n\tsx: number;\n\tsy: number;\n\tsnapped: boolean;\n}\n\n/**\n * The point a drawn point leaves from (the anchor before it, a shape's start, a handle's\n * anchor) and how it keeps to it.\n */\nexport interface VectorSnapLeave {\n\tat: Vec2;\n\t/**\n\t * Shift held: keep to 45° steps round `at` (`angle`), to a diagonal (`square`, a box's\n\t * corner) or to the nearer axis (`axis`, a dragged point).\n\t */\n\tconstrain?: 'angle' | 'square' | 'axis' | null;\n\t/** Construction guide angles through `at`, degrees counter-clockwise on screen. */\n\tangles?: readonly number[];\n\t/** Snap to the perpendicular foot and tangent points from `at` on paths (default true). */\n\tcurves?: boolean;\n}\n\n/** The construction guide angles new editors start with. */\nexport const VECTOR_CONSTRUCTION_ANGLES: readonly number[] = [0, 45, 90, 135];\n\n/** Where `constrain` puts `p` (unchanged without one). */\nexport function constrainToLeave(leave: VectorSnapLeave | null | undefined, p: Vec2): Vec2 {\n\tif (!leave?.constrain) return p;\n\tconst dx = p.x - leave.at.x;\n\tconst dy = p.y - leave.at.y;\n\tswitch (leave.constrain) {\n\t\tcase 'axis':\n\t\t\treturn Math.abs(dx) > Math.abs(dy) ? { x: p.x, y: leave.at.y } : { x: leave.at.x, y: p.y };\n\t\tcase 'square': {\n\t\t\tconst s = Math.max(Math.abs(dx), Math.abs(dy));\n\t\t\treturn { x: leave.at.x + Math.sign(dx || 1) * s, y: leave.at.y + Math.sign(dy || 1) * s };\n\t\t}\n\t\tdefault: {\n\t\t\tconst len = Math.hypot(dx, dy);\n\t\t\tconst a = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);\n\t\t\treturn { x: leave.at.x + Math.cos(a) * len, y: leave.at.y + Math.sin(a) * len };\n\t\t}\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Targets\n// ---------------------------------------------------------------------------\n\ntype TargetKind = 'anchor' | 'centre' | 'edge' | 'artboard';\n\ninterface PointTarget {\n\tp: Vec2;\n\tkind: 'anchor' | 'centre';\n}\n\n/** A line to line up with: where it runs on its axis and the target point it comes from. */\ninterface LineTarget {\n\tv: number;\n\tfrom: Vec2;\n\tkind: TargetKind;\n}\n\ninterface SegmentTarget {\n\tseg: Point[];\n\tminX: number;\n\tminY: number;\n\tmaxX: number;\n\tmaxY: number;\n}\n\n/** How many anchors, and how many segments, the targets gather at most. */\nconst BUDGET = 20_000;\n/** The first near segments paired up when looking for intersections. */\nconst CROSSING_PAIRS = 32;\n/** Parameter samples per segment for perpendicular / tangent roots, and bisection steps. */\nconst CURVE_SAMPLES = 48;\nconst BISECT = 50;\nconst EQUAL = 1e-6;\n\n/** First index of `sorted` whose key is not below `v` (`strict`: above `v`). */\nfunction bound<T>(sorted: readonly T[], key: (e: T) => number, v: number, strict: boolean): number {\n\tlet lo = 0;\n\tlet hi = sorted.length;\n\twhile (lo < hi) {\n\t\tconst mid = (lo + hi) >>> 1;\n\t\tconst k = key(sorted[mid]);\n\t\tif (strict ? k <= v : k < v) lo = mid + 1;\n\t\telse hi = mid;\n\t}\n\treturn lo;\n}\n\n/** The entries of `sorted` (ascending by `key`) whose key is within `tol` of `v`, by bisection. */\nfunction within<T>(sorted: readonly T[], key: (e: T) => number, v: number, tol: number): readonly T[] {\n\treturn sorted.slice(bound(sorted, key, v - tol, false), bound(sorted, key, v + tol, true));\n}\n\nconst linesNear = (lines: readonly LineTarget[], v: number, tol: number) => within(lines, (l) => l.v, v, tol);\n\nconst distance = (a: Vec2, b: Vec2) => Math.hypot(a.x - b.x, a.y - b.y);\nconst toPoint = (p: Vec2): Point => [p.x, p.y];\nconst toVec = (p: Point): Vec2 => ({ x: p[0], y: p[1] });\n\n/** What `collect` gathers from each path besides its anchors and bounds. */\ntype Gather = 'bounds' | 'segments' | 'drawing';\n\nconst LABELS: Record<VectorSnapKind, string> = {\n\tanchor: 'anchor',\n\tcentre: 'centre',\n\tintersect: 'intersect',\n\tperpendicular: 'perpendicular',\n\ttangent: 'tangent',\n\tpath: 'path',\n\tconstruction: 'angle',\n\talign: 'align',\n\tspacing: 'spacing',\n\tsize: 'size'\n};\n\nconst fmtLength = (n: number) => String(Math.round(n * 100) / 100);\n\n/** A node's own (possibly rotated) frame: its size there and its centre in document space. */\nfunction nodeFrame(node: VectorNode, parent: AffineMatrix, aabb?: VectorBounds | null): { width: number; height: number; centre: Vec2 } | null {\n\tconst angle = matrixRotation(multiplyMatrices(nodeMatrix(node), parent));\n\tconst b = angle ? vectorNodeBounds(node, multiplyMatrices(parent, rotateMatrix(-angle))) : aabb !== undefined ? aabb : vectorNodeBounds(node, parent);\n\tif (!b) return null;\n\tconst c = { x: b.x + b.width / 2, y: b.y + b.height / 2 };\n\treturn { width: b.width, height: b.height, centre: angle ? applyMatrix(rotateMatrix(angle), c) : c };\n}\n\n/** True when `id` is `node` or inside it. */\nfunction contains(node: VectorNode, ids: ReadonlySet<string>): boolean {\n\tif (ids.has(node.id)) return true;\n\treturn node.type === 'group' && node.children.some((c) => contains(c, ids));\n}\n\n/**\n * Snap targets in document space, gathered once per gesture. Build them with `forMove`,\n * `forResize`, `forDrawing`, `forAnchorDrag` or `forHandle`, then call `snapFrom`,\n * `snapRect` or `snapScale` on every pointer move. Tolerances are in document units (screen\n * pixels divided by the zoom).\n */\nexport class VectorSnapTargets {\n\tprivate readonly points: PointTarget[] = [];\n\tprivate readonly xs: LineTarget[] = [];\n\tprivate readonly ys: LineTarget[] = [];\n\tprivate readonly segments: SegmentTarget[] = [];\n\t/** Bounds of the objects a moved box spaces itself evenly with. */\n\tprivate readonly boxes: VectorBounds[] = [];\n\t/** Side lengths of the other objects (in their own frames), which a resize may match. */\n\tprivate readonly sizes: number[] = [];\n\n\tprivate constructor() {}\n\n\t/** How many targets of each sort were gathered. */\n\tget counts(): { points: number; lines: number; segments: number; boxes: number } {\n\t\treturn { points: this.points.length, lines: this.xs.length + this.ys.length, segments: this.segments.length, boxes: this.boxes.length };\n\t}\n\n\t/** Targets for moving nodes `ids`: the other art's anchors, bounds and centres, the artboard, and boxes to space evenly with. */\n\tstatic forMove(doc: VectorDocument, ids: readonly string[]): VectorSnapTargets {\n\t\treturn VectorSnapTargets.collect(doc, new Set(ids), 'bounds');\n\t}\n\n\t/** Targets for resizing nodes `ids`: as for a move, plus the other objects' sizes. */\n\tstatic forResize(doc: VectorDocument, ids: readonly string[]): VectorSnapTargets {\n\t\treturn VectorSnapTargets.collect(doc, new Set(ids), 'bounds');\n\t}\n\n\t/**\n\t * Targets for a drawn point (pen, shape tools): the art but `exclude`, its anchors (as\n\t * points and in line), centres, edges and segments. The anchors and segments of `exclude`\n\t * (the path being drawn, whose bounds grow with it) stay targets.\n\t */\n\tstatic forDrawing(doc: VectorDocument, exclude: readonly string[] = []): VectorSnapTargets {\n\t\tconst skip = new Set(exclude);\n\t\tconst t = VectorSnapTargets.collect(doc, skip, 'drawing', false);\n\t\tt.pushPaths(doc, skip, () => false, true);\n\t\treturn t.index();\n\t}\n\n\t/**\n\t * Targets for dragging anchors: the other art with its segments, and the anchors and\n\t * segments that stay put on the paths being reshaped (their bounds move along, so they are\n\t * no targets). The moving anchors are left out.\n\t */\n\tstatic forAnchorDrag(doc: VectorDocument, anchors: readonly VectorAnchorRef[]): VectorSnapTargets {\n\t\tconst moving = new Map<string, Set<string>>();\n\t\tfor (const r of anchors) {\n\t\t\tconst set = moving.get(r.nodeId) ?? new Set<string>();\n\t\t\tset.add(`${r.subpath}:${r.index}`);\n\t\t\tmoving.set(r.nodeId, set);\n\t\t}\n\t\tconst t = VectorSnapTargets.collect(doc, new Set(moving.keys()), 'segments', false);\n\t\tt.pushPaths(doc, new Set(moving.keys()), (id, s, i) => !!moving.get(id)?.has(`${s}:${i}`), true);\n\t\treturn t.index();\n\t}\n\n\t/**\n\t * Targets for dragging a handle of path `nodeId`: the other art and that path's anchors\n\t * (its bounds and the segments through the handle move with it, so they would chase it).\n\t */\n\tstatic forHandle(doc: VectorDocument, nodeId: string): VectorSnapTargets {\n\t\tconst skip = new Set([nodeId]);\n\t\tconst t = VectorSnapTargets.collect(doc, skip, 'bounds', false);\n\t\tt.pushPaths(doc, skip, () => false, false);\n\t\treturn t.index();\n\t}\n\n\t/** The anchors of the paths `ids` as targets (on them and in line), and their segments when `segments`. */\n\tprivate pushPaths(doc: VectorDocument, ids: ReadonlySet<string>, moving: (id: string, subpath: number, index: number) => boolean, segments: boolean) {\n\t\tlet anchors = BUDGET;\n\t\tlet segmentBudget = segments ? BUDGET : 0;\n\t\tconst visit = (list: readonly VectorNode[], parent: AffineMatrix) => {\n\t\t\tfor (const n of list) {\n\t\t\t\tconst m = multiplyMatrices(nodeMatrix(n), parent);\n\t\t\t\tif (n.type === 'group') visit(n.children, m);\n\t\t\t\telse if (ids.has(n.id) && n.visible !== false) {\n\t\t\t\t\tn.subpaths.forEach((sp, s) => {\n\t\t\t\t\t\tsp.anchors.forEach((a, i) => {\n\t\t\t\t\t\t\tif (anchors <= 0 || moving(n.id, s, i)) return;\n\t\t\t\t\t\t\tthis.pushAnchor(applyMatrix(m, a));\n\t\t\t\t\t\t\tanchors--;\n\t\t\t\t\t\t});\n\t\t\t\t\t\tsegmentBudget = this.pushSegments(sp, m, (i) => moving(n.id, s, i), segmentBudget);\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\tvisit(doc.nodes, IDENTITY_MATRIX);\n\t}\n\n\tprivate pushAnchor(p: Vec2) {\n\t\tthis.points.push({ p, kind: 'anchor' });\n\t\tthis.xs.push({ v: p.x, from: p, kind: 'anchor' });\n\t\tthis.ys.push({ v: p.y, from: p, kind: 'anchor' });\n\t}\n\n\t/** The segments of `sp` (mapped by `m`) that stay put while the anchors `moving` accepts move. */\n\tprivate pushSegments(sp: VectorSubpath, m: AffineMatrix, moving: (index: number) => boolean, budget: number): number {\n\t\tconst n = sp.anchors.length;\n\t\tconst count = subpathSegmentCount(sp);\n\t\tfor (let i = 0; i < count && budget > 0; i++) {\n\t\t\tif (moving(i) || moving((i + 1) % n)) continue;\n\t\t\tconst seg = subpathSegment(sp, i).map((q) => toPoint(applyMatrix(m, toVec(q))));\n\t\t\tlet minX = Infinity;\n\t\t\tlet minY = Infinity;\n\t\t\tlet maxX = -Infinity;\n\t\t\tlet maxY = -Infinity;\n\t\t\tfor (const [x, y] of seg) {\n\t\t\t\tminX = Math.min(minX, x);\n\t\t\t\tminY = Math.min(minY, y);\n\t\t\t\tmaxX = Math.max(maxX, x);\n\t\t\t\tmaxY = Math.max(maxY, y);\n\t\t\t}\n\t\t\tthis.segments.push({ seg, minX, minY, maxX, maxY });\n\t\t\tbudget--;\n\t\t}\n\t\treturn budget;\n\t}\n\n\tprivate pushRect(r: VectorBounds, kind: TargetKind, centre: Vec2 | null) {\n\t\tconst corners = [\n\t\t\t{ x: r.x, y: r.y },\n\t\t\t{ x: r.x + r.width, y: r.y },\n\t\t\t{ x: r.x + r.width, y: r.y + r.height },\n\t\t\t{ x: r.x, y: r.y + r.height }\n\t\t];\n\t\tfor (const p of corners) {\n\t\t\tthis.xs.push({ v: p.x, from: p, kind });\n\t\t\tthis.ys.push({ v: p.y, from: p, kind });\n\t\t}\n\t\tif (centre) {\n\t\t\tthis.points.push({ p: centre, kind: 'centre' });\n\t\t\tthis.xs.push({ v: centre.x, from: centre, kind: 'centre' });\n\t\t\tthis.ys.push({ v: centre.y, from: centre, kind: 'centre' });\n\t\t}\n\t}\n\n\t/** Sorts the targets for `within` (stable: of targets alike, the first gathered wins). */\n\tprivate index(): this {\n\t\tthis.points.sort((a, b) => a.p.x - b.p.x);\n\t\tthis.xs.sort((a, b) => a.v - b.v);\n\t\tthis.ys.sort((a, b) => a.v - b.v);\n\t\treturn this;\n\t}\n\n\t/**\n\t * Gathers targets from the artboard and every visible node but `exclude` (and what is inside\n\t * those): each leaf's anchors (also as lines for `drawing`), its segments (`segments`,\n\t * `drawing`), its bounds' corners as lines and its centre; and the top-level objects' boxes and\n\t * sizes (inside a group holding an excluded node, that group's other members).\n\t */\n\tprivate static collect(doc: VectorDocument, exclude: ReadonlySet<string>, gather: Gather, sort = true): VectorSnapTargets {\n\t\tconst self = new VectorSnapTargets();\n\t\tconst artboard = { x: 0, y: 0, width: doc.width, height: doc.height };\n\t\tself.pushRect(artboard, 'artboard', { x: doc.width / 2, y: doc.height / 2 });\n\t\tlet anchors = BUDGET;\n\t\tlet segments = BUDGET;\n\t\tconst visit = (list: readonly VectorNode[], parent: AffineMatrix) => {\n\t\t\tfor (const stored of list) {\n\t\t\t\tif (exclude.has(stored.id) || stored.visible === false) continue;\n\t\t\t\t// A mask group snaps to its masked result: its contents' shapes, clipped.\n\t\t\t\tif (isVectorMaskActive(stored) && !contains(stored, exclude)) {\n\t\t\t\t\tvisit(vectorAppearanceNodes(stored), multiplyMatrices(nodeMatrix(stored), parent));\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t// Other art snaps where it draws (path effects and live booleans applied).\n\t\t\t\tconst n = contains(stored, exclude) ? stored : expandVectorAppearance(stored);\n\t\t\t\tconst m = multiplyMatrices(nodeMatrix(n), parent);\n\t\t\t\tif (n.type === 'group') {\n\t\t\t\t\tvisit(n.children, m);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (anchors <= 0) continue;\n\t\t\t\tfor (const sp of n.subpaths) {\n\t\t\t\t\tfor (const a of sp.anchors) {\n\t\t\t\t\t\tif (anchors-- <= 0) break;\n\t\t\t\t\t\tconst p = applyMatrix(m, a);\n\t\t\t\t\t\tif (gather === 'drawing') self.pushAnchor(p);\n\t\t\t\t\t\telse self.points.push({ p, kind: 'anchor' });\n\t\t\t\t\t}\n\t\t\t\t\tif (gather !== 'bounds') segments = self.pushSegments(sp, m, () => false, segments);\n\t\t\t\t}\n\t\t\t\tconst b = vectorNodeBounds(n, parent);\n\t\t\t\tconst f = nodeFrame(n, parent, b);\n\t\t\t\tif (b) self.pushRect(b, 'edge', f?.centre ?? null);\n\t\t\t}\n\t\t};\n\t\tvisit(doc.nodes, IDENTITY_MATRIX);\n\t\t// Boxes to space evenly with and sizes to match: whole objects, opening groups that hold an excluded node.\n\t\tconst objects = (list: readonly VectorNode[], parent: AffineMatrix) => {\n\t\t\tfor (const n of list) {\n\t\t\t\tif (exclude.has(n.id) || n.visible === false || self.boxes.length >= BUDGET) continue;\n\t\t\t\tif (n.type === 'group' && contains(n, exclude)) {\n\t\t\t\t\tobjects(n.children, multiplyMatrices(nodeMatrix(n), parent));\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst b = vectorNodeBounds(n, parent);\n\t\t\t\tif (b) self.boxes.push(b);\n\t\t\t\tconst f = nodeFrame(n, parent, b);\n\t\t\t\tif (f) self.sizes.push(f.width, f.height);\n\t\t\t}\n\t\t};\n\t\tobjects(doc.nodes, IDENTITY_MATRIX);\n\t\treturn sort ? self.index() : self;\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Points\n\t// -------------------------------------------------------------------------\n\n\t/** Snaps a single point (see `snapFrom`). */\n\tsnapPoint(p: Vec2, tol: number): VectorPointSnap {\n\t\treturn this.snapFrom(p, null, tol);\n\t}\n\n\t/**\n\t * Snaps a point `p`, drawn or dragged from `leave` when given. With `leave.constrain` it keeps\n\t * to its angle round `leave.at`, sliding along that way into line with the nearest target.\n\t * Otherwise it lands, in order, on an anchor or centre, an intersection of two paths, the\n\t * perpendicular foot or a tangent point from `leave.at`, a path, a construction guide\n\t * through `leave.at` (where a target's line crosses it if one does nearby), or else lines up\n\t * with targets on x and y.\n\t */\n\tsnapFrom(p: Vec2, leave: VectorSnapLeave | null | undefined, tol: number): VectorPointSnap {\n\t\tconst none = (q: Vec2): VectorPointSnap => ({ point: q, kind: null, x: false, y: false, lines: [], labels: [] });\n\t\tconst at = (q: Vec2, kind: VectorSnapKind, lines: VectorGuideLine[] = []): VectorPointSnap => ({\n\t\t\tpoint: q,\n\t\t\tkind,\n\t\t\tx: true,\n\t\t\ty: true,\n\t\t\tlines,\n\t\t\tlabels: [this.label(q, kind)]\n\t\t});\n\t\tif (leave?.constrain) {\n\t\t\tconst q = constrainToLeave(leave, p);\n\t\t\tconst hit = this.slide(leave.at, { x: q.x - leave.at.x, y: q.y - leave.at.y }, q, tol, true);\n\t\t\treturn hit ? { point: hit.point, x: true, y: true, ...this.linedUp(hit.point, hit.from) } : none(q);\n\t\t}\n\t\tconst point = this.nearestPoint(p, tol);\n\t\tif (point) return at(point.p, point.kind);\n\t\tconst near = this.segmentsNear(p, tol);\n\t\tconst crossing = this.crossing(near, p, tol);\n\t\tif (crossing) return at(crossing, 'intersect');\n\t\tif (leave && leave.curves !== false) {\n\t\t\tconst foot = this.curvePoint(near, leave.at, p, tol);\n\t\t\tif (foot) return at(foot.point, foot.kind, [line('construction', leave.at, foot.point)]);\n\t\t}\n\t\tconst on = this.onSegment(near);\n\t\tif (on) return at(on, 'path');\n\t\tif (leave?.angles?.length) {\n\t\t\tconst c = construction(leave, p, tol);\n\t\t\tif (c) {\n\t\t\t\tconst hit = this.slide(leave.at, c.dir, c.point, tol, false);\n\t\t\t\tif (hit) {\n\t\t\t\t\tconst lined = this.linedUp(hit.point, hit.from);\n\t\t\t\t\treturn { point: hit.point, x: true, y: true, kind: lined.kind, lines: [line('construction', leave.at, hit.point), ...lined.lines], labels: lined.labels };\n\t\t\t\t}\n\t\t\t\treturn { point: c.point, kind: 'construction', x: true, y: true, lines: [line('construction', leave.at, c.point)], labels: [] };\n\t\t\t}\n\t\t}\n\t\tconst nearest = (lines: readonly LineTarget[], v: number) => {\n\t\t\tlet best: LineTarget | null = null;\n\t\t\tfor (const l of linesNear(lines, v, tol)) if (!best || Math.abs(l.v - v) < Math.abs(best.v - v)) best = l;\n\t\t\treturn best;\n\t\t};\n\t\tconst bx = nearest(this.xs, p.x);\n\t\tconst by = nearest(this.ys, p.y);\n\t\tif (!bx && !by) return none(p);\n\t\tconst out = { x: bx ? bx.v : p.x, y: by ? by.v : p.y };\n\t\tconst lines: VectorGuideLine[] = [];\n\t\tif (bx) lines.push(line('align', bx.from, { x: out.x, y: out.y }));\n\t\tif (by) lines.push(line('align', by.from, { x: out.x, y: out.y }));\n\t\treturn { point: out, kind: 'align', x: !!bx, y: !!by, lines, labels: [this.label(out, 'align')] };\n\t}\n\n\tprivate label(p: Vec2, kind: VectorSnapKind, text = LABELS[kind]): VectorGuideLabel {\n\t\treturn { kind, x: p.x, y: p.y, text };\n\t}\n\n\t/** The anchor or centre nearest `p`, within `tol`. */\n\tprivate nearestPoint(p: Vec2, tol: number): PointTarget | null {\n\t\tlet best: PointTarget | null = null;\n\t\tlet bestD = Infinity;\n\t\tfor (const t of within(this.points, (e) => e.p.x, p.x, tol)) {\n\t\t\tconst d = distance(t.p, p);\n\t\t\tif (d <= tol && d < bestD) {\n\t\t\t\tbest = t;\n\t\t\t\tbestD = d;\n\t\t\t}\n\t\t}\n\t\treturn best;\n\t}\n\n\t/**\n\t * Slides `q`, on the line through `at` along `dir` (beyond `at` only for a `ray`), to where\n\t * the nearest target line crosses it, at most twice `tol` away: where it lands and the\n\t * target point that line comes from.\n\t */\n\tprivate slide(at: Vec2, dir: Vec2, q: Vec2, tol: number, ray: boolean): { point: Vec2; from: Vec2 } | null {\n\t\tconst len2 = dir.x * dir.x + dir.y * dir.y;\n\t\tif (!Number.isFinite(len2) || len2 <= 1e-18) return null;\n\t\tconst reach = 2 * tol;\n\t\tlet best: { moved: number; point: Vec2; from: Vec2 } | null = null;\n\t\tfor (const vertical of [true, false]) {\n\t\t\tconst d = vertical ? dir.x : dir.y;\n\t\t\tif (Math.abs(d) <= 1e-9) continue;\n\t\t\tconst a = vertical ? at.x : at.y;\n\t\t\tfor (const l of linesNear(vertical ? this.xs : this.ys, vertical ? q.x : q.y, reach)) {\n\t\t\t\tconst s = (l.v - a) / d;\n\t\t\t\t// Exactly on the line, whatever the rounding along the way.\n\t\t\t\tconst hit = vertical ? { x: l.v, y: at.y + dir.y * s } : { x: at.x + dir.x * s, y: l.v };\n\t\t\t\tconst moved = distance(hit, q);\n\t\t\t\tif (moved <= reach && (!ray || s > 0) && (!best || moved < best.moved)) best = { moved, point: hit, from: l.from };\n\t\t\t}\n\t\t}\n\t\treturn best && { point: best.point, from: best.from };\n\t}\n\n\t/** The guides of a point `q` slid into line with the target at `from`: the line and \"align\", or the target's own label when `q` sits on it. */\n\tprivate linedUp(q: Vec2, from: Vec2): { kind: VectorSnapKind } & VectorSnapGuides {\n\t\tconst on = this.nearestPoint(q, 1e-9);\n\t\tif (on) return { kind: on.kind, lines: [], labels: [this.label(on.p, on.kind)] };\n\t\treturn { kind: 'align', lines: [line('align', from, q)], labels: [this.label(q, 'align')] };\n\t}\n\n\t/** Target segments whose control box reaches `p` and which pass within `tol` of it, nearest first. */\n\tprivate segmentsNear(p: Vec2, tol: number): { seg: Point[]; t: number; distance: number }[] {\n\t\tconst out: { seg: Point[]; t: number; distance: number }[] = [];\n\t\tconst pt = toPoint(p);\n\t\tfor (const s of this.segments) {\n\t\t\tif (p.x < s.minX - tol || p.x > s.maxX + tol || p.y < s.minY - tol || p.y > s.maxY + tol) continue;\n\t\t\tconst hit = segmentNearest(s.seg, pt);\n\t\t\tif (hit.distance <= tol) out.push({ seg: s.seg, t: hit.t, distance: hit.distance });\n\t\t}\n\t\treturn out.sort((a, b) => a.distance - b.distance);\n\t}\n\n\tprivate onSegment(near: readonly { seg: Point[]; t: number }[]): Vec2 | null {\n\t\treturn near.length ? toVec(segmentPoint(near[0].seg, near[0].t)) : null;\n\t}\n\n\t/** Where two of the target segments near `p` cross, the crossing nearest `p` within `tol`. */\n\tprivate crossing(near: readonly { seg: Point[] }[], p: Vec2, tol: number): Vec2 | null {\n\t\tconst list = near.slice(0, CROSSING_PAIRS);\n\t\tlet best: Vec2 | null = null;\n\t\tlet bestD = Infinity;\n\t\tfor (let i = 0; i < list.length; i++) {\n\t\t\tfor (let j = i + 1; j < list.length; j++) {\n\t\t\t\tfor (const hit of segmentIntersections(list[i].seg as BezierSegment, list[j].seg as BezierSegment)) {\n\t\t\t\t\tconst q = toVec(hit.point);\n\t\t\t\t\tconst d = distance(q, p);\n\t\t\t\t\tif (d <= tol && d < bestD) {\n\t\t\t\t\t\tbest = q;\n\t\t\t\t\t\tbestD = d;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn best;\n\t}\n\n\t/**\n\t * The foot of a perpendicular, or a tangent point, from `base` on a target segment near `p`\n\t * (within `tol` of it): roots of `(B(t) − base) · B′(t)` and `(B(t) − base) × B′(t)`\n\t * bracketed by sign changes of samples and narrowed by bisection.\n\t */\n\tprivate curvePoint(near: readonly { seg: Point[] }[], base: Vec2, p: Vec2, tol: number): { point: Vec2; kind: 'perpendicular' | 'tangent' } | null {\n\t\tlet best: { point: Vec2; kind: 'perpendicular' | 'tangent'; d: number } | null = null;\n\t\tfor (const { seg } of near) {\n\t\t\tfor (const kind of ['perpendicular', 'tangent'] as const) {\n\t\t\t\tif (kind === 'tangent' && seg.length === 2) continue;\n\t\t\t\tfor (const q of solveOnSegment(seg, base, kind === 'perpendicular' ? dotCondition : crossCondition)) {\n\t\t\t\t\tconst d = distance(q, p);\n\t\t\t\t\tif (d <= tol && (!best || d < best.d)) best = { point: q, kind, d };\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn best && { point: best.point, kind: best.kind };\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Boxes\n\t// -------------------------------------------------------------------------\n\n\t/**\n\t * Snaps a moving box `r` (the moved selection's bounds): its edge or centre nearest a target\n\t * line on each axis (`axes` limits which may move). With `spacing`, an axis that lines up\n\t * with nothing spaces the box evenly in its row or column. Returns the correction and guides.\n\t */\n\tsnapRect(r: VectorBounds, tol: number, options: { spacing?: boolean; axes?: { x?: boolean; y?: boolean } } = {}): VectorRectSnap {\n\t\tconst axes = { x: options.axes?.x !== false, y: options.axes?.y !== false };\n\t\tconst nearest = (lines: readonly LineTarget[], values: number[]) => {\n\t\t\tlet best: { d: number; from: Vec2 } | null = null;\n\t\t\tfor (const v of values) for (const l of linesNear(lines, v, tol)) if (!best || Math.abs(l.v - v) < Math.abs(best.d)) best = { d: l.v - v, from: l.from };\n\t\t\treturn best;\n\t\t};\n\t\tconst bx = axes.x ? nearest(this.xs, [r.x, r.x + r.width / 2, r.x + r.width]) : null;\n\t\tconst by = axes.y ? nearest(this.ys, [r.y, r.y + r.height / 2, r.y + r.height]) : null;\n\t\tconst out: VectorRectSnap = { dx: bx?.d ?? 0, dy: by?.d ?? 0, x: !!bx, y: !!by, lines: [], labels: [] };\n\t\tconst done = { ...r, x: r.x + out.dx, y: r.y + out.dy };\n\t\tif (bx) {\n\t\t\tconst x = bx.from.x;\n\t\t\tout.lines.push({ kind: 'align', x1: x, y1: Math.min(bx.from.y, done.y), x2: x, y2: Math.max(bx.from.y, done.y + done.height) });\n\t\t}\n\t\tif (by) {\n\t\t\tconst y = by.from.y;\n\t\t\tout.lines.push({ kind: 'align', x1: Math.min(by.from.x, done.x), y1: y, x2: Math.max(by.from.x, done.x + done.width), y2: y });\n\t\t}\n\t\tif (options.spacing && this.boxes.length) {\n\t\t\tfor (const horizontal of [true, false]) {\n\t\t\t\tif (horizontal ? out.x || !axes.x : out.y || !axes.y) continue;\n\t\t\t\tconst moved = { ...r, x: r.x + out.dx, y: r.y + out.dy };\n\t\t\t\tconst even = this.evenSpacing(moved, horizontal, tol);\n\t\t\t\tif (!even) continue;\n\t\t\t\tif (horizontal) {\n\t\t\t\t\tout.dx += even.shift;\n\t\t\t\t\tout.x = true;\n\t\t\t\t} else {\n\t\t\t\t\tout.dy += even.shift;\n\t\t\t\t\tout.y = true;\n\t\t\t\t}\n\t\t\t\tfor (const gap of even.gaps) this.gapMarks(gap, horizontal, tol, out);\n\t\t\t}\n\t\t}\n\t\tif (bx || by) out.labels.unshift(this.label({ x: r.x + out.dx + r.width / 2, y: r.y + out.dy + r.height / 2 }, 'align'));\n\t\treturn out;\n\t}\n\n\t/**\n\t * Equal spacing along x (`horizontal`) or y for the moved box `r`, among the objects of its\n\t * row (column): the shift within `tol` that centres it between its neighbours, or leaves it\n\t * as far from the neighbour before or after it as two other objects of the row are apart;\n\t * and the gaps that are equal then, as (start, end, where across).\n\t */\n\tprivate evenSpacing(r: VectorBounds, horizontal: boolean, tol: number): { shift: number; gaps: [number, number, number][] } | null {\n\t\tconst span = (b: VectorBounds, x: boolean): [number, number] => (x ? [b.x, b.x + b.width] : [b.y, b.y + b.height]);\n\t\tconst [r0, r1] = span(r, horizontal);\n\t\tconst len = r1 - r0;\n\t\tconst [c0, c1] = span(r, !horizontal);\n\t\tconst row = this.boxes\n\t\t\t.map((b) => ({ b, a0: span(b, horizontal)[0], a1: span(b, horizontal)[1] }))\n\t\t\t.filter((e) => span(e.b, !horizontal)[0] < c1 && span(e.b, !horizontal)[1] > c0 && (e.a1 <= r0 + tol || e.a0 >= r1 - tol))\n\t\t\t.sort((a, b) => a.a0 - b.a0);\n\t\tif (!row.length) return null;\n\t\t// Across the middle of what two boxes share (or of the space between them).\n\t\tconst mid = (a: VectorBounds, b: VectorBounds) => (Math.max(span(a, !horizontal)[0], span(b, !horizontal)[0]) + Math.min(span(a, !horizontal)[1], span(b, !horizontal)[1])) / 2;\n\t\tconst pairs: { g: number; gap: [number, number, number] }[] = [];\n\t\tfor (const p of row) {\n\t\t\tconst next = bound(row, (e) => e.a0, p.a1, false);\n\t\t\tconst q = row[next];\n\t\t\tif (!q) continue;\n\t\t\tconst straddles = p.a1 <= r0 + tol && q.a0 >= r1 - tol;\n\t\t\tif (q.a0 > p.a1 && !straddles) pairs.push({ g: q.a0 - p.a1, gap: [p.a1, q.a0, mid(p.b, q.b)] });\n\t\t}\n\t\tlet before: (typeof row)[number] | null = null;\n\t\tlet after: (typeof row)[number] | null = null;\n\t\tfor (const e of row) {\n\t\t\tif (e.a1 <= r0 + tol && (!before || e.a1 > before.a1)) before = e;\n\t\t\tif (e.a0 >= r1 - tol && (!after || e.a0 < after.a0)) after = e;\n\t\t}\n\t\tconst starts: { start: number; repeats: number | null }[] = [];\n\t\tif (before && after && after.a0 - before.a1 > len) starts.push({ start: (before.a1 + after.a0 - len) / 2, repeats: null });\n\t\tfor (const { g } of pairs) {\n\t\t\tif (before) starts.push({ start: before.a1 + g, repeats: g });\n\t\t\tif (after) starts.push({ start: after.a0 - g - len, repeats: g });\n\t\t}\n\t\tlet pick: { start: number; repeats: number | null } | null = null;\n\t\tfor (const s of starts) if (Math.abs(s.start - r0) <= tol && (!pick || Math.abs(s.start - r0) < Math.abs(pick.start - r0))) pick = s;\n\t\tif (!pick) return null;\n\t\tconst { start, repeats } = pick;\n\t\tconst at = horizontal ? { ...r, x: start } : { ...r, y: start };\n\t\tconst end = start + len;\n\t\tconst gaps: [number, number, number][] = [];\n\t\tif (before && (repeats === null || Math.abs(start - before.a1 - repeats) <= EQUAL)) gaps.push([before.a1, start, mid(before.b, at)]);\n\t\tif (after && (repeats === null || Math.abs(after.a0 - end - repeats) <= EQUAL)) gaps.push([end, after.a0, mid(after.b, at)]);\n\t\tif (repeats !== null) for (const p of pairs) if (Math.abs(p.g - repeats) <= EQUAL) gaps.push(p.gap);\n\t\treturn { shift: start - r0, gaps };\n\t}\n\n\t/** A gap mark: a line across the gap with a tick at each end, and its length as a label. */\n\tprivate gapMarks([u0, u1, c]: [number, number, number], horizontal: boolean, tick: number, out: VectorSnapGuides) {\n\t\tconst pt = (u: number, v: number): Vec2 => (horizontal ? { x: u, y: v } : { x: v, y: u });\n\t\tout.lines.push(line('spacing', pt(u0, c), pt(u1, c)), line('spacing', pt(u0, c - tick), pt(u0, c + tick)), line('spacing', pt(u1, c - tick), pt(u1, c + tick)));\n\t\tout.labels.push(this.label(pt((u0 + u1) / 2, c), 'spacing', fmtLength(u1 - u0)));\n\t}\n\n\t/** The other objects' side length nearest `length`, within `tol`, or null. */\n\tmatchSize(length: number, tol: number): number | null {\n\t\tlet best: number | null = null;\n\t\tfor (const s of this.sizes) if (Math.abs(s - length) <= tol && (best === null || Math.abs(s - length) < Math.abs(best - length))) best = s;\n\t\treturn best;\n\t}\n\n\t/**\n\t * Snaps a selection-box resize. `sx / sy` are the scale `scaleFor` gave for dragging\n\t * `handle` of `frame` (in the box's own, possibly rotated frame), `mods.shift` keeps the\n\t * proportions and `mods.alt` scales about the centre. The handle lands on an anchor or\n\t * centre within `tol`, else the corner or the side it moves lines up with the nearest\n\t * target, whatever the box's angle; a handle with one way to go (a side, or a proportional\n\t * corner) slides along it until one of them is on a target. An axis that snaps to nothing\n\t * may match another object's width or height instead.\n\t */\n\tsnapScale(\n\t\tframe: VectorBounds & { angle: number },\n\t\thandle: Exclude<VectorTransformHandle, 'rotate'>,\n\t\tsx: number,\n\t\tsy: number,\n\t\tmods: { shift?: boolean; alt?: boolean },\n\t\ttol: number\n\t): VectorScaleSnap {\n\t\tconst toDoc = frame.angle ? rotateMatrix(frame.angle) : IDENTITY_MATRIX;\n\t\tconst toLocal = frame.angle ? rotateMatrix(-frame.angle) : IDENTITY_MATRIX;\n\t\tconst corner = handle.length === 2;\n\t\tconst proportional = !!mods.shift;\n\t\tconst centre = { x: frame.x + frame.width / 2, y: frame.y + frame.height / 2 };\n\t\tconst origin = mods.alt ? centre : handlePos(opposite(handle), frame);\n\t\tconst hp = handlePos(handle, frame);\n\t\tconst d0 = { x: hp.x - origin.x, y: hp.y - origin.y };\n\t\tconst valid = (n: number, s: number) => Number.isFinite(n) && n * s > 0;\n\t\tconst nearest = (v: number, lines: readonly LineTarget[]) => {\n\t\t\tlet best: LineTarget | null = null;\n\t\t\tfor (const l of linesNear(lines, v, tol)) if (!best || Math.abs(l.v - v) < Math.abs(best.v - v)) best = l;\n\t\t\treturn best;\n\t\t};\n\t\ttype Snap = { hx: LineTarget | null; hy: LineTarget | null; on: PointTarget | null };\n\t\tlet nx = sx;\n\t\tlet ny = sy;\n\t\tlet snap: Snap = { hx: null, hy: null, on: null };\n\t\tif (corner && !proportional) {\n\t\t\t// The corner goes anywhere: onto an anchor or centre, else it takes a target's x, its y, or both.\n\t\t\tconst p = applyMatrix(toDoc, { x: origin.x + d0.x * sx, y: origin.y + d0.y * sy });\n\t\t\tconst hx = nearest(p.x, this.xs);\n\t\t\tconst hy = nearest(p.y, this.ys);\n\t\t\tconst on = this.nearestPoint(p, tol);\n\t\t\tconst tries: { tx: number | null; ty: number | null; s: Snap }[] = [];\n\t\t\tif (on) tries.push({ tx: on.p.x, ty: on.p.y, s: { hx: null, hy: null, on } });\n\t\t\ttries.push({ tx: hx?.v ?? null, ty: hy?.v ?? null, s: { hx, hy, on: null } });\n\t\t\ttries.push({ tx: hx?.v ?? null, ty: null, s: { hx, hy: null, on: null } });\n\t\t\ttries.push({ tx: null, ty: hy?.v ?? null, s: { hx: null, hy, on: null } });\n\t\t\tfor (const { tx, ty, s } of tries) {\n\t\t\t\tif (tx === null && ty === null) continue;\n\t\t\t\tconst q = applyMatrix(toLocal, { x: tx ?? p.x, y: ty ?? p.y });\n\t\t\t\tlet cx = (q.x - origin.x) / d0.x;\n\t\t\t\tlet cy = (q.y - origin.y) / d0.y;\n\t\t\t\t// Square to the page, the axis without a target stays exactly where it was.\n\t\t\t\tif (!frame.angle) {\n\t\t\t\t\tif (tx === null) cx = sx;\n\t\t\t\t\tif (ty === null) cy = sy;\n\t\t\t\t}\n\t\t\t\tif (valid(cx, sx) && valid(cy, sy)) {\n\t\t\t\t\tnx = cx;\n\t\t\t\t\tny = cy;\n\t\t\t\t\tsnap = s;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// One parameter `s`: the scale is `c + s·e` on each axis.\n\t\t\tconst movesX = handle === 'e' || handle === 'w';\n\t\t\tlet e: Vec2;\n\t\t\tlet c: Vec2;\n\t\t\tlet s0: number;\n\t\t\tif (corner) [e, c, s0] = [{ x: Math.sign(sx), y: Math.sign(sy) }, { x: 0, y: 0 }, Math.abs(sx)];\n\t\t\telse if (movesX) [e, c, s0] = proportional ? [{ x: 1, y: Math.sign(sx) }, { x: 0, y: 0 }, sx] : [{ x: 1, y: 0 }, { x: 0, y: 1 }, sx];\n\t\t\telse [e, c, s0] = proportional ? [{ x: Math.sign(sy), y: 1 }, { x: 0, y: 0 }, sy] : [{ x: 0, y: 1 }, { x: 1, y: 0 }, sy];\n\t\t\t// The points that may land on a target: the handle, and the two ends of its side.\n\t\t\tconst i = RING.indexOf(handle);\n\t\t\tconst ends = corner ? [] : [RING[(i + 1) % 8], RING[(i + 7) % 8]];\n\t\t\t// How far the handle travels per unit of `s`: a snap may not drag it far from the pointer.\n\t\t\tconst travel = Math.hypot(e.x * d0.x, e.y * d0.y);\n\t\t\tlet best: { s: number; inLine: boolean; moved: number; snap: Snap } | null = null;\n\t\t\tconst consider = (s: number, inLine: boolean, found: Snap) => {\n\t\t\t\tconst moved = Math.abs(s - s0) * travel;\n\t\t\t\tif (!valid(s, s0) || moved > 2 * tol) return;\n\t\t\t\tif (!best || (inLine === best.inLine ? moved < best.moved : !inLine)) best = { s, inLine, moved, snap: found };\n\t\t\t};\n\t\t\tfor (const h of [handle, ...ends]) {\n\t\t\t\tconst hv = handlePos(h, frame);\n\t\t\t\tconst v = { x: hv.x - origin.x, y: hv.y - origin.y };\n\t\t\t\tconst base = applyMatrix(toDoc, { x: origin.x + c.x * v.x, y: origin.y + c.y * v.y });\n\t\t\t\tconst dir = applyMatrix(toDoc, { x: e.x * v.x, y: e.y * v.y });\n\t\t\t\tconst p = { x: base.x + dir.x * s0, y: base.y + dir.y * s0 };\n\t\t\t\tconst l2 = dir.x * dir.x + dir.y * dir.y;\n\t\t\t\tconst on = l2 > 1e-18 ? this.nearestPoint(p, tol) : null;\n\t\t\t\t// An anchor or centre beside its way: it stops at the nearest point of the way.\n\t\t\t\tif (on) consider(((on.p.x - base.x) * dir.x + (on.p.y - base.y) * dir.y) / l2, false, { hx: null, hy: null, on });\n\t\t\t\tif (Math.abs(dir.x) > 1e-9) {\n\t\t\t\t\tconst hit = nearest(p.x, this.xs);\n\t\t\t\t\tif (hit) consider((hit.v - base.x) / dir.x, true, { hx: hit, hy: null, on: null });\n\t\t\t\t}\n\t\t\t\tif (Math.abs(dir.y) > 1e-9) {\n\t\t\t\t\tconst hit = nearest(p.y, this.ys);\n\t\t\t\t\tif (hit) consider((hit.v - base.y) / dir.y, true, { hx: null, hy: hit, on: null });\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst found = best as { s: number; snap: Snap } | null;\n\t\t\tif (found) {\n\t\t\t\tnx = c.x + found.s * e.x;\n\t\t\t\tny = c.y + found.s * e.y;\n\t\t\t\tsnap = found.snap;\n\t\t\t}\n\t\t}\n\t\tconst out: VectorScaleSnap = { sx: nx, sy: ny, snapped: false, lines: [], labels: [] };\n\t\tconst handleAt = () => applyMatrix(toDoc, { x: origin.x + d0.x * out.sx, y: origin.y + d0.y * out.sy });\n\t\tif (snap.on) {\n\t\t\tout.snapped = true;\n\t\t\tout.labels.push(this.label(snap.on.p, snap.on.kind));\n\t\t\treturn out;\n\t\t}\n\t\t// Sides that line up with nothing may match another object's width or height.\n\t\tconst movesX = handle !== 'n' && handle !== 's';\n\t\tconst movesY = handle !== 'e' && handle !== 'w';\n\t\tconst sized: ('x' | 'y')[] = [];\n\t\tif (!proportional) {\n\t\t\tif (movesX && !snap.hx && !(frame.angle && snap.hy) && frame.width > 1e-9) {\n\t\t\t\tconst len = this.matchSize(Math.abs(out.sx) * frame.width, tol);\n\t\t\t\tif (len !== null && len > 0) {\n\t\t\t\t\tout.sx = Math.sign(out.sx || 1) * (len / frame.width);\n\t\t\t\t\tsized.push('x');\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (movesY && !snap.hy && !(frame.angle && snap.hx) && frame.height > 1e-9) {\n\t\t\t\tconst len = this.matchSize(Math.abs(out.sy) * frame.height, tol);\n\t\t\t\tif (len !== null && len > 0) {\n\t\t\t\t\tout.sy = Math.sign(out.sy || 1) * (len / frame.height);\n\t\t\t\t\tsized.push('y');\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// The resized box in its frame, and its corners on the page.\n\t\tconst x0 = origin.x + (frame.x - origin.x) * out.sx;\n\t\tconst x1 = origin.x + (frame.x + frame.width - origin.x) * out.sx;\n\t\tconst y0 = origin.y + (frame.y - origin.y) * out.sy;\n\t\tconst y1 = origin.y + (frame.y + frame.height - origin.y) * out.sy;\n\t\tconst box = { x0: Math.min(x0, x1), x1: Math.max(x0, x1), y0: Math.min(y0, y1), y1: Math.max(y0, y1) };\n\t\tconst page = [\n\t\t\t{ x: box.x0, y: box.y0 },\n\t\t\t{ x: box.x1, y: box.y0 },\n\t\t\t{ x: box.x1, y: box.y1 },\n\t\t\t{ x: box.x0, y: box.y1 }\n\t\t].map((q) => applyMatrix(toDoc, q));\n\t\tconst pminY = Math.min(...page.map((q) => q.y));\n\t\tconst pmaxY = Math.max(...page.map((q) => q.y));\n\t\tconst pminX = Math.min(...page.map((q) => q.x));\n\t\tconst pmaxX = Math.max(...page.map((q) => q.x));\n\t\tif (snap.hx) out.lines.push({ kind: 'align', x1: snap.hx.v, y1: Math.min(snap.hx.from.y, pminY), x2: snap.hx.v, y2: Math.max(snap.hx.from.y, pmaxY) });\n\t\tif (snap.hy) out.lines.push({ kind: 'align', x1: Math.min(snap.hy.from.x, pminX), y1: snap.hy.v, x2: Math.max(snap.hy.from.x, pmaxX), y2: snap.hy.v });\n\t\tif (snap.hx || snap.hy) {\n\t\t\tout.snapped = true;\n\t\t\tout.labels.push(this.label(handleAt(), 'align'));\n\t\t}\n\t\tfor (const axis of sized) {\n\t\t\tout.snapped = true;\n\t\t\t// Along the matched side, just outside the box.\n\t\t\tconst off = tol * 2;\n\t\t\tconst [a, b] = axis === 'x' ? [{ x: box.x0, y: box.y1 + off }, { x: box.x1, y: box.y1 + off }] : [{ x: box.x1 + off, y: box.y0 }, { x: box.x1 + off, y: box.y1 }];\n\t\t\tconst pa = applyMatrix(toDoc, a);\n\t\t\tconst pb = applyMatrix(toDoc, b);\n\t\t\tout.lines.push(line('size', pa, pb));\n\t\t\tout.labels.push(this.label({ x: (pa.x + pb.x) / 2, y: (pa.y + pb.y) / 2 }, 'size', `${axis === 'x' ? 'W' : 'H'} ${fmtLength(axis === 'x' ? box.x1 - box.x0 : box.y1 - box.y0)}`));\n\t\t}\n\t\treturn out;\n\t}\n}\n\n/** The eight resize handles clockwise from the top left (each side sits between its two corners). */\nconst RING: readonly Exclude<VectorTransformHandle, 'rotate'>[] = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'];\n\nfunction opposite(h: Exclude<VectorTransformHandle, 'rotate'>): Exclude<VectorTransformHandle, 'rotate'> {\n\treturn RING[(RING.indexOf(h) + 4) % 8];\n}\n\nfunction handlePos(h: Exclude<VectorTransformHandle, 'rotate'>, r: VectorBounds): Vec2 {\n\tconst x = h.includes('w') ? r.x : h.includes('e') ? r.x + r.width : r.x + r.width / 2;\n\tconst y = h.startsWith('n') ? r.y : h.startsWith('s') ? r.y + r.height : r.y + r.height / 2;\n\treturn { x, y };\n}\n\nfunction line(kind: VectorGuideKind, a: Vec2, b: Vec2): VectorGuideLine {\n\treturn { kind, x1: a.x, y1: a.y, x2: b.x, y2: b.y };\n}\n\n/** The construction guide through `leave.at` nearest `p`, within `tol` (and `p` farther than that from `at`): its direction and where `p` lands. */\nfunction construction(leave: VectorSnapLeave, p: Vec2, tol: number): { dir: Vec2; point: Vec2 } | null {\n\tconst v = { x: p.x - leave.at.x, y: p.y - leave.at.y };\n\tif (Math.hypot(v.x, v.y) <= tol) return null;\n\tlet best: { dir: Vec2; point: Vec2; d: number } | null = null;\n\tfor (const a of leave.angles ?? []) {\n\t\t// Counter-clockwise as seen on the y-down page.\n\t\tconst r = (a * Math.PI) / 180;\n\t\t// Exactly along an axis for multiples of 90°.\n\t\tconst exact = (n: number) => (Math.abs(n) < 1e-12 ? 0 : n);\n\t\tconst dir = { x: exact(Math.cos(r)), y: exact(-Math.sin(r)) };\n\t\tconst k = v.x * dir.x + v.y * dir.y;\n\t\tconst point = { x: leave.at.x + dir.x * k, y: leave.at.y + dir.y * k };\n\t\tconst d = distance(point, p);\n\t\tif (d <= tol && (!best || d < best.d)) best = { dir, point, d };\n\t}\n\treturn best && { dir: best.dir, point: best.point };\n}\n\nconst dotCondition = (r: Point, d: Point) => r[0] * d[0] + r[1] * d[1];\nconst crossCondition = (r: Point, d: Point) => r[0] * d[1] - r[1] * d[0];\n\n/**\n * The points of a segment where `cond(B(t) − base, B′(t))` is zero: sign changes of samples,\n * narrowed by bisection, kept when the normalised condition is (nearly) zero there.\n */\nfunction solveOnSegment(seg: readonly Point[], base: Vec2, cond: (r: Point, d: Point) => number): Vec2[] {\n\tconst b: Point = [base.x, base.y];\n\tconst g = (t: number) => {\n\t\tconst p = segmentPoint(seg, t);\n\t\treturn cond([p[0] - b[0], p[1] - b[1]], segmentDerivative(seg, t));\n\t};\n\tconst vals: number[] = [];\n\tlet scale = 0;\n\tfor (let i = 0; i <= CURVE_SAMPLES; i++) {\n\t\tconst t = i / CURVE_SAMPLES;\n\t\tconst p = segmentPoint(seg, t);\n\t\tconst d = segmentDerivative(seg, t);\n\t\tconst r: Point = [p[0] - b[0], p[1] - b[1]];\n\t\tvals.push(cond(r, d));\n\t\tscale = Math.max(scale, Math.hypot(r[0], r[1]) * Math.hypot(d[0], d[1]));\n\t}\n\t// Every point qualifies (a line seen from a point on it): no particular solution.\n\tif (!(scale > 0) || vals.every((v) => Math.abs(v) <= scale * 1e-12)) return [];\n\tconst out: Vec2[] = [];\n\tfor (let i = 0; i < CURVE_SAMPLES; i++) {\n\t\tconst ga = vals[i];\n\t\tconst gb = vals[i + 1];\n\t\tif (!(Number.isFinite(ga) && Number.isFinite(gb)) || (ga > 0 === gb > 0 && ga !== 0)) continue;\n\t\tlet lo = i / CURVE_SAMPLES;\n\t\tlet hi = (i + 1) / CURVE_SAMPLES;\n\t\tlet flo = ga;\n\t\tfor (let k = 0; k < BISECT; k++) {\n\t\t\tconst m = (lo + hi) / 2;\n\t\t\tconst fm = g(m);\n\t\t\tif (fm === 0) {\n\t\t\t\tlo = hi = m;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (fm > 0 === flo > 0) {\n\t\t\t\tlo = m;\n\t\t\t\tflo = fm;\n\t\t\t} else hi = m;\n\t\t}\n\t\tconst t = (lo + hi) / 2;\n\t\tconst p = segmentPoint(seg, t);\n\t\tconst dv = segmentDerivative(seg, t);\n\t\tconst r = Math.hypot(p[0] - b[0], p[1] - b[1]);\n\t\tconst dl = Math.hypot(dv[0], dv[1]);\n\t\tif (!(r > 1e-9 && dl > 0) || Math.abs(cond([p[0] - b[0], p[1] - b[1]], dv)) > 1e-6 * r * dl) continue;\n\t\tif (!out.some((q) => Math.hypot(q.x - p[0], q.y - p[1]) <= 1e-9 * (1 + r))) out.push({ x: p[0], y: p[1] });\n\t}\n\treturn out;\n}\n","/**\n * Path editing on subpaths in a node's own space: anchor deletion that keeps the shape,\n * segment bending, Curvature-tool anchor moves, cutting at anchors (Scissors), Join, Average,\n * Remove Redundant Points and Smooth.\n *\n * Portions ported from (MIT OR Apache-2.0, © ArtCraft Team and contributors; see\n * THIRD_PARTY_NOTICES.md):\n * - vectorcraft-main/crates/pathops/src/edit.rs (`remove_anchor`, `golden_min`, `smooth`,\n * `remove_redundant_points`, `merge_pair`, `average`, `join`)\n * - vectorcraft-main/crates/geom/src/path.rs (`SubPath::cut_at`)\n * - vectorcraft-main/crates/tools/src/draw2/curvature.rs (`curvature_move`)\n * - figcraft-main/crates/engine/src/vectoredit/ops.rs (`bend`)\n */\nimport type { Point } from '../canvas/types.js';\nimport { fitCubicWithTangents } from '../canvas/curveFit.js';\nimport { segmentPoint } from '../canvas/pathops/bezier.js';\nimport { cloneAnchor, inferAnchorType, insertAnchor, moveAnchorHandle, subpathSegment, subpathSegmentCount } from './geometry.js';\nimport type { VectorAnchor, VectorSubpath, Vec2 } from './types.js';\n\nconst EPS = 1e-9;\n\n// ---------------------------------------------------------------------------\n// Small helpers\n// ---------------------------------------------------------------------------\n\nconst unit = (x: number, y: number): Point | null => {\n\tconst l = Math.hypot(x, y);\n\treturn l > 1e-12 && Number.isFinite(l) ? [x / l, y / l] : null;\n};\n\n/** Segment `i` as four control points (a line has its controls at the thirds of its chord). */\nfunction cubicOf(sp: VectorSubpath, i: number): Point[] {\n\tconst s = subpathSegment(sp, i);\n\tif (s.length === 4) return s;\n\tconst [a, b] = s;\n\treturn [a, [a[0] + (b[0] - a[0]) / 3, a[1] + (b[1] - a[1]) / 3], [a[0] + (2 * (b[0] - a[0])) / 3, a[1] + (2 * (b[1] - a[1])) / 3], b];\n}\n\nconst isLine = (sp: VectorSubpath, i: number) => subpathSegment(sp, i).length === 2;\n\n/** Direction the curve leaves `c[0]`. */\nfunction startTangent(c: readonly Point[]): Point {\n\tfor (let k = 1; k < 4; k++) {\n\t\tconst u = unit(c[k][0] - c[0][0], c[k][1] - c[0][1]);\n\t\tif (u) return u;\n\t}\n\treturn [1, 0];\n}\n\n/** Direction the curve arrives at `c[3]` (forward along the curve). */\nfunction endTangent(c: readonly Point[]): Point {\n\tfor (let k = 2; k >= 0; k--) {\n\t\tconst u = unit(c[3][0] - c[k][0], c[3][1] - c[k][1]);\n\t\tif (u) return u;\n\t}\n\treturn [1, 0];\n}\n\n/** `n + 1` points of a cubic at even parameters (`withStart` false skips the first). */\nfunction sample(c: readonly Point[], n: number, out: Point[], withStart: boolean) {\n\tfor (let i = withStart ? 0 : 1; i <= n; i++) out.push(segmentPoint(c, i / n));\n}\n\nconst onAnchor = (a: VectorAnchor, h: Vec2 | null | undefined) => !h || (Math.abs(h.x - a.x) < EPS && Math.abs(h.y - a.y) < EPS);\n\n/** Drops handles that sit on their anchor and sets the type the handles show (a symmetric pair that is no longer equal becomes smooth). */\nfunction tidy(a: VectorAnchor, keepType = true): VectorAnchor {\n\tconst next = cloneAnchor(a);\n\tif (onAnchor(next, next.in)) delete next.in;\n\tif (onAnchor(next, next.out)) delete next.out;\n\tconst shown = inferAnchorType(next);\n\tconst type = keepType && a.type ? (a.type === 'symmetric' && shown !== 'symmetric' ? (shown === 'corner' ? 'corner' : 'smooth') : a.type === 'smooth' && shown === 'corner' ? 'corner' : a.type) : shown;\n\tif (type === 'corner') delete next.type;\n\telse next.type = type;\n\treturn next;\n}\n\n/** Index of the anchor before / after `i` (round the start of a closed subpath), or -1. */\nfunction neighbours(sp: VectorSubpath, i: number): [number, number] {\n\tconst n = sp.anchors.length;\n\tconst wrap = sp.closed && n > 1;\n\treturn [i > 0 ? i - 1 : wrap ? n - 1 : -1, i < n - 1 ? i + 1 : wrap ? 0 : -1];\n}\n\n// ---------------------------------------------------------------------------\n// Deleting anchors without changing the shape\n// ---------------------------------------------------------------------------\n\n/**\n * The value of `f` with the least error for an argument in [a, b], by a golden-section search\n * (a fixed number of steps). An error that is not a number never wins.\n */\nfunction goldenMin<T extends { error: number }>(f: (x: number) => T, a: number, b: number): T {\n\tconst R = 0.618033988749895;\n\tlet x1 = b - R * (b - a);\n\tlet x2 = a + R * (b - a);\n\tlet f1 = f(x1);\n\tlet f2 = f(x2);\n\tfor (let i = 0; i < 40; i++) {\n\t\tif (f1.error < f2.error || Number.isNaN(f2.error)) {\n\t\t\tb = x2;\n\t\t\tx2 = x1;\n\t\t\tf2 = f1;\n\t\t\tx1 = b - R * (b - a);\n\t\t\tf1 = f(x1);\n\t\t} else {\n\t\t\ta = x1;\n\t\t\tx1 = x2;\n\t\t\tf1 = f2;\n\t\t\tx2 = a + R * (b - a);\n\t\t\tf2 = f(x2);\n\t\t}\n\t}\n\treturn f1.error <= f2.error ? f1 : f2;\n}\n\n/**\n * One cubic through `pts`: `N + 1` samples of a left curve then `N` of a right one. The new\n * curve passes their joint at some parameter k: the left samples sit at k·i/N, the right ones\n * at k + (1 − k)·i/N. Searches k for the closest fit.\n */\nfunction fitAcross(pts: Point[], N: number, t0: Point, t1: Point) {\n\tconst fit = (k: number) => {\n\t\tconst u: number[] = [];\n\t\tfor (let i = 0; i <= N; i++) u.push((k * i) / N);\n\t\tfor (let i = 1; i <= N; i++) u.push(k + ((1 - k) * i) / N);\n\t\treturn fitCubicWithTangents(pts, u, t0, t1);\n\t};\n\treturn goldenMin(fit, 0, 1);\n}\n\n/**\n * Removes anchor `index` without opening the path: its neighbours keep their handle directions\n * and their facing handles are refitted so one cubic follows the two segments that met there\n * as closely as possible (least maximum error over where the new curve passes the removed\n * point). Two straight segments become one; removing an end of an open subpath drops its\n * segment. Returns the subpath unchanged (a copy) when `index` is out of range.\n */\nexport function removeAnchorKeepingShape(sp: VectorSubpath, index: number): VectorSubpath {\n\tconst anchors = sp.anchors.map(cloneAnchor);\n\tconst n = anchors.length;\n\tif (index < 0 || index >= n) return { anchors, closed: sp.closed };\n\tif ((sp.closed || (index > 0 && index < n - 1)) && n >= 3) {\n\t\tconst pi = (index + n - 1) % n;\n\t\tconst ni = (index + 1) % n;\n\t\tif (!(isLine(sp, pi) && isLine(sp, index))) {\n\t\t\tconst left = cubicOf(sp, pi);\n\t\t\tconst right = cubicOf(sp, index);\n\t\t\tconst N = 16;\n\t\t\tconst pts: Point[] = [];\n\t\t\tsample(left, N, pts, true);\n\t\t\tsample(right, N, pts, false);\n\t\t\tconst t0 = startTangent(left);\n\t\t\tconst t1 = endTangent(right);\n\t\t\tconst { curve } = fitAcross(pts, N, t0, t1);\n\t\t\tif ([curve.p1, curve.p2].every((p) => Number.isFinite(p[0]) && Number.isFinite(p[1]))) {\n\t\t\t\tanchors[pi] = tidy({ ...anchors[pi], out: { x: curve.p1[0], y: curve.p1[1] } });\n\t\t\t\tanchors[ni] = tidy({ ...anchors[ni], in: { x: curve.p2[0], y: curve.p2[1] } });\n\t\t\t}\n\t\t}\n\t}\n\tanchors.splice(index, 1);\n\tlet closed = sp.closed;\n\tif (!closed && anchors.length) {\n\t\t// A new end has nothing beyond it.\n\t\tanchors[0] = tidy({ ...anchors[0], in: null });\n\t\tconst last = anchors.length - 1;\n\t\tanchors[last] = tidy({ ...anchors[last], out: null });\n\t}\n\tif (anchors.length < 3) closed = closed && anchors.length === 2 && anchors.some((a) => a.in || a.out);\n\treturn { anchors, closed };\n}\n\n/** Removes several anchors of a subpath, keeping the shape (see `removeAnchorKeepingShape`). */\nexport function removeAnchorsKeepingShape(sp: VectorSubpath, indices: readonly number[]): VectorSubpath {\n\tconst order = [...new Set(indices)].filter((i) => i >= 0 && i < sp.anchors.length).sort((a, b) => b - a);\n\treturn order.reduce((acc, i) => removeAnchorKeepingShape(acc, i), { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed });\n}\n\n// ---------------------------------------------------------------------------\n// Bending a segment\n// ---------------------------------------------------------------------------\n\n/**\n * Bends segment `segment` so the curve passes through `p` at parameter `t` (clamped to\n * 0.05–0.95): its handles go to the thirds of the chord plus one shared offset, so\n * `B(t) = L(t) + 3t(1 − t)·offset` exactly. The end anchors keep their types (a smooth or\n * symmetric end turns its other handle to stay in line).\n */\nexport function bendSegment(sp: VectorSubpath, segment: number, t: number, p: Vec2): VectorSubpath {\n\tconst anchors = sp.anchors.map(cloneAnchor);\n\tconst n = anchors.length;\n\tif (segment < 0 || segment >= subpathSegmentCount(sp) || !Number.isFinite(t) || !Number.isFinite(p.x) || !Number.isFinite(p.y)) return { anchors, closed: sp.closed };\n\tconst tc = Math.min(0.95, Math.max(0.05, t));\n\tconst ia = segment;\n\tconst ib = (segment + 1) % n;\n\tconst a = anchors[ia];\n\tconst b = anchors[ib];\n\tconst k = 3 * tc * (1 - tc);\n\tconst ox = (p.x - (a.x + (b.x - a.x) * tc)) / k;\n\tconst oy = (p.y - (a.y + (b.y - a.y) * tc)) / k;\n\tconst c1 = { x: a.x + (b.x - a.x) / 3 + ox, y: a.y + (b.y - a.y) / 3 + oy };\n\tconst c2 = { x: b.x + (a.x - b.x) / 3 + ox, y: b.y + (a.y - b.y) / 3 + oy };\n\tanchors[ia] = moveAnchorHandle(a, 'out', c1);\n\tanchors[ib] = moveAnchorHandle(b, 'in', c2);\n\treturn { anchors, closed: sp.closed };\n}\n\n/** The point at parameter `t` of segment `segment` (for tests and previews). */\nexport function subpathPointAt(sp: VectorSubpath, segment: number, t: number): Vec2 {\n\tconst [x, y] = segmentPoint(subpathSegment(sp, segment), t);\n\treturn { x, y };\n}\n\n// ---------------------------------------------------------------------------\n// Curvature-tool anchor move\n// ---------------------------------------------------------------------------\n\n/**\n * Moves anchor `index` to `q` the way the Curvature tool drags a point: only the two segments\n * at it change. A smooth or symmetric anchor turns its handles along the line through its\n * neighbours; the neighbours' handles on those segments, and a corner's own handles, keep\n * their directions while their lengths scale with their segment's chord, so the segments\n * beyond don't move and a corner without handles keeps none.\n */\nexport function curvatureMoveAnchor(sp: VectorSubpath, index: number, q: Vec2): VectorSubpath {\n\tconst anchors = sp.anchors.map(cloneAnchor);\n\tconst a = sp.anchors[index];\n\tif (!a) return { anchors, closed: sp.closed };\n\tconst own: [Vec2 | null, Vec2 | null] = [\n\t\ta.in && !onAnchor(a, a.in) ? { x: a.in.x - a.x, y: a.in.y - a.y } : null,\n\t\ta.out && !onAnchor(a, a.out) ? { x: a.out.x - a.x, y: a.out.y - a.y } : null\n\t];\n\tconst ends: [Vec2 | null, Vec2 | null] = [null, null];\n\tneighbours(sp, index).forEach((j, side) => {\n\t\tif (j < 0 || j === index) return;\n\t\tconst b = anchors[j];\n\t\tconst old = Math.hypot(a.x - b.x, a.y - b.y);\n\t\tconst s = old > 1e-9 ? Math.hypot(q.x - b.x, q.y - b.y) / old : 1;\n\t\tconst key = side === 0 ? 'out' : 'in';\n\t\tconst h = b[key];\n\t\tif (h) b[key] = { x: b.x + (h.x - b.x) * s, y: b.y + (h.y - b.y) * s };\n\t\tconst o = own[side];\n\t\tif (o) own[side] = { x: o.x * s, y: o.y * s };\n\t\tends[side] = { x: b.x, y: b.y };\n\t});\n\tconst moved: VectorAnchor = { x: q.x, y: q.y };\n\tif (own[0]) moved.in = { x: q.x + own[0].x, y: q.y + own[0].y };\n\tif (own[1]) moved.out = { x: q.x + own[1].x, y: q.y + own[1].y };\n\tif (a.type) moved.type = a.type;\n\tif (a.corner) moved.corner = { ...a.corner };\n\tconst [prev, next] = ends;\n\tif (a.type && a.type !== 'corner' && prev && next) {\n\t\tconst dx = next.x - prev.x;\n\t\tconst dy = next.y - prev.y;\n\t\tconst l = Math.hypot(dx, dy);\n\t\tif (l > 1e-9) {\n\t\t\t// A handle it lacks comes out a third of the way to that side's neighbour.\n\t\t\tconst len = (h: Vec2 | null, to: Vec2) => (h ? Math.hypot(h.x, h.y) : Math.hypot(q.x - to.x, q.y - to.y) / 3);\n\t\t\tlet li = len(own[0], prev);\n\t\t\tlet lo = len(own[1], next);\n\t\t\tif (a.type === 'symmetric') li = lo = (li + lo) / 2;\n\t\t\tmoved.in = { x: q.x - (dx * li) / l, y: q.y - (dy * li) / l };\n\t\t\tmoved.out = { x: q.x + (dx * lo) / l, y: q.y + (dy * lo) / l };\n\t\t}\n\t}\n\tanchors[index] = moved;\n\treturn { anchors, closed: sp.closed };\n}\n\n// ---------------------------------------------------------------------------\n// Scissors\n// ---------------------------------------------------------------------------\n\n/**\n * Cuts a subpath at the anchors `at`: each becomes the end of one open piece and the start of\n * the next, its handle off each piece dropped (the shape doesn't change). A closed subpath\n * opens at its first cut, so one cut leaves one piece whose ends coincide. The ends of an open\n * subpath and indices out of range don't cut; with no cut left the subpath comes back as it is.\n */\nexport function cutSubpathAt(sp: VectorSubpath, at: readonly number[]): VectorSubpath[] {\n\tconst n = sp.anchors.length;\n\tconst cuts = [...new Set(at)].filter((i) => Number.isInteger(i) && i >= 0 && i < n && (sp.closed || (i > 0 && i < n - 1))).sort((a, b) => a - b);\n\tif (!cuts.length || n < 2) return [{ anchors: sp.anchors.map(cloneAnchor), closed: sp.closed }];\n\tconst first = cuts[0];\n\t// A closed subpath's anchors from the first cut round to it again; an open one's as they are.\n\tconst run = sp.closed ? Array.from({ length: n + 1 }, (_, i) => sp.anchors[(first + i) % n]) : sp.anchors;\n\tconst cutSet = new Set(sp.closed ? [...cuts.map((c) => (c - first + n) % n), n] : cuts);\n\tconst pieces: VectorSubpath[] = [];\n\tlet cur: VectorAnchor[] = [];\n\trun.forEach((a, i) => {\n\t\tif (!cutSet.has(i)) {\n\t\t\tcur.push(cloneAnchor(a));\n\t\t\treturn;\n\t\t}\n\t\tif (cur.length) {\n\t\t\tcur.push(tidy({ ...a, out: null }, false));\n\t\t\tpieces.push({ anchors: cur, closed: false });\n\t\t\tcur = [];\n\t\t}\n\t\tif (i < n) cur.push(tidy({ ...a, in: null }, false));\n\t});\n\tif (cur.length > 1) pieces.push({ anchors: cur, closed: false });\n\treturn pieces;\n}\n\n/**\n * Cuts a subpath at parameter `t` of segment `segment` (Scissors on a segment): an anchor is\n * inserted there without changing the shape, then cut. Within `endTolerance` of the segment's\n * ends (in parameter) the existing anchor is cut instead.\n */\nexport function splitSubpathAt(sp: VectorSubpath, segment: number, t: number, endTolerance = 1e-6): VectorSubpath[] {\n\tconst n = sp.anchors.length;\n\tif (segment < 0 || segment >= subpathSegmentCount(sp)) return [{ anchors: sp.anchors.map(cloneAnchor), closed: sp.closed }];\n\tif (t <= endTolerance) return cutSubpathAt(sp, [segment]);\n\tif (t >= 1 - endTolerance) return cutSubpathAt(sp, [(segment + 1) % n]);\n\tconst { subpath, index } = insertAnchor(sp, segment, t);\n\treturn cutSubpathAt(subpath, [index]);\n}\n\n// ---------------------------------------------------------------------------\n// Join\n// ---------------------------------------------------------------------------\n\nconst endOf = (sp: VectorSubpath, last: boolean): VectorAnchor => sp.anchors[last ? sp.anchors.length - 1 : 0];\nconst dist = (a: Vec2, b: Vec2) => Math.hypot(a.x - b.x, a.y - b.y);\n\nfunction reverse(sp: VectorSubpath): VectorSubpath {\n\treturn {\n\t\tclosed: sp.closed,\n\t\tanchors: sp.anchors\n\t\t\t.slice()\n\t\t\t.reverse()\n\t\t\t.map((a) => {\n\t\t\t\tconst next: VectorAnchor = { x: a.x, y: a.y };\n\t\t\t\tif (a.out) next.in = { ...a.out };\n\t\t\t\tif (a.in) next.out = { ...a.in };\n\t\t\t\tif (a.type) next.type = a.type;\n\t\t\t\tif (a.corner) next.corner = { ...a.corner };\n\t\t\t\treturn next;\n\t\t\t})\n\t};\n}\n\n/**\n * Join: open subpaths are joined nearest-endpoints-first into one open subpath (endpoints\n * within `tolerance` merge into one anchor, otherwise a straight segment connects them). A\n * single open subpath is closed instead (its ends merged when they meet). Closed subpaths and\n * lone anchors pass through unchanged, before the joined one.\n */\nexport function joinSubpaths(subpaths: readonly VectorSubpath[], tolerance: number): VectorSubpath[] {\n\tconst closed: VectorSubpath[] = [];\n\tlet open: VectorSubpath[] = [];\n\tfor (const sp of subpaths) {\n\t\tconst copy = { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed };\n\t\tif (sp.closed || sp.anchors.length < 2) {\n\t\t\tif (sp.anchors.length) closed.push(copy);\n\t\t} else open.push(copy);\n\t}\n\tif (open.length === 1) {\n\t\tconst sp = open[0];\n\t\tconst first = sp.anchors[0];\n\t\tconst last = sp.anchors[sp.anchors.length - 1];\n\t\tif (sp.anchors.length > 2 && dist(first, last) <= tolerance) {\n\t\t\tsp.anchors.pop();\n\t\t\tsp.anchors[0] = tidy({ ...first, in: last.in ?? null }, false);\n\t\t} else {\n\t\t\tsp.anchors[0] = tidy({ ...first, in: null }, false);\n\t\t\tsp.anchors[sp.anchors.length - 1] = tidy({ ...sp.anchors[sp.anchors.length - 1], out: null }, false);\n\t\t}\n\t\tsp.closed = true;\n\t\treturn [...closed, sp];\n\t}\n\twhile (open.length > 1) {\n\t\t// The closest pair of endpoints on different subpaths.\n\t\tlet best = { d: Infinity, i: 0, j: 1, ei: true, ej: false };\n\t\tfor (let i = 0; i < open.length; i++) {\n\t\t\tfor (let j = i + 1; j < open.length; j++) {\n\t\t\t\tfor (const ei of [false, true]) {\n\t\t\t\t\tfor (const ej of [false, true]) {\n\t\t\t\t\t\tconst d = dist(endOf(open[i], ei), endOf(open[j], ej));\n\t\t\t\t\t\tif (d < best.d) best = { d, i, j, ei, ej };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tlet a = open[best.i];\n\t\tlet b = open[best.j];\n\t\topen = open.filter((_, k) => k !== best.j);\n\t\t// `a` ends at the meeting point, `b` starts there.\n\t\tif (!best.ei) a = reverse(a);\n\t\tif (best.ej) b = reverse(b);\n\t\tconst anchors = a.anchors;\n\t\tif (best.d <= tolerance) {\n\t\t\tconst [head, ...rest] = b.anchors;\n\t\t\tconst l = anchors[anchors.length - 1];\n\t\t\tanchors[anchors.length - 1] = tidy({ ...l, out: head.out ?? null }, false);\n\t\t\tanchors.push(...rest);\n\t\t} else {\n\t\t\tconst l = anchors[anchors.length - 1];\n\t\t\tanchors[anchors.length - 1] = tidy({ ...l, out: null }, false);\n\t\t\tanchors.push(tidy({ ...b.anchors[0], in: null }, false), ...b.anchors.slice(1));\n\t\t}\n\t\topen[best.i > best.j ? best.i - 1 : best.i] = { anchors, closed: false };\n\t}\n\treturn [...closed, ...open];\n}\n\n// ---------------------------------------------------------------------------\n// Smooth and Remove Redundant Points\n// ---------------------------------------------------------------------------\n\n/**\n * Smooth: pulls handles towards Catmull-Rom-like tangents (a third of the way to each\n * neighbour along the line through both). `amount` 0–1 (0 = unchanged, 1 = fully smooth).\n * Only the anchors in `only` change when it is given; endpoints of open subpaths keep their\n * handles. Subpaths of fewer than three anchors are unchanged.\n */\nexport function smoothSubpath(sp: VectorSubpath, amount = 1, only?: ReadonlySet<number>): VectorSubpath {\n\tconst t = Math.min(1, Math.max(0, amount));\n\tconst orig = sp.anchors;\n\tconst n = orig.length;\n\tconst anchors = orig.map(cloneAnchor);\n\tif (n < 3 || t === 0) return { anchors, closed: sp.closed };\n\tfor (let i = 0; i < n; i++) {\n\t\tif (!sp.closed && (i === 0 || i === n - 1)) continue;\n\t\tif (only && !only.has(i)) continue;\n\t\tconst prev = orig[(i + n - 1) % n];\n\t\tconst next = orig[(i + 1) % n];\n\t\tconst p = orig[i];\n\t\tconst dir = unit(next.x - prev.x, next.y - prev.y);\n\t\tif (!dir) continue;\n\t\tconst li = dist(p, prev) / 3;\n\t\tconst lo = dist(p, next) / 3;\n\t\tconst hin = p.in ?? p;\n\t\tconst hout = p.out ?? p;\n\t\tconst target = (h: Vec2, x: number, y: number) => ({ x: h.x + (x - h.x) * t, y: h.y + (y - h.y) * t });\n\t\tanchors[i] = tidy(\n\t\t\t{\n\t\t\t\tx: p.x,\n\t\t\t\ty: p.y,\n\t\t\t\tin: target(hin, p.x - dir[0] * li, p.y - dir[1] * li),\n\t\t\t\tout: target(hout, p.x + dir[0] * lo, p.y + dir[1] * lo)\n\t\t\t},\n\t\t\tfalse\n\t\t);\n\t}\n\treturn { anchors, closed: sp.closed };\n}\n\ninterface Seg {\n\tc: Point[];\n\tline: boolean;\n}\n\nfunction segsOf(sp: VectorSubpath): Seg[] {\n\tconst out: Seg[] = [];\n\tfor (let i = 0; i < subpathSegmentCount(sp); i++) {\n\t\tconst s = subpathSegment(sp, i);\n\t\tout.push(s.length === 2 ? { c: s, line: true } : { c: s, line: false });\n\t}\n\treturn out;\n}\n\n/** A subpath from consecutive segments (anchor types inferred; original types kept where they still hold). */\nfunction segsToSubpath(segs: readonly Seg[], closed: boolean, types: Map<string, VectorAnchor['type']>): VectorSubpath | null {\n\tif (!segs.length) return null;\n\tconst anchors: VectorAnchor[] = [];\n\tconst at = (p: Point): VectorAnchor => ({ x: p[0], y: p[1] });\n\tanchors.push(at(segs[0].c[0]));\n\tfor (const s of segs) {\n\t\tconst end = s.c[s.c.length - 1];\n\t\tif (!s.line) anchors[anchors.length - 1].out = { x: s.c[1][0], y: s.c[1][1] };\n\t\tconst next = at(end);\n\t\tif (!s.line) next.in = { x: s.c[2][0], y: s.c[2][1] };\n\t\tanchors.push(next);\n\t}\n\tif (closed) {\n\t\tconst last = anchors.pop()!;\n\t\tif (last.in) anchors[0].in = last.in;\n\t}\n\tconst typed = anchors.map((a) => {\n\t\tconst type = types.get(`${a.x},${a.y}`);\n\t\treturn tidy(type ? { ...a, type } : a, !!type);\n\t});\n\treturn { anchors: typed, closed };\n}\n\nconst isZeroSeg = (s: Seg) => s.c.every((p) => Math.abs(p[0] - s.c[0][0]) < 1e-9 && Math.abs(p[1] - s.c[0][1]) < 1e-9);\n\n/** One segment replacing `a` then `b` within `tol`, or null. */\nfunction mergePair(a: Seg, b: Seg, tol: number): Seg | null {\n\tconst a0 = a.c[0];\n\tconst a3 = a.c[a.c.length - 1];\n\tconst b3 = b.c[b.c.length - 1];\n\tif (a.line && b.line) {\n\t\tconst cx = b3[0] - a0[0];\n\t\tconst cy = b3[1] - a0[1];\n\t\tconst len = Math.hypot(cx, cy);\n\t\tif (len < 1e-12) return null;\n\t\tconst mx = a3[0] - a0[0];\n\t\tconst my = a3[1] - a0[1];\n\t\tconst along = (mx * cx + my * cy) / len;\n\t\treturn Math.abs((mx * cy - my * cx) / len) <= tol && along >= 0 && along <= len ? { c: [a0, b3], line: true } : null;\n\t}\n\tconst ca = a.line ? [a0, a0, a3, a3] : a.c;\n\tconst cb = b.line ? [a3, a3, b3, b3] : b.c;\n\tconst ta = endTangent(ca);\n\tconst tb = startTangent(cb);\n\t// Only merge across (near-)smooth joints.\n\tif (ta[0] * tb[0] + ta[1] * tb[1] < 0.99) return null;\n\tconst pts: Point[] = [];\n\tsample(ca, 16, pts, true);\n\tsample(cb, 16, pts, false);\n\tconst { curve, error } = fitAcross(pts, 16, startTangent(ca), endTangent(cb));\n\treturn error <= tol ? { c: [curve.p0, curve.p1, curve.p2, curve.p3], line: false } : null;\n}\n\n/**\n * Remove Redundant Points: drops anchors that can go without changing the shape by more than\n * `tolerance` (collinear corners, duplicate points, and smooth joints that one cubic can\n * replace).\n */\nexport function removeRedundantPoints(sp: VectorSubpath, tolerance: number): VectorSubpath {\n\tconst tol = Math.max(tolerance, 1e-9);\n\tconst types = new Map(sp.anchors.filter((a) => a.type).map((a) => [`${a.x},${a.y}`, a.type] as const));\n\tconst segs = segsOf(sp).filter((s) => !isZeroSeg(s));\n\tif (!segs.length) return { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed };\n\tconst minSegs = sp.closed ? 2 : 1;\n\tlet changed = true;\n\twhile (changed && segs.length > minSegs) {\n\t\tchanged = false;\n\t\tconst n = segs.length;\n\t\tconst joints = sp.closed ? n : n - 1;\n\t\tfor (let j = 0; j < joints; j++) {\n\t\t\tconst [i0, i1] = sp.closed ? [(j + n - 1) % n, j] : [j, j + 1];\n\t\t\tconst m = mergePair(segs[i0], segs[i1], tol);\n\t\t\tif (!m) continue;\n\t\t\tsegs[i0] = m;\n\t\t\tsegs.splice(i1, 1);\n\t\t\t// The merged segment now starts the loop.\n\t\t\tif (sp.closed && i1 === 0) segs.unshift(segs.pop()!);\n\t\t\tchanged = true;\n\t\t\tbreak;\n\t\t}\n\t}\n\treturn segsToSubpath(segs, sp.closed, types) ?? { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed };\n}\n","/**\n * Pencil helpers: lazy-mouse smoothing (a pulled string between the pointer and the pen) and\n * ink-to-shape recognition (a hand-drawn stroke that is a line, rectangle, ellipse or polygon).\n *\n * Portions ported from (MIT OR Apache-2.0, © ArtCraft Team and contributors; see\n * THIRD_PARTY_NOTICES.md):\n * - photocraft-main/crates/paint/src/dynamics.rs (`Smoother`: pulled-string mode, `string`,\n * `finish` with catch-up on stroke end)\n * - wordcraft-main/crates/engine/src/cmd/ink_shape.rs (`recognise`, `ellipse_rms`,\n * `snap_square`, `corners`, `turning`, `rdp`, `upright`)\n */\nimport type { Point } from '../canvas/types.js';\n\n// ---------------------------------------------------------------------------\n// Lazy mouse\n// ---------------------------------------------------------------------------\n\n/**\n * Pulled-string smoothing: the pen stays put until the pointer is more than `length` away,\n * then follows it at that distance, so small wobbles never reach the stroke. Feed pointer\n * positions to `push`; it returns the pen positions to draw through (possibly none).\n */\nexport class LazyMouse {\n\tprivate pos: Point | null = null;\n\tprivate last: Point | null = null;\n\n\t/** `length` is the string length in the units of the points (0 turns smoothing off). */\n\tconstructor(readonly length: number) {}\n\n\tpush(p: Point): Point[] {\n\t\tthis.last = p;\n\t\tif (!(this.length > 0)) return [p];\n\t\tconst pos = this.pos;\n\t\tif (!pos) {\n\t\t\tthis.pos = p;\n\t\t\treturn [p];\n\t\t}\n\t\tconst dx = p[0] - pos[0];\n\t\tconst dy = p[1] - pos[1];\n\t\tconst d = Math.hypot(dx, dy);\n\t\tif (d <= this.length) return [];\n\t\tconst k = (d - this.length) / d;\n\t\tconst next: Point = [pos[0] + dx * k, pos[1] + dy * k];\n\t\tthis.pos = next;\n\t\treturn [next];\n\t}\n\n\t/** The string from the pen to the pointer while they are apart, else null. */\n\tstring(): [Point, Point] | null {\n\t\tconst { pos, last } = this;\n\t\tif (!(this.length > 0) || !pos || !last || (pos[0] === last[0] && pos[1] === last[1])) return null;\n\t\treturn [pos, last];\n\t}\n\n\t/** End of stroke: the pen catches up with the pointer (the point to add, if it lagged). */\n\tfinish(): Point[] {\n\t\tconst { pos, last } = this;\n\t\tif (!(this.length > 0) || !pos || !last || (pos[0] === last[0] && pos[1] === last[1])) return [];\n\t\tthis.pos = last;\n\t\treturn [last];\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Ink to shape\n// ---------------------------------------------------------------------------\n\n/** A shape recognised in a stroke, in the stroke's coordinates. */\nexport type VectorInkShape =\n\t| { kind: 'line'; a: Point; b: Point }\n\t| { kind: 'rect'; x: number; y: number; width: number; height: number }\n\t| { kind: 'ellipse'; x: number; y: number; width: number; height: number }\n\t| { kind: 'polygon'; points: Point[] };\n\nexport interface RecognizeInkOptions {\n\t/** Strokes whose longer side is smaller than this stay ink (default 6). */\n\tminSize?: number;\n}\n\n/** Most corners a recognised polygon may have. */\nexport const VECTOR_INK_MAX_CORNERS = 8;\nconst MAX_POINTS = 100_000;\n\n/** \"line\", \"square\", \"rectangle\", \"circle\", \"ellipse\", \"triangle\" or \"polygon\". */\nexport function vectorInkShapeName(shape: VectorInkShape): string {\n\tswitch (shape.kind) {\n\t\tcase 'line':\n\t\t\treturn 'line';\n\t\tcase 'rect':\n\t\t\treturn shape.width === shape.height ? 'square' : 'rectangle';\n\t\tcase 'ellipse':\n\t\t\treturn shape.width === shape.height ? 'circle' : 'ellipse';\n\t\tcase 'polygon':\n\t\t\treturn shape.points.length === 3 ? 'triangle' : 'polygon';\n\t}\n}\n\nconst dist = (a: Point, b: Point) => Math.hypot(b[0] - a[0], b[1] - a[1]);\n\nfunction bbox(pts: readonly Point[]): [number, number, number, number] {\n\tif (!pts.length) return [0, 0, 0, 0];\n\tlet [x0, y0] = pts[0];\n\tlet [x1, y1] = pts[0];\n\tfor (const [x, y] of pts) {\n\t\tx0 = Math.min(x0, x);\n\t\ty0 = Math.min(y0, y);\n\t\tx1 = Math.max(x1, x);\n\t\ty1 = Math.max(y1, y);\n\t}\n\treturn [x0, y0, x1 - x0, y1 - y0];\n}\n\nfunction segDistance(a: Point, b: Point, p: Point): number {\n\tconst dx = b[0] - a[0];\n\tconst dy = b[1] - a[1];\n\tconst len2 = dx * dx + dy * dy;\n\tconst t = len2 > 0 ? Math.min(1, Math.max(0, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / len2)) : 0;\n\treturn dist([a[0] + t * dx, a[1] + t * dy], p);\n}\n\nfunction polygonDistance(c: readonly Point[], p: Point): number {\n\tlet best = Infinity;\n\tfor (let i = 0; i < c.length; i++) best = Math.min(best, segDistance(c[i], c[(i + 1) % c.length], p));\n\treturn best;\n}\n\n/** RMS of each point's normalised radius minus one on the ellipse inscribed in the box (0 on it). */\nfunction ellipseRms(p: readonly Point[], x0: number, y0: number, w: number, h: number): number {\n\tconst rx = w / 2;\n\tconst ry = h / 2;\n\tif (rx <= 0 || ry <= 0 || !p.length) return Infinity;\n\tconst cx = x0 + rx;\n\tconst cy = y0 + ry;\n\tlet sum = 0;\n\tfor (const [x, y] of p) sum += (Math.hypot((x - cx) / rx, (y - cy) / ry) - 1) ** 2;\n\treturn Math.sqrt(sum / p.length);\n}\n\n/** A box drawn nearly square made square (same centre). */\nfunction snapSquare(x: number, y: number, w: number, h: number): [number, number, number, number] {\n\tif (Math.abs(w - h) <= 0.12 * Math.max(w, h)) {\n\t\tconst s = (w + h) / 2;\n\t\treturn [x + (w - s) / 2, y + (h - s) / 2, s, s];\n\t}\n\treturn [x, y, w, h];\n}\n\n/** How far the path turns at `b` coming from `a` towards `c`, in degrees (0 = straight on). */\nfunction turning(a: Point, b: Point, c: Point): number {\n\tconst u = [b[0] - a[0], b[1] - a[1]];\n\tconst v = [c[0] - b[0], c[1] - b[1]];\n\tconst lu = Math.hypot(u[0], u[1]);\n\tconst lv = Math.hypot(v[0], v[1]);\n\tif (lu <= 0 || lv <= 0) return 0;\n\treturn (Math.acos(Math.min(1, Math.max(-1, (u[0] * v[0] + u[1] * v[1]) / (lu * lv)))) * 180) / Math.PI;\n}\n\n/** Ramer–Douglas–Peucker reduction of an open polyline (iterative), keeping both ends. */\nfunction rdp(p: readonly Point[], eps: number): Point[] {\n\tconst n = p.length;\n\tif (n < 3) return p.slice();\n\tconst keep = new Array<boolean>(n).fill(false);\n\tkeep[0] = keep[n - 1] = true;\n\tconst stack: [number, number][] = [[0, n - 1]];\n\twhile (stack.length) {\n\t\tconst [a, b] = stack.pop()!;\n\t\tlet far = a;\n\t\tlet farD = 0;\n\t\tfor (let i = a + 1; i < b; i++) {\n\t\t\tconst d = segDistance(p[a], p[b], p[i]);\n\t\t\tif (d > farD) {\n\t\t\t\tfarD = d;\n\t\t\t\tfar = i;\n\t\t\t}\n\t\t}\n\t\tif (farD > eps) {\n\t\t\tkeep[far] = true;\n\t\t\tstack.push([a, far], [far, b]);\n\t\t}\n\t}\n\treturn p.filter((_, i) => keep[i]);\n}\n\n/** Corners of the closed path through `p`: RDP split at the point farthest from the start, then flat and crowded corners merged away. */\nfunction corners(p: readonly Point[], diag: number): Point[] {\n\tconst eps = 0.05 * diag;\n\tconst start = p[0];\n\tlet far = 0;\n\tlet farD = 0;\n\tp.forEach((q, i) => {\n\t\tconst d = dist(start, q);\n\t\tif (d > farD) {\n\t\t\tfarD = d;\n\t\t\tfar = i;\n\t\t}\n\t});\n\tconst a = p.slice(0, far);\n\tconst b = p.slice(far);\n\tconst ring = rdp([...a, ...b.slice(0, 1)], eps);\n\tconst back = rdp([...b, start], eps);\n\t// The halves share the far point and the start.\n\tring.pop();\n\tback.pop();\n\tring.push(...back);\n\twhile (ring.length > 3) {\n\t\tconst n = ring.length;\n\t\tconst turn = (i: number) => turning(ring[(i + n - 1) % n], ring[i], ring[(i + 1) % n]);\n\t\tlet flat = 0;\n\t\tlet flatT = Infinity;\n\t\tfor (let i = 0; i < n; i++) {\n\t\t\tconst t = turn(i);\n\t\t\tif (t < flatT) {\n\t\t\t\tflatT = t;\n\t\t\t\tflat = i;\n\t\t\t}\n\t\t}\n\t\tlet short = -1;\n\t\tfor (let j = 0; j < n; j++) {\n\t\t\tif (dist(ring[j], ring[(j + 1) % n]) < 0.1 * diag) {\n\t\t\t\tshort = j;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (flatT < 30) ring.splice(flat, 1);\n\t\telse if (short >= 0) {\n\t\t\t// Of the two crowded corners keep the sharper.\n\t\t\tconst k = turn(short) < turn((short + 1) % n) ? short : (short + 1) % n;\n\t\t\tring.splice(k, 1);\n\t\t} else break;\n\t}\n\treturn ring;\n}\n\n/** A quadrilateral whose sides run (nearly) across and down, alternately. */\nfunction upright(c: readonly Point[]): boolean {\n\tconst n = c.length;\n\tconst dirs = c.map((a, i) => {\n\t\tconst b = c[(i + 1) % n];\n\t\tconst deg = ((((Math.atan2(b[1] - a[1], b[0] - a[0]) * 180) / Math.PI) % 180) + 180) % 180;\n\t\tif (deg <= 15 || deg >= 165) return 'across';\n\t\tif (deg >= 75 && deg <= 105) return 'down';\n\t\treturn null;\n\t});\n\treturn dirs.every((d) => d !== null) && dirs.every((d, i) => d !== dirs[(i + 1) % n]);\n}\n\n/**\n * The shape a hand-drawn stroke draws, if it is one: an open stroke is a line when it stays\n * close to the chord between its ends; a closed one is an ellipse when its points sit on the\n * ellipse inscribed in their bounds, else a polygon of 3–8 corners when that polygon stays\n * close to every point (an upright four-cornered one is a rectangle). Nearly square boxes\n * become squares and circles. Anything else (scribbles, curves, letters) is null.\n */\nexport function recognizeInkShape(points: readonly Point[], options: RecognizeInkOptions = {}): VectorInkShape | null {\n\tconst minSize = options.minSize ?? 6;\n\tconst p: Point[] = [];\n\tfor (const q of points.slice(0, MAX_POINTS)) {\n\t\tif (!Number.isFinite(q[0]) || !Number.isFinite(q[1]) || Math.abs(q[0]) >= 1e9 || Math.abs(q[1]) >= 1e9) continue;\n\t\tconst last = p[p.length - 1];\n\t\tif (!last || last[0] !== q[0] || last[1] !== q[1]) p.push([q[0], q[1]]);\n\t}\n\tif (p.length < 2) return null;\n\tconst first = p[0];\n\tconst last = p[p.length - 1];\n\tconst [x0, y0, w, h] = bbox(p);\n\tconst size = Math.max(w, h);\n\tconst diag = Math.hypot(w, h);\n\tif (size < minSize) return null;\n\tlet length = 0;\n\tfor (let i = 1; i < p.length; i++) length += dist(p[i - 1], p[i]);\n\tconst gap = dist(first, last);\n\t// A line: the stroke barely leaves the chord between its ends and doesn't double back.\n\tif (gap > 0.85 * length && gap >= minSize) {\n\t\tconst off = Math.max(...p.map((q) => segDistance(first, last, q)));\n\t\tif (off <= Math.max(0.06 * gap, (1.5 * minSize) / 6)) return { kind: 'line', a: first, b: last };\n\t}\n\t// Other shapes are closed: the stroke ends near where it began and goes around.\n\tif (gap > 0.2 * size || length < 2 * size) return null;\n\tconst rms = ellipseRms(p, x0, y0, w, h);\n\tconst roundish = w > 0.2 * size && h > 0.2 * size;\n\tconst ellipse = (): VectorInkShape => {\n\t\tconst [x, y, sw, sh] = snapSquare(x0, y0, w, h);\n\t\treturn { kind: 'ellipse', x, y, width: sw, height: sh };\n\t};\n\tif (roundish && rms < 0.06) return ellipse();\n\tconst c = corners(p, diag);\n\tconst fits = c.length >= 3 && p.every((q) => polygonDistance(c, q) <= 0.08 * diag);\n\tif (fits && c.length === 4 && upright(c)) {\n\t\tconst [bx, by, bw, bh] = bbox(c);\n\t\tconst [x, y, sw, sh] = snapSquare(bx, by, bw, bh);\n\t\treturn { kind: 'rect', x, y, width: sw, height: sh };\n\t}\n\tif (fits && c.length <= VECTOR_INK_MAX_CORNERS) return { kind: 'polygon', points: c };\n\t// A wobbly round stroke is still an ellipse.\n\treturn roundish && rms < 0.1 ? ellipse() : null;\n}\n","/**\n * Pathfinder operands, the Shape Builder and fill regions over a vector document.\n *\n * Everything works in document space: each path is mapped through its own transform and its\n * parents', with live corners cut (`subpathsToBezier`), so rotated nodes and nodes inside\n * rotated groups take part as drawn. The controller maps results back into the space of the node\n * that receives them.\n *\n * - **Pathfinder operands**: a filled path is its fill area (every subpath closed); an unfilled\n * path with a stroke is the area its stroke paints (alignment, caps, joins and arrowheads\n * included) and results from it are filled with the stroke's paint; for Divide an unfilled open\n * path is a cutting line instead.\n * - **Shape Builder**: the planar map of the selected paths (`PlanarMap`). Closed subpaths bound\n * faces whatever their paint (a stroke-only shape's inside is a face), filled open subpaths that\n * enclose an area are closed by an invisible edge, and unfilled open paths cut the faces they\n * cross; their pieces between junctions can be erased. Merging faces unites them along the\n * map's own edges and takes the area away from every shape covering them. Erasing (Alt) also\n * takes the edges of the other outlines, cut where paths meet: a closed subpath opens there,\n * an open one splits, and an edge bounding a face deleted in the same gesture goes with it.\n * - **Fill regions** (paint bucket): the planar map of every visible outline; the region around a\n * point is traced from a ray (`PlanarMap.regionAt`).\n *\n * The Shape Builder follows vectorcraft-main/crates/tools/src/builder.rs (`BuilderMap`, `hit`,\n * `sides`, `line_between`, `erased_edges`, `Hit::Edge`, `shape_builder_shapes`, `sample_polyline`)\n * and crates/engine/src/cmd/buildcmds.rs (`sb_merge`), with figcraft-main/crates/engine/src/shapebuilder.rs\n * (strokes as their outlined area). MIT OR Apache-2.0, © ArtCraft Team and contributors. See\n * THIRD_PARTY_NOTICES.md.\n */\nimport type { Point } from '../canvas/types.js';\nimport {\n\tbezierPathBounds,\n\tcontourSignedArea,\n\tcloseContour,\n\tdistanceToBezierPath,\n\tpathToContours,\n\tsegmentPoint,\n\tsegmentTangent,\n\tcontoursToPath,\n\ttype BezierPath,\n\ttype BezierSegment\n} from '../canvas/pathops/bezier.js';\nimport { PlanarMap, cutOutPiece, type PlanarChain } from '../canvas/pathops/planar.js';\nimport type { PathfinderOperation, PathfinderShape } from '../canvas/pathops/pathfinder.js';\nimport { hasPaint, multiplyMatrices, nodeMatrix, subpathsToBezier, vectorPathWorldBezier, vectorStrokeOutline, IDENTITY_MATRIX, type AffineMatrix } from './geometry.js';\nimport { vectorMaskedGeometry } from './mask.js';\nimport { expandVectorAppearance } from './appearance.js';\nimport type { Vec2, VectorDocument, VectorNode, VectorPaint, VectorPathNode, VectorSubpath } from './types.js';\n\n/** A path node with the matrix of its parent space (parent → document). */\nexport interface VectorPathEntry {\n\tnode: VectorPathNode;\n\tparent: AffineMatrix;\n}\n\n/**\n * The path nodes inside `roots` (or every node when `roots` is null), bottom to top, with their\n * parent matrices. Hidden nodes are skipped, and locked ones unless `locked` is true. Live nodes\n * (path effects, live booleans) take part as drawn (`expandVectorAppearance`: their entries are\n * the expanded paths, whose ids may not be in the document) unless `expand` is false.\n */\nexport function vectorPathEntries(doc: VectorDocument, roots: readonly string[] | null, options: { locked?: boolean; expand?: boolean } = {}): VectorPathEntry[] {\n\tconst out: VectorPathEntry[] = [];\n\tconst visit = (stored: VectorNode, parent: AffineMatrix, inside: boolean) => {\n\t\tif (stored.visible === false || (stored.locked && !options.locked)) return;\n\t\t// As drawn: effects and live booleans evaluated, mask groups as their masked shapes.\n\t\tconst node = options.expand === false ? stored : expandVectorAppearance(vectorMaskedGeometry(stored));\n\t\tconst picked = inside || roots === null || roots.includes(node.id);\n\t\tif (node.type === 'group') {\n\t\t\tconst m = multiplyMatrices(nodeMatrix(node), parent);\n\t\t\tfor (const c of node.children) visit(c, m, picked);\n\t\t} else if (picked) out.push({ node, parent });\n\t};\n\tfor (const n of doc.nodes) visit(n, IDENTITY_MATRIX, false);\n\treturn out;\n}\n\nconst usable = (sp: VectorSubpath) => sp.anchors.length > 1;\n\n/** The entry's outline in document space with the given subpaths. */\nfunction worldOf(entry: VectorPathEntry, subpaths: VectorSubpath[]): BezierPath {\n\treturn vectorPathWorldBezier({ ...entry.node, subpaths }, entry.parent);\n}\n\n/** Whether a subpath, closed, encloses an area (rather than retracing itself). */\nexport function subpathEnclosesArea(sp: VectorSubpath): boolean {\n\tconst contour = pathToContours(subpathsToBezier([{ ...sp, closed: true }]))[0];\n\tif (!contour) return false;\n\tconst segs = closeContour(contour, true);\n\tconst b = bezierPathBounds(contoursToPath([{ segments: segs, closed: true }]));\n\tif (!b) return false;\n\tconst w = b.maxX - b.minX;\n\tconst h = b.maxY - b.minY;\n\treturn Math.abs(contourSignedArea(segs)) > 1e-9 * (w * w + h * h);\n}\n\n/** How an operand takes part in a Pathfinder operation: its fill, its stroke's area, a cutting line or its bare outline. */\nexport type VectorPathfinderPaint = 'fill' | 'stroke' | 'line' | 'outline';\n\nconst paintKey = (paint: VectorPaint | undefined) => (typeof paint === 'string' ? paint.trim().toLowerCase() : JSON.stringify(paint ?? null));\n\n/** The Pathfinder shape of a path (document space), and what it stands for. */\nexport function vectorPathfinderShape(entry: VectorPathEntry, op: PathfinderOperation): PathfinderShape & { paint: VectorPathfinderPaint } {\n\tconst { node } = entry;\n\tconst subpaths = node.subpaths.filter(usable);\n\tconst closed = subpaths.map((s) => ({ ...s, closed: true }));\n\tconst fillRule = node.fillRule ?? 'nonzero';\n\tif (hasPaint(node.fill)) return { path: worldOf(entry, closed), fillRule, key: `fill:${paintKey(node.fill)}`, paint: 'fill' };\n\tconst open = subpaths.length > 0 && subpaths.every((s) => !s.closed);\n\tif (op === 'divide' && open) return { path: worldOf(entry, subpaths), line: true, paint: 'line' };\n\tif (hasPaint(node.stroke) && (node.strokeWidth ?? 1) > 0) {\n\t\t// Node transforms are rigid, so the stroke laid out in document space keeps its width.\n\t\tconst path = vectorStrokeOutline({ ...node, subpaths }, entry.parent);\n\t\treturn { path, fillRule: 'nonzero', key: `fill:${paintKey(node.stroke)}`, paint: 'stroke' };\n\t}\n\treturn { path: worldOf(entry, closed), fillRule, key: 'none', paint: 'outline' };\n}\n\nconst STROKE_KEYS = [\n\t'stroke',\n\t'strokeWidth',\n\t'strokeLinecap',\n\t'strokeLinejoin',\n\t'strokeDasharray',\n\t'strokeAlign',\n\t'startArrow',\n\t'endArrow',\n\t'arrowAlign',\n\t'startArrowScale',\n\t'endArrowScale',\n\t'strokeWidthProfile'\n] as const;\n\n/**\n * Style of a Pathfinder or Shape Builder result taken from `node`: the node's own style for a\n * filled area, the stroke's paint as a fill for a stroke's area, or (for Outline edges) a stroke\n * in the paint of the area the edge bounds. Never carries the node's id, transform or geometry.\n */\nexport function vectorResultStyle(node: VectorPathNode, paint: VectorPathfinderPaint, edge = false): Omit<VectorPathNode, 'id' | 'subpaths'> {\n\tconst { id: _id, subpaths: _s, transform: _t, effects: _e, booleanOperation: _b, ...rest } = node;\n\tconst base: Omit<VectorPathNode, 'id' | 'subpaths'> = { ...rest, fillRule: 'nonzero' };\n\tif (edge) {\n\t\tfor (const k of STROKE_KEYS) delete (base as Record<string, unknown>)[k];\n\t\tconst stroke = paint === 'stroke' || !hasPaint(node.fill) ? node.stroke : node.fill;\n\t\treturn { ...base, fill: null, stroke: stroke ?? '#000000', strokeWidth: node.strokeWidth ?? 1 };\n\t}\n\tif (paint === 'stroke') {\n\t\tfor (const k of STROKE_KEYS) delete (base as Record<string, unknown>)[k];\n\t\treturn { ...base, fill: node.stroke ?? null, stroke: null };\n\t}\n\treturn base;\n}\n\n// ---------------------------------------------------------------------------\n// Shape Builder\n// ---------------------------------------------------------------------------\n\n/**\n * A run of a path between the points where other paths meet it (or it ends): a piece of an\n * unfilled open path, or an edge of another path's outline.\n */\nexport interface VectorBuilderLinePiece {\n\tpath: BezierPath;\n\tsegments: BezierSegment[];\n\t/** A loop with no junction on it (the whole subpath). */\n\tclosed: boolean;\n\t/** Index of the entry it belongs to (the front-most where outlines coincide). */\n\tsource: number;\n\tbounds: { minX: number; minY: number; maxX: number; maxY: number };\n}\n\n/** The Shape Builder's planar map of a set of paths. */\nexport interface VectorShapeBuilderMap {\n\tentries: VectorPathEntry[];\n\tmap: PlanarMap;\n\t/** Entries with no closed subpath (cutting lines; their pieces can be erased). */\n\tlines: boolean[];\n\tpieces: VectorBuilderLinePiece[];\n\t/** The edges of the other entries' outlines, cut where paths meet (built on first use: erasing). */\n\treadonly edges: VectorBuilderLinePiece[];\n\t/** How far an edge may lie from the outline it came from (document units). */\n\ttolerance: number;\n}\n\n/** What a Shape Builder gesture touches at a point. */\nexport type VectorBuilderHit = { face: number } | { line: number } | { edge: number };\n\n/** The faces, line pieces and edges a gesture touched, in the order touched. */\nexport interface VectorBuilderTouched {\n\tfaces: number[];\n\tlines: number[];\n\tedges: number[];\n}\n\n/**\n * The Shape Builder map of `entries` (bottom to top). Filled open subpaths that enclose an area\n * are closed. Null when nothing can be mapped.\n */\nexport function vectorShapeBuilderMap(entries: readonly VectorPathEntry[]): VectorShapeBuilderMap | null {\n\tconst lines: boolean[] = [];\n\tconst inputs = entries.map((entry) => {\n\t\tconst filled = hasPaint(entry.node.fill);\n\t\tconst subpaths = entry.node.subpaths.filter(usable).map((sp) => (!sp.closed && filled && subpathEnclosesArea(sp) ? { ...sp, closed: true } : sp));\n\t\tlines.push(!subpaths.some((s) => s.closed));\n\t\treturn { path: worldOf(entry, subpaths), fillRule: entry.node.fillRule ?? 'nonzero' };\n\t});\n\tconst map = PlanarMap.build(inputs);\n\tif (!map) return null;\n\tlet chains: PlanarChain[] | null = null;\n\tconst runs = (line: boolean) =>\n\t\t(chains ??= map.chains())\n\t\t\t.filter((c) => lines[c.source] === line && !(line && c.closed))\n\t\t\t.map((c) => {\n\t\t\t\tconst path = contoursToPath([{ segments: c.segments, closed: c.closed }]);\n\t\t\t\treturn { path, segments: c.segments, closed: c.closed, source: c.source, bounds: bezierPathBounds(path)! };\n\t\t\t});\n\tconst pieces = lines.some(Boolean) ? runs(true) : [];\n\tlet edges: VectorBuilderLinePiece[] | null = null;\n\tlet size = 0;\n\tfor (const input of inputs) {\n\t\tconst b = bezierPathBounds(input.path);\n\t\tif (b) size = Math.max(size, b.maxX - b.minX, b.maxY - b.minY);\n\t}\n\treturn {\n\t\tentries: [...entries],\n\t\tmap,\n\t\tlines,\n\t\tpieces,\n\t\tget edges() {\n\t\t\treturn (edges ??= runs(false));\n\t\t},\n\t\ttolerance: 0.2 * Math.max(size / 2000, 0.25)\n\t};\n}\n\n/** The run of `pieces` nearest `p` within `tol`, with its distance. */\nfunction pieceNear(pieces: readonly VectorBuilderLinePiece[], p: Vec2, tol: number): { index: number; d: number } | null {\n\tconst point: Point = [p.x, p.y];\n\tlet best: { index: number; d: number } | null = null;\n\tpieces.forEach((piece, index) => {\n\t\tconst b = piece.bounds;\n\t\tif (p.x < b.minX - tol || p.x > b.maxX + tol || p.y < b.minY - tol || p.y > b.maxY + tol) return;\n\t\tconst d = distanceToBezierPath(piece.path, point);\n\t\tif (d <= tol && (!best || d < best.d)) best = { index, d };\n\t});\n\treturn best;\n}\n\n/**\n * The face under `p`, or with `lineTolerance` (erasing) first the nearer of the line piece and\n * the outline edge that close.\n */\nexport function vectorShapeBuilderHit(m: VectorShapeBuilderMap, p: Vec2, lineTolerance: number | null = null): VectorBuilderHit | null {\n\tif (lineTolerance !== null && lineTolerance > 0) {\n\t\tconst line = pieceNear(m.pieces, p, lineTolerance);\n\t\tconst edge = pieceNear(m.edges, p, lineTolerance);\n\t\tif (edge && (!line || edge.d < line.d)) return { edge: edge.index };\n\t\tif (line) return { line: line.index };\n\t}\n\tconst face = m.map.faceAt([p.x, p.y]);\n\treturn face >= 0 ? { face } : null;\n}\n\n/** Points along a polyline no more than `step` apart (the pointer's path through faces). */\nexport function samplePolyline(points: readonly Vec2[], step: number): Vec2[] {\n\tconst out: Vec2[] = [];\n\tconst s = Math.max(step, 1e-3);\n\tpoints.forEach((p, i) => {\n\t\tif (i > 0) {\n\t\t\tconst a = points[i - 1];\n\t\t\tconst n = Math.min(4000, Math.ceil(Math.hypot(p.x - a.x, p.y - a.y) / s));\n\t\t\tfor (let k = 1; k < n; k++) out.push({ x: a.x + ((p.x - a.x) * k) / n, y: a.y + ((p.y - a.y) * k) / n });\n\t\t}\n\t\tout.push(p);\n\t});\n\treturn out;\n}\n\n/** Adds what a point hits to `touched` (each face or piece once). */\nexport function addBuilderHit(touched: VectorBuilderTouched, hit: VectorBuilderHit | null): boolean {\n\tif (!hit) return false;\n\tconst [list, i] = 'face' in hit ? [touched.faces, hit.face] : 'line' in hit ? [touched.lines, hit.line] : [touched.edges, hit.edge];\n\tif (list.includes(i)) return false;\n\tlist.push(i);\n\treturn true;\n}\n\n/** The faces just either side of the middle of a line piece (-1 outside every face). */\nfunction sides(m: VectorShapeBuilderMap, piece: VectorBuilderLinePiece): [number, number] {\n\tconst seg = piece.segments[Math.floor(piece.segments.length / 2)];\n\tconst mid = segmentPoint(seg, 0.5);\n\tconst t = segmentTangent(seg, 0.5);\n\tconst chord = Math.hypot(seg[seg.length - 1][0] - seg[0][0], seg[seg.length - 1][1] - seg[0][1]);\n\tconst off = Math.max(1e-6, Math.min(0.05, 0.05 * chord));\n\treturn [m.map.faceAt([mid[0] - t[1] * off, mid[1] + t[0] * off]), m.map.faceAt([mid[0] + t[1] * off, mid[1] - t[0] * off])];\n}\n\n/**\n * The edges a gesture deletes: those it touched that bound none of the faces it touched (an edge\n * of a deleted or merged face goes with the face, so an erase drag that crosses an edge into a\n * face deletes the face).\n */\nexport function vectorBuilderErasedEdges(m: VectorShapeBuilderMap, touched: VectorBuilderTouched): number[] {\n\tif (!touched.edges.length) return [];\n\tconst faces = new Set(touched.faces);\n\treturn touched.edges.filter((k) => m.edges[k] && !sides(m, m.edges[k]).some((f) => f >= 0 && faces.has(f)));\n}\n\n/**\n * What becomes of one entry: unchanged (absent), removed (`null`), or new outlines (document\n * space). `fill`: what is left of its area, filled non-zero (subpaths an erased edge opened stay\n * open); `outline`: its own outline with erased edges cut out, its fill rule kept; `pieces`: the\n * pieces of a cutting line that stay, each its own path.\n */\nexport type VectorBuilderReplacement = { kind: 'fill'; path: BezierPath } | { kind: 'outline'; path: BezierPath } | { kind: 'pieces'; paths: BezierPath[] } | null;\n\nexport interface VectorShapeBuilderPlan {\n\treplace: Map<number, VectorBuilderReplacement>;\n\t/** The merged shape (document space), styled like entry `style`; `currentFill` when only outlines enclosed it. */\n\tmerged: { path: BezierPath; style: number; currentFill: boolean } | null;\n}\n\n/**\n * What a gesture does: merging (or with `erase`, deleting) the touched faces takes them away\n * from every shape covering them; line pieces touched (erasing) or lying between touched faces\n * go; erased edges (`vectorBuilderErasedEdges`) are cut out of every outline they lie on;\n * merging adds the union of the faces as a new shape styled like the top-most shape over the\n * first face touched (or, for a face only outlines enclose, the top-most shape with the current\n * fill).\n */\nexport function planShapeBuilder(m: VectorShapeBuilderMap, touched: VectorBuilderTouched, erase: boolean): VectorShapeBuilderPlan {\n\tconst replace = new Map<number, VectorBuilderReplacement>();\n\tconst picked = new Set(touched.faces);\n\tconst faces = m.map.faces;\n\tconst sourcesOf = faces.map((_, i) => m.map.faceSources(i));\n\tconst cuts = erase ? vectorBuilderErasedEdges(m, touched).map((k) => m.edges[k]) : [];\n\tm.entries.forEach((entry, i) => {\n\t\tif (m.lines[i]) {\n\t\t\tconst own = m.pieces.flatMap((piece, k) => (piece.source === i ? [k] : []));\n\t\t\tconst keep = own.filter((k) => !touched.lines.includes(k) && !sides(m, m.pieces[k]).every((f) => f >= 0 && picked.has(f)));\n\t\t\tif (keep.length === own.length) return;\n\t\t\treplace.set(i, keep.length ? { kind: 'pieces', paths: keep.map((k) => m.pieces[k].path) } : null);\n\t\t\treturn;\n\t\t}\n\t\tconst inUnion = touched.faces.some((f) => sourcesOf[f].includes(i));\n\t\tif (!inUnion && !cuts.length) return;\n\t\tlet rest: BezierPath;\n\t\tif (inUnion) {\n\t\t\tconst kept = faces.flatMap((_, f) => (sourcesOf[f].includes(i) && !picked.has(f) ? [f] : []));\n\t\t\trest = kept.length ? m.map.merge(kept) : [];\n\t\t} else rest = worldOf(entry, entry.node.subpaths.filter(usable));\n\t\t// Each erased edge comes off every outline it lies on (shared edges from each shape).\n\t\tconst fillCloses = hasPaint(entry.node.fill);\n\t\tlet cut = false;\n\t\tfor (const piece of cuts) {\n\t\t\tconst r = rest.length ? cutOutPiece(rest, piece, fillCloses, m.tolerance) : null;\n\t\t\tif (r) {\n\t\t\t\trest = r;\n\t\t\t\tcut = true;\n\t\t\t}\n\t\t}\n\t\tif (!inUnion && !cut) return;\n\t\treplace.set(i, rest.length ? { kind: inUnion ? 'fill' : 'outline', path: rest } : null);\n\t});\n\tlet merged: VectorShapeBuilderPlan['merged'] = null;\n\tif (!erase && touched.faces.length) {\n\t\tconst path = m.map.merge(touched.faces);\n\t\tconst covered = touched.faces.map((f) => sourcesOf[f]).find((s) => s.length);\n\t\tconst lastShape = m.lines.lastIndexOf(false);\n\t\tconst style = covered ? covered[covered.length - 1] : lastShape >= 0 ? lastShape : m.entries.length - 1;\n\t\tif (path.length) merged = { path, style, currentFill: !covered };\n\t}\n\treturn { replace, merged };\n}\n\n// ---------------------------------------------------------------------------\n// Fill regions\n// ---------------------------------------------------------------------------\n\n/**\n * The planar map of every visible path's outline as drawn (locked ones included), for the paint\n * bucket. `gap` closes gaps up to that distance between loose ends.\n */\nexport function vectorFillMap(doc: VectorDocument, gap = 0): PlanarMap | null {\n\tconst entries = vectorPathEntries(doc, null, { locked: true });\n\treturn PlanarMap.build(\n\t\tentries.map((e) => ({ path: worldOf(e, e.node.subpaths.filter(usable)), fills: false })),\n\t\t{ gap, maxSegments: 20000 }\n\t);\n}\n","import { HistoryManager } from '../history/HistoryManager.js';\nimport type { Command } from '../history/types.js';\nimport { fitCubics } from '../canvas/curveFit.js';\nimport { pathBoolean } from '../canvas/pathops/boolean.js';\nimport { pathfinder as runPathfinder } from '../canvas/pathops/pathfinder.js';\nimport type { PlanarMap } from '../canvas/pathops/planar.js';\nimport { bezierPathBounds, bezierPathToSvg, transformBezierPath, type BezierPath } from '../canvas/pathops/bezier.js';\nimport type { Point } from '../canvas/types.js';\nimport {\n\tbezierToSubpaths,\n\tboundsFromPoints,\n\tboundsIntersect,\n\tcloneVectorDocument,\n\tcloneVectorNode,\n\tcollectPathNodes,\n\tconvertAnchor,\n\tellipseSubpath,\n\tfindVectorNode,\n\thitTestVectorNode,\n\thitTestVectorNodes,\n\tinsertAnchor,\n\tlineSubpath,\n\tlocateVectorNode,\n\tmoveAnchorHandle,\n\tnearestSegment,\n\thasPaint,\n\tnodeMatrix,\n\tinvertMatrix,\n\tapplyMatrix,\n\tapplyMatrixLinear,\n\tmultiplyMatrices,\n\tmatrixRotation,\n\tvectorWorldMatrix,\n\tvectorParentMatrix,\n\tvectorPathWorldBezier,\n\tIDENTITY_MATRIX,\n\tpolygonSubpath,\n\trectSubpath,\n\tremoveAnchors,\n\treverseSubpath,\n\trotateMatrix,\n\tscaleMatrix,\n\tstarSubpath,\n\tsubpathsToBezier,\n\ttransformVectorNode,\n\ttranslateAnchor,\n\ttranslateMatrix,\n\tunionBounds,\n\tvectorNodeBounds,\n\twalkVectorNodes,\n\ttype AffineMatrix\n} from './geometry.js';\nimport {\n\tcornerBisectorPoint,\n\tcornerRadiusChange,\n\tfitCornerRadius,\n\tnextVectorCornerKind,\n\tsetAnchorCorner,\n\tsetSubpathCorners,\n\tvectorCorners,\n\ttype VectorCornerInfo\n} from './corners.js';\nimport { strokeWidthProfileOf, vectorStrokeOutlinePath, vectorStrokeParts } from './stroke.js';\nimport { nearestStrokePosition, strokePositionPoint } from './varstroke.js';\nimport {\n\tgenerateWidthProfile,\n\tinsertWidthPoint,\n\tpressureWidthProfile,\n\tremoveWidthPoints,\n\treverseWidthProfile,\n\tsanitizeWidthProfile,\n\tsetWidthPoint,\n\twidthProfileAt,\n\twidthProfileMaxScale,\n\twidthProfilePreset,\n\ttype VectorTaperOptions,\n\ttype VectorWaveOptions,\n\ttype VectorWidthPreset,\n\tVECTOR_WIDTH_PROFILE_MAX_POINTS,\n\tVECTOR_WIDTH_SAME_T\n} from './width.js';\nimport { parseVectorSvg, vectorDocumentToSvg, vectorPathLocalBounds, type VectorSvgExportOptions } from './svg.js';\nimport {\n\tapplyVectorGradientPatch,\n\tdefaultVectorGradient,\n\tinsertVectorGradientStop,\n\tisVectorGradient,\n\tsanitizeVectorGradient,\n\tmoveVectorGradientHandle,\n\tmoveVectorGradientStop,\n\tremoveVectorGradientStop,\n\tsetVectorGradientVector,\n\tsortedVectorGradientStops,\n\tvectorGradientMidpointFromPosition,\n\ttype VectorGradientHandle,\n\ttype VectorGradientPatch\n} from './gradient.js';\nimport {\n\tgetVectorGradientAnnotator,\n\thitVectorGradientAnnotator,\n\tvectorGradientBarPosition,\n\tVECTOR_GRADIENT_OFF_BAR,\n\tVECTOR_GRADIENT_STOP_GAP,\n\ttype VectorGradientAnnotator,\n\ttype VectorGradientPart,\n\ttype VectorGradientTarget\n} from './gradientTool.js';\nimport {\n\texpandVectorBlend,\n\tisVectorBlendGroup,\n\trefreshVectorBlends,\n\treleaseVectorBlend,\n\tvectorBlendKeyTargets\n} from './blendGroup.js';\nimport {\n\tcreateVectorEffect,\n\texpandVectorAppearance,\n\thasOwnVectorAppearance,\n\thasVectorAppearance,\n\tisVectorLiveBoolean,\n\tnormalizeVectorEffectParams,\n\tvectorAppearanceOutline,\n\tvectorEnteredGroups,\n\tvectorEnvelopeFrame,\n\tvectorNodeEnvelope,\n\ttype VectorEnvelopeParams,\n\tVECTOR_EFFECT_LABELS\n} from './appearance.js';\nimport { isVectorMaskActive, isVectorMaskGroup, isVectorMaskType, vectorMaskedGeometry } from './mask.js';\nimport { applyVectorHomography, vectorHomography, vectorHomographyKeepsQuad } from './perspective.js';\nimport { distortVectorNode, vectorBoxCorners, vectorDistortQuad, vectorShearForSide, vectorSkewMatrix, VECTOR_DISTORT_TOLERANCE, type VectorDistortMode } from './distort.js';\nimport {\n\tdragVectorMeshPoint,\n\tpullVectorMesh,\n\tresizeVectorMesh,\n\tvectorMeshEditPoints,\n\tvectorMeshFromOutline,\n\tvectorMeshOutline,\n\tvectorMeshParamAt\n} from './warp.js';\nimport { constrainToLeave, VECTOR_CONSTRUCTION_ANGLES, VectorSnapTargets, type VectorSnapGuides, type VectorSnapLeave } from './snap.js';\nimport {\n\tbendSegment as bendSubpathSegment,\n\tcurvatureMoveAnchor,\n\tcutSubpathAt,\n\tjoinSubpaths,\n\tremoveAnchorsKeepingShape,\n\tremoveRedundantPoints as removeRedundantSubpathPoints,\n\tsmoothSubpath,\n\tsplitSubpathAt\n} from './pathEdit.js';\nimport { eraseFromFill, eraseFromOutline, eraserRegion, knifePath, polylineBounds, simplifyPolyline } from './pathCut.js';\nimport { LazyMouse, recognizeInkShape, vectorInkShapeName, type VectorInkShape } from './ink.js';\nimport {\n\taddBuilderHit,\n\tplanShapeBuilder,\n\tsamplePolyline,\n\tvectorFillMap,\n\tvectorPathEntries,\n\tvectorPathfinderShape,\n\tvectorResultStyle,\n\tvectorBuilderErasedEdges,\n\tvectorShapeBuilderHit,\n\tvectorShapeBuilderMap,\n\ttype VectorBuilderTouched,\n\ttype VectorShapeBuilderMap\n} from './builder.js';\nimport { VECTOR_BLEND_MODES, type VectorBlendMode, type VectorFilter, type VectorFilterType, type VectorGradient, type VectorPaintLayer, type VectorStrokeLayer, type VectorWarpMesh } from './types.js';\nimport {\n\tisVectorBlendMode,\n\tnewVectorStrokeLayer,\n\tnormalizeVectorPaintStacks,\n\tVECTOR_STROKE_SETTING_KEYS,\n\tsanitizeVectorFills,\n\tsanitizeVectorStrokeSettings,\n\tsanitizeVectorStrokes,\n\tvectorFills,\n\tvectorPaintLayerNodes,\n\tvectorStrokes,\n\tvectorTargetPaint,\n\twithVectorFills,\n\twithVectorStrokes,\n\twithVectorTargetPaint\n} from './paintLayers.js';\nimport { createVectorFilter, isVectorFilterType, normalizeVectorFilterParams, VECTOR_FILTER_LABELS } from './filters.js';\nimport type {\n\tVec2,\n\tVectorEffect,\n\tVectorEffectType,\n\tVectorAlign,\n\tVectorAnchor,\n\tVectorAnchorRef,\n\tVectorAnchorType,\n\tVectorArrange,\n\tVectorAverageAxis,\n\tVectorBlend,\n\tVectorBlendOptions,\n\tVectorBooleanOperation,\n\tVectorBounds,\n\tVectorCornerKind,\n\tVectorDefaults,\n\tVectorDistribute,\n\tVectorDocument,\n\tVectorGroupNode,\n\tVectorHandleRef,\n\tVectorMask,\n\tVectorMaskType,\n\tVectorModifiers,\n\tVectorNode,\n\tVectorPathfinderOperation,\n\tVectorPathNode,\n\tVectorSegmentRef,\n\tVectorStyle,\n\tVectorSubpath,\n\tVectorTool,\n\tVectorTransformHandle,\n\tVectorViewport,\n\tVectorWidthPoint\n} from './types.js';\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport interface VectorSnapOptions {\n\t/** Grid size in document units (0 = off, default 0). Points and boxes that snap to no object snap to it. */\n\tgrid: number;\n\t/**\n\t * Snap to other objects (default true): anchors, paths and their intersections, bounding-box\n\t * edges and centres, the artboard, construction angles, equal spacing and matching sizes.\n\t */\n\tobjects: boolean;\n\t/** Show smart guide lines and labels while snapping (default true). */\n\tguides: boolean;\n\t/** How near a target pulls, in screen pixels (default 5). */\n\tthreshold: number;\n\t/** Construction guide angles through the point a drawn point leaves from, in degrees (default 0, 45, 90, 135). */\n\tangles: readonly number[];\n}\n\n/** Snap options new editors start with. Hold Cmd / Ctrl (`mod`) during a drag to bypass snapping. */\nexport const DEFAULT_VECTOR_SNAP: Readonly<VectorSnapOptions> = { grid: 0, objects: true, guides: true, threshold: 5, angles: VECTOR_CONSTRUCTION_ANGLES };\n\n/** How path-editing gestures and the pencil, knife and eraser behave. */\nexport interface VectorPathEditOptions {\n\t/**\n\t * Deleting anchors refits the neighbouring handles so the shape stays (default true). Off,\n\t * the neighbours are joined with their handles as they are.\n\t */\n\tkeepShapeOnDelete: boolean;\n\t/**\n\t * Dragging one anchor with the direct tool bends its two segments like the Curvature tool,\n\t * keeping the segments beyond still (default false). Alt held during the drag flips it.\n\t */\n\tcurvatureMoves: boolean;\n\t/** Pencil lazy-mouse smoothing, 0–1: the pulled string is up to 100 screen pixels long (default 0, off). */\n\tpencilSmoothing: number;\n\t/** Pencil strokes that look like a line, rectangle, ellipse or polygon become that shape (default false). */\n\trecognizeShapes: boolean;\n\t/** Eraser diameter in document units (default 10). */\n\teraserSize: number;\n\t/**\n\t * Record pointer pressure on pencil strokes as a width profile: `pen` (default) for pen input\n\t * only, `all` for any pointer that reports pressure, `off` never.\n\t */\n\tpencilPressure: 'pen' | 'all' | 'off';\n\t/**\n\t * Paint bucket: gaps up to this distance (document units) between loose ends of outlines are\n\t * closed when finding the region to fill (default 0, off).\n\t */\n\tfillGap: number;\n}\n\n/** The gap the toolbars' \"Close gaps\" toggle sets (document units). */\nexport const VECTOR_FILL_GAP = 6;\n\n/** Path-editing options new editors start with. */\nexport const DEFAULT_VECTOR_PATH_EDIT: Readonly<VectorPathEditOptions> = {\n\tkeepShapeOnDelete: true,\n\tcurvatureMoves: false,\n\tpencilSmoothing: 0,\n\trecognizeShapes: false,\n\teraserSize: 10,\n\tpencilPressure: 'pen',\n\tfillGap: 0\n};\n\n/** Longest pencil lazy-mouse string (at smoothing 1), in screen pixels. */\nexport const VECTOR_PENCIL_STRING_PX = 100;\n\nexport interface VectorEditorOptions {\n\tdocument?: VectorDocument;\n\t/** History to record into, e.g. one shared with the rest of the app (default: a new one). */\n\thistory?: HistoryManager;\n\t/** Starting tool (default `select`). */\n\ttool?: VectorTool;\n\t/** Style and settings for new shapes and paths. */\n\tdefaults?: VectorDefaults;\n\tviewport?: Partial<VectorViewport>;\n\tsnap?: Partial<VectorSnapOptions>;\n\tediting?: Partial<VectorPathEditOptions>;\n\t/** Pick distance for anchors, handles and outlines in screen pixels (default 6). */\n\thitRadius?: number;\n\t/** Id factory for new nodes. */\n\tcreateId?: () => string;\n\t/** Called after the document changes (edits, undo, redo, load). */\n\tonChange?: (document: VectorDocument) => void;\n\t/** Start in Free Distort mode (see `setFreeDistort`). */\n\tfreeDistort?: boolean;\n}\n\n/** A pointer event as the controller needs it. Positions are in screen pixels relative to the canvas. */\nexport interface VectorPointerInput extends VectorModifiers {\n\t/** 0 = primary, 1 = middle (pans), 2 = secondary. Default 0. */\n\tbutton?: number;\n\t/** `PointerEvent.pressure` (0–1), recorded on pencil strokes. */\n\tpressure?: number;\n\t/** `PointerEvent.pointerType` (`mouse`, `pen`, `touch`). */\n\tpointerType?: string;\n}\n\n/** One width point of a path's stroke profile; `subpath` is the subpath it is shown on. */\nexport interface VectorWidthPointRef {\n\tnodeId: string;\n\tindex: number;\n\tsubpath: number;\n}\n\n/** A spot on a stroke (width tool): where a drag would add a width point. Positions in document units. */\nexport interface VectorStrokeSpot {\n\tnodeId: string;\n\tsubpath: number;\n\t/** Position along the subpath by length, 0–1. */\n\tt: number;\n\tpoint: Vec2;\n\t/** Ends of the stroke's width across the path there. */\n\tleft: Vec2;\n\tright: Vec2;\n}\n\n/**\n * The width point being edited numerically (double-click with the width tool): `index` is the\n * point's, or null for a new point at `t` (a double-click on a stroke away from its points).\n * `left` / `right` are side widths in document units (how far the stroke reaches on each side),\n * `point` is where it sits on the path, in document space.\n */\nexport interface VectorWidthPointEdit {\n\tnodeId: string;\n\tsubpath: number;\n\tindex: number | null;\n\tt: number;\n\tleft: number;\n\tright: number;\n\tpoint: Vec2;\n}\n\n/** A key press as the controller needs it. */\nexport interface VectorKeyInput extends VectorModifiers {\n\t/** `KeyboardEvent.key`. */\n\tkey: string;\n}\n\n/** What the pointer is over. */\nexport interface VectorHover {\n\tnodeId: string | null;\n\tanchor?: VectorAnchorRef;\n\thandle?: VectorHandleRef;\n\t/** A segment under the pointer (direct tool and scissors); `point` is in document space. */\n\tsegment?: { nodeId: string; subpath: number; segment: number; t: number; point: Vec2 };\n\ttransformHandle?: VectorTransformHandle;\n\t/** A live corner widget (direct tool): the corner's anchor. */\n\tcorner?: VectorAnchorRef;\n\t/** A width point (width tool): its centre or the end of its left / right side. */\n\twidthPoint?: VectorWidthPointRef & { part: 'center' | 'left' | 'right' };\n\t/** A spot on a stroke away from its width points (width tool). */\n\tstrokeSpot?: VectorStrokeSpot;\n\t/** A part of the gradient annotator (gradient tool). */\n\tgradient?: VectorGradientPart;\n\t/** A control point of the selected envelope's mesh (`index`), or a spot inside it (`index` null): Envelope tool. */\n\tenvelope?: { nodeId: string; index: number | null };\n}\n\n/** The drag in progress, for overlays and status text. */\nexport type VectorInteraction =\n\t| { type: 'marquee'; bounds: VectorBounds }\n\t| { type: 'move'; dx: number; dy: number }\n\t| { type: 'scale'; handle: VectorTransformHandle; sx: number; sy: number }\n\t| { type: 'rotate'; angle: number }\n\t| { type: 'anchors'; dx: number; dy: number }\n\t| { type: 'handle'; ref: VectorHandleRef }\n\t| { type: 'bend'; ref: VectorSegmentRef }\n\t| { type: 'corner'; ref: VectorAnchorRef; radius: number }\n\t| { type: 'draw'; tool: VectorTool; nodeId: string; bounds: VectorBounds; string?: [Vec2, Vec2] }\n\t| { type: 'stroke'; tool: 'knife' | 'eraser'; points: Vec2[]; width: number }\n\t| { type: 'builder'; mode: 'merge' | 'erase'; points: Vec2[] }\n\t| { type: 'width'; ref: VectorWidthPointRef; mode: 'width' | 'move' | 'copy'; width: number }\n\t| { type: 'pen'; nodeId: string }\n\t/** A gradient tool drag: drawing a new vector, or a handle, stop, midpoint or the bar (`removing`: a stop dragged off the bar). */\n\t| { type: 'gradient'; part: 'create' | VectorGradientPart['kind']; handle?: VectorGradientHandle; removing?: boolean }\n\t/** Free Distort: the selection box's corners as dragged so far (document space, clockwise from the top left). */\n\t| { type: 'distort'; mode: VectorDistortMode | 'skew'; corners: Vec2[] }\n\t/** Envelope tool: a mesh point (`index`) or the surface (`index` null) being dragged. */\n\t| { type: 'envelope'; nodeId: string; index: number | null }\n\t| { type: 'pan' };\n\n/** A snapshot of the editor. The same object is returned until something changes. */\nexport interface VectorEditorState {\n\tdocument: VectorDocument;\n\ttool: VectorTool;\n\t/** Selected node ids (top-level nodes or group members picked with the direct tool). */\n\tselection: readonly string[];\n\t/** Selected anchors (direct tool). */\n\tselectedAnchors: readonly VectorAnchorRef[];\n\t/** The selected width points (width tool, all on one path); Delete removes them. */\n\tselectedWidthPoints: readonly VectorWidthPointRef[];\n\t/** The width point open for numeric editing (double-click with the width tool), if any. */\n\twidthPointEdit: VectorWidthPointEdit | null;\n\t/** The path whose anchors are being edited, if any. */\n\teditingId: string | null;\n\t/** Axis-aligned bounds of the selected nodes in document units. */\n\tselectionBounds: VectorBounds | null;\n\t/** The selection box, rotated with the selection when its nodes share a rotation. */\n\tselectionFrame: VectorSelectionFrame | null;\n\tviewport: VectorViewport;\n\tdefaults: Readonly<Required<Pick<VectorDefaults, 'polygonSides' | 'starPoints' | 'starInnerRatio' | 'cornerRadius'>> & Pick<VectorDefaults, 'cornerKind'> & VectorStyle>;\n\tsnap: VectorSnapOptions;\n\tediting: VectorPathEditOptions;\n\t/** Smart guides of the snap in progress, in document units (null when there are none). */\n\tguides: VectorSnapGuides | null;\n\thitRadius: number;\n\thover: VectorHover | null;\n\t/** A CSS cursor for the canvas. */\n\tcursor: string;\n\tinteraction: VectorInteraction | null;\n\t/** The path the pen tool is adding to. */\n\tpen: { nodeId: string; subpath: number } | null;\n\t/** Last pointer position in document units (for the pen preview), or null when outside. */\n\tpointer: Vec2 | null;\n\t/** Shape Builder and paint bucket highlight: faces, line pieces and the gesture's trail. */\n\tbuilder: VectorBuilderPreview | null;\n\t/** The Blend tool's first click: the shape (and anchor of its first subpath) the next click blends from. */\n\tblendStart: VectorBlendStart | null;\n\t/** Options new blends get. */\n\tblendDefaults: VectorBlendOptions;\n\t/** The paint the gradient tool and gradient editor work on, and the selected stop (index into the sorted stops). */\n\t/** The paint the gradient tool and editor work on: `target`, and `layer` of its stack (null: the top layer, through the shorthand). */\n\tgradientEdit: { target: VectorGradientTarget; stop: number | null; layer: number | null };\n\t/**\n\t * Free Distort mode of the selection box: dragging a corner distorts in perspective (each corner\n\t * on its own; Shift+Alt+Mod: symmetric perspective), a side moves its two corners, and Mod-dragging\n\t * a side skews along it (Alt: about the centre).\n\t */\n\tfreeDistort: boolean;\n\tcanUndo: boolean;\n\tcanRedo: boolean;\n\tundoLabel: string;\n\tredoLabel: string;\n\t/** Bumped on every change. */\n\trevision: number;\n}\n\nexport type VectorActionResult = { ok: true } | { ok: false; message: string };\n\n/** What Make Clipping Mask / Make Opacity Mask return: the new group's id, or why not. */\nexport type VectorMaskResult = { ok: true; id: string } | { ok: false; message: string };\n\n/** The shape the Blend tool blends from (its first click). */\nexport interface VectorBlendStart {\n\tnodeId: string;\n\t/** The clicked anchor of its first subpath, or null for the shape as a whole. */\n\tanchor: number | null;\n}\n\n/** Blend settings and options of `VectorEditorController.blend`. */\nexport interface VectorBlendInput extends VectorBlendOptions {\n\t/** Nodes to blend (default: the selection), in stacking order unless `keepOrder`. */\n\tids?: readonly string[];\n\t/** Blend in the order given instead of stacking order. */\n\tkeepOrder?: boolean;\n\t/** Start anchor of each key's first subpath, by key id. */\n\tstarts?: Record<string, number>;\n\tname?: string;\n}\n\nconst BLEND_OPTION_KEYS = ['spacing', 'steps', 'distance', 'orientToSpine', 'linear'] as const;\n\n/** The blend options in `input`, valid ones only. */\nfunction blendOptionsOf(input: Partial<VectorBlendOptions>): VectorBlendOptions {\n\tconst out: VectorBlendOptions = {};\n\tif (input.spacing === 'steps' || input.spacing === 'distance' || input.spacing === 'smoothColor') out.spacing = input.spacing;\n\tif (typeof input.steps === 'number' && Number.isFinite(input.steps)) out.steps = Math.min(1000, Math.max(1, Math.round(input.steps)));\n\tif (typeof input.distance === 'number' && Number.isFinite(input.distance) && input.distance > 0) out.distance = input.distance;\n\tif (typeof input.orientToSpine === 'boolean') out.orientToSpine = input.orientToSpine;\n\tif (typeof input.linear === 'boolean') out.linear = input.linear;\n\treturn out;\n}\n\n/**\n * What the Shape Builder or paint bucket highlights, in document units: the faces (filled paths\n * as SVG path data) under the pointer or touched by the drag, the line pieces an erase would\n * delete, and the drag's trail. `mode` is `merge`, `erase` (Alt) or `fill` (paint bucket).\n */\nexport interface VectorBuilderPreview {\n\tmode: 'merge' | 'erase' | 'fill';\n\tfaces: BezierPath[];\n\tlines: BezierPath[];\n\ttrail: Vec2[] | null;\n}\n\n/** Result of a Shape Builder gesture. */\nexport interface VectorShapeBuilderResult {\n\tok: boolean;\n\tmessage?: string;\n\t/** Id of the merged shape (none when erasing). */\n\tmerged: string | null;\n\tfaces: number;\n\tlines: number;\n\t/** Outline edges erased (cut out of the paths they lie on). */\n\tedges: number;\n}\n\n/** A face of the Shape Builder map (document units). */\nexport interface VectorShapeBuilderFace {\n\tindex: number;\n\t/** SVG path data. */\n\td: string;\n\tarea: number;\n\t/** Ids of the paths whose fill covers it, bottom to top (empty for an area only outlines enclose). */\n\tsources: string[];\n\t/** A point inside it. */\n\tpoint: Vec2 | null;\n}\n\nexport interface AddVectorNodeOptions {\n\t/** Group to add into (default: the root). */\n\tparentId?: string;\n\t/** Position in the parent's list (default: on top). */\n\tindex?: number;\n\t/** Select the new node (default true). */\n\tselect?: boolean;\n\t/** Undo step label. */\n\tlabel?: string;\n}\n\n/** A shape description for `addShape` (and the agent command of the same name). */\nexport interface VectorShapeInput extends VectorStyle {\n\tkind: 'rect' | 'ellipse' | 'polygon' | 'star' | 'line';\n\tx: number;\n\ty: number;\n\twidth: number;\n\theight: number;\n\t/** Live corner radius of a rectangle, polygon or star (default: the editor default). */\n\tcornerRadius?: number;\n\tcornerKind?: VectorCornerKind;\n\tsides?: number;\n\tpoints?: number;\n\tinnerRatio?: number;\n\tname?: string;\n}\n\n/** Every field of `VectorStyle`, in the order style panels and commands list them. */\nexport const VECTOR_STYLE_KEYS = [\n\t'fill',\n\t'stroke',\n\t'strokeWidth',\n\t'strokeLinecap',\n\t'strokeLinejoin',\n\t'strokeDasharray',\n\t'fillRule',\n\t'strokeAlign',\n\t'startArrow',\n\t'endArrow',\n\t'arrowAlign',\n\t'startArrowScale',\n\t'endArrowScale',\n\t'strokeWidthProfile',\n\t'fills',\n\t'strokes'\n] as const satisfies readonly (keyof VectorStyle)[];\n\n/** Clipboard payload type written by `copy()`. */\nexport const VECTOR_CLIPBOARD_TYPE = 'pixerate/vector';\n\nconst DEFAULT_DEFAULTS = {\n\tfill: '#d4d4d8',\n\tstroke: '#18181b',\n\tstrokeWidth: 2,\n\tpolygonSides: 6,\n\tstarPoints: 5,\n\tstarInnerRatio: 0.5,\n\tcornerRadius: 0\n};\n\nconst MIN_ZOOM = 0.05;\nconst MAX_ZOOM = 64;\n/** Screen distance of the rotation handle above the selection box. */\nexport const VECTOR_ROTATE_HANDLE_OFFSET = 24;\n\n// ---------------------------------------------------------------------------\n// Helpers shared with the render module\n// ---------------------------------------------------------------------------\n\nconst sameRef = (a: VectorAnchorRef, b: VectorAnchorRef) => a.nodeId === b.nodeId && a.subpath === b.subpath && a.index === b.index;\n\n/** Ids of the path nodes whose anchors are shown (the edited path plus selected paths). */\nexport function getEditablePathIds(state: Pick<VectorEditorState, 'document' | 'selection' | 'editingId' | 'tool' | 'pen'>): string[] {\n\tif (state.tool !== 'direct' && state.tool !== 'pen') return [];\n\tconst ids = new Set<string>();\n\tif (state.pen) ids.add(state.pen.nodeId);\n\tif (state.tool === 'direct') {\n\t\tif (state.editingId) ids.add(state.editingId);\n\t\tfor (const id of state.selection) {\n\t\t\tconst node = findVectorNode(state.document.nodes, id);\n\t\t\tif (node && !node.locked) for (const p of collectPathNodes(node)) if (!p.locked) ids.add(p.id);\n\t\t}\n\t}\n\treturn [...ids].filter((id) => findVectorNode(state.document.nodes, id)?.type === 'path');\n}\n\n/** Every anchor drawn on the canvas. */\nexport function getVisibleAnchors(state: Pick<VectorEditorState, 'document' | 'selection' | 'editingId' | 'tool' | 'pen'>): VectorAnchorRef[] {\n\tconst out: VectorAnchorRef[] = [];\n\tfor (const id of getEditablePathIds(state)) {\n\t\tconst node = findVectorNode(state.document.nodes, id) as VectorPathNode;\n\t\tnode.subpaths.forEach((sp, s) => sp.anchors.forEach((_, i) => out.push({ nodeId: id, subpath: s, index: i })));\n\t}\n\treturn out;\n}\n\n/**\n * Handles drawn on the canvas: those of selected anchors and of the segments next to them\n * (direct tool), or of the last anchor of the pen path.\n */\nexport function getVisibleHandles(\n\tstate: Pick<VectorEditorState, 'document' | 'selection' | 'editingId' | 'tool' | 'pen' | 'selectedAnchors'>\n): VectorHandleRef[] {\n\tconst out: VectorHandleRef[] = [];\n\tconst seen = new Set<string>();\n\tconst push = (node: VectorPathNode, s: number, i: number, handle: 'in' | 'out') => {\n\t\tconst sp = node.subpaths[s];\n\t\tconst a = sp?.anchors[i];\n\t\tif (!a || !a[handle]) return;\n\t\tconst key = `${node.id}:${s}:${i}:${handle}`;\n\t\tif (seen.has(key)) return;\n\t\tseen.add(key);\n\t\tout.push({ nodeId: node.id, subpath: s, index: i, handle });\n\t};\n\tif (state.tool === 'pen' && state.pen) {\n\t\tconst node = findVectorNode(state.document.nodes, state.pen.nodeId);\n\t\tconst sp = node?.type === 'path' ? node.subpaths[state.pen.subpath] : undefined;\n\t\tif (node?.type === 'path' && sp) {\n\t\t\tconst last = sp.anchors.length - 1;\n\t\t\tpush(node, state.pen.subpath, last, 'in');\n\t\t\tpush(node, state.pen.subpath, last, 'out');\n\t\t}\n\t\treturn out;\n\t}\n\tif (state.tool !== 'direct') return out;\n\tconst editable = new Set(getEditablePathIds(state));\n\tfor (const ref of state.selectedAnchors) {\n\t\tif (!editable.has(ref.nodeId)) continue;\n\t\tconst node = findVectorNode(state.document.nodes, ref.nodeId) as VectorPathNode;\n\t\tconst sp = node.subpaths[ref.subpath];\n\t\tif (!sp) continue;\n\t\tconst n = sp.anchors.length;\n\t\tpush(node, ref.subpath, ref.index, 'in');\n\t\tpush(node, ref.subpath, ref.index, 'out');\n\t\tconst prev = ref.index > 0 ? ref.index - 1 : sp.closed ? n - 1 : -1;\n\t\tconst next = ref.index < n - 1 ? ref.index + 1 : sp.closed ? 0 : -1;\n\t\tif (prev >= 0) push(node, ref.subpath, prev, 'out');\n\t\tif (next >= 0) push(node, ref.subpath, next, 'in');\n\t}\n\treturn out;\n}\n\n/** Live corner widgets sit at least this far inside their corner (screen px along each side of a right angle). */\nexport const VECTOR_CORNER_WIDGET_INSET = 10;\n\n/** A live corner widget of the direct tool: drag it to round the corners, Alt-click to change their kind. */\nexport interface VectorCornerWidget {\n\tref: VectorAnchorRef;\n\t/** Document position of the widget. */\n\tx: number;\n\ty: number;\n\t/** The radius stored on the anchor and the radius drawn (no larger than fits). */\n\tradius: number;\n\tdrawnRadius: number;\n\tkind: VectorCornerKind;\n\t/** The corner in its node's own space. */\n\tcorner: VectorCornerInfo;\n}\n\n/**\n * The live corner widgets the direct tool shows: on each editable path, at its selected corner\n * anchors (or at every corner when none of its anchors is selected). Corners whose sides are\n * shorter than three insets on screen hide theirs. Dragging one sets the radius of all of them.\n * Ported from ArtCraft vectorcraft `crates/tools/src/corners.rs` (MIT OR Apache-2.0, see\n * THIRD_PARTY_NOTICES.md), as is the `corner` drag below.\n */\nexport function getVectorCornerWidgets(\n\tstate: Pick<VectorEditorState, 'document' | 'selection' | 'editingId' | 'tool' | 'pen' | 'selectedAnchors' | 'viewport'>\n): VectorCornerWidget[] {\n\tif (state.tool !== 'direct' || state.pen) return [];\n\tconst zoom = state.viewport.zoom;\n\tconst out: VectorCornerWidget[] = [];\n\tfor (const id of getEditablePathIds(state)) {\n\t\tconst node = findVectorNode(state.document.nodes, id) as VectorPathNode;\n\t\tconst picked = state.selectedAnchors.filter((r) => r.nodeId === id);\n\t\tconst m = vectorWorldMatrix(state.document.nodes, id) ?? IDENTITY_MATRIX;\n\t\tfor (const c of vectorCorners(node.subpaths)) {\n\t\t\tif (picked.length && !picked.some((r) => r.subpath === c.subpath && r.index === c.index)) continue;\n\t\t\tconst side = Math.min(c.lu, c.lv);\n\t\t\tif (side * zoom < 3 * VECTOR_CORNER_WIDGET_INSET) continue;\n\t\t\tconst a = node.subpaths[c.subpath].anchors[c.index];\n\t\t\tconst radius = a.corner?.radius ?? 0;\n\t\t\tconst drawn = fitCornerRadius(c, radius);\n\t\t\t// On the bisector at the centre of the arc as drawn, at least the inset in and at most half the shorter side.\n\t\t\tconst k = Math.min(Math.max(drawn / c.sin, VECTOR_CORNER_WIDGET_INSET / (zoom * Math.sqrt(1 + c.cos))), side / 2);\n\t\t\tconst p = applyMatrix(m, cornerBisectorPoint(c, k));\n\t\t\tout.push({ ref: { nodeId: id, subpath: c.subpath, index: c.index }, x: p.x, y: p.y, radius, drawnRadius: drawn, kind: a.corner?.kind ?? 'round', corner: c });\n\t\t}\n\t}\n\treturn out;\n}\n\n/**\n * The line a path's stroke follows (node space; shortened under `tip` arrowheads) and the\n * distance a side scale of 1 reaches from it (half the drawn width: twice the stroke width for\n * inside / outside strokes). Null for paths without a visible stroke.\n */\nexport function vectorStrokeWidthGeometry(node: VectorPathNode): { line: BezierPath; unit: number } | null {\n\tif (!hasPaint(node.stroke) || !((node.strokeWidth ?? 1) > 0)) return null;\n\tconst parts = vectorStrokeParts(subpathsToBezier(node.subpaths), node);\n\treturn parts.line.length ? { line: parts.line, unit: parts.drawWidth / 2 } : null;\n}\n\n/** A width point of the width tool, in document units. */\nexport interface VectorWidthHandle {\n\tref: VectorWidthPointRef;\n\t/** The point on the path. */\n\tx: number;\n\ty: number;\n\t/** Ends of its left and right sides. */\n\tleft: Vec2;\n\tright: Vec2;\n\tt: number;\n\t/** Side scales (multiples of half the stroke width). */\n\tleftScale: number;\n\trightScale: number;\n\tselected: boolean;\n}\n\n/**\n * Moves the points at `indices` of `profile` by `dt` along the path (kept in 0–1) and re-sorts\n * it (stably, so steps keep their order); `renumber` maps each old index to its new one.\n */\nfunction shiftWidthPoints(profile: readonly VectorWidthPoint[], indices: readonly number[], dt: number): { points: VectorWidthPoint[]; renumber: Map<number, number> } {\n\tconst moving = new Set(indices);\n\tconst tagged = profile.map((p, i) => ({ p: moving.has(i) ? { ...p, t: Math.min(Math.max(p.t + dt, 0), 1) } : { ...p }, i }));\n\ttagged.sort((a, b) => a.p.t - b.p.t || a.i - b.i);\n\treturn { points: tagged.map((x) => x.p), renumber: new Map(tagged.map((x, k) => [x.i, k])) };\n}\n\n/**\n * The width points the width tool shows: every profile point on every subpath of the stroke\n * under the pointer, the selected paths and the stroke being edited. Ported from ArtCraft\n * vectorcraft `crates/tools/src/distort/width.rs` (MIT OR Apache-2.0, see\n * THIRD_PARTY_NOTICES.md), as are the `width` drag and hover below.\n */\nexport function getVectorWidthHandles(\n\tstate: Pick<VectorEditorState, 'document' | 'selection' | 'tool' | 'hover' | 'interaction' | 'selectedWidthPoints'>\n): VectorWidthHandle[] {\n\tif (state.tool !== 'width') return [];\n\tconst ids = new Set<string>();\n\tfor (const id of state.selection) {\n\t\tconst n = findVectorNode(state.document.nodes, id);\n\t\tif (n) for (const p of collectPathNodes(n)) ids.add(p.id);\n\t}\n\tif (state.hover?.nodeId) ids.add(state.hover.nodeId);\n\tif (state.interaction?.type === 'width') ids.add(state.interaction.ref.nodeId);\n\tfor (const r of state.selectedWidthPoints) ids.add(r.nodeId);\n\tconst out: VectorWidthHandle[] = [];\n\tfor (const id of ids) {\n\t\tconst node = findVectorNode(state.document.nodes, id);\n\t\tif (node?.type !== 'path' || node.visible === false || !node.strokeWidthProfile?.length) continue;\n\t\tconst g = vectorStrokeWidthGeometry(node);\n\t\tif (!g) continue;\n\t\tconst profile = sanitizeWidthProfile(node.strokeWidthProfile);\n\t\tconst m = vectorWorldMatrix(state.document.nodes, id) ?? IDENTITY_MATRIX;\n\t\tconst contours = node.subpaths.length;\n\t\tfor (let sp = 0; sp < contours; sp++) {\n\t\t\tprofile.forEach((w, index) => {\n\t\t\t\tconst at = strokePositionPoint(g.line, sp, w.t);\n\t\t\t\tif (!at) return;\n\t\t\t\tconst n = { x: at.tangent[1], y: -at.tangent[0] };\n\t\t\t\tconst c = { x: at.point[0], y: at.point[1] };\n\t\t\t\tconst p = applyMatrix(m, c);\n\t\t\t\tconst l = applyMatrix(m, { x: c.x + n.x * w.left * g.unit, y: c.y + n.y * w.left * g.unit });\n\t\t\t\tconst r = applyMatrix(m, { x: c.x - n.x * w.right * g.unit, y: c.y - n.y * w.right * g.unit });\n\t\t\t\tout.push({\n\t\t\t\t\tref: { nodeId: id, index, subpath: sp },\n\t\t\t\t\tx: p.x,\n\t\t\t\t\ty: p.y,\n\t\t\t\t\tleft: l,\n\t\t\t\t\tright: r,\n\t\t\t\t\tt: w.t,\n\t\t\t\t\tleftScale: w.left,\n\t\t\t\t\trightScale: w.right,\n\t\t\t\t\tselected: state.selectedWidthPoints.some((r) => r.nodeId === id && r.index === index)\n\t\t\t\t});\n\t\t\t});\n\t\t}\n\t}\n\treturn out;\n}\n\n/** Looks up an anchor (positions in its node's own space). */\nexport function getAnchor(doc: VectorDocument, ref: VectorAnchorRef): VectorAnchor | undefined {\n\tconst node = findVectorNode(doc.nodes, ref.nodeId);\n\treturn node?.type === 'path' ? node.subpaths[ref.subpath]?.anchors[ref.index] : undefined;\n}\n\n/** An anchor with its position and handles in document space (its node's transforms applied). */\nexport function getAnchorWorld(doc: VectorDocument, ref: VectorAnchorRef): { x: number; y: number; in?: Vec2; out?: Vec2; type?: VectorAnchorType } | undefined {\n\tconst a = getAnchor(doc, ref);\n\tif (!a) return undefined;\n\tconst m = vectorWorldMatrix(doc.nodes, ref.nodeId) ?? IDENTITY_MATRIX;\n\tconst p = applyMatrix(m, a);\n\treturn { x: p.x, y: p.y, ...(a.in ? { in: applyMatrix(m, a.in) } : {}), ...(a.out ? { out: applyMatrix(m, a.out) } : {}), ...(a.type ? { type: a.type } : {}) };\n}\n\n/**\n * The selection box: `angle` is the shared rotation of the selected nodes (0 when they\n * differ), `x / y / width / height` the bounds in the box's own frame (document space\n * rotated by −angle). A frame point `q` is at `rotateMatrix(angle)` applied to `q`.\n */\nexport interface VectorSelectionFrame {\n\tx: number;\n\ty: number;\n\twidth: number;\n\theight: number;\n\tangle: number;\n}\n\n/** The selection frame of nodes `ids` (see `VectorSelectionFrame`), or null when they have no points. */\nexport function getVectorSelectionFrame(doc: VectorDocument, ids: readonly string[]): VectorSelectionFrame | null {\n\tconst nodes = ids.map((id) => ({ node: findVectorNode(doc.nodes, id), parent: vectorParentMatrix(doc.nodes, id) ?? IDENTITY_MATRIX })).filter((n) => n.node);\n\tif (!nodes.length) return null;\n\tconst angles = nodes.map((n) => matrixRotation(multiplyMatrices(nodeMatrix(n.node!), n.parent)));\n\tconst same = angles.every((a) => Math.abs(normalizeAngle(a - angles[0])) < 1e-6);\n\tconst angle = same ? angles[0] : 0;\n\tconst unrotate = rotateMatrix(-angle);\n\tconst b = unionBounds(nodes.map((n) => vectorNodeBounds(n.node!, multiplyMatrices(n.parent, unrotate))));\n\treturn b ? { ...b, angle } : null;\n}\n\nconst normalizeAngle = (a: number) => ((((a + 180) % 360) + 360) % 360) - 180;\n\n/** Screen positions of the selection box handles for a box given in screen pixels. */\nexport function getTransformHandlePositions(box: VectorBounds): { handle: VectorTransformHandle; x: number; y: number }[] {\n\tconst { x, y, width: w, height: h } = box;\n\treturn [\n\t\t{ handle: 'nw', x, y },\n\t\t{ handle: 'n', x: x + w / 2, y },\n\t\t{ handle: 'ne', x: x + w, y },\n\t\t{ handle: 'e', x: x + w, y: y + h / 2 },\n\t\t{ handle: 'se', x: x + w, y: y + h },\n\t\t{ handle: 's', x: x + w / 2, y: y + h },\n\t\t{ handle: 'sw', x, y: y + h },\n\t\t{ handle: 'w', x, y: y + h / 2 },\n\t\t{ handle: 'rotate', x: x + w / 2, y: y - VECTOR_ROTATE_HANDLE_OFFSET }\n\t];\n}\n\n/**\n * Screen geometry of a selection box. `box` and each handle's `x / y` are the unrotated box\n * in screen pixels; draw them inside a group with `transform` (a `rotate(angle cx cy)`),\n * or use `screen` for the true on-screen positions (hit-testing).\n */\nexport interface VectorSelectionBoxGeometry {\n\tbox: VectorBounds;\n\tangle: number;\n\ttransform: string;\n\thandles: { handle: VectorTransformHandle; x: number; y: number; screen: Vec2 }[];\n\t/** The four corners on screen, clockwise from the top left. */\n\tcorners: Vec2[];\n}\n\nexport function getVectorSelectionBoxGeometry(frame: VectorSelectionFrame, viewport: VectorViewport): VectorSelectionBoxGeometry {\n\tconst z = viewport.zoom;\n\tconst box = { x: frame.x * z + viewport.x, y: frame.y * z + viewport.y, width: frame.width * z, height: frame.height * z };\n\tconst rot = rotateMatrix(frame.angle, { x: viewport.x, y: viewport.y });\n\tconst toScreen = (p: Vec2): Vec2 => (frame.angle ? applyMatrix(rot, p) : { x: p.x, y: p.y });\n\tconst handles = getTransformHandlePositions(box).map((h) => ({ ...h, screen: toScreen(h) }));\n\tconst corners = [\n\t\t{ x: box.x, y: box.y },\n\t\t{ x: box.x + box.width, y: box.y },\n\t\t{ x: box.x + box.width, y: box.y + box.height },\n\t\t{ x: box.x, y: box.y + box.height }\n\t].map(toScreen);\n\tconst r = (n: number) => Math.round(n * 1000) / 1000;\n\treturn { box, angle: frame.angle, transform: frame.angle ? `rotate(${r(frame.angle)} ${r(viewport.x)} ${r(viewport.y)})` : '', handles, corners };\n}\n\n/**\n * The node the Envelope tool edits: the one selected node with an enabled envelope, with the\n * envelope's mesh and box (node space) and the node → document matrix. Null otherwise.\n */\nexport function getVectorEnvelopeTarget(\n\tdoc: VectorDocument,\n\tselection: readonly string[]\n): { node: VectorNode; frame: { effectId: string; bounds: VectorBounds; mesh: VectorWarpMesh }; world: AffineMatrix } | null {\n\tif (selection.length !== 1) return null;\n\tconst node = findVectorNode(doc.nodes, selection[0]);\n\tif (!node || !vectorNodeEnvelope(node)) return null;\n\tconst frame = vectorEnvelopeFrame(node);\n\treturn frame ? { node, frame, world: vectorWorldMatrix(doc.nodes, node.id) ?? IDENTITY_MATRIX } : null;\n}\n\nconst RESIZE_CURSOR_ORDER = ['ew-resize', 'nwse-resize', 'ns-resize', 'nesw-resize'];\nconst HANDLE_ANGLES: Partial<Record<VectorTransformHandle, number>> = { e: 0, se: 45, s: 90, sw: 135, w: 180, nw: 225, n: 270, ne: 315 };\n\n/** The resize cursor for a handle of a box rotated by `angle` degrees. */\nexport function vectorResizeCursor(handle: VectorTransformHandle, angle = 0): string {\n\tif (handle === 'rotate') return 'grab';\n\tconst a = (((HANDLE_ANGLES[handle]! + angle) % 180) + 180) % 180;\n\treturn RESIZE_CURSOR_ORDER[Math.round(a / 45) % 4];\n}\n\n\nconst TOOL_LABELS: Record<VectorTool, string> = {\n\tselect: 'Select',\n\tdirect: 'Direct Select',\n\tpen: 'Pen',\n\tpencil: 'Pencil',\n\trect: 'Rectangle',\n\tellipse: 'Ellipse',\n\tpolygon: 'Polygon',\n\tstar: 'Star',\n\tline: 'Line',\n\tscissors: 'Scissors',\n\tknife: 'Knife',\n\teraser: 'Eraser',\n\twidth: 'Width',\n\tshapeBuilder: 'Shape Builder',\n\tpaintBucket: 'Paint Bucket',\n\tblend: 'Blend',\n\tgradient: 'Gradient',\n\tenvelope: 'Envelope Mesh'\n};\n\nconst TOOL_KEYS: Record<string, VectorTool> = {\n\tv: 'select',\n\ta: 'direct',\n\tp: 'pen',\n\tn: 'pencil',\n\tr: 'rect',\n\to: 'ellipse',\n\ty: 'polygon',\n\ts: 'star',\n\tl: 'line',\n\tc: 'scissors',\n\tk: 'knife',\n\te: 'eraser',\n\tw: 'width',\n\tm: 'shapeBuilder',\n\tb: 'paintBucket',\n\tj: 'blend',\n\tg: 'gradient',\n\tu: 'envelope'\n};\n\nexport function vectorToolLabel(tool: VectorTool): string {\n\treturn TOOL_LABELS[tool];\n}\n\n/** Keyboard shortcut of each tool (single letters, no modifiers). */\nexport const VECTOR_TOOL_SHORTCUTS: Readonly<Record<VectorTool, string>> = {\n\tselect: 'V',\n\tdirect: 'A',\n\tpen: 'P',\n\tpencil: 'N',\n\trect: 'R',\n\tellipse: 'O',\n\tpolygon: 'Y',\n\tstar: 'S',\n\tline: 'L',\n\tscissors: 'C',\n\tknife: 'K',\n\teraser: 'E',\n\twidth: 'W',\n\tshapeBuilder: 'M',\n\tpaintBucket: 'B',\n\tblend: 'J',\n\tgradient: 'G',\n\tenvelope: 'U'\n};\n\n/** Short glyphs for tool buttons (pair them with `vectorToolLabel` for the accessible name). */\nexport const VECTOR_TOOL_GLYPHS: Readonly<Record<VectorTool, string>> = {\n\tselect: '↖',\n\tdirect: '⌖',\n\tpen: '✒',\n\tpencil: '✎',\n\trect: '▭',\n\tellipse: '◯',\n\tpolygon: '⬡',\n\tstar: '☆',\n\tline: '╱',\n\tscissors: '✂',\n\tknife: '⟋',\n\teraser: '⌫',\n\twidth: '⇹',\n\tshapeBuilder: '⊕',\n\tpaintBucket: '◍',\n\tblend: '⧓',\n\tgradient: '◐',\n\tenvelope: '⌗'\n};\n\n/** Tools that drag out a shape (they snap while hovering). */\nconst SHAPE_TOOLS: ReadonlySet<VectorTool> = new Set(['rect', 'ellipse', 'polygon', 'star', 'line']);\n\nconst BOOLEAN_OPS: Record<VectorBooleanOperation, 'union' | 'difference' | 'intersection' | 'xor'> = {\n\tunion: 'union',\n\tsubtract: 'difference',\n\tintersect: 'intersection',\n\texclude: 'xor'\n};\n\nconst BOOLEAN_LABELS: Record<VectorBooleanOperation, string> = { union: 'Union', subtract: 'Subtract', intersect: 'Intersect', exclude: 'Exclude' };\n\n/** Names of the Pathfinder operations (undo steps, groups and buttons). */\nexport const VECTOR_PATHFINDER_LABELS: Readonly<Record<VectorPathfinderOperation, string>> = {\n\tdivide: 'Divide',\n\ttrim: 'Trim',\n\tmerge: 'Merge',\n\tcrop: 'Crop',\n\toutline: 'Outline',\n\tminusBack: 'Minus Back'\n};\n\n/** Replaces nodes by id anywhere in the tree (structural sharing for untouched branches). */\n/**\n * Every path with its fill and stroke stacks reconciled (see `normalizeVectorPaintStacks`): the\n * same array, and the same node objects, where nothing changes.\n */\nfunction normalizeVectorTree(nodes: readonly VectorNode[], prefer: 'stack' | 'shorthand'): VectorNode[] {\n\tlet changed = false;\n\tconst out = nodes.map((n) => {\n\t\tlet next: VectorNode = n.type === 'path' ? normalizeVectorPaintStacks(n, prefer) : n;\n\t\tif (next.type === 'group') {\n\t\t\tconst children = normalizeVectorTree(next.children, prefer);\n\t\t\tif (children !== next.children) next = { ...next, children };\n\t\t}\n\t\tif (next !== n) changed = true;\n\t\treturn next;\n\t});\n\treturn changed ? out : (nodes as VectorNode[]);\n}\n\n/** A document as loaded: a copy, stacks preferred over stale shorthands. */\nfunction loadedDocument(doc: VectorDocument): VectorDocument {\n\tconst copy = cloneVectorDocument(doc);\n\tconst nodes = normalizeVectorTree(copy.nodes, 'stack');\n\treturn nodes === copy.nodes ? copy : { ...copy, nodes };\n}\n\nfunction mapTree(nodes: readonly VectorNode[], fn: (node: VectorNode) => VectorNode | null): VectorNode[] {\n\tlet changed = false;\n\tconst out: VectorNode[] = [];\n\tfor (const node of nodes) {\n\t\tlet next = fn(node);\n\t\tif (next && next.type === 'group') {\n\t\t\tconst children = mapTree(next.children, fn);\n\t\t\tif (children !== next.children) next = { ...next, children };\n\t\t}\n\t\tif (next !== node) changed = true;\n\t\tif (next) out.push(next);\n\t}\n\treturn changed ? out : (nodes as VectorNode[]);\n}\n\n/** Replaces node `id` anywhere in the tree with `replacements` (none removes it). */\nfunction spliceTree(nodes: readonly VectorNode[], id: string, replacements: readonly VectorNode[]): VectorNode[] {\n\tlet changed = false;\n\tconst out: VectorNode[] = [];\n\tfor (const node of nodes) {\n\t\tif (node.id === id) {\n\t\t\tchanged = true;\n\t\t\tout.push(...replacements);\n\t\t\tcontinue;\n\t\t}\n\t\tif (node.type === 'group') {\n\t\t\tconst children = spliceTree(node.children, id, replacements);\n\t\t\tif (children !== node.children) {\n\t\t\t\tchanged = true;\n\t\t\t\tout.push({ ...node, children });\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tout.push(node);\n\t}\n\treturn changed ? out : (nodes as VectorNode[]);\n}\n\nconst toPoint = (p: Vec2): Point => [p.x, p.y];\n\n/** A node's place and state (transform, visibility, lock, operand operation), for a node taking its place. */\nconst placementOf = (n: VectorNode) => ({\n\t...(n.transform ? { transform: n.transform } : {}),\n\t...(n.visible === false ? { visible: false } : {}),\n\t...(n.locked ? { locked: true } : {}),\n\t...(n.booleanOperation ? { booleanOperation: n.booleanOperation } : {})\n});\n\nconst isRoot = (m: AffineMatrix) => m === IDENTITY_MATRIX || (m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0);\n\nlet idCounter = 0;\nconst defaultCreateId = () => `vn-${Date.now().toString(36)}${(++idCounter).toString(36)}`;\n\n// ---------------------------------------------------------------------------\n// Controller\n// ---------------------------------------------------------------------------\n\ninterface Snapshot {\n\tdoc: VectorDocument;\n\tselection: string[];\n\tanchors: VectorAnchorRef[];\n\teditingId: string | null;\n\tpen: { nodeId: string; subpath: number } | null;\n}\n\n/** Snap targets of a drag, gathered on its first move that needs them. */\ntype DragTargets = { targets?: VectorSnapTargets };\n\ntype Drag =\n\t| ({ kind: 'move'; start: Vec2; origin: VectorBounds | null; before: Snapshot; base: VectorDocument; moved: boolean; duplicated: boolean; clickId: string | null } & DragTargets)\n\t| ({ kind: 'scale'; handle: VectorTransformHandle; start: Vec2; frame: VectorSelectionFrame; before: Snapshot; base: VectorDocument; op?: string } & DragTargets)\n\t| {\n\t\t\tkind: 'envelope';\n\t\t\tnodeId: string;\n\t\t\teffectId: string;\n\t\t\t/** The mesh at the start (node space) and the document it belongs to. */\n\t\t\tbase: VectorWarpMesh;\n\t\t\tbaseDoc: VectorDocument;\n\t\t\tbefore: Snapshot;\n\t\t\tindex: number | null;\n\t\t\ts: number;\n\t\t\tt: number;\n\t\t\tstart: Vec2;\n\t\t\tmoved: boolean;\n\t }\n\t| { kind: 'rotate'; start: Vec2; center: Vec2; before: Snapshot; base: VectorDocument }\n\t| { kind: 'marquee'; start: Vec2; base: string[]; baseAnchors: VectorAnchorRef[]; anchorsMode: boolean }\n\t| ({ kind: 'anchors'; start: Vec2; before: Snapshot; base: VectorDocument; moved: boolean; clickRef: VectorAnchorRef | null; lead: VectorAnchorRef | null } & DragTargets)\n\t| ({ kind: 'handle'; ref: VectorHandleRef; before: Snapshot; base: VectorDocument } & DragTargets)\n\t| { kind: 'bend'; ref: VectorSegmentRef; t: number; start: Vec2; before: Snapshot; base: VectorDocument; moved: boolean }\n\t| { kind: 'corner'; widget: VectorCornerWidget; refs: VectorAnchorRef[]; start: Vec2; before: Snapshot; base: VectorDocument; moved: boolean; alt: boolean; shift: boolean }\n\t| { kind: 'stroke'; tool: 'knife' | 'eraser'; points: Vec2[]; before: Snapshot }\n\t| { kind: 'builder'; erase: boolean; points: Vec2[]; touched: VectorBuilderTouched; map: VectorShapeBuilderMap }\n\t| { kind: 'pull'; ref: VectorAnchorRef; before: Snapshot; base: VectorDocument; label: string; outOnly: boolean; closing: boolean; altBreaks: boolean }\n\t| ({ kind: 'shape'; tool: VectorTool; start: Vec2; nodeId: string; before: Snapshot } & DragTargets)\n\t| { kind: 'pencil'; nodeId: string; points: Point[]; before: Snapshot; lazy: LazyMouse; pressures: number[] | null; pressure: number }\n\t| {\n\t\t\tkind: 'width';\n\t\t\tref: VectorWidthPointRef;\n\t\t\t/** `copy`: Alt-drag a centre to slide a copy of the point. */\n\t\t\tmode: 'width' | 'move' | 'copy';\n\t\t\tside: 'both' | 'left' | 'right';\n\t\t\t/** The index the drag started on (the source of a copy). */\n\t\t\tsource: number;\n\t\t\t/** Other selected points of the path that move or widen with it (indices into `profile`). */\n\t\t\tgroup: number[];\n\t\t\t/** Shift-click (adding the point): a click without a move keeps the other selected points. */\n\t\t\tshift: boolean;\n\t\t\t/** Old index → new index after a group move re-sorted the profile. */\n\t\t\trenumber: Map<number, number> | null;\n\t\t\t/** The stroke line's point and left normal where the drag started (node space). */\n\t\t\torigin: Vec2;\n\t\t\tnormal: Vec2;\n\t\t\t/** Half the drawn stroke width: side scale 1 reaches this far. */\n\t\t\tunit: number;\n\t\t\tprofile: VectorWidthPoint[];\n\t\t\tbefore: Snapshot;\n\t\t\tbase: VectorDocument;\n\t\t\tstart: Vec2;\n\t\t\tmoved: boolean;\n\t\t\tcreated: boolean;\n\t }\n\t| {\n\t\t\tkind: 'gradient';\n\t\t\t/** What was grabbed: null draws a new vector over `targets`. */\n\t\t\tpart: VectorGradientPart | null;\n\t\t\tnodeId: string | null;\n\t\t\ttargets: string[];\n\t\t\tannotator: VectorGradientAnnotator | null;\n\t\t\tstart: Vec2;\n\t\t\tstartDoc: Vec2;\n\t\t\tbefore: Snapshot;\n\t\t\tbase: VectorDocument;\n\t\t\tmoved: boolean;\n\t\t\t/** The dragged stop's index now (null once dragged off the bar). */\n\t\t\tstop: number | null;\n\t }\n\t| { kind: 'pan'; start: Vec2; viewport: VectorViewport };\n\n/**\n * Headless state for a vector editor: a document of paths and groups, select / direct /\n * pen / pencil / shape tools driven by pointer and key events, undo, transforms, boolean\n * operations, alignment, grouping, SVG import / export and a viewport. Any UI forwards\n * pointer events (in canvas pixels) and renders `getState()`; `getState()` returns the same\n * object until something changes, so it works with `useSyncExternalStore` and Svelte runes.\n */\nexport class VectorEditorController {\n\treadonly history: HistoryManager;\n\tprivate doc: VectorDocument;\n\tprivate selection: string[] = [];\n\tprivate anchors: VectorAnchorRef[] = [];\n\tprivate widthPoints: VectorWidthPointRef[] = [];\n\tprivate widthEdit: VectorWidthPointEdit | null = null;\n\tprivate editingId: string | null = null;\n\tprivate pen: { nodeId: string; subpath: number } | null = null;\n\tprivate tool: VectorTool;\n\tprivate defaults: VectorEditorState['defaults'];\n\tprivate viewport: VectorViewport;\n\tprivate snap: VectorSnapOptions;\n\tprivate editing: VectorPathEditOptions;\n\tprivate hitRadius: number;\n\tprivate hover: VectorHover | null = null;\n\tprivate pointer: Vec2 | null = null;\n\tprivate drag: Drag | null = null;\n\tprivate interaction: VectorInteraction | null = null;\n\tprivate guides: VectorSnapGuides | null = null;\n\t/** Whether the last `snapDrawn` landed on an object (guides may be hidden). */\n\tprivate snappedToObject = false;\n\t/** Drawing targets for hovering the pen and shape tools, kept while the document and the excluded path stay the same. */\n\tprivate hoverTargets: { doc: VectorDocument; exclude: string; targets: VectorSnapTargets } | null = null;\n\tprivate spaceHeld = false;\n\tprivate clipboard: VectorNode[] | null = null;\n\t/** Shape Builder map of the selection, kept while the document and the selection stay the same. */\n\tprivate builderCache: { doc: VectorDocument; key: string; map: VectorShapeBuilderMap | null } | null = null;\n\t/** Paint bucket map of the visible outlines. */\n\tprivate fillCache: { doc: VectorDocument; gap: number; map: PlanarMap | null } | null = null;\n\tprivate builderPreview: VectorBuilderPreview | null = null;\n\tprivate blendStart: VectorBlendStart | null = null;\n\t/** Options new blends get (the toolbars edit them along with the selected blend's). */\n\tprivate blendDefaults: VectorBlendOptions = {};\n\tprivate gradientEdit: VectorEditorState['gradientEdit'] = { target: 'fill', stop: null, layer: null };\n\tprivate freeDistort = false;\n\t/** What the highlight shows, to skip notifying when the pointer stays over the same face. */\n\tprivate builderHoverKey: string | null = null;\n\tprivate readonly createId: () => string;\n\tprivate readonly onChange?: (document: VectorDocument) => void;\n\tprivate readonly listeners = new Set<(state: VectorEditorState) => void>();\n\tprivate readonly cleanups: Array<() => void> = [];\n\tprivate applying = false;\n\t/** Set while a commit records a change that was already applied, so `restore` still reports it. */\n\tprivate committing = false;\n\tprivate revision = 0;\n\tprivate state!: VectorEditorState;\n\n\tconstructor(options: VectorEditorOptions = {}) {\n\t\tthis.history = options.history ?? new HistoryManager();\n\t\tthis.doc = options.document ? loadedDocument(options.document) : { width: 800, height: 600, nodes: [] };\n\t\tthis.doc = this.refreshed(this.doc);\n\t\tthis.tool = options.tool ?? 'select';\n\t\tthis.defaults = { ...DEFAULT_DEFAULTS, ...(options.defaults ?? {}) } as VectorEditorState['defaults'];\n\t\tthis.viewport = { x: 0, y: 0, zoom: 1, ...(options.viewport ?? {}) };\n\t\tthis.snap = { ...DEFAULT_VECTOR_SNAP, ...(options.snap ?? {}) };\n\t\tthis.editing = { ...DEFAULT_VECTOR_PATH_EDIT, ...(options.editing ?? {}) };\n\t\tthis.hitRadius = options.hitRadius ?? 6;\n\t\tthis.createId = options.createId ?? defaultCreateId;\n\t\tthis.onChange = options.onChange;\n\t\tthis.freeDistort = !!options.freeDistort;\n\t\tthis.state = this.computeState();\n\t\tthis.connect();\n\t}\n\n\t/** Listens to the history (once; again after `destroy`). */\n\tprivate connect(): void {\n\t\tif (this.cleanups.length) return;\n\t\tthis.cleanups.push(\n\t\t\tthis.history.subscribe((h) => {\n\t\t\t\tif (!this.applying && this.historyDiffers(h)) this.notify();\n\t\t\t})\n\t\t);\n\t\t// Catch up on history changes made while disconnected.\n\t\tif (this.historyDiffers(this.history.getState())) this.state = this.computeState();\n\t}\n\n\tprivate historyDiffers(h: ReturnType<HistoryManager['getState']>): boolean {\n\t\tconst s = this.state;\n\t\treturn s.canUndo !== h.canUndo || s.canRedo !== h.canRedo || s.undoLabel !== h.undoName || s.redoLabel !== h.redoName;\n\t}\n\n\t/**\n\t * Stops listening to the history and drops all listeners. A later `subscribe` reconnects, so a\n\t * controller torn down by React StrictMode's double effect keeps working when it is reused.\n\t */\n\tdestroy(): void {\n\t\tthis.cleanups.splice(0).forEach((fn) => fn());\n\t\tthis.listeners.clear();\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// State\n\t// -------------------------------------------------------------------------\n\n\t/** The current snapshot (stable until the next change). */\n\tgetState = (): VectorEditorState => this.state;\n\n\t/** Calls `listener` after every change. Returns an unsubscribe function. */\n\tsubscribe = (listener: (state: VectorEditorState) => void): (() => void) => {\n\t\tthis.connect();\n\t\tthis.listeners.add(listener);\n\t\treturn () => {\n\t\t\tthis.listeners.delete(listener);\n\t\t};\n\t};\n\n\tgetDocument(): VectorDocument {\n\t\treturn this.doc;\n\t}\n\n\tgetNode(id: string): VectorNode | undefined {\n\t\treturn findVectorNode(this.doc.nodes, id);\n\t}\n\n\tprivate computeState(): VectorEditorState {\n\t\tconst h = this.history.getState();\n\t\treturn {\n\t\t\tdocument: this.doc,\n\t\t\ttool: this.tool,\n\t\t\tselection: this.selection,\n\t\t\tselectedAnchors: this.anchors,\n\t\t\tselectedWidthPoints: this.widthPoints,\n\t\t\twidthPointEdit: this.widthEdit,\n\t\t\teditingId: this.editingId,\n\t\t\tselectionBounds: this.boundsOf(this.selection),\n\t\t\tselectionFrame: this.frameOf(this.selectedRoots()),\n\t\t\tviewport: this.viewport,\n\t\t\tdefaults: this.defaults,\n\t\t\tsnap: this.snap,\n\t\t\tediting: this.editing,\n\t\t\tguides: this.guides,\n\t\t\thitRadius: this.hitRadius,\n\t\t\thover: this.hover,\n\t\t\tcursor: this.cursorFor(),\n\t\t\tinteraction: this.interaction,\n\t\t\tpen: this.pen,\n\t\t\tpointer: this.pointer,\n\t\t\tbuilder: this.builderPreview,\n\t\t\tblendStart: this.blendStart,\n\t\t\tblendDefaults: this.blendDefaults,\n\t\t\tgradientEdit: this.gradientEdit,\n\t\t\tfreeDistort: this.freeDistort,\n\t\t\tcanUndo: h.canUndo,\n\t\t\tcanRedo: h.canRedo,\n\t\t\tundoLabel: h.undoName,\n\t\t\tredoLabel: h.redoName,\n\t\t\trevision: this.revision\n\t\t};\n\t}\n\n\t/** `doc` with its blends' steps brought up to date (the same object when they already are). */\n\tprivate refreshed(doc: VectorDocument): VectorDocument {\n\t\t// Shorthand fills and strokes written by any edit go to the top layer of their stack.\n\t\tconst nodes = refreshVectorBlends(normalizeVectorTree(doc.nodes, 'shorthand'));\n\t\treturn nodes === doc.nodes ? doc : { ...doc, nodes };\n\t}\n\n\tprivate notify(docChanged = false) {\n\t\t// Blends regenerate their steps after every edit of a key (drags included).\n\t\tconst fresh = this.refreshed(this.doc);\n\t\tif (fresh !== this.doc) this.doc = fresh;\n\t\tif (this.blendStart && !this.getNode(this.blendStart.nodeId)) this.blendStart = null;\n\t\tthis.revision++;\n\t\tthis.state = this.computeState();\n\t\tif (docChanged) this.onChange?.(this.doc);\n\t\tconst state = this.state;\n\t\tthis.listeners.forEach((listener) => {\n\t\t\ttry {\n\t\t\t\tlistener(state);\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('[VectorEditorController] Error in state listener:', error);\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate boundsOf(ids: readonly string[]): VectorBounds | null {\n\t\treturn unionBounds(\n\t\t\tids.map((id) => {\n\t\t\t\tconst n = this.getNode(id);\n\t\t\t\treturn n ? vectorNodeBounds(n, vectorParentMatrix(this.doc.nodes, id) ?? IDENTITY_MATRIX) : null;\n\t\t\t})\n\t\t);\n\t}\n\n\tprivate frameOf(ids: readonly string[]): VectorSelectionFrame | null {\n\t\treturn getVectorSelectionFrame(this.doc, ids);\n\t}\n\n\t/** Local → document matrix of a node in `doc` (default: the current document). */\n\tprivate worldOf(id: string, doc: VectorDocument = this.doc): AffineMatrix {\n\t\treturn vectorWorldMatrix(doc.nodes, id) ?? IDENTITY_MATRIX;\n\t}\n\n\t/** A document point in node `id`'s own space. */\n\tprivate toLocal(id: string, p: Vec2, doc: VectorDocument = this.doc): Vec2 {\n\t\treturn applyMatrix(invertMatrix(this.worldOf(id, doc)), p);\n\t}\n\n\tprivate cursorFor(): string {\n\t\tif (this.drag?.kind === 'pan' || this.spaceHeld) return this.drag ? 'grabbing' : 'grab';\n\t\tif (this.drag?.kind === 'rotate') return 'grabbing';\n\t\tif (this.drag?.kind === 'move') return 'move';\n\t\tconst h = this.hover;\n\t\tswitch (this.tool) {\n\t\t\tcase 'select':\n\t\t\t\tif (h?.transformHandle && this.freeDistort && h.transformHandle !== 'rotate') return 'crosshair';\n\t\t\t\tif (h?.transformHandle) return vectorResizeCursor(h.transformHandle, this.frameOf(this.selectedRoots())?.angle ?? 0);\n\t\t\t\treturn h?.nodeId ? 'move' : 'default';\n\t\t\tcase 'envelope':\n\t\t\t\tif (this.drag?.kind === 'envelope') return 'grabbing';\n\t\t\t\tif (h?.envelope) return h.envelope.index === null ? 'grab' : 'pointer';\n\t\t\t\treturn h?.nodeId ? 'pointer' : 'default';\n\t\t\tcase 'direct':\n\t\t\t\tif (h?.anchor || h?.handle || h?.corner) return 'pointer';\n\t\t\t\tif (h?.segment) return 'copy';\n\t\t\t\treturn h?.nodeId ? 'move' : 'default';\n\t\t\tcase 'pen':\n\t\t\t\treturn h?.anchor ? 'pointer' : 'crosshair';\n\t\t\tcase 'width':\n\t\t\t\tif (this.drag?.kind === 'width') return this.drag.mode === 'width' ? 'ew-resize' : 'grabbing';\n\t\t\t\tif (h?.widthPoint) return h.widthPoint.part === 'center' ? 'grab' : 'ew-resize';\n\t\t\t\treturn h?.strokeSpot ? 'copy' : 'default';\n\t\t\tcase 'shapeBuilder':\n\t\t\t\tif (!this.builderPreview?.faces.length && !this.builderPreview?.lines.length) return 'crosshair';\n\t\t\t\treturn this.builderPreview.mode === 'erase' ? 'not-allowed' : 'cell';\n\t\t\tcase 'paintBucket':\n\t\t\t\treturn this.builderPreview ? 'cell' : 'crosshair';\n\t\t\tcase 'blend':\n\t\t\t\treturn h?.nodeId ? (this.blendStart && this.blendStart.nodeId !== h.nodeId ? 'copy' : 'pointer') : 'crosshair';\n\t\t\tcase 'gradient': {\n\t\t\t\tconst part = this.drag?.kind === 'gradient' ? this.drag.part : h?.gradient;\n\t\t\t\tif (!part) return 'crosshair';\n\t\t\t\tif (part.kind === 'bar') return this.drag ? 'grabbing' : 'copy';\n\t\t\t\tif (part.kind === 'handle' && part.handle === 'start') return this.drag ? 'grabbing' : 'move';\n\t\t\t\treturn 'pointer';\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\treturn 'crosshair';\n\t\t}\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Undoable changes\n\t// -------------------------------------------------------------------------\n\n\tprivate snapshot(): Snapshot {\n\t\treturn { doc: this.doc, selection: this.selection, anchors: this.anchors, editingId: this.editingId, pen: this.pen };\n\t}\n\n\tprivate restore(s: Snapshot) {\n\t\tconst docChanged = s.doc !== this.doc || this.committing;\n\t\t// Undo / redo can renumber width points: drop the ones that are gone and close the editor.\n\t\tif (!this.committing) this.widthEdit = null;\n\t\tthis.committing = false;\n\t\tthis.applying = true;\n\t\ttry {\n\t\t\tthis.doc = s.doc;\n\t\t\tthis.selection = s.selection.filter((id) => this.getNode(id));\n\t\t\tthis.anchors = s.anchors.filter((r) => getAnchor(this.doc, r));\n\t\t\tthis.editingId = s.editingId && this.getNode(s.editingId) ? s.editingId : null;\n\t\t\tthis.pen = s.pen && (this.getNode(s.pen.nodeId) as VectorPathNode | undefined)?.subpaths?.[s.pen.subpath] ? s.pen : null;\n\t\t\tif (this.widthPoints.length) {\n\t\t\t\tconst kept = this.widthPoints.filter((r) => {\n\t\t\t\t\tconst n = this.getNode(r.nodeId);\n\t\t\t\t\treturn n?.type === 'path' && !!n.strokeWidthProfile?.[r.index];\n\t\t\t\t});\n\t\t\t\tif (kept.length !== this.widthPoints.length) this.widthPoints = kept;\n\t\t\t}\n\t\t} finally {\n\t\t\tthis.applying = false;\n\t\t}\n\t\tthis.notify(docChanged);\n\t}\n\n\t/**\n\t * Records the change from `before` to the current state as one undo step. Consecutive\n\t * changes with the same `merge` key (a slider drag) fold into one step.\n\t */\n\tprivate commit(name: string, before: Snapshot, merge?: string) {\n\t\tthis.doc = this.refreshed(this.doc);\n\t\tconst after = this.snapshot();\n\t\tif (before.doc === after.doc) {\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tconst command: Command = {\n\t\t\tname,\n\t\t\texecute: () => this.restore(after),\n\t\t\tundo: () => this.restore(before),\n\t\t\tredo: () => this.restore(after),\n\t\t\t...(merge ? { tag: merge } : {})\n\t\t};\n\t\tthis.committing = true;\n\t\tif (merge) this.history.executeMerged(command, { key: merge });\n\t\telse this.history.execute(command);\n\t\tthis.committing = false;\n\t}\n\n\t/** Runs `fn` (which updates fields) and records it as one undo step (merged by `merge` key). */\n\tprivate change(name: string, fn: () => void, merge?: string): void {\n\t\tconst before = this.snapshot();\n\t\tfn();\n\t\tthis.commit(name, before, merge);\n\t}\n\n\t/** Ends a merged edit (a slider released): the next change starts a new undo step. */\n\tendMergedEdit(): void {\n\t\tthis.history.endGesture();\n\t}\n\n\tprivate setDoc(nodes: VectorNode[]) {\n\t\tif (nodes !== this.doc.nodes) this.doc = { ...this.doc, nodes };\n\t}\n\n\tprivate replaceNode(id: string, next: VectorNode | null) {\n\t\tthis.setDoc(mapTree(this.doc.nodes, (n) => (n.id === id ? next : n)));\n\t}\n\n\tprivate freshIds<T extends VectorNode>(node: T): T {\n\t\tconst copy = cloneVectorNode(node);\n\t\tconst used = new Set<string>();\n\t\twalkVectorNodes(this.doc.nodes, (n) => used.add(n.id));\n\t\tconst next = () => {\n\t\t\tlet id = this.createId();\n\t\t\twhile (used.has(id)) id = this.createId();\n\t\t\tused.add(id);\n\t\t\treturn id;\n\t\t};\n\t\tconst assign = (n: VectorNode) => {\n\t\t\tn.id = next();\n\t\t\tif (n.type === 'group') n.children.forEach(assign);\n\t\t};\n\t\tassign(copy);\n\t\treturn copy;\n\t}\n\n\tprivate newId(): string {\n\t\tlet id = this.createId();\n\t\twhile (this.getNode(id)) id = this.createId();\n\t\treturn id;\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Viewport\n\t// -------------------------------------------------------------------------\n\n\tscreenToDocument(p: Vec2): Vec2 {\n\t\tconst v = this.viewport;\n\t\treturn { x: (p.x - v.x) / v.zoom, y: (p.y - v.y) / v.zoom };\n\t}\n\n\tdocumentToScreen(p: Vec2): Vec2 {\n\t\tconst v = this.viewport;\n\t\treturn { x: p.x * v.zoom + v.x, y: p.y * v.zoom + v.y };\n\t}\n\n\tsetViewport(viewport: Partial<VectorViewport>): void {\n\t\tconst next = { ...this.viewport, ...viewport };\n\t\tnext.zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, next.zoom));\n\t\tif (next.x === this.viewport.x && next.y === this.viewport.y && next.zoom === this.viewport.zoom) return;\n\t\tthis.viewport = next;\n\t\tthis.notify();\n\t}\n\n\t/** Zooms by `factor` keeping the document point under `screen` in place. */\n\tzoomAt(screen: Vec2, factor: number): void {\n\t\tconst zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, this.viewport.zoom * factor));\n\t\tconst doc = this.screenToDocument(screen);\n\t\tthis.setViewport({ zoom, x: screen.x - doc.x * zoom, y: screen.y - doc.y * zoom });\n\t}\n\n\tpanBy(dx: number, dy: number): void {\n\t\tthis.setViewport({ x: this.viewport.x + dx, y: this.viewport.y + dy });\n\t}\n\n\t/** Fits the artboard (or `bounds`) into a canvas of `size` screen pixels. */\n\tzoomToFit(size: { width: number; height: number }, options: { padding?: number; bounds?: VectorBounds } = {}): void {\n\t\tconst padding = options.padding ?? 40;\n\t\tconst b = options.bounds ?? { x: 0, y: 0, width: this.doc.width, height: this.doc.height };\n\t\tconst zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, Math.min((size.width - padding * 2) / (b.width || 1), (size.height - padding * 2) / (b.height || 1))));\n\t\tthis.setViewport({ zoom, x: (size.width - b.width * zoom) / 2 - b.x * zoom, y: (size.height - b.height * zoom) / 2 - b.y * zoom });\n\t}\n\n\t/** Zoom at the canvas centre-ish: positive `delta` zooms out (wheel convention). Use with `wheel`. */\n\twheel(screen: Vec2, delta: { x: number; y: number }, mods: VectorModifiers = {}): void {\n\t\tif (mods.mod) this.zoomAt(screen, Math.exp(-delta.y * 0.01));\n\t\telse this.panBy(-(mods.shift ? delta.y : delta.x), -(mods.shift ? 0 : delta.y));\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Settings\n\t// -------------------------------------------------------------------------\n\n\tsetTool(tool: VectorTool): void {\n\t\tif (tool === this.tool) return;\n\t\tthis.finishPath();\n\t\tthis.cancelDrag();\n\t\tthis.tool = tool;\n\t\tthis.hover = null;\n\t\tthis.guides = null;\n\t\tthis.widthPoints = [];\n\t\tthis.widthEdit = null;\n\t\tthis.builderPreview = null;\n\t\tthis.builderHoverKey = null;\n\t\tthis.blendStart = null;\n\t\tif (tool !== 'direct') {\n\t\t\tthis.anchors = [];\n\t\t\tthis.editingId = null;\n\t\t}\n\t\tthis.notify();\n\t}\n\n\tsetDefaults(defaults: Partial<VectorDefaults>): void {\n\t\tthis.defaults = { ...this.defaults, ...defaults };\n\t\tthis.notify();\n\t}\n\n\tsetSnap(snap: Partial<VectorSnapOptions>): void {\n\t\tthis.snap = { ...this.snap, ...snap };\n\t\tif (!this.snap.guides || !this.snap.objects) this.guides = null;\n\t\tthis.notify();\n\t}\n\n\t/** Changes how deleting, dragging anchors, the pencil and the eraser behave. */\n\tsetEditing(editing: Partial<VectorPathEditOptions>): void {\n\t\tconst next = { ...this.editing, ...editing };\n\t\tnext.pencilSmoothing = Math.min(1, Math.max(0, Number.isFinite(next.pencilSmoothing) ? next.pencilSmoothing : 0));\n\t\tif (!(next.eraserSize > 0)) next.eraserSize = this.editing.eraserSize;\n\t\tthis.editing = next;\n\t\tthis.notify();\n\t}\n\n\t/** Turns object snapping on or off (the toolbar's snapping toggle). */\n\ttoggleSnapping(on: boolean = !this.snap.objects): void {\n\t\tthis.setSnap({ objects: on });\n\t}\n\n\tprivate gridValue(v: number): number {\n\t\tconst g = this.snap.grid;\n\t\treturn g > 0 ? Math.round(v / g) * g : v;\n\t}\n\n\tprivate snapPoint(p: Vec2): Vec2 {\n\t\treturn { x: this.gridValue(p.x), y: this.gridValue(p.y) };\n\t}\n\n\t/** The pull distance in document units. */\n\tprivate snapTolerance(): number {\n\t\treturn this.snap.threshold / this.viewport.zoom;\n\t}\n\n\tprivate setGuides(guides: VectorSnapGuides | null) {\n\t\tthis.guides = guides && this.snap.guides && (guides.lines.length || guides.labels.length) ? guides : null;\n\t}\n\n\t/**\n\t * Where a drawn or dragged point `p` goes: onto the smart guide `targets` (gathered by\n\t * `gather` when missing), else onto the grid, kept to `leave`'s constraint. Cmd / Ctrl\n\t * bypasses snapping.\n\t */\n\tprivate snapDrawn(p: Vec2, leave: VectorSnapLeave | null, input: VectorModifiers, targets: () => VectorSnapTargets): Vec2 {\n\t\tthis.snappedToObject = false;\n\t\tif (input.mod) {\n\t\t\tthis.setGuides(null);\n\t\t\treturn constrainToLeave(leave, p);\n\t\t}\n\t\tif (this.snap.objects) {\n\t\t\tconst withAngles = leave ? { ...leave, angles: this.snap.angles } : null;\n\t\t\tconst res = targets().snapFrom(p, withAngles, this.snapTolerance());\n\t\t\tif (res.kind) {\n\t\t\t\tthis.snappedToObject = true;\n\t\t\t\tthis.setGuides(res);\n\t\t\t\t// An axis lined up with nothing still goes to the grid.\n\t\t\t\treturn leave?.constrain ? res.point : { x: res.x ? res.point.x : this.gridValue(res.point.x), y: res.y ? res.point.y : this.gridValue(res.point.y) };\n\t\t\t}\n\t\t}\n\t\tthis.setGuides(null);\n\t\treturn constrainToLeave(leave, this.snapPoint(p));\n\t}\n\n\t/** Drawing targets for the pen and shape tools, reused while the document stays the same. */\n\tprivate drawingTargets(exclude: string | null): VectorSnapTargets {\n\t\tconst key = exclude ?? '';\n\t\tconst cached = this.hoverTargets;\n\t\tif (cached && cached.doc === this.doc && cached.exclude === key) return cached.targets;\n\t\tconst targets = VectorSnapTargets.forDrawing(this.doc, exclude ? [exclude] : []);\n\t\tthis.hoverTargets = { doc: this.doc, exclude: key, targets };\n\t\treturn targets;\n\t}\n\n\t/** Where the pointer at `p` would place a point with the pen or a shape tool (and the guides showing why). */\n\tprivate hoverSnap(p: Vec2, input: VectorModifiers): Vec2 {\n\t\tif (this.tool === 'pen') {\n\t\t\tconst sp = this.pen ? (this.getNode(this.pen.nodeId) as VectorPathNode | undefined)?.subpaths[this.pen.subpath] : undefined;\n\t\t\tif (this.pen && sp && this.hover?.anchor) {\n\t\t\t\tthis.setGuides(null);\n\t\t\t\treturn p;\n\t\t\t}\n\t\t\tconst last = this.pen && sp ? getAnchorWorld(this.doc, { nodeId: this.pen.nodeId, subpath: this.pen.subpath, index: sp.anchors.length - 1 }) : undefined;\n\t\t\tconst leave = last ? { at: { x: last.x, y: last.y }, constrain: input.shift ? ('angle' as const) : null } : null;\n\t\t\treturn this.snapDrawn(p, leave, input, () => this.drawingTargets(this.pen?.nodeId ?? null));\n\t\t}\n\t\tif (SHAPE_TOOLS.has(this.tool)) return this.snapDrawn(p, null, input, () => this.drawingTargets(null));\n\t\tthis.setGuides(null);\n\t\treturn p;\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Document\n\t// -------------------------------------------------------------------------\n\n\t/** Replaces the document. Undoable unless `{ history: false }` (then history is cleared). */\n\tsetDocument(doc: VectorDocument, options: { history?: boolean; label?: string } = {}): void {\n\t\tthis.cancelDrag();\n\t\tconst next = loadedDocument(doc);\n\t\tif (options.history === false) {\n\t\t\tthis.doc = next;\n\t\t\tthis.selection = [];\n\t\t\tthis.anchors = [];\n\t\t\tthis.editingId = null;\n\t\t\tthis.pen = null;\n\t\t\tthis.history.clear();\n\t\t\tthis.notify(true);\n\t\t\treturn;\n\t\t}\n\t\tthis.change(options.label ?? 'Replace Document', () => {\n\t\t\tthis.doc = next;\n\t\t\tthis.selection = [];\n\t\t\tthis.anchors = [];\n\t\t\tthis.editingId = null;\n\t\t\tthis.pen = null;\n\t\t});\n\t}\n\n\t/** Changes the artboard size or background. */\n\tsetArtboard(patch: Partial<Pick<VectorDocument, 'width' | 'height' | 'background'>>): void {\n\t\tthis.change('Change Artboard', () => {\n\t\t\tthis.doc = { ...this.doc, ...patch };\n\t\t});\n\t}\n\n\t/** Replaces the document with parsed SVG markup (one undo step). */\n\tloadSvg(svg: string, options: { history?: boolean } = {}): VectorActionResult {\n\t\ttry {\n\t\t\tconst doc = parseVectorSvg(svg, { createId: () => this.createId() });\n\t\t\tthis.setDocument(doc, { history: options.history, label: 'Import SVG' });\n\t\t\treturn { ok: true };\n\t\t} catch (error) {\n\t\t\treturn { ok: false, message: error instanceof Error ? error.message : String(error) };\n\t\t}\n\t}\n\n\t/** Adds the nodes of SVG markup on top of the document, selected. */\n\timportSvg(svg: string): VectorActionResult {\n\t\ttry {\n\t\t\tconst parsed = parseVectorSvg(svg);\n\t\t\tconst nodes = parsed.nodes.map((n) => this.freshIds(n));\n\t\t\tif (!nodes.length) return { ok: false, message: 'The SVG has no shapes the editor can read.' };\n\t\t\tthis.change('Import SVG', () => {\n\t\t\t\tthis.setDoc([...this.doc.nodes, ...nodes]);\n\t\t\t\tthis.selection = nodes.map((n) => n.id);\n\t\t\t\tthis.anchors = [];\n\t\t\t});\n\t\t\treturn { ok: true };\n\t\t} catch (error) {\n\t\t\treturn { ok: false, message: error instanceof Error ? error.message : String(error) };\n\t\t}\n\t}\n\n\ttoSvg(options?: VectorSvgExportOptions): string {\n\t\treturn vectorDocumentToSvg(this.doc, options);\n\t}\n\n\ttoJSON(): VectorDocument {\n\t\treturn cloneVectorDocument(this.doc);\n\t}\n\n\t/** Adds a node (ids are kept unless taken). Returns its id. */\n\taddNode(node: VectorNode, options: AddVectorNodeOptions = {}): string {\n\t\tconst taken = (() => {\n\t\t\tlet clash = false;\n\t\t\twalkVectorNodes([node], (n) => {\n\t\t\t\tif (this.getNode(n.id)) clash = true;\n\t\t\t});\n\t\t\treturn clash;\n\t\t})();\n\t\tconst next = taken ? this.freshIds(node) : cloneVectorNode(node);\n\t\tthis.change(options.label ?? `Add ${next.type === 'group' ? 'Group' : (next.name ?? 'Path')}`, () => {\n\t\t\tconst insert = (list: VectorNode[]) => {\n\t\t\t\tconst copy = list.slice();\n\t\t\t\tcopy.splice(options.index ?? copy.length, 0, next);\n\t\t\t\treturn copy;\n\t\t\t};\n\t\t\tif (options.parentId) {\n\t\t\t\tthis.setDoc(mapTree(this.doc.nodes, (n) => (n.id === options.parentId && n.type === 'group' ? { ...n, children: insert(n.children) } : n)));\n\t\t\t} else this.setDoc(insert(this.doc.nodes));\n\t\t\tif (options.select !== false) {\n\t\t\t\tthis.selection = [next.id];\n\t\t\t\tthis.anchors = [];\n\t\t\t}\n\t\t});\n\t\treturn next.id;\n\t}\n\n\t/** Builds a path node for a shape with the current defaults (not added). */\n\tcreateShapeNode(input: VectorShapeInput): VectorPathNode {\n\t\tconst b = { x: input.x, y: input.y, width: input.width, height: input.height };\n\t\tconst d = this.defaults;\n\t\tlet subpath: VectorSubpath;\n\t\tswitch (input.kind) {\n\t\t\tcase 'rect':\n\t\t\t\tsubpath = rectSubpath(b);\n\t\t\t\tbreak;\n\t\t\tcase 'ellipse':\n\t\t\t\tsubpath = ellipseSubpath(b);\n\t\t\t\tbreak;\n\t\t\tcase 'polygon':\n\t\t\t\tsubpath = polygonSubpath(b, input.sides ?? d.polygonSides);\n\t\t\t\tbreak;\n\t\t\tcase 'star':\n\t\t\t\tsubpath = starSubpath(b, input.points ?? d.starPoints, input.innerRatio ?? d.starInnerRatio);\n\t\t\t\tbreak;\n\t\t\tcase 'line':\n\t\t\t\tsubpath = lineSubpath({ x: b.x, y: b.y }, { x: b.x + b.width, y: b.y + b.height });\n\t\t\t\tbreak;\n\t\t}\n\t\tconst radius = input.cornerRadius ?? d.cornerRadius;\n\t\tconst kind = input.cornerKind ?? d.cornerKind;\n\t\t// Live corners: the shape keeps its sharp anchors, so the radius stays editable.\n\t\tif ((input.kind === 'rect' || input.kind === 'polygon' || input.kind === 'star') && radius > 0) subpath = setSubpathCorners(subpath, radius, kind);\n\t\tconst names = { rect: 'Rectangle', ellipse: 'Ellipse', polygon: 'Polygon', star: 'Star', line: 'Line' };\n\t\tconst style = this.styleFor(input.kind === 'line');\n\t\tconst own: VectorStyle = {};\n\t\tfor (const key of VECTOR_STYLE_KEYS) {\n\t\t\tif (input[key] !== undefined) (own as any)[key] = input[key];\n\t\t}\n\t\t// Given style edits the defaults' top layers; given stacks replace theirs.\n\t\treturn withStyle({ id: this.newId(), type: 'path', name: input.name ?? names[input.kind], ...style, subpaths: [subpath] }, own) as VectorPathNode;\n\t}\n\n\t/** Adds a shape drawn with the current defaults. Returns its id. */\n\taddShape(input: VectorShapeInput, options: AddVectorNodeOptions = {}): string {\n\t\tconst node = this.createShapeNode(input);\n\t\treturn this.addNode(node, { label: `Add ${node.name}`, ...options });\n\t}\n\n\t/** Adds a path from SVG path data. Returns its id. */\n\taddPath(d: string, style: VectorStyle & { name?: string } = {}, options: AddVectorNodeOptions = {}): string {\n\t\tconst subpaths = bezierToSubpaths(d);\n\t\tconst { name, ...own } = style;\n\t\tconst node = withStyle({ id: this.newId(), type: 'path', name: name ?? 'Path', ...this.styleFor(false), subpaths }, own) as VectorPathNode;\n\t\treturn this.addNode(node, { label: 'Add Path', ...options });\n\t}\n\n\tprivate styleFor(open: boolean): VectorStyle {\n\t\tconst d = this.defaults;\n\t\tconst out: VectorStyle = { fill: open ? null : (d.fill ?? null), stroke: d.stroke ?? null, strokeWidth: d.strokeWidth ?? 1 };\n\t\tif (d.strokeLinecap) out.strokeLinecap = d.strokeLinecap;\n\t\tif (d.strokeLinejoin) out.strokeLinejoin = d.strokeLinejoin;\n\t\tif (d.strokeDasharray) out.strokeDasharray = d.strokeDasharray;\n\t\tif (d.fillRule) out.fillRule = d.fillRule;\n\t\tif (d.strokeAlign && d.strokeAlign !== 'center') out.strokeAlign = d.strokeAlign;\n\t\tfor (const key of ['startArrow', 'endArrow', 'arrowAlign', 'startArrowScale', 'endArrowScale'] as const) if (d[key]) (out as any)[key] = d[key];\n\t\tif (d.strokeWidthProfile?.length) out.strokeWidthProfile = d.strokeWidthProfile.map((p) => ({ ...p }));\n\t\t// Default stacks: new shapes get every layer (open paths no fills).\n\t\tlet styled: VectorStyle = out;\n\t\tif (Array.isArray(d.fills)) styled = withVectorFills(styled, open ? [] : d.fills);\n\t\tif (Array.isArray(d.strokes)) styled = withVectorStrokes(styled, d.strokes);\n\t\treturn styled;\n\t}\n\n\t/** Merges `patch` into a node (paths take style and geometry; groups name / visibility / opacity). */\n\tupdateNode(id: string, patch: Partial<VectorPathNode> | Partial<VectorGroupNode>, options: { label?: string } = {}): VectorActionResult {\n\t\tconst node = this.getNode(id);\n\t\tif (!node) return { ok: false, message: `There is no node \"${id}\".` };\n\t\tconst { id: _id, type: _type, ...rest } = patch as any;\n\t\tthis.change(options.label ?? 'Edit Node', () => this.replaceNode(id, { ...node, ...rest } as VectorNode));\n\t\treturn { ok: true };\n\t}\n\n\t/** Applies style (and opacity) to the selected (or given) nodes; groups pass it to every path inside. */\n\tsetStyle(style: VectorStyle & { opacity?: number }, ids: readonly string[] = this.selection, options: { label?: string } = {}): VectorActionResult {\n\t\tconst targets = new Set<string>();\n\t\tfor (const id of ids) {\n\t\t\tconst node = this.getNode(id);\n\t\t\tif (!node) return { ok: false, message: `There is no node \"${id}\".` };\n\t\t\tfor (const p of collectPathNodes(node)) targets.add(p.id);\n\t\t}\n\t\tif (!targets.size) return { ok: false, message: 'Nothing is selected.' };\n\t\tthis.change(options.label ?? 'Change Style', () => {\n\t\t\tthis.setDoc(mapTree(this.doc.nodes, (n) => (targets.has(n.id) ? withStyle(n as VectorPathNode, style) : n)));\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/**\n\t * Sets the variable-width profile of the selected (or given) paths: a preset id\n\t * (`uniform` removes the profile), points, or a parametric taper / wave. Returns an error for\n\t * an unknown preset.\n\t */\n\tsetWidthProfile(\n\t\tprofile: VectorWidthPreset | readonly VectorWidthPoint[] | { taper?: VectorTaperOptions; wave?: VectorWaveOptions } | null,\n\t\toptions: { ids?: readonly string[] } = {}\n\t): VectorActionResult {\n\t\tlet points: VectorWidthPoint[];\n\t\tif (profile === null) points = [];\n\t\telse if (typeof profile === 'string') {\n\t\t\tconst preset = widthProfilePreset(profile);\n\t\t\tif (!preset) return { ok: false, message: `There is no width profile \"${profile}\".` };\n\t\t\tpoints = preset;\n\t\t} else if (Array.isArray(profile)) points = sanitizeWidthProfile(profile);\n\t\telse {\n\t\t\tconst p = profile as { taper?: VectorTaperOptions; wave?: VectorWaveOptions };\n\t\t\tpoints = p.taper || p.wave ? generateWidthProfile(p) : [];\n\t\t}\n\t\tthis.widthPoints = [];\n\t\tthis.widthEdit = null;\n\t\treturn this.setStyle({ strokeWidthProfile: points.length ? points : null }, options.ids ?? this.selection, { label: points.length ? 'Width Profile' : 'Uniform Width' });\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Gradients\n\t// -------------------------------------------------------------------------\n\n\t/** Which paint (fill or stroke) the gradient tool and editor work on. */\n\tsetGradientTarget(target: VectorGradientTarget, layer: number | null = null): void {\n\t\tif (target === this.gradientEdit.target && layer === this.gradientEdit.layer) return;\n\t\tthis.gradientEdit = { target, stop: null, layer };\n\t\tthis.notify();\n\t}\n\n\t/** Selects a stop of the gradient being edited (index into its sorted stops), or none. */\n\tselectGradientStop(index: number | null): void {\n\t\tif (index === this.gradientEdit.stop) return;\n\t\tthis.gradientEdit = { ...this.gradientEdit, stop: index };\n\t\tthis.notify();\n\t}\n\n\t/** The gradient tool's annotator for the selection (screen pixels), or null when its paint is not a gradient. */\n\tgetGradientAnnotator(): VectorGradientAnnotator | null {\n\t\treturn getVectorGradientAnnotator(this.doc, this.selection, this.viewport, this.gradientEdit.target, this.gradientEdit.layer);\n\t}\n\n\t/** Unlocked paths of `ids` (default: the selection). */\n\tprivate gradientPaths(ids: readonly string[] = this.selection): VectorPathNode[] {\n\t\tconst out = new Map<string, VectorPathNode>();\n\t\tfor (const id of ids) {\n\t\t\tconst node = this.getNode(id);\n\t\t\tif (node && !node.locked) for (const p of collectPathNodes(node)) if (!p.locked) out.set(p.id, p);\n\t\t}\n\t\treturn [...out.values()];\n\t}\n\n\t/**\n\t * Edits the gradient of the `target` paint (default: the editor's) of the selected (or given)\n\t * paths with `patch`: a colour or no paint becomes a gradient (`patch.type`, default linear,\n\t * from the colour to white). Consecutive edits with the same `merge` key are one undo step.\n\t */\n\tsetGradient(patch: VectorGradientPatch, options: { ids?: readonly string[]; target?: VectorGradientTarget; layer?: number | null; label?: string; merge?: string } = {}): VectorActionResult {\n\t\tconst target = options.target ?? this.gradientEdit.target;\n\t\tconst layer = options.layer !== undefined ? options.layer : options.target === undefined ? this.gradientEdit.layer : null;\n\t\tconst paths = this.gradientPaths(options.ids ?? this.selection);\n\t\tif (!paths.length) return { ok: false, message: 'Nothing is selected.' };\n\t\tconst next = new Map<string, VectorPathNode>();\n\t\tlet first: VectorGradient | null = null;\n\t\tfor (const path of paths) {\n\t\t\tconst paint = vectorTargetPaint(path, target, layer);\n\t\t\tconst base = isVectorGradient(paint) ? paint : defaultVectorGradient(patch.type ?? 'linear', paint);\n\t\t\tconst g = applyVectorGradientPatch(base, patch);\n\t\t\tif (!g) return { ok: false, message: 'That is not a valid gradient: it needs at least one stop with an offset and a colour.' };\n\t\t\tfirst ??= g;\n\t\t\tnext.set(path.id, withVectorTargetPaint(path, target, layer, g));\n\t\t}\n\t\tthis.change(options.label ?? 'Gradient', () => this.setDoc(mapTree(this.doc.nodes, (n) => next.get(n.id) ?? n)), options.merge);\n\t\tconst stops = (first as VectorGradient | null)?.stops.length ?? 0;\n\t\tif (this.gradientEdit.stop !== null && this.gradientEdit.stop >= stops) this.gradientEdit = { ...this.gradientEdit, stop: null };\n\t\treturn { ok: true };\n\t}\n\n\t/** Applies `fn` to every target gradient of the selection (paths whose paint is not a gradient are skipped). */\n\tprivate editGradients(label: string, fn: (stops: VectorGradient['stops']) => VectorGradient['stops'] | null, merge?: string): VectorActionResult {\n\t\tconst { target, layer } = this.gradientEdit;\n\t\tconst paths = this.gradientPaths().filter((p) => isVectorGradient(vectorTargetPaint(p, target, layer)));\n\t\tif (!paths.length) return { ok: false, message: 'The selection has no gradient.' };\n\t\tconst next = new Map<string, VectorPathNode>();\n\t\tfor (const path of paths) {\n\t\t\tconst g = vectorTargetPaint(path, target, layer) as VectorGradient;\n\t\t\tconst stops = fn(sortedVectorGradientStops(g));\n\t\t\tif (!stops) return { ok: false, message: 'A gradient keeps at least two stops.' };\n\t\t\tnext.set(path.id, withVectorTargetPaint(path, target, layer, { ...g, stops }));\n\t\t}\n\t\tthis.change(label, () => this.setDoc(mapTree(this.doc.nodes, (n) => next.get(n.id) ?? n)), merge);\n\t\treturn { ok: true };\n\t}\n\n\t/** Changes stop `index` (colour, opacity, offset or midpoint) of the selection's gradients; selects it. */\n\tsetGradientStop(index: number, patch: { color?: string; opacity?: number; offset?: number; midpoint?: number }, options: { merge?: string } = {}): VectorActionResult {\n\t\tlet moved = index;\n\t\tconst result = this.editGradients(\n\t\t\t'Gradient Stop',\n\t\t\t(stops) => {\n\t\t\t\tif (!stops[index]) return stops;\n\t\t\t\tlet next = stops.map((s, i) => {\n\t\t\t\t\tif (i !== index) return s;\n\t\t\t\t\tconst out = { ...s };\n\t\t\t\t\tif (patch.color !== undefined) out.color = patch.color;\n\t\t\t\t\tif (patch.opacity !== undefined && Number.isFinite(patch.opacity)) {\n\t\t\t\t\t\tconst o = Math.min(1, Math.max(0, patch.opacity));\n\t\t\t\t\t\tif (o < 1) out.opacity = o;\n\t\t\t\t\t\telse delete out.opacity;\n\t\t\t\t\t}\n\t\t\t\t\tif (patch.midpoint !== undefined && Number.isFinite(patch.midpoint)) {\n\t\t\t\t\t\tconst m = Math.min(0.95, Math.max(0.05, patch.midpoint));\n\t\t\t\t\t\tif (Math.abs(m - 0.5) > 1e-3) out.midpoint = m;\n\t\t\t\t\t\telse delete out.midpoint;\n\t\t\t\t\t}\n\t\t\t\t\treturn out;\n\t\t\t\t});\n\t\t\t\tif (patch.offset !== undefined && Number.isFinite(patch.offset)) ({ stops: next, index: moved } = moveVectorGradientStop(next, index, patch.offset));\n\t\t\t\treturn next;\n\t\t\t},\n\t\t\toptions.merge\n\t\t);\n\t\tif (result.ok) this.selectGradientStop(moved);\n\t\treturn result;\n\t}\n\n\t/** Adds a stop at `offset` (painting what the gradient paints there) to the selection's gradients and selects it. */\n\taddGradientStop(offset: number): VectorActionResult {\n\t\tlet index = 0;\n\t\tconst { target, layer } = this.gradientEdit;\n\t\tconst result = this.editGradients('Add Gradient Stop', (stops) => {\n\t\t\tconst path = this.gradientPaths().find((p) => isVectorGradient(vectorTargetPaint(p, target, layer)));\n\t\t\tconst r = insertVectorGradientStop({ ...(vectorTargetPaint(path!, target, layer) as VectorGradient), stops }, offset);\n\t\t\tindex = r.index;\n\t\t\treturn r.stops;\n\t\t});\n\t\tif (result.ok) this.selectGradientStop(index);\n\t\treturn result;\n\t}\n\n\t/** Removes stop `index` of the selection's gradients (never below two stops). */\n\tremoveGradientStop(index: number): VectorActionResult {\n\t\tconst result = this.editGradients('Delete Gradient Stop', (stops) => removeVectorGradientStop(stops, index));\n\t\tif (result.ok) this.selectGradientStop(null);\n\t\treturn result;\n\t}\n\n\t/** Reverses the selection's gradients (stops and midpoints). */\n\treverseGradient(): VectorActionResult {\n\t\treturn this.setGradient({ reverse: true }, { label: 'Reverse Gradient' });\n\t}\n\n\t/** `doc` with path `id`'s `target` paint replaced. */\n\tprivate withGradient(doc: VectorDocument, id: string, target: VectorGradientTarget, g: VectorGradient): VectorDocument {\n\t\tconst layer = this.gradientEdit.layer;\n\t\treturn { ...doc, nodes: mapTree(doc.nodes, (n) => (n.id === id && n.type === 'path' ? withVectorTargetPaint(n, target, layer, g) : n)) };\n\t}\n\n\tprivate downGradient(screen: Vec2, p: Vec2, input: VectorPointerInput) {\n\t\tconst before = this.snapshot();\n\t\tconst a = this.getGradientAnnotator();\n\t\tlet part = a ? hitVectorGradientAnnotator(a, screen) : null;\n\t\tif (a && part) {\n\t\t\t// Alt on a radial gradient's centre grabs its focal point (they sit together until moved).\n\t\t\tif (part.kind === 'handle' && part.handle === 'start' && input.alt && a.gradient.type === 'radial') part = { kind: 'handle', handle: 'focal' };\n\t\t\tif (part.kind === 'stop') this.gradientEdit = { ...this.gradientEdit, stop: part.index };\n\t\t\tthis.drag = { kind: 'gradient', part, nodeId: a.nodeId, targets: [a.nodeId], annotator: a, start: screen, startDoc: p, before, base: this.doc, moved: false, stop: part.kind === 'stop' ? part.index : null };\n\t\t\tthis.interaction = { type: 'gradient', part: part.kind, ...(part.kind === 'handle' ? { handle: part.handle } : {}) };\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\t// Elsewhere a drag draws a new vector over the path under the pointer (selecting it) or the selection.\n\t\tconst hit = this.hitPath(p);\n\t\tif (hit && !this.gradientPaths().some((n) => n.id === hit.id)) {\n\t\t\tthis.selection = [hit.id];\n\t\t\tthis.gradientEdit = { ...this.gradientEdit, stop: null };\n\t\t}\n\t\tconst targets = this.gradientPaths().map((n) => n.id);\n\t\tif (!targets.length) {\n\t\t\tif (this.selection.length) this.clearSelection();\n\t\t\treturn;\n\t\t}\n\t\tthis.drag = { kind: 'gradient', part: null, nodeId: null, targets, annotator: null, start: screen, startDoc: p, before, base: this.doc, moved: false, stop: null };\n\t\tthis.interaction = { type: 'gradient', part: 'create' };\n\t\tthis.notify();\n\t}\n\n\tprivate moveGradient(drag: Extract<Drag, { kind: 'gradient' }>, screen: Vec2, p: Vec2, input: VectorPointerInput) {\n\t\tif (!drag.moved && Math.hypot(screen.x - drag.start.x, screen.y - drag.start.y) < 3) return;\n\t\tdrag.moved = true;\n\t\tconst target = this.gradientEdit.target;\n\t\tconst base = drag.base;\n\t\tconst constrained = (from: Vec2, to: Vec2) => {\n\t\t\tif (!input.shift) return to;\n\t\t\tconst ang = Math.round(Math.atan2(to.y - from.y, to.x - from.x) / (Math.PI / 4)) * (Math.PI / 4);\n\t\t\tconst len = Math.hypot(to.x - from.x, to.y - from.y);\n\t\t\treturn { x: from.x + Math.cos(ang) * len, y: from.y + Math.sin(ang) * len };\n\t\t};\n\t\tif (!drag.part) {\n\t\t\tconst end = constrained(drag.startDoc, p);\n\t\t\tlet doc = base;\n\t\t\tfor (const id of drag.targets) {\n\t\t\t\tconst node = findVectorNode(base.nodes, id) as VectorPathNode | undefined;\n\t\t\t\tif (!node) continue;\n\t\t\t\tconst paint = vectorTargetPaint(node, target, this.gradientEdit.layer);\n\t\t\t\tconst g = isVectorGradient(paint) ? paint : defaultVectorGradient('linear', paint);\n\t\t\t\tdoc = this.withGradient(doc, id, target, setVectorGradientVector(g, vectorPathLocalBounds(node), this.toLocal(id, drag.startDoc, base), this.toLocal(id, end, base)));\n\t\t\t}\n\t\t\tthis.doc = doc;\n\t\t\tthis.notify(true);\n\t\t\treturn;\n\t\t}\n\t\tconst id = drag.nodeId!;\n\t\tconst node = findVectorNode(base.nodes, id) as VectorPathNode;\n\t\tconst g = vectorTargetPaint(node, target, this.gradientEdit.layer) as VectorGradient;\n\t\tconst bounds = vectorPathLocalBounds(node);\n\t\tconst local = (q: Vec2) => this.toLocal(id, q, base);\n\t\tconst part = drag.part;\n\t\tlet next: VectorGradient = g;\n\t\tif (part.kind === 'handle') {\n\t\t\tlet to = p;\n\t\t\t// Shift keeps the vector at 45° steps from the start.\n\t\t\tif (part.handle === 'end' || part.handle === 'radius') to = constrained(this.screenToDocument(drag.annotator!.start), p);\n\t\t\tnext = moveVectorGradientHandle(g, bounds, part.handle, local(to), local(drag.startDoc));\n\t\t} else if (part.kind === 'bar') {\n\t\t\tnext = moveVectorGradientHandle(g, bounds, 'start', local(p), local(drag.startDoc));\n\t\t} else {\n\t\t\tconst a = drag.annotator!;\n\t\t\tconst pos = vectorGradientBarPosition(a, screen);\n\t\t\tconst stops = sortedVectorGradientStops(g);\n\t\t\tif (part.kind === 'stop') {\n\t\t\t\tconst off = Math.abs(pos.across - VECTOR_GRADIENT_STOP_GAP) > VECTOR_GRADIENT_OFF_BAR;\n\t\t\t\tconst removed = off ? removeVectorGradientStop(stops, part.index) : null;\n\t\t\t\tif (removed) {\n\t\t\t\t\tnext = { ...g, stops: removed };\n\t\t\t\t\tdrag.stop = null;\n\t\t\t\t} else {\n\t\t\t\t\tconst r = moveVectorGradientStop(stops, part.index, Math.min(1, Math.max(0, pos.offset)));\n\t\t\t\t\tnext = { ...g, stops: r.stops };\n\t\t\t\t\tdrag.stop = r.index;\n\t\t\t\t}\n\t\t\t\tthis.interaction = { type: 'gradient', part: 'stop', ...(removed ? { removing: true } : {}) };\n\t\t\t\tthis.gradientEdit = { ...this.gradientEdit, stop: drag.stop };\n\t\t\t} else {\n\t\t\t\tconst m = vectorGradientMidpointFromPosition(stops, part.index, pos.offset);\n\t\t\t\tif (m !== null) next = { ...g, stops: stops.map((s, i) => (i !== part.index ? s : Math.abs(m - 0.5) < 1e-3 ? (({ midpoint: _m, ...rest }) => rest)(s) : { ...s, midpoint: m })) };\n\t\t\t}\n\t\t}\n\t\tthis.doc = this.withGradient(base, id, target, next);\n\t\tthis.notify(true);\n\t}\n\n\tprivate upGradient(drag: Extract<Drag, { kind: 'gradient' }>) {\n\t\tconst part = drag.part;\n\t\tif (!drag.moved) {\n\t\t\tif (part?.kind === 'bar') {\n\t\t\t\tthis.addGradientStop(part.offset);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tconst label = !part\n\t\t\t? 'Gradient'\n\t\t\t: part.kind === 'stop'\n\t\t\t\t? drag.stop === null\n\t\t\t\t\t? 'Delete Gradient Stop'\n\t\t\t\t\t: 'Move Gradient Stop'\n\t\t\t\t: part.kind === 'midpoint'\n\t\t\t\t\t? 'Gradient Midpoint'\n\t\t\t\t\t: part.kind === 'bar' || (part.kind === 'handle' && part.handle === 'start')\n\t\t\t\t\t\t? 'Move Gradient'\n\t\t\t\t\t\t: 'Gradient Handle';\n\t\treturn this.commit(label, drag.before);\n\t}\n\n\t/** Removes one width point of a path's profile. */\n\tremoveWidthPoint(ref: Pick<VectorWidthPointRef, 'nodeId' | 'index'>): VectorActionResult {\n\t\treturn this.removeWidthPoints([ref]);\n\t}\n\n\t/** Removes width points (default: the selected ones — the width tool's Delete), as one undo step. */\n\tremoveWidthPoints(refs: readonly Pick<VectorWidthPointRef, 'nodeId' | 'index'>[] = this.widthPoints): VectorActionResult {\n\t\tconst byNode = new Map<string, number[]>();\n\t\tfor (const r of refs) {\n\t\t\tconst node = this.getNode(r.nodeId);\n\t\t\tif (node?.type !== 'path' || !node.strokeWidthProfile?.[r.index]) continue;\n\t\t\tbyNode.set(r.nodeId, [...(byNode.get(r.nodeId) ?? []), r.index]);\n\t\t}\n\t\tif (!byNode.size) return { ok: false, message: 'There is no such width point.' };\n\t\tconst count = [...byNode.values()].reduce((n, l) => n + new Set(l).size, 0);\n\t\tthis.widthPoints = [];\n\t\tthis.widthEdit = null;\n\t\tthis.change(count > 1 ? 'Delete Width Points' : 'Delete Width Point', () => {\n\t\t\tfor (const [id, indices] of byNode) {\n\t\t\t\tconst node = this.getNode(id) as VectorPathNode;\n\t\t\t\tconst rest = removeWidthPoints(sanitizeWidthProfile(node.strokeWidthProfile), indices);\n\t\t\t\tthis.replaceNode(id, withStyle(node, { strokeWidthProfile: rest.length ? rest : null }));\n\t\t\t}\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** Selects width points (width tool); points on other paths than the first are dropped. */\n\tselectWidthPoints(refs: readonly VectorWidthPointRef[]): void {\n\t\tconst first = refs[0]?.nodeId;\n\t\tconst next: VectorWidthPointRef[] = [];\n\t\tfor (const r of refs) {\n\t\t\tif (r.nodeId !== first || next.some((q) => q.index === r.index)) continue;\n\t\t\tconst n = this.getNode(r.nodeId);\n\t\t\tif (n?.type === 'path' && n.strokeWidthProfile?.[r.index]) next.push({ nodeId: r.nodeId, index: r.index, subpath: r.subpath ?? 0 });\n\t\t}\n\t\tthis.widthPoints = next;\n\t\tthis.notify();\n\t}\n\n\t/**\n\t * Sets one width point numerically: `index` names an existing point of path `nodeId`, null\n\t * (or left out) adds one at `values.t`. `left` / `right` are side widths in document units\n\t * (how far the stroke reaches on each side of the path), `t` the position along each subpath\n\t * (0–1). A point moved onto another one's `t` makes a step there. Selects the point and\n\t * refreshes an open width-point editor; one undo step. Ported from ArtCraft vectorcraft's\n\t * Width Point Edit dialog (`crates/tools/src/distort/width.rs`, MIT OR Apache-2.0).\n\t */\n\tsetWidthPoint(target: { nodeId: string; index?: number | null }, values: { t?: number; left?: number; right?: number }): VectorActionResult {\n\t\tconst node = this.getNode(target.nodeId);\n\t\tif (node?.type !== 'path') return { ok: false, message: 'There is no such path.' };\n\t\tconst g = vectorStrokeWidthGeometry(node);\n\t\tif (!g || !(g.unit > 0)) return { ok: false, message: 'The path has no visible stroke.' };\n\t\tconst num = (v: unknown) => (typeof v === 'number' && Number.isFinite(v) ? v : undefined);\n\t\tconst t = num(values.t);\n\t\tconst left = num(values.left);\n\t\tconst right = num(values.right);\n\t\tconst scale = (w: number | undefined) => (w === undefined ? undefined : Math.max(0, w) / g.unit);\n\t\tconst profile = sanitizeWidthProfile(node.strokeWidthProfile);\n\t\tlet points: VectorWidthPoint[];\n\t\tlet index: number;\n\t\tconst existing = target.index ?? null;\n\t\tif (existing === null) {\n\t\t\tif (t === undefined) return { ok: false, message: 'A new width point needs a position t.' };\n\t\t\tif (profile.length >= VECTOR_WIDTH_PROFILE_MAX_POINTS) return { ok: false, message: 'The profile has too many width points.' };\n\t\t\t({ points, index } = insertWidthPoint(profile.length ? profile : null, t, { left: scale(left), right: scale(right) }, { step: true }));\n\t\t} else {\n\t\t\tconst p = profile[existing];\n\t\t\tif (!p) return { ok: false, message: 'There is no such width point.' };\n\t\t\tconst sides = { left: scale(left) ?? p.left, right: scale(right) ?? p.right };\n\t\t\tconst tt = t === undefined ? p.t : Math.min(Math.max(t, 0), 1);\n\t\t\tif (Math.abs(tt - p.t) <= VECTOR_WIDTH_SAME_T) {\n\t\t\t\tpoints = setWidthPoint(profile, existing, sides);\n\t\t\t\tindex = existing;\n\t\t\t} else {\n\t\t\t\t// Free to pass its neighbours: take it out and put it back at its new place.\n\t\t\t\tconst rest = removeWidthPoints(profile, [existing]);\n\t\t\t\tif (rest.length) ({ points, index } = insertWidthPoint(rest, tt, sides, { step: true }));\n\t\t\t\telse {\n\t\t\t\t\tpoints = [{ t: tt, ...sides }];\n\t\t\t\t\tindex = 0;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tconst subpath = this.widthEdit?.nodeId === node.id ? this.widthEdit.subpath : this.widthPoints.find((r) => r.nodeId === node.id)?.subpath ?? 0;\n\t\tconst edit = this.widthEdit;\n\t\tthis.change('Width Point', () => this.replaceNode(node.id, withStyle(node, { strokeWidthProfile: points })));\n\t\tthis.widthPoints = [{ nodeId: node.id, index, subpath }];\n\t\tif (edit?.nodeId === node.id && (edit.index === existing || existing === null)) this.widthEdit = this.widthEditFor(node.id, subpath, index, null);\n\t\tthis.notify();\n\t\treturn { ok: true };\n\t}\n\n\t/**\n\t * Opens the width-point editor (what a double-click with the width tool does) on a width\n\t * point, or on a spot of a stroke (`t` without `index`: applying it adds a point there).\n\t * Returns false when the path has no visible stroke.\n\t */\n\topenWidthPointEdit(target: { nodeId: string; subpath?: number; index?: number | null; t?: number }): boolean {\n\t\tconst edit = this.widthEditFor(target.nodeId, target.subpath ?? 0, target.index ?? null, target.t ?? null);\n\t\tif (!edit) return false;\n\t\tthis.widthEdit = edit;\n\t\tthis.widthPoints = edit.index === null ? [] : [{ nodeId: edit.nodeId, index: edit.index, subpath: edit.subpath }];\n\t\tthis.notify();\n\t\treturn true;\n\t}\n\n\t/** Closes the width-point editor without changing anything. */\n\tcloseWidthPointEdit(): void {\n\t\tif (!this.widthEdit) return;\n\t\tthis.widthEdit = null;\n\t\tthis.notify();\n\t}\n\n\t/** Applies `values` to the width point being edited (see `setWidthPoint`) and closes the editor. */\n\tcommitWidthPointEdit(values: { t?: number; left?: number; right?: number }): VectorActionResult {\n\t\tconst edit = this.widthEdit;\n\t\tif (!edit) return { ok: false, message: 'No width point is being edited.' };\n\t\tconst r = this.setWidthPoint({ nodeId: edit.nodeId, index: edit.index }, edit.index === null ? { t: edit.t, ...values } : values);\n\t\tif (r.ok) this.closeWidthPointEdit();\n\t\treturn r;\n\t}\n\n\t/** The editor's view of point `index` (or of the stroke at `t` when index is null) of `nodeId`. */\n\tprivate widthEditFor(nodeId: string, subpath: number, index: number | null, t: number | null): VectorWidthPointEdit | null {\n\t\tconst node = this.getNode(nodeId);\n\t\tif (node?.type !== 'path') return null;\n\t\tconst g = vectorStrokeWidthGeometry(node);\n\t\tif (!g) return null;\n\t\tconst profile = sanitizeWidthProfile(node.strokeWidthProfile);\n\t\tconst p = index === null ? null : profile[index];\n\t\tif (index !== null && !p) return null;\n\t\tconst tt = p ? p.t : Math.min(Math.max(t ?? 0, 0), 1);\n\t\tconst sides = p ?? widthProfileAt(profile.length ? profile : null, tt);\n\t\tconst sp = Math.min(Math.max(0, subpath), Math.max(0, node.subpaths.length - 1));\n\t\tconst at = strokePositionPoint(g.line, sp, tt);\n\t\tconst round = (v: number) => Math.round(v * 1e6) / 1e6;\n\t\tconst q = at ? applyMatrix(this.worldOf(nodeId), { x: at.point[0], y: at.point[1] }) : { x: 0, y: 0 };\n\t\tconst point = { x: round(q.x), y: round(q.y) };\n\t\treturn { nodeId, subpath: sp, index, t: round(tt), left: round(sides.left * g.unit), right: round(sides.right * g.unit), point };\n\t}\n\n\t/**\n\t * Sets the live corner radius (and optionally the kind) of anchors: `refs`, else the selected\n\t * anchors (direct tool), else every anchor of the `ids` / selected paths. Radius 0 makes the\n\t * corners sharp again. Corners shrink to fit when drawn, so any radius is accepted.\n\t */\n\tsetCornerRadius(radius: number, options: { kind?: VectorCornerKind; refs?: readonly VectorAnchorRef[]; ids?: readonly string[] } = {}): VectorActionResult {\n\t\tif (!Number.isFinite(radius) || radius < 0) return { ok: false, message: 'The radius must be a number of at least 0.' };\n\t\tconst refs = this.cornerTargets(options);\n\t\tif (!refs.length) return { ok: false, message: 'Nothing is selected.' };\n\t\tthis.change(radius > 0 ? 'Corner Radius' : 'Sharpen Corners', () => {\n\t\t\tthis.doc = this.updateAnchors(this.doc, refs, (a) => setAnchorCorner(a, radius, options.kind));\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** Sets the live corner kind of the same anchors as `setCornerRadius`, keeping their radii. */\n\tsetCornerKind(kind: VectorCornerKind, options: { refs?: readonly VectorAnchorRef[]; ids?: readonly string[] } = {}): VectorActionResult {\n\t\tconst refs = this.cornerTargets(options);\n\t\tif (!refs.length) return { ok: false, message: 'Nothing is selected.' };\n\t\tthis.change('Corner Kind', () => {\n\t\t\tthis.doc = this.updateAnchors(this.doc, refs, (a) => setAnchorCorner(a, a.corner?.radius ?? 0, kind));\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** Anchors a corner change applies to (see `setCornerRadius`). */\n\tprivate cornerTargets(options: { refs?: readonly VectorAnchorRef[]; ids?: readonly string[] }): VectorAnchorRef[] {\n\t\tif (options.refs) return options.refs.filter((r) => getAnchor(this.doc, r));\n\t\tif (!options.ids && this.tool === 'direct' && this.anchors.length) return this.anchors.slice();\n\t\tconst out: VectorAnchorRef[] = [];\n\t\tfor (const id of options.ids ?? this.selection) {\n\t\t\tconst node = this.getNode(id);\n\t\t\tif (!node) continue;\n\t\t\tfor (const p of collectPathNodes(node)) {\n\t\t\t\tif (p.locked) continue;\n\t\t\t\tp.subpaths.forEach((sp, s) => sp.anchors.forEach((_, i) => out.push({ nodeId: p.id, subpath: s, index: i })));\n\t\t\t}\n\t\t}\n\t\treturn out;\n\t}\n\n\t/**\n\t * Outline Stroke: replaces each selected (or given) path's stroke with a filled path of the\n\t * area it paints (alignment, joins, caps, dashes and arrowheads included), placed above a copy\n\t * of the path that keeps the fill (left out when there is none). Returns the new paths' ids.\n\t */\n\toutlineStroke(options: { ids?: readonly string[] } = {}): string[] {\n\t\tconst roots = options.ids ?? this.selectedRoots();\n\t\tconst strokesOf = (p: VectorPathNode) => vectorPaintLayerNodes(p).filter((l) => l.kind === 'stroke' && l.drawn && (l.node.strokeWidth ?? 1) > 0);\n\t\tconst paths: VectorPathNode[] = [];\n\t\tfor (const id of roots) {\n\t\t\tconst node = this.getNode(id);\n\t\t\tif (node) for (const p of collectPathNodes(node)) if (!p.locked && strokesOf(p).length) paths.push(p);\n\t\t}\n\t\tif (!paths.length) return [];\n\t\tconst created: string[] = [];\n\t\tconst replace = new Map<string, VectorNode[]>();\n\t\tfor (const p of paths) {\n\t\t\tconst { effects, ...rest } = cloneVectorNode(p);\n\t\t\tconst unstroked = withVectorStrokes(rest as VectorPathNode, []);\n\t\t\tfor (const key of VECTOR_STROKE_SETTING_KEYS) delete unstroked[key];\n\t\t\tconst list: VectorNode[] = vectorFills(p).length ? [{ ...unstroked, ...(effects ? { effects } : {}) }] : [];\n\t\t\t// One filled path per stroke layer, bottom to top, painted (and blended) like it.\n\t\t\tconst layers = strokesOf(p);\n\t\t\tfor (const l of layers) {\n\t\t\t\t// In the node's own space: the new paths keep its transform.\n\t\t\t\t// As drawn: path effects applied (the outline itself carries none).\n\t\t\t\tconst outline = bezierToSubpaths(vectorStrokeOutlinePath(vectorAppearanceOutline(p), l.node));\n\t\t\t\tif (!outline.length) continue;\n\t\t\t\tconst id = this.newId();\n\t\t\t\tconst { strokes: _s, fills: _f, ...plain } = unstroked;\n\t\t\t\tconst name = layers.length > 1 ? `${p.name ?? 'Path'} Stroke ${l.index + 1}` : `${p.name ?? 'Path'} Stroke`;\n\t\t\t\tconst fill: VectorPaintLayer = { paint: l.layer.paint };\n\t\t\t\tif (l.layer.opacity !== undefined) fill.opacity = l.layer.opacity;\n\t\t\t\tif (l.layer.blendMode) fill.blendMode = l.layer.blendMode;\n\t\t\t\tlist.push(withVectorFills({ ...plain, id, name, subpaths: outline, stroke: null, fillRule: 'nonzero' } as VectorPathNode, [fill]));\n\t\t\t\tcreated.push(id);\n\t\t\t}\n\t\t\treplace.set(p.id, list);\n\t\t}\n\t\tif (!created.length) return [];\n\t\tthis.change('Outline Stroke', () => {\n\t\t\tlet nodes = this.doc.nodes;\n\t\t\tfor (const [id, list] of replace) nodes = spliceTree(nodes, id, list);\n\t\t\tthis.setDoc(nodes);\n\t\t\tthis.selection = created;\n\t\t\tthis.anchors = [];\n\t\t\tthis.editingId = null;\n\t\t});\n\t\treturn created;\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Fill and stroke stacks, blend modes, filters\n\t// -------------------------------------------------------------------------\n\n\t/** Paths of the selected (or given) nodes whose stacks can be edited (groups give every path inside; blend steps are left out). */\n\tprivate stackTargets(ids: readonly string[] | undefined): VectorPathNode[] | string {\n\t\tconst list = ids ?? this.selection;\n\t\tif (!list.length) return 'Nothing is selected.';\n\t\tconst out = new Map<string, VectorPathNode>();\n\t\tfor (const id of list) {\n\t\t\tconst node = this.getNode(id);\n\t\t\tif (!node) return `There is no node \"${id}\".`;\n\t\t\tfor (const p of collectPathNodes(node)) if (!p.blendStep) out.set(p.id, p);\n\t\t}\n\t\tif (!out.size) return 'The selection has no paths (fills and strokes belong to paths).';\n\t\treturn [...out.values()];\n\t}\n\n\t/** Applies `fn` to the `kind` stack of every target path as one undo step (an error string from any path cancels it). */\n\tprivate editStacks(\n\t\tkind: 'fill' | 'stroke',\n\t\tids: readonly string[] | undefined,\n\t\tlabel: string,\n\t\tfn: (stack: VectorStrokeLayer[], path: VectorPathNode) => VectorStrokeLayer[] | string,\n\t\tmerge?: string\n\t): VectorActionResult {\n\t\tconst targets = this.stackTargets(ids);\n\t\tif (typeof targets === 'string') return { ok: false, message: targets };\n\t\tconst next = new Map<string, VectorNode>();\n\t\tfor (const path of targets) {\n\t\t\tconst stack = kind === 'fill' ? vectorFills(path) : vectorStrokes(path);\n\t\t\tconst result = fn(stack.map((l) => ({ ...l })), path);\n\t\t\tif (typeof result === 'string') return { ok: false, message: result };\n\t\t\tnext.set(path.id, kind === 'fill' ? withVectorFills(path, result) : withVectorStrokes(path, result));\n\t\t}\n\t\tthis.change(label, () => this.setDoc(mapTree(this.doc.nodes, (n) => next.get(n.id) ?? n)), merge);\n\t\treturn { ok: true };\n\t}\n\n\t/**\n\t * Reads a layer patch from untrusted input: paint, opacity, blend mode, visibility and (strokes)\n\t * the stroke settings, where `null` removes a setting. Returns an error string for invalid values.\n\t */\n\tprivate layerPatch(kind: 'fill' | 'stroke', patch: Record<string, unknown>): { set: Partial<VectorStrokeLayer>; unset: string[] } | string {\n\t\tconst set: Record<string, unknown> = {};\n\t\tconst unset: string[] = [];\n\t\tif ('paint' in patch) {\n\t\t\tconst paint = patch.paint;\n\t\t\tif (paint === null || (typeof paint === 'string' && paint.trim())) set.paint = paint === null ? null : (paint as string).trim();\n\t\t\telse {\n\t\t\t\tconst g = sanitizeVectorGradient(paint);\n\t\t\t\tif (!g) return 'paint must be a CSS colour, a gradient { type, stops: [{ offset, color }] } or null.';\n\t\t\t\tset.paint = g;\n\t\t\t}\n\t\t}\n\t\tif (patch.opacity !== undefined) {\n\t\t\tif (typeof patch.opacity !== 'number' || !Number.isFinite(patch.opacity)) return 'opacity must be a number from 0 to 1.';\n\t\t\tconst o = Math.min(1, Math.max(0, patch.opacity));\n\t\t\tif (o < 1) set.opacity = o;\n\t\t\telse unset.push('opacity');\n\t\t}\n\t\tif (patch.blendMode !== undefined) {\n\t\t\tif (!isVectorBlendMode(patch.blendMode)) return `Unknown blend mode \"${String(patch.blendMode)}\". Use one of: ${VECTOR_BLEND_MODES.join(', ')}.`;\n\t\t\tif (patch.blendMode !== 'normal') set.blendMode = patch.blendMode;\n\t\t\telse unset.push('blendMode');\n\t\t}\n\t\tif (patch.visible !== undefined) {\n\t\t\tif (typeof patch.visible !== 'boolean') return 'visible must be true or false.';\n\t\t\tif (patch.visible) unset.push('visible');\n\t\t\telse set.visible = false;\n\t\t}\n\t\tif (kind === 'stroke') {\n\t\t\tconst settings = sanitizeVectorStrokeSettings(patch);\n\t\t\tfor (const key of VECTOR_STROKE_SETTING_KEYS) {\n\t\t\t\tif (!(key in patch) || patch[key] === undefined) continue;\n\t\t\t\tif (patch[key] === null || (key === 'strokeDasharray' && Array.isArray(patch[key]) && !(patch[key] as unknown[]).length)) unset.push(key);\n\t\t\t\telse if (settings[key] === undefined) return `Invalid ${key}: ${JSON.stringify(patch[key])}.`;\n\t\t\t\telse set[key] = settings[key];\n\t\t\t}\n\t\t}\n\t\treturn { set: set as Partial<VectorStrokeLayer>, unset };\n\t}\n\n\t/**\n\t * Adds a fill or stroke layer to the selected (or given) paths (groups: every path inside), at\n\t * the top of the stack or at `index` (0 is the bottom). A new stroke takes the settings of the\n\t * stack's top stroke unless `layer` gives them. One undo step.\n\t */\n\taddPaintLayer(kind: 'fill' | 'stroke', layer: Record<string, unknown> = {}, options: { ids?: readonly string[]; index?: number } = {}): VectorActionResult {\n\t\tconst patch = this.layerPatch(kind, layer);\n\t\tif (typeof patch === 'string') return { ok: false, message: patch };\n\t\treturn this.editStacks(kind, options.ids, kind === 'fill' ? 'Add Fill' : 'Add Stroke', (stack, path) => {\n\t\t\tconst base: VectorStrokeLayer = kind === 'fill' ? { paint: '#808080' } : newVectorStrokeLayer(path, {});\n\t\t\tconst added = { ...base, ...patch.set } as VectorStrokeLayer;\n\t\t\tfor (const key of patch.unset) delete (added as unknown as Record<string, unknown>)[key];\n\t\t\tconst at = options.index === undefined ? stack.length : Math.min(stack.length, Math.max(0, Math.round(options.index)));\n\t\t\treturn [...stack.slice(0, at), added, ...stack.slice(at)];\n\t\t});\n\t}\n\n\t/**\n\t * Changes layer `index` (0 is the bottom) of the fill or stroke stack of the selected (or given)\n\t * paths: paint, opacity, blend mode, visibility and, for strokes, any stroke setting (`null`\n\t * removes it). Fails, changing nothing, when a path has no layer `index`. With `merge` (true, or\n\t * a key), consecutive edits of the layer fold into one undo step (a slider drag).\n\t */\n\tupdatePaintLayer(kind: 'fill' | 'stroke', index: number, patch: Record<string, unknown>, options: { ids?: readonly string[]; merge?: boolean | string } = {}): VectorActionResult {\n\t\tconst parsed = this.layerPatch(kind, patch);\n\t\tif (typeof parsed === 'string') return { ok: false, message: parsed };\n\t\tconst merge = options.merge === true ? `paint-layer:${kind}:${index}` : options.merge || undefined;\n\t\treturn this.editStacks(\n\t\t\tkind,\n\t\t\toptions.ids,\n\t\t\tkind === 'fill' ? 'Edit Fill' : 'Edit Stroke',\n\t\t\t(stack, path) => {\n\t\t\t\tif (!stack[index]) return `Path \"${path.id}\" has no ${kind} ${index} (it has ${stack.length}; 0 is the bottom).`;\n\t\t\t\tconst layer = { ...stack[index], ...parsed.set } as VectorStrokeLayer;\n\t\t\t\tfor (const key of parsed.unset) delete (layer as unknown as Record<string, unknown>)[key];\n\t\t\t\treturn stack.map((l, i) => (i === index ? layer : l));\n\t\t\t},\n\t\t\tmerge\n\t\t);\n\t}\n\n\t/** Removes layer `index` of the fill or stroke stack of the selected (or given) paths. */\n\tremovePaintLayer(kind: 'fill' | 'stroke', index: number, options: { ids?: readonly string[] } = {}): VectorActionResult {\n\t\treturn this.editStacks(kind, options.ids, kind === 'fill' ? 'Remove Fill' : 'Remove Stroke', (stack, path) =>\n\t\t\tstack[index] ? stack.filter((_, i) => i !== index) : `Path \"${path.id}\" has no ${kind} ${index} (it has ${stack.length}; 0 is the bottom).`\n\t\t);\n\t}\n\n\t/** Moves layer `from` of the fill or stroke stack to position `to` (0 is the bottom) on the selected (or given) paths. */\n\tmovePaintLayer(kind: 'fill' | 'stroke', from: number, to: number, options: { ids?: readonly string[] } = {}): VectorActionResult {\n\t\treturn this.editStacks(kind, options.ids, kind === 'fill' ? 'Reorder Fills' : 'Reorder Strokes', (stack, path) => {\n\t\t\tif (!stack[from]) return `Path \"${path.id}\" has no ${kind} ${from} (it has ${stack.length}; 0 is the bottom).`;\n\t\t\tconst out = stack.slice();\n\t\t\tconst [layer] = out.splice(from, 1);\n\t\t\tout.splice(Math.min(out.length, Math.max(0, Math.round(to))), 0, layer);\n\t\t\treturn out;\n\t\t});\n\t}\n\n\t/** Replaces the whole fill or stroke stack of the selected (or given) paths (malformed layers are dropped). */\n\tsetPaintLayers(kind: 'fill' | 'stroke', layers: readonly unknown[], options: { ids?: readonly string[] } = {}): VectorActionResult {\n\t\tconst clean = kind === 'fill' ? sanitizeVectorFills(layers) : sanitizeVectorStrokes(layers);\n\t\treturn this.editStacks(kind, options.ids, kind === 'fill' ? 'Set Fills' : 'Set Strokes', () => clean as VectorStrokeLayer[]);\n\t}\n\n\t/** Sets the blend mode of the selected (or given) nodes (paths or groups); `normal` removes it. */\n\tsetBlendMode(mode: VectorBlendMode, options: { ids?: readonly string[] } = {}): VectorActionResult {\n\t\tif (!isVectorBlendMode(mode)) return { ok: false, message: `Unknown blend mode \"${String(mode)}\". Use one of: ${VECTOR_BLEND_MODES.join(', ')}.` };\n\t\tconst ids = options.ids ?? this.selectedRoots();\n\t\tif (!ids.length) return { ok: false, message: 'Nothing is selected.' };\n\t\tfor (const id of ids) if (!this.getNode(id)) return { ok: false, message: `There is no node \"${id}\".` };\n\t\tconst set = new Set(ids);\n\t\tthis.change('Blend Mode', () =>\n\t\t\tthis.setDoc(\n\t\t\t\tmapTree(this.doc.nodes, (n) => {\n\t\t\t\t\tif (!set.has(n.id) || (n.blendMode ?? 'normal') === mode) return n;\n\t\t\t\t\tconst { blendMode: _b, ...rest } = n;\n\t\t\t\t\treturn (mode === 'normal' ? rest : { ...rest, blendMode: mode }) as VectorNode;\n\t\t\t\t})\n\t\t\t)\n\t\t);\n\t\treturn { ok: true };\n\t}\n\n\t/**\n\t * Adds a filter of `type` to the selected (or given) nodes, at the end of each stack (or at\n\t * `index`), with default parameters merged with `params`. One undo step; returns the new filter\n\t * ids (one per node, in order), empty for an unknown type or nothing selected.\n\t */\n\taddFilter(type: VectorFilterType, options: { ids?: readonly string[]; params?: Record<string, unknown>; index?: number } = {}): string[] {\n\t\tif (!isVectorFilterType(type)) return [];\n\t\tconst targets = this.effectTargets(options.ids);\n\t\tif (!targets.length) return [];\n\t\tconst created: string[] = [];\n\t\tconst next = new Map<string, VectorNode>();\n\t\tfor (const node of targets) {\n\t\t\tconst list = node.filters ?? [];\n\t\t\tlet k = list.length + 1;\n\t\t\twhile (list.some((f) => f.id === `filter${k}`)) k++;\n\t\t\tconst filter = createVectorFilter(type, `filter${k}`, options.params)!;\n\t\t\tconst at = options.index === undefined ? list.length : Math.min(list.length, Math.max(0, Math.round(options.index)));\n\t\t\tnext.set(node.id, { ...node, filters: [...list.slice(0, at), filter, ...list.slice(at)] } as VectorNode);\n\t\t\tcreated.push(filter.id);\n\t\t}\n\t\tthis.change(`Add ${VECTOR_FILTER_LABELS[type]}`, () => this.setDoc(mapTree(this.doc.nodes, (n) => next.get(n.id) ?? n)));\n\t\treturn created;\n\t}\n\n\tprivate editFilters(nodeId: string, label: string, fn: (filters: VectorFilter[]) => VectorFilter[] | string, merge?: string): VectorActionResult {\n\t\tconst node = this.getNode(nodeId);\n\t\tif (!node) return { ok: false, message: `There is no node \"${nodeId}\".` };\n\t\tconst result = fn(node.filters ?? []);\n\t\tif (typeof result === 'string') return { ok: false, message: result };\n\t\tthis.change(label, () => this.replaceNode(nodeId, (result.length ? { ...node, filters: result } : (({ filters: _f, ...rest }) => rest)(node)) as VectorNode), merge);\n\t\treturn { ok: true };\n\t}\n\n\t/**\n\t * Turns a filter on or off and changes its parameters (invalid values are ignored). With\n\t * `merge`, consecutive updates of the same filter fold into one undo step.\n\t */\n\tupdateFilter(nodeId: string, filterId: string, patch: { enabled?: boolean; params?: Record<string, unknown> }, options: { merge?: boolean | string } = {}): VectorActionResult {\n\t\tconst merge = options.merge === true ? `filter:${nodeId}:${filterId}` : options.merge || undefined;\n\t\tconst current = this.getNode(nodeId)?.filters?.find((f) => f.id === filterId);\n\t\treturn this.editFilters(\n\t\t\tnodeId,\n\t\t\t`Edit ${current ? VECTOR_FILTER_LABELS[current.type] : 'Filter'}`,\n\t\t\t(list) => {\n\t\t\t\tconst i = list.findIndex((f) => f.id === filterId);\n\t\t\t\tif (i < 0) return `Node \"${nodeId}\" has no filter \"${filterId}\".`;\n\t\t\t\tconst f = list[i];\n\t\t\t\tconst params = patch.params ? normalizeVectorFilterParams(f.type, patch.params, f.params as never) : f.params;\n\t\t\t\tconst next = { ...f, params, ...(patch.enabled !== undefined ? { enabled: patch.enabled } : {}) } as VectorFilter;\n\t\t\t\tif (next.enabled !== false) delete (next as { enabled?: boolean }).enabled;\n\t\t\t\treturn list.map((x, j) => (j === i ? next : x));\n\t\t\t},\n\t\t\tmerge\n\t\t);\n\t}\n\n\t/** Removes a filter from a node's stack. */\n\tremoveFilter(nodeId: string, filterId: string): VectorActionResult {\n\t\treturn this.editFilters(nodeId, 'Remove Filter', (list) => (list.some((f) => f.id === filterId) ? list.filter((f) => f.id !== filterId) : `Node \"${nodeId}\" has no filter \"${filterId}\".`));\n\t}\n\n\t/** Moves a filter to position `index` of its node's stack. */\n\tmoveFilter(nodeId: string, filterId: string, index: number): VectorActionResult {\n\t\treturn this.editFilters(nodeId, 'Reorder Filters', (list) => {\n\t\t\tconst i = list.findIndex((f) => f.id === filterId);\n\t\t\tif (i < 0) return `Node \"${nodeId}\" has no filter \"${filterId}\".`;\n\t\t\tconst out = list.slice();\n\t\t\tconst [f] = out.splice(i, 1);\n\t\t\tout.splice(Math.min(out.length, Math.max(0, Math.round(index))), 0, f);\n\t\t\treturn out;\n\t\t});\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Path effects\n\t// -------------------------------------------------------------------------\n\n\t/** Selected (or given) nodes that can carry effects: the roots of the selection. */\n\tprivate effectTargets(ids?: readonly string[]): VectorNode[] {\n\t\treturn (ids ?? this.selectedRoots()).map((id) => this.getNode(id)).filter((n): n is VectorNode => !!n && !n.blendStep);\n\t}\n\n\t/**\n\t * Adds an effect of `type` to the selected (or given) nodes, at the bottom of each stack (or\n\t * at `index`), with default parameters merged with `params`. A repeater's copies start one\n\t * shape width apart. One undo step; returns the new effect ids (one per node, in order).\n\t */\n\taddEffect(type: VectorEffectType, options: { ids?: readonly string[]; params?: Record<string, unknown>; index?: number } = {}): string[] {\n\t\tconst targets = this.effectTargets(options.ids);\n\t\tif (!targets.length || !createVectorEffect(type, '')) return [];\n\t\tconst created: string[] = [];\n\t\tconst next = new Map<string, VectorNode>();\n\t\tfor (const node of targets) {\n\t\t\tconst list = node.effects ?? [];\n\t\t\tlet k = list.length + 1;\n\t\t\twhile (list.some((e) => e.id === `fx${k}`)) k++;\n\t\t\tconst params: Record<string, unknown> = { ...(options.params ?? {}) };\n\t\t\tif (type === 'repeater' && params.position === undefined) {\n\t\t\t\tconst b = bezierPathBounds(vectorAppearanceOutline(node));\n\t\t\t\tif (b && b.maxX - b.minX > 0) params.position = { x: Math.round((b.maxX - b.minX) * 1.25 * 100) / 100, y: 0 };\n\t\t\t}\n\t\t\tconst effect = createVectorEffect(type, `fx${k}`, params)!;\n\t\t\tconst at = options.index === undefined ? list.length : Math.min(list.length, Math.max(0, Math.round(options.index)));\n\t\t\tnext.set(node.id, { ...node, effects: [...list.slice(0, at), effect, ...list.slice(at)] } as VectorNode);\n\t\t\tcreated.push(effect.id);\n\t\t}\n\t\tthis.change(`Add ${VECTOR_EFFECT_LABELS[type]}`, () => this.setDoc(mapTree(this.doc.nodes, (n) => next.get(n.id) ?? n)));\n\t\treturn created;\n\t}\n\n\tprivate editEffects(nodeId: string, label: string, fn: (effects: VectorEffect[]) => VectorEffect[] | string, merge?: string): VectorActionResult {\n\t\tconst node = this.getNode(nodeId);\n\t\tif (!node) return { ok: false, message: `There is no node \"${nodeId}\".` };\n\t\tconst result = fn(node.effects ?? []);\n\t\tif (typeof result === 'string') return { ok: false, message: result };\n\t\tthis.change(label, () => this.replaceNode(nodeId, (result.length ? { ...node, effects: result } : (({ effects: _e, ...rest }) => rest)(node)) as VectorNode), merge);\n\t\treturn { ok: true };\n\t}\n\n\t/**\n\t * Turns an effect on or off and changes its parameters (unknown or invalid values are\n\t * ignored). With `merge` (true, or a key of your own), consecutive updates of the same effect\n\t * fold into one undo step, as a slider drag should; call `endMergedEdit` when it ends.\n\t */\n\tupdateEffect(nodeId: string, effectId: string, patch: { enabled?: boolean; params?: Record<string, unknown> }, options: { merge?: boolean | string } = {}): VectorActionResult {\n\t\tconst merge = options.merge === true ? `effect:${nodeId}:${effectId}` : options.merge || undefined;\n\t\tconst node = this.getNode(nodeId);\n\t\tconst current = node?.effects?.find((e) => e.id === effectId);\n\t\tconst label = current ? VECTOR_EFFECT_LABELS[current.type] ?? 'Effect' : 'Effect';\n\t\treturn this.editEffects(\n\t\t\tnodeId,\n\t\t\t`Edit ${label}`,\n\t\t\t(list) => {\n\t\t\t\tconst i = list.findIndex((e) => e.id === effectId);\n\t\t\t\tif (i < 0) return `Node \"${nodeId}\" has no effect \"${effectId}\".`;\n\t\t\t\tconst e = list[i];\n\t\t\t\tconst raw = patch.params && e.type === 'envelope' && node ? this.envelopeParams(node, e, patch.params) : patch.params;\n\t\t\t\tconst params = raw ? normalizeVectorEffectParams(e.type, raw, e.params as never) : e.params;\n\t\t\t\tconst next = { ...e, params, ...(patch.enabled !== undefined ? { enabled: patch.enabled } : {}) } as VectorEffect;\n\t\t\t\tif (next.enabled !== false) delete (next as { enabled?: boolean }).enabled;\n\t\t\t\treturn list.map((x, j) => (j === i ? next : x));\n\t\t\t},\n\t\t\tmerge\n\t\t);\n\t}\n\n\t/** Removes an effect from a node's stack. */\n\tremoveEffect(nodeId: string, effectId: string): VectorActionResult {\n\t\treturn this.editEffects(nodeId, 'Remove Effect', (list) => (list.some((e) => e.id === effectId) ? list.filter((e) => e.id !== effectId) : `Node \"${nodeId}\" has no effect \"${effectId}\".`));\n\t}\n\n\t/** Moves an effect to position `index` of its stack (0 is applied first). */\n\tmoveEffect(nodeId: string, effectId: string, index: number): VectorActionResult {\n\t\treturn this.editEffects(nodeId, 'Reorder Effects', (list) => {\n\t\t\tconst i = list.findIndex((e) => e.id === effectId);\n\t\t\tif (i < 0) return `Node \"${nodeId}\" has no effect \"${effectId}\".`;\n\t\t\tconst to = Math.min(list.length - 1, Math.max(0, Math.round(index)));\n\t\t\tconst out = list.slice();\n\t\t\tconst [e] = out.splice(i, 1);\n\t\t\tout.splice(to, 0, e);\n\t\t\treturn out;\n\t\t});\n\t}\n\n\t/**\n\t * Expand Appearance: bakes the path effects and live booleans of the selected (or given)\n\t * nodes, and of everything inside them, into plain paths. A node keeps its id (a group when\n\t * it draws as several shapes); the shapes it made get new ids. Returns the ids of the expanded\n\t * nodes (empty when none was live).\n\t */\n\texpandAppearance(options: { ids?: readonly string[] } = {}): string[] {\n\t\tconst roots = (options.ids ?? this.selectedRoots()).map((id) => this.getNode(id)).filter((n): n is VectorNode => !!n && hasVectorAppearance(n));\n\t\tif (!roots.length) return [];\n\t\tconst fresh = (n: VectorNode): VectorNode => {\n\t\t\tconst id = n.id.includes('~fx') ? this.newId() : n.id;\n\t\t\treturn n.type === 'group' ? { ...n, id, children: n.children.map(fresh) } : { ...n, id };\n\t\t};\n\t\tconst bake = (n: VectorNode): VectorNode => {\n\t\t\tif (!hasVectorAppearance(n)) return n;\n\t\t\tif (!hasOwnVectorAppearance(n)) return { ...(n as VectorGroupNode), children: (n as VectorGroupNode).children.map(bake) };\n\t\t\tlet e = expandVectorAppearance(n);\n\t\t\t// A live group that draws as one shape becomes that path, in the group's place.\n\t\t\tif (e.type === 'group' && e.children.length === 1 && e.children[0].type === 'path') {\n\t\t\t\tconst { id: _id, opacity, name, ...child } = e.children[0];\n\t\t\t\tconst k = (e.opacity ?? 1) * (opacity ?? 1);\n\t\t\t\te = { ...child, id: e.id, name: e.name ?? name, ...placementOf(e), ...(k < 1 ? { opacity: k } : {}) } as VectorPathNode;\n\t\t\t}\n\t\t\treturn fresh(e);\n\t\t};\n\t\tconst next = new Map(roots.map((n) => [n.id, bake(n)]));\n\t\tthis.change('Expand Appearance', () => {\n\t\t\tthis.setDoc(mapTree(this.doc.nodes, (n) => next.get(n.id) ?? n));\n\t\t\tthis.anchors = [];\n\t\t\tthis.editingId = null;\n\t\t});\n\t\treturn roots.map((n) => n.id);\n\t}\n\n\trename(id: string, name: string): VectorActionResult {\n\t\treturn this.updateNode(id, { name }, { label: 'Rename' });\n\t}\n\n\tsetVisible(id: string, visible: boolean): VectorActionResult {\n\t\treturn this.updateNode(id, { visible }, { label: visible ? 'Show' : 'Hide' });\n\t}\n\n\tsetLocked(id: string, locked: boolean): VectorActionResult {\n\t\tconst result = this.updateNode(id, { locked }, { label: locked ? 'Lock' : 'Unlock' });\n\t\tif (locked && result.ok && this.selection.includes(id)) {\n\t\t\tthis.selection = this.selection.filter((s) => s !== id);\n\t\t\tthis.notify();\n\t\t}\n\t\treturn result;\n\t}\n\n\t/** Removes nodes by id. */\n\tremoveNodes(ids: readonly string[], options: { label?: string } = {}): VectorActionResult {\n\t\tconst drop = new Set(ids.filter((id) => this.getNode(id)));\n\t\tif (!drop.size) return { ok: false, message: 'Nothing to delete.' };\n\t\tthis.change(options.label ?? 'Delete', () => {\n\t\t\t// Deleting a mask releases its group (the next child must not become the mask).\n\t\t\tthis.setDoc(mapTree(this.doc.nodes, (n) => (drop.has(n.id) ? null : isVectorMaskGroup(n) && drop.has(n.children[0].id) ? { ...n, mask: null } : n)));\n\t\t\tthis.selection = this.selection.filter((id) => !drop.has(id) && this.getNode(id));\n\t\t\tthis.anchors = this.anchors.filter((r) => getAnchor(this.doc, r));\n\t\t\tif (this.editingId && !this.getNode(this.editingId)) this.editingId = null;\n\t\t\tif (this.pen && !this.getNode(this.pen.nodeId)) this.pen = null;\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Selection\n\t// -------------------------------------------------------------------------\n\n\tselect(ids: readonly string[], options: { add?: boolean; toggle?: boolean } = {}): void {\n\t\tconst valid = ids.filter((id) => this.getNode(id));\n\t\tlet next: string[];\n\t\tif (options.toggle) {\n\t\t\tconst set = new Set(this.selection);\n\t\t\tfor (const id of valid) set.has(id) ? set.delete(id) : set.add(id);\n\t\t\tnext = [...set];\n\t\t} else next = options.add ? [...new Set([...this.selection, ...valid])] : valid;\n\t\tthis.selection = next;\n\t\tif (!options.add && !options.toggle) this.anchors = this.anchors.filter((r) => next.includes(r.nodeId));\n\t\tthis.notify();\n\t}\n\n\tselectAll(): void {\n\t\tif (this.tool === 'direct' && this.editingId) {\n\t\t\tthis.anchors = getVisibleAnchors(this.view());\n\t\t} else {\n\t\t\tthis.selection = this.doc.nodes.filter((n) => !n.locked && n.visible !== false).map((n) => n.id);\n\t\t}\n\t\tthis.notify();\n\t}\n\n\tclearSelection(): void {\n\t\tif (!this.selection.length && !this.anchors.length && !this.editingId) return;\n\t\tthis.selection = [];\n\t\tthis.anchors = [];\n\t\tthis.editingId = null;\n\t\tthis.notify();\n\t}\n\n\tselectAnchors(refs: readonly VectorAnchorRef[], options: { add?: boolean } = {}): void {\n\t\tconst valid = refs.filter((r) => getAnchor(this.doc, r));\n\t\tthis.anchors = options.add ? [...this.anchors, ...valid.filter((r) => !this.anchors.some((a) => sameRef(a, r)))] : valid;\n\t\tif (valid.length && !this.editingId) this.editingId = valid[0].nodeId;\n\t\tthis.notify();\n\t}\n\n\t/** Top-level ids of the selection (members of selected groups are covered by the group). */\n\tprivate selectedRoots(): string[] {\n\t\treturn this.selection.filter((id) => {\n\t\t\tlet covered = false;\n\t\t\tconst loc = locateVectorNode(this.doc.nodes, id);\n\t\t\tlet parent = loc?.parent ?? null;\n\t\t\twhile (parent) {\n\t\t\t\tif (this.selection.includes(parent.id)) covered = true;\n\t\t\t\tparent = locateVectorNode(this.doc.nodes, parent.id)?.parent ?? null;\n\t\t\t}\n\t\t\treturn !covered;\n\t\t});\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Transforms\n\t// -------------------------------------------------------------------------\n\n\t/** `m`, given in a frame rotated by `angle`, as a document-space matrix. */\n\tprivate inFrame(angle: number, m: AffineMatrix): AffineMatrix {\n\t\tif (!angle) return m;\n\t\treturn multiplyMatrices(multiplyMatrices(rotateMatrix(-angle), m), rotateMatrix(angle));\n\t}\n\n\t/** The selection frame's corners in document space, clockwise from the top left. */\n\tprivate frameCorners(frame: VectorSelectionFrame): Vec2[] {\n\t\tconst rot = rotateMatrix(frame.angle);\n\t\treturn vectorBoxCorners(frame).map((c) => (frame.angle ? applyMatrix(rot, c) : c));\n\t}\n\n\t/** `doc` with nodes `ids` sent through `f` (a map of document space), each in its own space (see `distortVectorNode`). */\n\tprivate distorted(doc: VectorDocument, ids: ReadonlySet<string>, f: (p: Vec2) => Vec2, tolerance = VECTOR_DISTORT_TOLERANCE): VectorDocument {\n\t\tconst visit = (list: readonly VectorNode[], parent: AffineMatrix): VectorNode[] => {\n\t\t\tlet changed = false;\n\t\t\tconst out = list.map((n) => {\n\t\t\t\tlet next = n;\n\t\t\t\tif (ids.has(n.id) && !n.locked) next = distortVectorNode(n, parent, f, tolerance);\n\t\t\t\telse if (n.type === 'group') {\n\t\t\t\t\tconst children = visit(n.children, multiplyMatrices(nodeMatrix(n), parent));\n\t\t\t\t\tif (children !== n.children) next = { ...n, children };\n\t\t\t\t}\n\t\t\t\tif (next !== n) changed = true;\n\t\t\t\treturn next;\n\t\t\t});\n\t\t\treturn changed ? out : (list as VectorNode[]);\n\t\t};\n\t\tconst nodes = visit(doc.nodes, IDENTITY_MATRIX);\n\t\treturn nodes === doc.nodes ? doc : { ...doc, nodes };\n\t}\n\n\t/**\n\t * Free Distort mode of the selection box (`state.freeDistort`): dragging a corner distorts the\n\t * selection in perspective, each corner on its own (Shift+Alt+Mod: symmetric perspective, the\n\t * corner beside it moving the other way); dragging a side moves its two corners; Mod-dragging a\n\t * side skews along it (Alt: about the centre). Each drag is one undo step.\n\t */\n\tsetFreeDistort(on: boolean): void {\n\t\tif (this.freeDistort === on) return;\n\t\tthis.freeDistort = on;\n\t\tthis.notify();\n\t}\n\n\t/**\n\t * Distorts the selected (or given) nodes so the corners of their selection box (clockwise from\n\t * the top left, in the box's own rotation) land on `corners` (document space): a projective map,\n\t * so straight lines stay straight. Curves are followed within `tolerance` (document units).\n\t * One undo step.\n\t */\n\tdistort(corners: readonly Vec2[], options: { ids?: readonly string[]; tolerance?: number } = {}): VectorActionResult {\n\t\tconst ids = options.ids ?? this.selectedRoots();\n\t\tconst frame = this.frameOf(ids);\n\t\tif (!frame) return { ok: false, message: 'Nothing is selected.' };\n\t\tif (!(frame.width > 0 && frame.height > 0)) return { ok: false, message: 'The selection has no area to distort.' };\n\t\tconst from = this.frameCorners(frame);\n\t\tconst h = corners.length === 4 ? vectorHomography(from, corners) : null;\n\t\tif (!h || !vectorHomographyKeepsQuad(h, from)) return { ok: false, message: 'The four corners must make a convex quadrilateral, clockwise from the top left.' };\n\t\tthis.change('Distort', () => {\n\t\t\tthis.doc = this.distorted(this.doc, new Set(ids), (p) => applyVectorHomography(h, p) ?? p, options.tolerance);\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/**\n\t * Skews the selected (or given) nodes along the selection box's own axes by `ax` degrees\n\t * horizontally (x' = x + tan(ax)·y: vertical lines lean right for positive angles) and `ay`\n\t * vertically, about `origin` (default: the box centre, document space). One undo step.\n\t */\n\tskew(ax: number, ay = 0, options: { ids?: readonly string[]; origin?: Vec2 } = {}): VectorActionResult {\n\t\tconst frame = this.frameOf(options.ids ?? this.selectedRoots());\n\t\tif (!frame) return { ok: false, message: 'Nothing is selected.' };\n\t\tif (![ax, ay].every((v) => Number.isFinite(v) && Math.abs(v) < 89.9)) return { ok: false, message: 'Skew angles must be between −89.9° and 89.9°.' };\n\t\tconst origin = applyMatrix(rotateMatrix(-frame.angle), options.origin ?? this.frameCenter(frame));\n\t\treturn this.transform(this.inFrame(frame.angle, vectorSkewMatrix(ax, ay, origin)), { ids: options.ids, label: 'Skew' });\n\t}\n\n\t/** A selection box handle dragged in Free Distort mode. */\n\tprivate moveDistort(drag: Extract<Drag, { kind: 'scale' }>, p: Vec2, input: VectorPointerInput) {\n\t\tconst angle = drag.frame.angle;\n\t\tconst unrotate = (v: Vec2) => (angle ? applyMatrix(rotateMatrix(-angle), v) : v);\n\t\tconst q = unrotate(p);\n\t\tconst q0 = unrotate(drag.start);\n\t\tconst d = { x: q.x - q0.x, y: q.y - q0.y };\n\t\tconst ids = new Set(this.selectedRoots());\n\t\tconst from = this.frameCorners(drag.frame);\n\t\tconst corner = drag.handle.length === 2;\n\t\tthis.setGuides(null);\n\t\tif (!corner && input.mod) {\n\t\t\tconst m = this.inFrame(angle, vectorShearForSide(drag.frame, drag.handle, d, !!input.alt));\n\t\t\tdrag.op = 'Skew';\n\t\t\tthis.doc = this.transformed(drag.base, ids, m);\n\t\t\tthis.interaction = { type: 'distort', mode: 'skew', corners: from.map((c) => applyMatrix(m, c)) };\n\t\t\tthis.notify(true);\n\t\t\treturn;\n\t\t}\n\t\tconst mode: VectorDistortMode = corner && input.mod && input.alt && input.shift ? 'perspective' : 'distort';\n\t\tconst rot = rotateMatrix(angle);\n\t\tconst quad = vectorDistortQuad(drag.frame, drag.handle, d, mode).map((c) => (angle ? applyMatrix(rot, c) : c));\n\t\tconst h = vectorHomography(from, quad);\n\t\t// A quad that folds over keeps the last good shape.\n\t\tif (!h || !vectorHomographyKeepsQuad(h, from)) return;\n\t\tdrag.op = mode === 'perspective' ? 'Perspective' : 'Distort';\n\t\tthis.doc = this.distorted(drag.base, ids, (pt) => applyVectorHomography(h, pt) ?? pt);\n\t\tthis.interaction = { type: 'distort', mode, corners: quad };\n\t\tthis.notify(true);\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Envelopes\n\t// -------------------------------------------------------------------------\n\n\t/** Selected (or given) roots that can carry an envelope. */\n\tprivate envelopeTargets(ids?: readonly string[]): VectorNode[] {\n\t\treturn this.effectTargets(ids).filter((n) => !n.locked);\n\t}\n\n\t/** Envelope parameters for `node` after `patch`: a preset switched to a mesh becomes the mesh it stood for. */\n\tprivate envelopeParams(node: VectorNode, current: Extract<VectorEffect, { type: 'envelope' }> | null, patch: Record<string, unknown>): Record<string, unknown> {\n\t\tif (patch.style !== 'mesh' || patch.mesh !== undefined || !current || current.params.style === 'mesh') return patch;\n\t\tconst frame = vectorEnvelopeFrame(node, current.id);\n\t\tif (!frame) return patch;\n\t\tconst rows = typeof patch.rows === 'number' ? patch.rows : current.params.rows;\n\t\tconst columns = typeof patch.columns === 'number' ? patch.columns : current.params.columns;\n\t\treturn { ...patch, mesh: resizeVectorMesh(frame.mesh, columns, rows) };\n\t}\n\n\t/**\n\t * Gives the selected (or given) nodes an envelope distortion, or changes the one they have: a\n\t * warp preset (`style` with `bend`, `horizontal` and `vertical` in percent and `orientation`) or\n\t * a mesh (`style: 'mesh'` with `rows` and `columns`, and optionally its `mesh` points). The\n\t * envelope stays live (an `envelope` effect: edit it with the Envelope tool, Expand Appearance\n\t * bakes it). One undo step (with `merge`, consecutive changes fold into one, as a slider drag\n\t * should). Returns the envelope of each node.\n\t */\n\tenvelope(params: Partial<VectorEnvelopeParams> = {}, options: { ids?: readonly string[]; merge?: boolean } = {}): { nodeId: string; effectId: string }[] {\n\t\tconst targets = this.envelopeTargets(options.ids);\n\t\tif (!targets.length) return [];\n\t\tconst out: { nodeId: string; effectId: string }[] = [];\n\t\tconst next = new Map<string, VectorNode>();\n\t\tconst raw = params as Record<string, unknown>;\n\t\tfor (const node of targets) {\n\t\t\tconst list = node.effects ?? [];\n\t\t\tconst current = (list.find((e) => e.type === 'envelope') as Extract<VectorEffect, { type: 'envelope' }> | undefined) ?? null;\n\t\t\tif (current) {\n\t\t\t\tconst p = normalizeVectorEffectParams('envelope', this.envelopeParams(node, current, raw), current.params)!;\n\t\t\t\tconst { enabled: _e, ...rest } = current;\n\t\t\t\tnext.set(node.id, { ...node, effects: list.map((e) => (e === current ? { ...rest, params: p } : e)) } as VectorNode);\n\t\t\t\tout.push({ nodeId: node.id, effectId: current.id });\n\t\t\t} else {\n\t\t\t\tlet k = list.length + 1;\n\t\t\t\twhile (list.some((e) => e.id === `fx${k}`)) k++;\n\t\t\t\tconst effect = createVectorEffect('envelope', `fx${k}`, raw)!;\n\t\t\t\tnext.set(node.id, { ...node, effects: [...list, effect] } as VectorNode);\n\t\t\t\tout.push({ nodeId: node.id, effectId: effect.id });\n\t\t\t}\n\t\t}\n\t\tconst made = out.some((o) => !targets.find((n) => n.id === o.nodeId)?.effects?.some((e) => e.id === o.effectId));\n\t\tconst merge = options.merge ? `envelope:${out.map((o) => o.nodeId).join(',')}` : undefined;\n\t\tthis.change(made ? (params.style === 'mesh' ? 'Make Envelope with Mesh' : 'Make Envelope with Warp') : 'Edit Envelope', () => this.setDoc(mapTree(this.doc.nodes, (n) => next.get(n.id) ?? n)), merge);\n\t\treturn out;\n\t}\n\n\t/**\n\t * Make with Top Object: the topmost selected object's outline (a closed path) becomes the envelope\n\t * of the other selected objects (grouped when there are several; they must share a parent), as a\n\t * mesh of `columns × rows` patches whose border follows the outline. The top object is used up.\n\t * One undo step; returns the id of the enveloped node.\n\t */\n\tmakeEnvelopeWithTopObject(options: { ids?: readonly string[]; rows?: number; columns?: number } = {}): VectorActionResult & { nodeId?: string } {\n\t\tconst ids = options.ids ?? this.selectedRoots();\n\t\tif (ids.length < 2) return { ok: false, message: 'Select the objects and, on top of them, the shape to use as the envelope.' };\n\t\tconst order = new Map<string, number>();\n\t\twalkVectorNodes(this.doc.nodes, (n) => order.set(n.id, order.size));\n\t\tconst sorted = ids.filter((id) => order.has(id)).sort((a, b) => order.get(a)! - order.get(b)!);\n\t\tconst topId = sorted[sorted.length - 1];\n\t\tconst top = this.getNode(topId);\n\t\tconst outline = top?.type === 'path' ? top.subpaths.find((sp) => sp.closed && sp.anchors.length > 1) : undefined;\n\t\tif (!top || !outline) return { ok: false, message: 'The top object must be a path with a closed outline.' };\n\t\tconst contents = sorted.slice(0, -1).map((id) => this.getNode(id)!);\n\t\tif (contents.some((n) => n.locked)) return { ok: false, message: 'Locked objects cannot be enveloped.' };\n\t\tconst parents = contents.map((n) => locateVectorNode(this.doc.nodes, n.id)?.parent?.id ?? null);\n\t\tif (parents.some((p) => p !== parents[0])) return { ok: false, message: 'The objects to envelope must be in the same group.' };\n\t\t// The node that gets the envelope: the one object, or a new group of them in place of the topmost.\n\t\tconst target: VectorNode =\n\t\t\tcontents.length === 1 ? contents[0] : { id: this.newId(), type: 'group', name: 'Envelope', children: contents.map((n) => n) };\n\t\tconst parentToDoc = parents[0] ? this.worldOf(parents[0]) : IDENTITY_MATRIX;\n\t\tconst targetToDoc = multiplyMatrices(nodeMatrix(target), parentToDoc);\n\t\tconst toLocal = multiplyMatrices(this.worldOf(topId), invertMatrix(targetToDoc));\n\t\tconst local = {\n\t\t\tclosed: true,\n\t\t\tanchors: outline.anchors.map((a) => ({\n\t\t\t\t...applyMatrix(toLocal, a),\n\t\t\t\t...(a.in ? { in: applyMatrix(toLocal, a.in) } : {}),\n\t\t\t\t...(a.out ? { out: applyMatrix(toLocal, a.out) } : {})\n\t\t\t}))\n\t\t};\n\t\tconst mesh = vectorMeshFromOutline(local, options.columns ?? 4, options.rows ?? 4);\n\t\tif (!mesh) return { ok: false, message: 'The top object’s outline is too small to use as an envelope.' };\n\t\tconst list = target.effects ?? [];\n\t\tlet k = list.length + 1;\n\t\twhile (list.some((e) => e.id === `fx${k}`)) k++;\n\t\tconst effect = createVectorEffect('envelope', `fx${k}`, { style: 'mesh', mesh })!;\n\t\tconst enveloped = { ...target, effects: [...list, effect] } as VectorNode;\n\t\tthis.change('Make Envelope with Top Object', () => {\n\t\t\tlet nodes = spliceTree(this.doc.nodes, topId, []);\n\t\t\tif (contents.length === 1) nodes = spliceTree(nodes, target.id, [enveloped]);\n\t\t\telse {\n\t\t\t\tconst last = contents[contents.length - 1].id;\n\t\t\t\tfor (const n of contents.slice(0, -1)) nodes = spliceTree(nodes, n.id, []);\n\t\t\t\tnodes = spliceTree(nodes, last, [enveloped]);\n\t\t\t}\n\t\t\tthis.setDoc(nodes);\n\t\t\tthis.selection = [target.id];\n\t\t\tthis.anchors = [];\n\t\t\tthis.editingId = null;\n\t\t});\n\t\treturn { ok: true, nodeId: target.id };\n\t}\n\n\t/**\n\t * Release: the selected (or given) nodes lose their envelope, and its shape (the mesh's border)\n\t * comes back as a path above each of them. One undo step; returns the new paths' ids.\n\t */\n\treleaseEnvelope(options: { ids?: readonly string[] } = {}): string[] {\n\t\tconst targets = this.envelopeTargets(options.ids).filter((n) => n.effects?.some((e) => e.type === 'envelope'));\n\t\tif (!targets.length) return [];\n\t\tconst created: string[] = [];\n\t\tconst replace = new Map<string, VectorNode[]>();\n\t\tfor (const node of targets) {\n\t\t\tconst effect = node.effects!.find((e) => e.type === 'envelope')!;\n\t\t\tconst frame = vectorEnvelopeFrame(node, effect.id);\n\t\t\tconst rest = node.effects!.filter((e) => e !== effect);\n\t\t\tconst { effects: _e, ...plain } = node;\n\t\t\tconst kept = (rest.length ? { ...plain, effects: rest } : plain) as VectorNode;\n\t\t\tif (!frame) {\n\t\t\t\treplace.set(node.id, [kept]);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst id = this.newId();\n\t\t\tcreated.push(id);\n\t\t\tconst shape: VectorPathNode = {\n\t\t\t\tid,\n\t\t\t\ttype: 'path',\n\t\t\t\tname: 'Envelope',\n\t\t\t\t...(node.transform ? { transform: node.transform } : {}),\n\t\t\t\tsubpaths: [vectorMeshOutline(frame.mesh)],\n\t\t\t\tfill: null,\n\t\t\t\tstroke: '#808080',\n\t\t\t\tstrokeWidth: 1\n\t\t\t};\n\t\t\treplace.set(node.id, [kept, shape]);\n\t\t}\n\t\tthis.change('Release Envelope', () => {\n\t\t\tlet nodes = this.doc.nodes;\n\t\t\tfor (const [id, list] of replace) nodes = spliceTree(nodes, id, list);\n\t\t\tthis.setDoc(nodes);\n\t\t\tthis.selection = [...targets.map((n) => n.id), ...created];\n\t\t\tthis.anchors = [];\n\t\t});\n\t\treturn created;\n\t}\n\n\t/** The selected node the Envelope tool edits, with its mesh (node space) and node → document matrix. */\n\tprivate envelopeTarget() {\n\t\tconst target = getVectorEnvelopeTarget(this.doc, this.selection);\n\t\treturn target && !target.node.locked ? target : null;\n\t}\n\n\t/** The envelope mesh point under `screen` (handles first), or a spot inside the mesh. */\n\tprivate hitEnvelope(screen: Vec2, p: Vec2): { target: NonNullable<ReturnType<VectorEditorController['envelopeTarget']>>; index: number | null; s: number; t: number } | null {\n\t\tconst target = this.envelopeTarget();\n\t\tif (!target) return null;\n\t\tconst { mesh } = target.frame;\n\t\tconst pts = vectorMeshEditPoints(mesh);\n\t\tlet best: number | null = null;\n\t\tlet bestD = Infinity;\n\t\tfor (const index of [...pts.handles.map((h) => h.index), ...pts.anchors]) {\n\t\t\tconst s = this.documentToScreen(applyMatrix(target.world, mesh.points[index]));\n\t\t\tconst d = Math.hypot(s.x - screen.x, s.y - screen.y);\n\t\t\t// Handles come first, so an anchor wins only when it is clearly nearer.\n\t\t\tif (d <= this.hitRadius && d < bestD - 0.5) {\n\t\t\t\tbest = index;\n\t\t\t\tbestD = d;\n\t\t\t}\n\t\t}\n\t\tif (best !== null) return { target, index: best, s: 0, t: 0 };\n\t\tconst local = applyMatrix(invertMatrix(target.world), p);\n\t\tconst at = vectorMeshParamAt(mesh, local);\n\t\tif (at.distance > this.tolerance()) return null;\n\t\treturn { target, index: null, s: at.s, t: at.t };\n\t}\n\n\tprivate downEnvelope(screen: Vec2, p: Vec2, input: VectorPointerInput) {\n\t\tconst hit = this.hitEnvelope(screen, p);\n\t\tif (hit) {\n\t\t\tconst { node, frame } = hit.target;\n\t\t\tthis.drag = {\n\t\t\t\tkind: 'envelope',\n\t\t\t\tnodeId: node.id,\n\t\t\t\teffectId: frame.effectId,\n\t\t\t\tbase: frame.mesh,\n\t\t\t\tbaseDoc: this.doc,\n\t\t\t\tbefore: this.snapshot(),\n\t\t\t\tindex: hit.index,\n\t\t\t\ts: hit.s,\n\t\t\t\tt: hit.t,\n\t\t\t\tstart: this.toLocal(node.id, p),\n\t\t\t\tmoved: false\n\t\t\t};\n\t\t\tthis.interaction = { type: 'envelope', nodeId: node.id, index: hit.index };\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\t// Elsewhere: pick the object to edit.\n\t\tconst node = hitTestVectorNodes(this.doc.nodes, p, this.tolerance(), { entered: this.enteredGroups() });\n\t\tif (node) this.selection = input.shift ? (this.selection.includes(node.id) ? this.selection.filter((id) => id !== node.id) : [...this.selection, node.id]) : [node.id];\n\t\telse if (!input.shift) this.selection = [];\n\t\tthis.anchors = [];\n\t\tthis.notify();\n\t}\n\n\tprivate moveEnvelope(drag: Extract<Drag, { kind: 'envelope' }>, p: Vec2) {\n\t\tconst local = this.toLocal(drag.nodeId, p, drag.baseDoc);\n\t\tconst d = { x: local.x - drag.start.x, y: local.y - drag.start.y };\n\t\tif (!drag.moved && Math.hypot(d.x, d.y) * this.viewport.zoom < 1) return;\n\t\tdrag.moved = true;\n\t\tconst mesh = drag.index !== null ? dragVectorMeshPoint(drag.base, drag.index, d) : pullVectorMesh(drag.base, drag.s, drag.t, d);\n\t\tconst nodes = mapTree(drag.baseDoc.nodes, (n) => {\n\t\t\tif (n.id !== drag.nodeId) return n;\n\t\t\tconst effects = (n.effects ?? []).map((e) =>\n\t\t\t\te.id === drag.effectId && e.type === 'envelope'\n\t\t\t\t\t? { ...e, params: { ...e.params, style: 'mesh' as const, mesh, columns: mesh.us.length - 1, rows: mesh.vs.length - 1 } }\n\t\t\t\t\t: e\n\t\t\t);\n\t\t\treturn { ...n, effects } as VectorNode;\n\t\t});\n\t\tthis.doc = { ...drag.baseDoc, nodes };\n\t\tthis.notify(true);\n\t}\n\n\t/** Applies a matrix to the selected (or given) nodes as one undo step. */\n\ttransform(matrix: AffineMatrix, options: { ids?: readonly string[]; label?: string } = {}): VectorActionResult {\n\t\tconst ids = new Set(options.ids ?? this.selectedRoots());\n\t\tif (!ids.size) return { ok: false, message: 'Nothing is selected.' };\n\t\tthis.change(options.label ?? 'Transform', () => {\n\t\t\tthis.doc = this.transformed(this.doc, ids, matrix);\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** `doc` with nodes `ids` moved by `m`, given in document space. */\n\tprivate transformed(doc: VectorDocument, ids: ReadonlySet<string>, m: AffineMatrix): VectorDocument {\n\t\tconst visit = (list: readonly VectorNode[], parent: AffineMatrix): VectorNode[] => {\n\t\t\tlet changed = false;\n\t\t\tconst out = list.map((n) => {\n\t\t\t\tlet next = n;\n\t\t\t\tif (ids.has(n.id) && !n.locked) {\n\t\t\t\t\t// `m` in the node's parent space: parent → document, m, document → parent.\n\t\t\t\t\tconst local = isRoot(parent) ? m : multiplyMatrices(multiplyMatrices(parent, m), invertMatrix(parent));\n\t\t\t\t\tnext = transformVectorNode(n, local);\n\t\t\t\t} else if (n.type === 'group') {\n\t\t\t\t\tconst children = visit(n.children, multiplyMatrices(nodeMatrix(n), parent));\n\t\t\t\t\tif (children !== n.children) next = { ...n, children };\n\t\t\t\t}\n\t\t\t\tif (next !== n) changed = true;\n\t\t\t\treturn next;\n\t\t\t});\n\t\t\treturn changed ? out : (list as VectorNode[]);\n\t\t};\n\t\tconst nodes = visit(doc.nodes, IDENTITY_MATRIX);\n\t\treturn nodes === doc.nodes ? doc : { ...doc, nodes };\n\t}\n\n\ttranslate(dx: number, dy: number, options: { ids?: readonly string[] } = {}): VectorActionResult {\n\t\tif (this.tool === 'direct' && this.anchors.length && !options.ids) {\n\t\t\tthis.change('Move Points', () => {\n\t\t\t\tthis.doc = this.movedAnchors(this.doc, this.anchors, dx, dy);\n\t\t\t});\n\t\t\treturn { ok: true };\n\t\t}\n\t\treturn this.transform(translateMatrix(dx, dy), { ids: options.ids, label: 'Move' });\n\t}\n\n\t/** The selection frame's centre in document space. */\n\tprivate frameCenter(frame: VectorSelectionFrame): Vec2 {\n\t\treturn applyMatrix(rotateMatrix(frame.angle), { x: frame.x + frame.width / 2, y: frame.y + frame.height / 2 });\n\t}\n\n\t/**\n\t * Scales along the selection box's own axes (rotated with the selection when its nodes\n\t * share a rotation) around `origin` (default: the box centre, in document space).\n\t */\n\tscale(sx: number, sy: number = sx, options: { ids?: readonly string[]; origin?: Vec2 } = {}): VectorActionResult {\n\t\tconst frame = this.frameOf(options.ids ?? this.selectedRoots());\n\t\tif (!frame) return { ok: false, message: 'Nothing is selected.' };\n\t\tconst origin = applyMatrix(rotateMatrix(-frame.angle), options.origin ?? this.frameCenter(frame));\n\t\treturn this.transform(this.inFrame(frame.angle, scaleMatrix(sx, sy, origin)), { ids: options.ids, label: 'Scale' });\n\t}\n\n\t/** Rotates clockwise by `degrees` around `origin` (default: the selection centre). */\n\trotate(degrees: number, options: { ids?: readonly string[]; origin?: Vec2 } = {}): VectorActionResult {\n\t\tconst frame = this.frameOf(options.ids ?? this.selectedRoots());\n\t\tif (!frame) return { ok: false, message: 'Nothing is selected.' };\n\t\tconst origin = options.origin ?? this.frameCenter(frame);\n\t\treturn this.transform(rotateMatrix(degrees, origin), { ids: options.ids, label: 'Rotate' });\n\t}\n\n\t/** Sets each selected (or given) node's rotation to `degrees`, turning it around its own centre. */\n\tsetRotation(degrees: number, options: { ids?: readonly string[] } = {}): VectorActionResult {\n\t\tconst ids = options.ids ?? this.selectedRoots();\n\t\tif (!ids.length) return { ok: false, message: 'Nothing is selected.' };\n\t\tthis.change('Set Rotation', () => {\n\t\t\tfor (const id of ids) {\n\t\t\t\tconst frame = this.frameOf([id]);\n\t\t\t\tif (!frame) continue;\n\t\t\t\tconst delta = normalizeAngle(degrees - frame.angle);\n\t\t\t\tif (Math.abs(delta) > 1e-9) this.doc = this.transformed(this.doc, new Set([id]), rotateMatrix(delta, this.frameCenter(frame)));\n\t\t\t}\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** Mirrors along the selection box's own axes. */\n\tflip(axis: 'horizontal' | 'vertical', options: { ids?: readonly string[] } = {}): VectorActionResult {\n\t\tconst frame = this.frameOf(options.ids ?? this.selectedRoots());\n\t\tif (!frame) return { ok: false, message: 'Nothing is selected.' };\n\t\tconst c = { x: frame.x + frame.width / 2, y: frame.y + frame.height / 2 };\n\t\tconst m = axis === 'horizontal' ? scaleMatrix(-1, 1, c) : scaleMatrix(1, -1, c);\n\t\treturn this.transform(this.inFrame(frame.angle, m), { ids: options.ids, label: axis === 'horizontal' ? 'Flip Horizontal' : 'Flip Vertical' });\n\t}\n\n\t/**\n\t * Aligns the selected nodes to their combined bounds, or to the artboard when one node is\n\t * selected (or `to: 'artboard'`).\n\t */\n\talign(edge: VectorAlign, options: { ids?: readonly string[]; to?: 'selection' | 'artboard' } = {}): VectorActionResult {\n\t\tconst ids = options.ids ?? this.selectedRoots();\n\t\tif (!ids.length) return { ok: false, message: 'Nothing is selected.' };\n\t\tconst target = options.to === 'artboard' || ids.length === 1 ? { x: 0, y: 0, width: this.doc.width, height: this.doc.height } : this.boundsOf(ids)!;\n\t\tthis.change(`Align ${edge[0].toUpperCase()}${edge.slice(1)}`, () => {\n\t\t\tfor (const id of ids) {\n\t\t\t\tconst b = this.boundsOf([id]);\n\t\t\t\tif (!b) continue;\n\t\t\t\tlet dx = 0;\n\t\t\t\tlet dy = 0;\n\t\t\t\tif (edge === 'left') dx = target.x - b.x;\n\t\t\t\tif (edge === 'center') dx = target.x + target.width / 2 - (b.x + b.width / 2);\n\t\t\t\tif (edge === 'right') dx = target.x + target.width - (b.x + b.width);\n\t\t\t\tif (edge === 'top') dy = target.y - b.y;\n\t\t\t\tif (edge === 'middle') dy = target.y + target.height / 2 - (b.y + b.height / 2);\n\t\t\t\tif (edge === 'bottom') dy = target.y + target.height - (b.y + b.height);\n\t\t\t\tif (dx || dy) this.doc = this.transformed(this.doc, new Set([id]), translateMatrix(dx, dy));\n\t\t\t}\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** Spaces three or more selected nodes evenly (equal gaps). */\n\tdistribute(direction: VectorDistribute, options: { ids?: readonly string[] } = {}): VectorActionResult {\n\t\tconst ids = options.ids ?? this.selectedRoots();\n\t\tconst items = ids.map((id) => ({ id, b: this.boundsOf([id]) })).filter((i): i is { id: string; b: VectorBounds } => !!i.b);\n\t\tif (items.length < 3) return { ok: false, message: 'Select at least three objects to distribute.' };\n\t\tconst horizontal = direction === 'horizontal';\n\t\tconst pos = (b: VectorBounds) => (horizontal ? b.x : b.y);\n\t\tconst size = (b: VectorBounds) => (horizontal ? b.width : b.height);\n\t\titems.sort((a, b) => pos(a.b) - pos(b.b));\n\t\tconst first = items[0].b;\n\t\tconst last = items[items.length - 1].b;\n\t\tconst total = pos(last) + size(last) - pos(first);\n\t\tconst gap = (total - items.reduce((s, i) => s + size(i.b), 0)) / (items.length - 1);\n\t\tthis.change(horizontal ? 'Distribute Horizontally' : 'Distribute Vertically', () => {\n\t\t\tlet cursor = pos(first);\n\t\t\tfor (const item of items) {\n\t\t\t\tconst d = cursor - pos(item.b);\n\t\t\t\tif (Math.abs(d) > 1e-9) this.doc = this.transformed(this.doc, new Set([item.id]), horizontal ? translateMatrix(d, 0) : translateMatrix(0, d));\n\t\t\t\tcursor += size(item.b) + gap;\n\t\t\t}\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Structure\n\t// -------------------------------------------------------------------------\n\n\t/** Copies the selection (offset by `offset`, default 10, 10) on top of each original. */\n\tduplicate(options: { ids?: readonly string[]; offset?: Vec2 } = {}): string[] {\n\t\tconst ids = options.ids ?? this.selectedRoots();\n\t\tconst offset = options.offset ?? { x: 10, y: 10 };\n\t\tconst created: string[] = [];\n\t\tif (!ids.length) return created;\n\t\tthis.change('Duplicate', () => {\n\t\t\tthis.doc = this.withDuplicates(this.doc, ids, offset, created);\n\t\t\tthis.selection = created;\n\t\t\tthis.anchors = [];\n\t\t});\n\t\treturn created;\n\t}\n\n\tprivate withDuplicates(doc: VectorDocument, ids: readonly string[], offset: Vec2, created: string[]): VectorDocument {\n\t\tconst set = new Set(ids);\n\t\tconst m = translateMatrix(offset.x, offset.y);\n\t\tconst insert = (list: readonly VectorNode[]): VectorNode[] => {\n\t\t\tconst out: VectorNode[] = [];\n\t\t\tfor (const node of list) {\n\t\t\t\tconst n = node.type === 'group' ? { ...node, children: insert(node.children) } : node;\n\t\t\t\tout.push(n);\n\t\t\t\tif (set.has(node.id)) {\n\t\t\t\t\tconst copy = transformVectorNode(this.freshIds(node), m);\n\t\t\t\t\tcreated.push(copy.id);\n\t\t\t\t\tout.push(copy);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn out;\n\t\t};\n\t\treturn { ...doc, nodes: insert(doc.nodes) };\n\t}\n\n\t/** Groups the selected nodes (they must share a parent). Returns the group id. */\n\tgroup(options: { ids?: readonly string[]; name?: string } = {}): string | null {\n\t\tconst ids = options.ids ?? this.selectedRoots();\n\t\tif (!ids.length) return null;\n\t\tconst locs = ids.map((id) => locateVectorNode(this.doc.nodes, id));\n\t\tif (locs.some((l) => !l)) return null;\n\t\tconst parent = locs[0]!.parent?.id ?? null;\n\t\tif (locs.some((l) => (l!.parent?.id ?? null) !== parent)) return null;\n\t\tconst id = this.newId();\n\t\tthis.change('Group', () => {\n\t\t\tconst set = new Set(ids);\n\t\t\tconst regroup = (list: readonly VectorNode[]): VectorNode[] => {\n\t\t\t\tconst members = list.filter((n) => set.has(n.id));\n\t\t\t\tconst top = Math.max(...members.map((m) => list.indexOf(m)));\n\t\t\t\tconst out: VectorNode[] = [];\n\t\t\t\tlist.forEach((n, i) => {\n\t\t\t\t\tif (i === top) out.push({ id, type: 'group', name: options.name ?? 'Group', children: members });\n\t\t\t\t\tif (!set.has(n.id)) out.push(n);\n\t\t\t\t});\n\t\t\t\treturn out;\n\t\t\t};\n\t\t\tif (parent === null) this.setDoc(regroup(this.doc.nodes));\n\t\t\telse this.setDoc(mapTree(this.doc.nodes, (n) => (n.id === parent && n.type === 'group' ? { ...n, children: regroup(n.children) } : n)));\n\t\t\tthis.selection = [id];\n\t\t\tthis.anchors = [];\n\t\t});\n\t\treturn id;\n\t}\n\n\t/** Replaces selected groups with their children. */\n\tungroup(options: { ids?: readonly string[] } = {}): VectorActionResult {\n\t\tconst groups = (options.ids ?? this.selection).map((id) => this.getNode(id)).filter((n): n is VectorGroupNode => n?.type === 'group');\n\t\tif (!groups.length) return { ok: false, message: 'Select a group to ungroup.' };\n\t\tconst set = new Set(groups.map((g) => g.id));\n\t\tthis.change('Ungroup', () => {\n\t\t\tconst flatten = (list: readonly VectorNode[]): VectorNode[] => {\n\t\t\t\tconst out: VectorNode[] = [];\n\t\t\t\tlet changed = false;\n\t\t\t\tfor (const n of list) {\n\t\t\t\t\tif (set.has(n.id) && n.type === 'group') {\n\t\t\t\t\t\tchanged = true;\n\t\t\t\t\t\tconst opacity = n.opacity;\n\t\t\t\t\t\tconst m = nodeMatrix(n);\n\t\t\t\t\t\tout.push(\n\t\t\t\t\t\t\t...n.children\n\t\t\t\t\t\t\t\t.map((c) => (c.blendStep ? (({ blendStep: _s, locked: _l, ...rest }) => rest as VectorNode)(c) : c))\n\t\t\t\t\t\t\t\t.map((c) => (c.booleanOperation ? (({ booleanOperation: _b, ...rest }) => rest as VectorNode)(c) : c))\n\t\t\t\t\t\t\t\t.map((c) => (n.transform ? transformVectorNode(c, m) : c))\n\t\t\t\t\t\t\t\t.map((c) => (opacity !== undefined && opacity < 1 ? { ...c, opacity: (c.opacity ?? 1) * opacity } : c))\n\t\t\t\t\t\t);\n\t\t\t\t\t} else if (n.type === 'group') {\n\t\t\t\t\t\tconst children = flatten(n.children);\n\t\t\t\t\t\tif (children !== n.children) changed = true;\n\t\t\t\t\t\tout.push(children !== n.children ? { ...n, children } : n);\n\t\t\t\t\t} else out.push(n);\n\t\t\t\t}\n\t\t\t\treturn changed ? out : (list as VectorNode[]);\n\t\t\t};\n\t\t\tthis.setDoc(flatten(this.doc.nodes));\n\t\t\tthis.selection = groups.flatMap((g) => g.children.map((c) => c.id));\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** Changes stacking order within each node's parent. */\n\tarrange(op: VectorArrange, options: { ids?: readonly string[] } = {}): VectorActionResult {\n\t\tconst set = new Set(options.ids ?? this.selectedRoots());\n\t\tif (!set.size) return { ok: false, message: 'Nothing is selected.' };\n\t\tconst reorder = (list: readonly VectorNode[]): VectorNode[] => {\n\t\t\tconst out = list.map((n) => (n.type === 'group' ? { ...n, children: reorder(n.children) } : n));\n\t\t\tif (op === 'front') return [...out.filter((n) => !set.has(n.id)), ...out.filter((n) => set.has(n.id))];\n\t\t\tif (op === 'back') return [...out.filter((n) => set.has(n.id)), ...out.filter((n) => !set.has(n.id))];\n\t\t\tif (op === 'forward') {\n\t\t\t\tfor (let i = out.length - 2; i >= 0; i--) if (set.has(out[i].id) && !set.has(out[i + 1].id)) [out[i], out[i + 1]] = [out[i + 1], out[i]];\n\t\t\t} else {\n\t\t\t\tfor (let i = 1; i < out.length; i++) if (set.has(out[i].id) && !set.has(out[i - 1].id)) [out[i], out[i - 1]] = [out[i - 1], out[i]];\n\t\t\t}\n\t\t\treturn out;\n\t\t};\n\t\tconst labels = { front: 'Bring to Front', forward: 'Bring Forward', backward: 'Send Backward', back: 'Send to Back' };\n\t\tthis.change(labels[op], () => this.setDoc(reorder(this.doc.nodes)));\n\t\treturn { ok: true };\n\t}\n\n\t/**\n\t * Combines the selected paths (two or more) into one with a boolean operation. `subtract`\n\t * removes the upper shapes from the lowest one. The result takes the lowest shape's style\n\t * and place. Shapes take part as drawn (path effects and live booleans applied).\n\t *\n\t * With `live`, the operands stay editable instead: a lone live boolean group gets the\n\t * operation (its operands follow it again), operands of live boolean groups get it as their\n\t * own, and anything else (two or more nodes with one parent) is wrapped in a new live boolean\n\t * group. Returns the result's (or the live group's) id, or null.\n\t */\n\tbooleanOp(op: VectorBooleanOperation, options: { ids?: readonly string[]; live?: boolean } = {}): string | null {\n\t\tconst roots = options.ids ?? this.selectedRoots();\n\t\tif (options.live) return this.liveBooleanOp(op, roots);\n\t\tconst entries = vectorPathEntries(this.doc, roots, { locked: true });\n\t\tif (entries.length < 2) return null;\n\t\tconst result = pathBoolean(\n\t\t\tBOOLEAN_OPS[op],\n\t\t\tentries.map((e) => ({\n\t\t\t\tpath: vectorPathWorldBezier({ ...e.node, subpaths: e.node.subpaths.filter((s) => s.anchors.length > 1).map((s) => ({ ...s, closed: true })) }, e.parent),\n\t\t\t\tfillRule: e.node.fillRule ?? 'nonzero'\n\t\t\t}))\n\t\t);\n\t\tconst base = entries[0].node;\n\t\tconst anchorRoot = roots.find((r) => this.holds(r, base.id)) ?? base.id;\n\t\tconst subpaths = bezierToSubpaths(this.intoParentSpace(anchorRoot, result.path));\n\t\tif (result.error || !subpaths.length) return null;\n\t\tconst id = this.newId();\n\t\tconst { transform: _t, effects: _e, booleanOperation: _b, ...baseRest } = cloneVectorNode(base);\n\t\tconst node: VectorPathNode = { ...baseRest, id, name: BOOLEAN_LABELS[op], subpaths };\n\t\tconst drop = new Set([...entries.map((e) => e.node.id), ...roots]);\n\t\tthis.change(BOOLEAN_LABELS[op], () => {\n\t\t\tthis.setDoc(mapTree(this.doc.nodes, (n) => (n.id === anchorRoot ? node : drop.has(n.id) ? null : n)));\n\t\t\tthis.selection = [id];\n\t\t\tthis.anchors = [];\n\t\t\tthis.editingId = null;\n\t\t});\n\t\treturn id;\n\t}\n\n\t/** Whether stored node `rootId` holds `id` as drawn (`id` may be an expanded path inside it). */\n\tprivate holds(rootId: string, id: string): boolean {\n\t\tconst n = this.getNode(rootId);\n\t\treturn !!n && (!!findVectorNode([n], id) || !!findVectorNode([expandVectorAppearance(n)], id) || !!findVectorNode([vectorMaskedGeometry(n)], id));\n\t}\n\n\tprivate liveBooleanOp(op: VectorBooleanOperation, roots: readonly string[]): string | null {\n\t\tconst nodes = roots.map((id) => this.getNode(id));\n\t\tif (!nodes.length || nodes.some((n) => !n)) return null;\n\t\tconst label = `Live ${BOOLEAN_LABELS[op]}`;\n\t\tconst lone = nodes.length === 1 ? nodes[0] : null;\n\t\tif (isVectorLiveBoolean(lone)) {\n\t\t\tconst children = lone.children.map((c) => (c.booleanOperation ? (({ booleanOperation: _b, ...rest }) => rest as VectorNode)(c) : c));\n\t\t\tthis.change(label, () => this.replaceNode(lone.id, { ...lone, liveBoolean: op, children }));\n\t\t\treturn lone.id;\n\t\t}\n\t\tconst parents = roots.map((id) => locateVectorNode(this.doc.nodes, id)?.parent ?? null);\n\t\tif (parents.every((g) => isVectorLiveBoolean(g))) {\n\t\t\tconst set = new Set(roots);\n\t\t\tthis.change(label, () =>\n\t\t\t\tthis.setDoc(\n\t\t\t\t\tmapTree(this.doc.nodes, (n) => {\n\t\t\t\t\t\tif (n.type !== 'group' || !n.liveBoolean || !n.children.some((c) => set.has(c.id))) return n;\n\t\t\t\t\t\tconst group = n;\n\t\t\t\t\t\tconst children = group.children.map((c) => {\n\t\t\t\t\t\t\tif (!set.has(c.id)) return c;\n\t\t\t\t\t\t\tconst { booleanOperation: _b, ...rest } = c;\n\t\t\t\t\t\t\treturn (op === group.liveBoolean ? rest : { ...rest, booleanOperation: op }) as VectorNode;\n\t\t\t\t\t\t});\n\t\t\t\t\t\treturn { ...group, children };\n\t\t\t\t\t})\n\t\t\t\t)\n\t\t\t);\n\t\t\treturn parents[0]!.id;\n\t\t}\n\t\tif (nodes.length < 2) return null;\n\t\tconst parent = parents[0]?.id ?? null;\n\t\tif (parents.some((g) => (g?.id ?? null) !== parent)) return null;\n\t\tconst id = this.newId();\n\t\tthis.change(label, () => {\n\t\t\tconst set = new Set(roots);\n\t\t\tconst wrap = (list: readonly VectorNode[]): VectorNode[] => {\n\t\t\t\tconst members = list.filter((n) => set.has(n.id)).map((c) => (c.booleanOperation ? (({ booleanOperation: _b, ...rest }) => rest as VectorNode)(c) : c));\n\t\t\t\tconst top = Math.max(...list.map((n, i) => (set.has(n.id) ? i : -1)));\n\t\t\t\tconst out: VectorNode[] = [];\n\t\t\t\tlist.forEach((n, i) => {\n\t\t\t\t\tif (i === top) out.push({ id, type: 'group', name: BOOLEAN_LABELS[op], liveBoolean: op, children: members });\n\t\t\t\t\tif (!set.has(n.id)) out.push(n);\n\t\t\t\t});\n\t\t\t\treturn out;\n\t\t\t};\n\t\t\tif (parent === null) this.setDoc(wrap(this.doc.nodes));\n\t\t\telse this.setDoc(mapTree(this.doc.nodes, (n) => (n.id === parent && n.type === 'group' ? { ...n, children: wrap(n.children) } : n)));\n\t\t\tthis.selection = [id];\n\t\t\tthis.anchors = [];\n\t\t\tthis.editingId = null;\n\t\t});\n\t\treturn id;\n\t}\n\n\t/** Joins the selected paths into one compound path (subpaths kept, lowest style). */\n\tcombine(options: { ids?: readonly string[] } = {}): string | null {\n\t\tconst roots = options.ids ?? this.selectedRoots();\n\t\tconst entries = vectorPathEntries(this.doc, roots, { locked: true });\n\t\tif (entries.length < 2) return null;\n\t\tconst paths = entries.map((e) => e.node);\n\t\tconst id = this.newId();\n\t\tconst first = roots.find((r) => this.holds(r, paths[0].id)) ?? paths[0].id;\n\t\tconst subpaths = entries.flatMap((e) => bezierToSubpaths(this.intoParentSpace(first, vectorPathWorldBezier(e.node, e.parent))));\n\t\tconst { transform: _t, effects: _e, booleanOperation: _b, ...baseRest } = cloneVectorNode(paths[0]);\n\t\tconst node: VectorPathNode = { ...baseRest, id, name: 'Compound Path', subpaths };\n\t\tconst drop = new Set([...paths.map((p) => p.id), ...roots]);\n\t\tthis.change('Combine', () => {\n\t\t\tthis.setDoc(mapTree(this.doc.nodes, (n) => (n.id === first ? node : drop.has(n.id) ? null : n)));\n\t\t\tthis.selection = [id];\n\t\t\tthis.anchors = [];\n\t\t});\n\t\treturn id;\n\t}\n\n\t/** A document-space path mapped into the parent space of node `id`. */\n\tprivate intoParentSpace(id: string, path: BezierPath): BezierPath {\n\t\tconst parent = vectorParentMatrix(this.doc.nodes, id) ?? IDENTITY_MATRIX;\n\t\treturn isRoot(parent) ? path : transformBezierPath(path, invertMatrix(parent));\n\t}\n\n\t/** Splits selected compound paths into one path per subpath. */\n\tbreakApart(options: { ids?: readonly string[] } = {}): VectorActionResult {\n\t\tconst paths = (options.ids ?? this.selection).map((id) => this.getNode(id)).filter((n): n is VectorPathNode => n?.type === 'path' && n.subpaths.length > 1);\n\t\tif (!paths.length) return { ok: false, message: 'Select a compound path to break apart.' };\n\t\tconst created: string[] = [];\n\t\tconst set = new Set(paths.map((p) => p.id));\n\t\tthis.change('Break Apart', () => {\n\t\t\tconst split = (list: readonly VectorNode[]): VectorNode[] =>\n\t\t\t\tlist.flatMap((n): VectorNode[] => {\n\t\t\t\t\tif (n.type === 'group') return [{ ...n, children: split(n.children) }];\n\t\t\t\t\tif (!set.has(n.id)) return [n];\n\t\t\t\t\treturn n.subpaths.map((sp, i) => {\n\t\t\t\t\t\tconst id = i === 0 ? n.id : this.newId();\n\t\t\t\t\t\tcreated.push(id);\n\t\t\t\t\t\treturn { ...n, id, subpaths: [sp] };\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\tthis.setDoc(split(this.doc.nodes));\n\t\t\tthis.selection = created;\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Pathfinder, Shape Builder and paint bucket\n\t// -------------------------------------------------------------------------\n\n\t/**\n\t * Pathfinder: Divide, Trim, Merge, Crop, Outline or Minus Back over the selected (or given)\n\t * paths, in document space (rotated nodes and groups included, live corners cut). Unfilled\n\t * stroked paths take part as the area their stroke paints (Divide: open ones cut instead).\n\t * The results replace the operands at the place of the top-most one, grouped when there are\n\t * several; each keeps the style of the operand whose area it is. Returns the new path ids,\n\t * or an empty list when nothing came out.\n\t */\n\tpathfinder(op: VectorPathfinderOperation, options: { ids?: readonly string[] } = {}): string[] {\n\t\tconst roots = options.ids ?? this.selectedRoots();\n\t\tconst entries = vectorPathEntries(this.doc, roots);\n\t\tif (entries.length < (op === 'divide' || op === 'outline' ? 1 : 2)) return [];\n\t\tconst shapes = entries.map((e) => vectorPathfinderShape(e, op));\n\t\tconst result = runPathfinder(op, shapes);\n\t\tif (result.error || !result.pieces.length) return [];\n\t\tconst last = entries[entries.length - 1].node.id;\n\t\tconst topRoot = [...roots].reverse().find((r) => this.holds(r, last)) ?? last;\n\t\tconst nodes: VectorPathNode[] = [];\n\t\tfor (const piece of result.pieces) {\n\t\t\tconst subpaths = bezierToSubpaths(this.intoParentSpace(topRoot, piece.path)).map((sp) => (piece.open ? { ...sp, closed: false } : sp));\n\t\t\tif (!subpaths.length) continue;\n\t\t\tconst source = entries[piece.source].node;\n\t\t\tconst style = vectorResultStyle(source, shapes[piece.source].paint, op === 'outline');\n\t\t\tnodes.push({ ...style, type: 'path', id: this.newId(), name: source.name ?? 'Path', subpaths });\n\t\t}\n\t\tif (!nodes.length) return [];\n\t\tconst label = VECTOR_PATHFINDER_LABELS[op];\n\t\tconst container: VectorNode = nodes.length > 1 ? { id: this.newId(), type: 'group', name: label, children: nodes } : nodes[0];\n\t\tconst drop = new Set([...entries.map((e) => e.node.id), ...roots]);\n\t\tthis.change(label, () => {\n\t\t\tthis.setDoc(mapTree(this.doc.nodes, (n) => (n.id === topRoot ? container : drop.has(n.id) ? null : n)));\n\t\t\tthis.selection = [container.id];\n\t\t\tthis.anchors = [];\n\t\t\tthis.editingId = null;\n\t\t});\n\t\treturn nodes.map((n) => n.id);\n\t}\n\n\t/** The Shape Builder map of the selected (or given) paths, cached per document and selection. */\n\t// -------------------------------------------------------------------------\n\t// Blends\n\t// -------------------------------------------------------------------------\n\n\t/** Selected (or given) blend groups; a selected key or step counts as its blend. */\n\tprivate blendTargets(ids?: readonly string[]): (VectorGroupNode & { blend: VectorBlend })[] {\n\t\tconst out = new Map<string, VectorGroupNode & { blend: VectorBlend }>();\n\t\tfor (const id of ids ?? this.selection) {\n\t\t\tconst node = this.getNode(id);\n\t\t\tconst group = isVectorBlendGroup(node) ? node : locateVectorNode(this.doc.nodes, id)?.parent;\n\t\t\tif (isVectorBlendGroup(group)) out.set(group.id, group);\n\t\t}\n\t\treturn [...out.values()];\n\t}\n\n\t/**\n\t * Blends two or more selected (or given) paths or groups: they become the keys of a live\n\t * blend group (bottom to top, or in the order given with `keepOrder`) whose in-betweens are\n\t * regenerated whenever a key or the options change. The keys must share a parent. Returns\n\t * the blend group's id, or null.\n\t */\n\tblend(input: VectorBlendInput = {}): string | null {\n\t\tconst ids = [...new Set(input.ids ?? this.selectedRoots())].filter((id) => {\n\t\t\tconst n = this.getNode(id);\n\t\t\treturn !!n && !n.blendStep && !n.locked;\n\t\t});\n\t\tif (ids.length < 2) return null;\n\t\tconst locs = ids.map((id) => locateVectorNode(this.doc.nodes, id)!);\n\t\tconst parent = locs[0].parent?.id ?? null;\n\t\tif (locs.some((l) => (l.parent?.id ?? null) !== parent)) return null;\n\t\tconst order = input.keepOrder ? ids : ids.slice().sort((a, b) => locs[ids.indexOf(a)].index - locs[ids.indexOf(b)].index);\n\t\tconst blend: VectorBlend = blendOptionsOf({ ...this.blendDefaults, ...input });\n\t\tif (input.starts) {\n\t\t\tconst starts = Object.fromEntries(Object.entries(input.starts).filter(([k, v]) => order.includes(k) && Number.isInteger(v) && v >= 0));\n\t\t\tif (Object.keys(starts).length) blend.starts = starts;\n\t\t}\n\t\tconst id = this.newId();\n\t\tthis.change('Blend', () => {\n\t\t\tconst set = new Set(order);\n\t\t\tconst regroup = (list: readonly VectorNode[]): VectorNode[] => {\n\t\t\t\tconst members = order.map((m) => list.find((n) => n.id === m)!);\n\t\t\t\tconst top = Math.max(...members.map((m) => list.indexOf(m)));\n\t\t\t\tconst out: VectorNode[] = [];\n\t\t\t\tlist.forEach((n, i) => {\n\t\t\t\t\tif (i === top) out.push({ id, type: 'group', name: input.name ?? 'Blend', blend, children: members });\n\t\t\t\t\tif (!set.has(n.id)) out.push(n);\n\t\t\t\t});\n\t\t\t\treturn out;\n\t\t\t};\n\t\t\tif (parent === null) this.setDoc(regroup(this.doc.nodes));\n\t\t\telse this.setDoc(mapTree(this.doc.nodes, (n) => (n.id === parent && n.type === 'group' ? { ...n, children: regroup(n.children) } : n)));\n\t\t\tthis.selection = [id];\n\t\t\tthis.anchors = [];\n\t\t\tthis.editingId = null;\n\t\t});\n\t\treturn id;\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Masks\n\t// -------------------------------------------------------------------------\n\n\t/**\n\t * Make Clipping Mask (`clip`) / Make Opacity Mask (`alpha`, `luminance`): groups the selected (or\n\t * given) objects into a mask group whose mask is the topmost of them (or `maskId`), moved to the\n\t * bottom of the group. They must be two or more, unlocked, with one parent. One undo step.\n\t * Returns the group's id, or why it could not.\n\t */\n\tmakeMask(type: VectorMaskType, options: { ids?: readonly string[]; maskId?: string; invert?: boolean; name?: string } = {}): VectorMaskResult {\n\t\tif (!isVectorMaskType(type)) return { ok: false, message: `Unknown mask type \"${type}\": use clip, alpha or luminance.` };\n\t\tconst ids = [...new Set(options.ids ?? this.selectedRoots())];\n\t\tif (ids.length < 2) return { ok: false, message: 'Select at least two objects: the topmost becomes the mask, the ones below show through it.' };\n\t\tfor (const id of ids) {\n\t\t\tconst n = this.getNode(id);\n\t\t\tif (!n) return { ok: false, message: `There is no node \"${id}\".` };\n\t\t\tif (n.locked) return { ok: false, message: `\"${id}\" is locked; unlock it first.` };\n\t\t\tif (n.blendStep) return { ok: false, message: `\"${id}\" is a blend step; select its blend instead.` };\n\t\t}\n\t\tconst locs = ids.map((id) => locateVectorNode(this.doc.nodes, id)!);\n\t\tconst parent = locs[0].parent?.id ?? null;\n\t\tif (locs.some((l) => (l.parent?.id ?? null) !== parent)) return { ok: false, message: 'The objects must share a parent (group them, or pick objects at one level).' };\n\t\tconst order = ids.slice().sort((a, b) => locs[ids.indexOf(a)].index - locs[ids.indexOf(b)].index);\n\t\tconst maskId = options.maskId ?? order[order.length - 1];\n\t\tif (!order.includes(maskId)) return { ok: false, message: `The mask \"${maskId}\" must be one of the objects (${order.map((id) => `\"${id}\"`).join(', ')}).` };\n\t\tconst id = this.newId();\n\t\tconst mask: VectorMask = options.invert ? { type, invert: true } : { type };\n\t\tthis.change(type === 'clip' ? 'Make Clipping Mask' : 'Make Opacity Mask', () => {\n\t\t\tconst set = new Set(order);\n\t\t\tconst regroup = (list: readonly VectorNode[]): VectorNode[] => {\n\t\t\t\tconst members = order.map((m) => list.find((n) => n.id === m)!);\n\t\t\t\tconst top = Math.max(...members.map((m) => list.indexOf(m)));\n\t\t\t\tconst children = [members.find((m) => m.id === maskId)!, ...members.filter((m) => m.id !== maskId)];\n\t\t\t\tconst out: VectorNode[] = [];\n\t\t\t\tlist.forEach((n, i) => {\n\t\t\t\t\tif (i === top) out.push({ id, type: 'group', name: options.name ?? (type === 'clip' ? 'Clip Group' : 'Opacity Mask'), mask, children });\n\t\t\t\t\tif (!set.has(n.id)) out.push(n);\n\t\t\t\t});\n\t\t\t\treturn out;\n\t\t\t};\n\t\t\tif (parent === null) this.setDoc(regroup(this.doc.nodes));\n\t\t\telse this.setDoc(mapTree(this.doc.nodes, (n) => (n.id === parent && n.type === 'group' ? { ...n, children: regroup(n.children) } : n)));\n\t\t\tthis.selection = [id];\n\t\t\tthis.anchors = [];\n\t\t\tthis.editingId = null;\n\t\t});\n\t\treturn { ok: true, id };\n\t}\n\n\t/** Mask groups among `ids` (default: the selection), or holding one of them directly. */\n\tprivate maskTargets(ids?: readonly string[]): VectorGroupNode[] {\n\t\tconst out = new Map<string, VectorGroupNode>();\n\t\tfor (const id of ids ?? this.selection) {\n\t\t\tconst n = this.getNode(id);\n\t\t\tif (isVectorMaskGroup(n)) out.set(n.id, n);\n\t\t\telse {\n\t\t\t\tconst parent = locateVectorNode(this.doc.nodes, id)?.parent;\n\t\t\t\tif (isVectorMaskGroup(parent)) out.set(parent.id, parent);\n\t\t\t}\n\t\t}\n\t\treturn [...out.values()];\n\t}\n\n\t/**\n\t * Release: the selected (or given) mask groups stop masking. The mask goes back on top of the\n\t * contents, drawn as itself again. A group with appearance of its own (filters, a blend mode,\n\t * path effects) stays a plain group; otherwise it is replaced by its children. One undo step.\n\t * Returns the released nodes' ids (the mask last).\n\t */\n\treleaseMask(options: { ids?: readonly string[] } = {}): string[] {\n\t\tconst groups = this.maskTargets(options.ids);\n\t\tif (!groups.length) return [];\n\t\tconst released: string[] = [];\n\t\tthis.change('Release Mask', () => {\n\t\t\tfor (const g of groups) {\n\t\t\t\tconst [mask, ...contents] = g.children;\n\t\t\t\tconst children = [...contents, mask];\n\t\t\t\treleased.push(...children.map((c) => c.id));\n\t\t\t\tconst { mask: _m, ...rest } = g;\n\t\t\t\tconst keep = !!(g.filters?.length || (g.blendMode && g.blendMode !== 'normal') || g.effects?.length);\n\t\t\t\tif (keep) {\n\t\t\t\t\tthis.replaceNode(g.id, { ...rest, children });\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst m = nodeMatrix(g);\n\t\t\t\tconst opacity = g.opacity;\n\t\t\t\tconst flat = children\n\t\t\t\t\t.map((c) => (g.transform ? transformVectorNode(c, m) : c))\n\t\t\t\t\t.map((c) => (opacity !== undefined && opacity < 1 ? { ...c, opacity: (c.opacity ?? 1) * opacity } : c));\n\t\t\t\tthis.setDoc(spliceTree(this.doc.nodes, g.id, flat));\n\t\t\t}\n\t\t\tthis.selection = released;\n\t\t\tthis.anchors = [];\n\t\t\tthis.editingId = null;\n\t\t});\n\t\treturn released;\n\t}\n\n\t/**\n\t * Changes the type or inversion of the selected (or given) mask groups, or turns their masks off\n\t * (`enabled: false` hides the mask, so the contents draw whole) and on. One undo step.\n\t */\n\tsetMaskOptions(patch: Partial<VectorMask> & { enabled?: boolean }, options: { ids?: readonly string[] } = {}): VectorActionResult {\n\t\tif (patch.type !== undefined && !isVectorMaskType(patch.type)) return { ok: false, message: `Unknown mask type \"${patch.type}\": use clip, alpha or luminance.` };\n\t\tif (patch.invert !== undefined && typeof patch.invert !== 'boolean') return { ok: false, message: 'invert must be true or false.' };\n\t\tif (patch.enabled !== undefined && typeof patch.enabled !== 'boolean') return { ok: false, message: 'enabled must be true or false.' };\n\t\tconst groups = this.maskTargets(options.ids);\n\t\tif (!groups.length) return { ok: false, message: 'Select a mask group (made with Make Clipping Mask or Make Opacity Mask).' };\n\t\tconst set = new Set(groups.map((g) => g.id));\n\t\tthis.change('Mask Options', () => {\n\t\t\tthis.setDoc(\n\t\t\t\tmapTree(this.doc.nodes, (n) => {\n\t\t\t\t\tif (!set.has(n.id) || !isVectorMaskGroup(n)) return n;\n\t\t\t\t\tconst type = patch.type ?? n.mask.type;\n\t\t\t\t\tconst invert = patch.invert ?? !!n.mask.invert;\n\t\t\t\t\tconst [source, ...rest] = n.children;\n\t\t\t\t\tconst shown = patch.enabled === undefined || patch.enabled === (source.visible !== false) ? source : patch.enabled ? (({ visible: _v, ...r }) => r as VectorNode)(source) : { ...source, visible: false };\n\t\t\t\t\treturn { ...n, mask: invert ? { type, invert } : { type }, ...(shown !== source ? { children: [shown, ...rest] } : {}) };\n\t\t\t\t})\n\t\t\t);\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/**\n\t * Edit Mask: selects the mask of the selected (or given) mask group, or of the group holding the\n\t * selection. A path mask opens in the direct tool for anchor editing (`editPath: false` keeps the\n\t * tool). Returns the mask's id, or null.\n\t */\n\teditMask(options: { id?: string; editPath?: boolean } = {}): string | null {\n\t\tconst group = this.maskTargets(options.id ? [options.id] : undefined)[0];\n\t\tif (!group) return null;\n\t\tconst mask = group.children[0];\n\t\tthis.selection = [mask.id];\n\t\tthis.anchors = [];\n\t\tthis.editingId = null;\n\t\tif (mask.type === 'path' && options.editPath !== false && !mask.locked) {\n\t\t\tthis.tool = 'direct';\n\t\t\tthis.editingId = mask.id;\n\t\t}\n\t\tthis.notify();\n\t\treturn mask.id;\n\t}\n\n\t/** Edit Contents: selects what the selected (or given) mask group masks. Returns their ids. */\n\teditMaskContents(options: { id?: string } = {}): string[] {\n\t\tconst group = this.maskTargets(options.id ? [options.id] : undefined)[0];\n\t\tif (!group) return [];\n\t\tthis.selection = group.children.slice(1).map((c) => c.id);\n\t\tthis.anchors = [];\n\t\tthis.editingId = null;\n\t\tthis.notify();\n\t\treturn [...this.selection];\n\t}\n\n\t/** Sets the options new blends get (spacing, steps, distance, orientation, morph). */\n\tsetBlendDefaults(patch: Partial<VectorBlendOptions>): void {\n\t\tthis.blendDefaults = { ...this.blendDefaults, ...blendOptionsOf(patch) };\n\t\tthis.notify();\n\t}\n\n\t/**\n\t * Adds a node as the last key of a blend (it must share the blend's parent); `start` is the\n\t * anchor of its first subpath the blend starts from. One undo step.\n\t */\n\taddBlendKey(blendId: string, nodeId: string, options: { start?: number | null } = {}): VectorActionResult {\n\t\tconst group = this.getNode(blendId);\n\t\tconst node = this.getNode(nodeId);\n\t\tif (!isVectorBlendGroup(group)) return { ok: false, message: `\"${blendId}\" is not a blend.` };\n\t\tif (!node || node.blendStep || node.locked || node.id === blendId) return { ok: false, message: `\"${nodeId}\" cannot be a key.` };\n\t\tconst parentOf = (id: string) => locateVectorNode(this.doc.nodes, id)?.parent?.id ?? null;\n\t\tif (parentOf(blendId) !== parentOf(nodeId)) return { ok: false, message: 'The new key must sit beside the blend (same parent).' };\n\t\tconst start = options.start;\n\t\tthis.change('Blend', () => {\n\t\t\tconst blend = start !== null && start !== undefined ? { ...group.blend, starts: { ...group.blend.starts, [nodeId]: start } } : group.blend;\n\t\t\t// The key leaves its place and joins the blend.\n\t\t\tconst m = invertMatrix(nodeMatrix(group));\n\t\t\tconst key = group.transform ? transformVectorNode(node, m) : node;\n\t\t\t// Two passes: the key must not be removed again from inside the blend.\n\t\t\tconst without = mapTree(this.doc.nodes, (n) => (n.id === nodeId ? null : n));\n\t\t\tthis.setDoc(spliceTree(without, blendId, [{ ...group, blend, children: [...group.children, key] }]));\n\t\t\tthis.selection = [blendId];\n\t\t\tthis.anchors = [];\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** Changes the spacing, steps, distance, orientation or morph of the selected (or given) blends. */\n\tsetBlendOptions(patch: Partial<VectorBlendOptions>, options: { ids?: readonly string[] } = {}): VectorActionResult {\n\t\tconst groups = this.blendTargets(options.ids);\n\t\tif (!groups.length) return { ok: false, message: 'Select a blend.' };\n\t\tconst clean = blendOptionsOf(patch);\n\t\tif (!Object.keys(clean).length) return { ok: false, message: 'No valid blend options were given.' };\n\t\tconst set = new Set(groups.map((g) => g.id));\n\t\tthis.change('Blend Options', () => {\n\t\t\tthis.setDoc(mapTree(this.doc.nodes, (n) => (set.has(n.id) && n.type === 'group' && n.blend ? { ...n, blend: { ...n.blend, ...clean } } : n)));\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** Expand: turns the selected (or given) blends into plain groups of their keys and steps. Returns their ids. */\n\texpandBlend(options: { ids?: readonly string[] } = {}): string[] {\n\t\tconst groups = this.blendTargets(options.ids);\n\t\tif (!groups.length) return [];\n\t\tconst set = new Set(groups.map((g) => g.id));\n\t\tconst used = new Set<string>();\n\t\twalkVectorNodes(this.doc.nodes, (n) => used.add(n.id));\n\t\tconst fresh = () => {\n\t\t\tlet id = this.createId();\n\t\t\twhile (used.has(id)) id = this.createId();\n\t\t\tused.add(id);\n\t\t\treturn id;\n\t\t};\n\t\tthis.change('Expand Blend', () => {\n\t\t\tthis.setDoc(mapTree(this.doc.nodes, (n) => (set.has(n.id) && n.type === 'group' ? expandVectorBlend(n, fresh) : n)));\n\t\t\tthis.selection = [...set];\n\t\t\tthis.anchors = [];\n\t\t});\n\t\treturn [...set];\n\t}\n\n\t/** Release: replaces the selected (or given) blends with their keys (and a replaced spine as a path). */\n\treleaseBlend(options: { ids?: readonly string[] } = {}): string[] {\n\t\tconst groups = this.blendTargets(options.ids);\n\t\tif (!groups.length) return [];\n\t\tconst released: string[] = [];\n\t\tthis.change('Release Blend', () => {\n\t\t\tfor (const g of groups) {\n\t\t\t\tconst nodes = releaseVectorBlend(g, () => this.newId());\n\t\t\t\treleased.push(...nodes.map((n) => n.id));\n\t\t\t\tthis.setDoc(spliceTree(this.doc.nodes, g.id, nodes));\n\t\t\t}\n\t\t\tthis.selection = released;\n\t\t\tthis.anchors = [];\n\t\t});\n\t\treturn released;\n\t}\n\n\t/**\n\t * Replace Spine: the selected blend's steps follow the selected path instead of the straight\n\t * lines between its keys. The path's first subpath becomes the spine (the path is removed)\n\t * and the keys move onto it, spread evenly by arc length.\n\t */\n\treplaceBlendSpine(options: { ids?: readonly string[] } = {}): VectorActionResult {\n\t\tconst roots = options.ids ?? this.selectedRoots();\n\t\tconst groups = this.blendTargets(roots);\n\t\tconst paths = roots.map((id) => this.getNode(id)).filter((n): n is VectorPathNode => n?.type === 'path' && !n.blendStep && !isVectorBlendGroup(locateVectorNode(this.doc.nodes, n.id)?.parent));\n\t\tif (groups.length !== 1 || paths.length !== 1) return { ok: false, message: 'Select one blend and one path to be its spine.' };\n\t\tconst group = groups[0];\n\t\tconst path = paths[0];\n\t\tconst first = path.subpaths.find((sp) => sp.anchors.length >= 2);\n\t\tif (!first) return { ok: false, message: 'The spine needs a path with at least two points.' };\n\t\t// The path's first subpath in the blend group's own space.\n\t\tconst m = multiplyMatrices(this.worldOf(path.id), invertMatrix(this.worldOf(group.id)));\n\t\tconst spine = transformVectorNode<VectorPathNode>({ id: '', type: 'path', subpaths: [first] }, m).subpaths[0];\n\t\tthis.change('Replace Spine', () => {\n\t\t\tconst withSpine: VectorGroupNode = { ...group, blend: { ...group.blend, spine } };\n\t\t\tconst targets = vectorBlendKeyTargets(withSpine);\n\t\t\tlet k = 0;\n\t\t\tconst children = withSpine.children.map((c) => {\n\t\t\t\tif (c.blendStep) return c;\n\t\t\t\tconst target = targets[k++];\n\t\t\t\tconst b = vectorNodeBounds(c);\n\t\t\t\tif (!target || !b) return c;\n\t\t\t\tconst dx = target[0] - (b.x + b.width / 2);\n\t\t\t\tconst dy = target[1] - (b.y + b.height / 2);\n\t\t\t\treturn Math.hypot(dx, dy) > 1e-9 ? transformVectorNode(c, translateMatrix(dx, dy)) : c;\n\t\t\t});\n\t\t\tthis.setDoc(mapTree(this.doc.nodes, (n) => (n.id === path.id ? null : n.id === group.id ? { ...withSpine, children } : n)));\n\t\t\tthis.selection = [group.id];\n\t\t\tthis.anchors = [];\n\t\t\tif (this.editingId === path.id) this.editingId = null;\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\tprivate shapeBuilderMap(ids?: readonly string[]): VectorShapeBuilderMap | null {\n\t\tconst roots = ids ?? this.selectedRoots();\n\t\tconst key = roots.join('\\u0000');\n\t\tif (this.builderCache?.doc === this.doc && this.builderCache.key === key) return this.builderCache.map;\n\t\t// The Shape Builder edits the shapes it takes area from: their stored geometry.\n\t\tconst entries = roots.length ? vectorPathEntries(this.doc, roots, { expand: false }) : [];\n\t\tconst map = entries.length ? vectorShapeBuilderMap(entries) : null;\n\t\tthis.builderCache = { doc: this.doc, key, map };\n\t\treturn map;\n\t}\n\n\t/** The faces the Shape Builder offers for the selected (or given) paths (document units). */\n\tgetShapeBuilderFaces(options: { ids?: readonly string[] } = {}): VectorShapeBuilderFace[] {\n\t\tconst m = this.shapeBuilderMap(options.ids);\n\t\tif (!m) return [];\n\t\treturn m.map.faces.map((f, index) => {\n\t\t\tconst p = m.map.faceInterior(index);\n\t\t\treturn {\n\t\t\t\tindex,\n\t\t\t\td: bezierPathToSvg(f.path, { precision: 3 }),\n\t\t\t\tarea: f.area,\n\t\t\t\tsources: m.map.faceSources(index).map((i) => m.entries[i].node.id),\n\t\t\t\tpoint: p ? { x: p[0], y: p[1] } : null\n\t\t\t};\n\t\t});\n\t}\n\n\t/** Pick distance of the Shape Builder's erase for line pieces (document units). */\n\tprivate builderLineTolerance(): number {\n\t\treturn Math.max(this.hitRadius, 4) / this.viewport.zoom;\n\t}\n\n\t/**\n\t * Shape Builder gesture along `points` (document units; one point is a click) over the faces\n\t * the selected (or given) paths make. Merges the faces touched into one shape styled like the\n\t * top-most shape over the first face (the current fill for a face only outlines enclose);\n\t * with `erase` deletes them, the pieces of unfilled open paths and the edges of other outlines\n\t * (cut where paths meet) within `tolerance`: a deleted edge opens a closed subpath there,\n\t * splits an open one or takes a subpath it runs all the way round, and edges bounding a face\n\t * deleted in the same gesture go with the face. Every shape covering a touched face loses\n\t * that area; paths keep their own transforms. One undo step.\n\t */\n\tshapeBuilder(points: readonly Vec2[], options: { ids?: readonly string[]; erase?: boolean; tolerance?: number } = {}): VectorShapeBuilderResult {\n\t\tconst none = (message: string): VectorShapeBuilderResult => ({ ok: false, message, merged: null, faces: 0, lines: 0, edges: 0 });\n\t\tif (!points.length || !points.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))) return none('Give the points of the gesture.');\n\t\tconst m = this.shapeBuilderMap(options.ids);\n\t\tif (!m) return none('Select paths that make faces.');\n\t\tconst erase = !!options.erase;\n\t\tconst tol = erase ? (options.tolerance ?? this.builderLineTolerance()) : null;\n\t\tconst touched: VectorBuilderTouched = { faces: [], lines: [], edges: [] };\n\t\tconst size = m.map.faces.reduce((s, f) => Math.max(s, f.bounds.maxX - f.bounds.minX, f.bounds.maxY - f.bounds.minY), 1);\n\t\tfor (const p of samplePolyline(points, Math.max(size / 2000, 0.25 / this.viewport.zoom))) addBuilderHit(touched, vectorShapeBuilderHit(m, p, tol));\n\t\tif (!touched.faces.length && !touched.lines.length && !touched.edges.length) return none('There is no face under the pointer.');\n\t\treturn this.applyShapeBuilder(m, touched, erase);\n\t}\n\n\tprivate applyShapeBuilder(m: VectorShapeBuilderMap, touched: VectorBuilderTouched, erase: boolean): VectorShapeBuilderResult {\n\t\tconst plan = planShapeBuilder(m, touched, erase);\n\t\tlet mergedId: string | null = null;\n\t\tconst created = new Map<string, string[]>();\n\t\tthis.change(erase ? 'Shape Builder Delete' : 'Shape Builder', () => {\n\t\t\tlet nodes = this.doc.nodes;\n\t\t\tm.entries.forEach((entry, i) => {\n\t\t\t\tconst node = entry.node;\n\t\t\t\tconst toLocal = (path: BezierPath) => transformBezierPath(path, invertMatrix(multiplyMatrices(nodeMatrix(node), entry.parent)));\n\t\t\t\tconst list: VectorNode[] = [];\n\t\t\t\tconst r = plan.replace.get(i);\n\t\t\t\tif (r === undefined) list.push(node);\n\t\t\t\telse if (r?.kind === 'fill') list.push({ ...node, fillRule: 'nonzero', subpaths: bezierToSubpaths(toLocal(r.path)) });\n\t\t\t\telse if (r?.kind === 'outline') list.push({ ...node, subpaths: bezierToSubpaths(toLocal(r.path)) });\n\t\t\t\telse if (r?.kind === 'pieces') {\n\t\t\t\t\tr.paths.forEach((path, k) =>\n\t\t\t\t\t\tlist.push({ ...node, id: k === 0 ? node.id : this.newId(), subpaths: bezierToSubpaths(toLocal(path)).map((sp) => ({ ...sp, closed: false })) })\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (plan.merged && plan.merged.style === i) {\n\t\t\t\t\tmergedId = this.newId();\n\t\t\t\t\tconst merged: VectorPathNode = {\n\t\t\t\t\t\t...vectorResultStyle(node, 'fill'),\n\t\t\t\t\t\ttype: 'path',\n\t\t\t\t\t\tid: mergedId,\n\t\t\t\t\t\tname: 'Shape',\n\t\t\t\t\t\t...(node.transform ? { transform: node.transform } : {}),\n\t\t\t\t\t\tsubpaths: bezierToSubpaths(toLocal(plan.merged.path))\n\t\t\t\t\t};\n\t\t\t\t\tif (plan.merged.currentFill || !hasPaint(merged.fill)) merged.fill = hasPaint(this.defaults.fill) ? this.defaults.fill! : '#808080';\n\t\t\t\t\tlist.push(merged);\n\t\t\t\t}\n\t\t\t\tif (r === undefined && list.length === 1) return;\n\t\t\t\tcreated.set(node.id, list.map((n) => n.id));\n\t\t\t\tnodes = spliceTree(nodes, node.id, list);\n\t\t\t});\n\t\t\tthis.setDoc(nodes);\n\t\t\t// The shapes stay selected so the next gesture works on them too.\n\t\t\tconst selection = this.selection.flatMap((id) => created.get(id) ?? [id]);\n\t\t\tif (mergedId && !selection.includes(mergedId)) selection.push(mergedId);\n\t\t\tthis.selection = selection.filter((id, k) => !!this.getNode(id) && selection.indexOf(id) === k);\n\t\t\tthis.anchors = [];\n\t\t\tthis.editingId = null;\n\t\t});\n\t\tthis.builderCache = null;\n\t\treturn { ok: true, merged: mergedId, faces: touched.faces.length, lines: touched.lines.length, edges: erase ? vectorBuilderErasedEdges(m, touched).length : 0 };\n\t}\n\n\t/** The paint bucket's map of the visible outlines, cached per document and gap. */\n\tprivate fillMap(gap: number): PlanarMap | null {\n\t\tif (this.fillCache?.doc === this.doc && this.fillCache.gap === gap) return this.fillCache.map;\n\t\tconst map = vectorFillMap(this.doc, gap);\n\t\tthis.fillCache = { doc: this.doc, gap, map };\n\t\treturn map;\n\t}\n\n\t/**\n\t * Paint bucket: the region the visible outlines enclose around `point` (document units),\n\t * crossings split and islands left as holes, becomes a new path filled with the current fill\n\t * default, placed just below the selection (or on top when nothing is selected). `gap`\n\t * (default `editing.fillGap`) closes gaps up to that size between loose ends. Returns the new\n\t * path's id, or null when nothing encloses the point.\n\t */\n\tfillRegion(point: Vec2, options: { gap?: number } = {}): string | null {\n\t\tif (!Number.isFinite(point.x) || !Number.isFinite(point.y)) return null;\n\t\tconst region = this.fillMap(options.gap ?? this.editing.fillGap)?.regionAt([point.x, point.y]);\n\t\tif (!region?.length) return null;\n\t\t// Below the lowest selected node, in its parent.\n\t\tlet below: string | null = null;\n\t\tconst roots = new Set(this.selectedRoots());\n\t\twalkVectorNodes(this.doc.nodes, (n) => {\n\t\t\tif (!below && roots.has(n.id)) below = n.id;\n\t\t});\n\t\tconst subpaths = bezierToSubpaths(below ? this.intoParentSpace(below, region) : region);\n\t\tif (!subpaths.length) return null;\n\t\tconst id = this.newId();\n\t\tconst node: VectorPathNode = { id, type: 'path', name: 'Fill', ...this.styleFor(false), stroke: null, fillRule: 'nonzero', subpaths };\n\t\tif (!hasPaint(node.fill)) node.fill = '#808080';\n\t\tconst target = below as string | null;\n\t\tthis.change('Fill Region', () => {\n\t\t\tif (target) {\n\t\t\t\tconst current = this.getNode(target)!;\n\t\t\t\tthis.setDoc(spliceTree(this.doc.nodes, target, [node, current]));\n\t\t\t} else this.setDoc([...this.doc.nodes, node]);\n\t\t\tif (!target) this.selection = [id];\n\t\t\tthis.anchors = [];\n\t\t\tthis.editingId = null;\n\t\t});\n\t\treturn id;\n\t}\n\n\t/** Updates the Shape Builder / paint bucket highlight for the pointer at `p`; true when it changed. */\n\tprivate hoverBuilder(p: Vec2, alt: boolean): boolean {\n\t\tif (this.tool === 'paintBucket') {\n\t\t\tconst region = this.fillMap(this.editing.fillGap)?.regionAt([p.x, p.y]) ?? null;\n\t\t\tconst key = region ? bezierPathToSvg(region, { precision: 2 }) : null;\n\t\t\tif (key === this.builderHoverKey) return false;\n\t\t\tthis.builderHoverKey = key;\n\t\t\tthis.builderPreview = region ? { mode: 'fill', faces: [region], lines: [], trail: null } : null;\n\t\t\treturn true;\n\t\t}\n\t\tconst m = this.selectedRoots().length ? this.shapeBuilderMap() : null;\n\t\tconst hit = m ? vectorShapeBuilderHit(m, p, alt ? this.builderLineTolerance() : null) : null;\n\t\tconst key = hit ? `${alt ? 'erase' : 'merge'}:${JSON.stringify(hit)}` : null;\n\t\tif (key === this.builderHoverKey) return false;\n\t\tthis.builderHoverKey = key;\n\t\tconst touched: VectorBuilderTouched = { faces: [], lines: [], edges: [] };\n\t\taddBuilderHit(touched, hit);\n\t\tthis.builderPreview = hit && m ? this.builderPreviewOf(m, touched, alt, null) : null;\n\t\treturn true;\n\t}\n\n\tprivate builderPreviewOf(m: VectorShapeBuilderMap, touched: VectorBuilderTouched, erase: boolean, trail: Vec2[] | null): VectorBuilderPreview {\n\t\tconst faces = m.map.faces;\n\t\treturn {\n\t\t\tmode: erase ? 'erase' : 'merge',\n\t\t\tfaces: touched.faces.map((f) => faces[f].path),\n\t\t\t// Edges bounding a face touched too go with the face, so they are not shown apart.\n\t\t\tlines: [...touched.lines.map((k) => m.pieces[k].path), ...(erase ? vectorBuilderErasedEdges(m, touched).map((k) => m.edges[k].path) : [])],\n\t\t\ttrail\n\t\t};\n\t}\n\n\tprivate downBuilder(p: Vec2, input: VectorPointerInput) {\n\t\tthis.builderHoverKey = null;\n\t\tif (this.tool === 'paintBucket') {\n\t\t\tthis.fillRegion(p);\n\t\t\tthis.builderPreview = null;\n\t\t\tthis.hoverBuilder(p, false);\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tconst m = this.selectedRoots().length ? this.shapeBuilderMap() : null;\n\t\tif (!m) return;\n\t\tconst erase = !!input.alt;\n\t\tconst touched: VectorBuilderTouched = { faces: [], lines: [], edges: [] };\n\t\taddBuilderHit(touched, vectorShapeBuilderHit(m, p, erase ? this.builderLineTolerance() : null));\n\t\tthis.drag = { kind: 'builder', erase, points: [p], touched, map: m };\n\t\tthis.interaction = { type: 'builder', mode: erase ? 'erase' : 'merge', points: [p] };\n\t\tthis.builderPreview = this.builderPreviewOf(m, touched, erase, [p]);\n\t\tthis.notify();\n\t}\n\n\tprivate moveBuilder(drag: Extract<Drag, { kind: 'builder' }>, p: Vec2) {\n\t\tconst last = drag.points[drag.points.length - 1];\n\t\tif (Math.hypot(p.x - last.x, p.y - last.y) * this.viewport.zoom < 1) return;\n\t\tconst tol = drag.erase ? this.builderLineTolerance() : null;\n\t\tfor (const q of samplePolyline([last, p], 2 / this.viewport.zoom)) addBuilderHit(drag.touched, vectorShapeBuilderHit(drag.map, q, tol));\n\t\tdrag.points.push(p);\n\t\tthis.interaction = { type: 'builder', mode: drag.erase ? 'erase' : 'merge', points: drag.points.slice() };\n\t\tthis.builderPreview = this.builderPreviewOf(drag.map, drag.touched, drag.erase, drag.points.slice());\n\t\tthis.notify();\n\t}\n\n\tprivate upBuilder(drag: Extract<Drag, { kind: 'builder' }>) {\n\t\tthis.builderPreview = null;\n\t\tthis.builderHoverKey = null;\n\t\tif (!drag.touched.faces.length && !drag.touched.lines.length && !drag.touched.edges.length) {\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tthis.applyShapeBuilder(drag.map, drag.touched, drag.erase);\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Anchor editing\n\t// -------------------------------------------------------------------------\n\n\tprivate updateAnchors(doc: VectorDocument, refs: readonly VectorAnchorRef[], fn: (a: VectorAnchor, ref: VectorAnchorRef) => VectorAnchor): VectorDocument {\n\t\tconst byNode = new Map<string, VectorAnchorRef[]>();\n\t\tfor (const r of refs) byNode.set(r.nodeId, [...(byNode.get(r.nodeId) ?? []), r]);\n\t\tconst nodes = mapTree(doc.nodes, (n) => {\n\t\t\tconst list = byNode.get(n.id);\n\t\t\tif (!list || n.type !== 'path' || n.locked) return n;\n\t\t\tconst subpaths = n.subpaths.map((sp, s) => {\n\t\t\t\tconst hits = list.filter((r) => r.subpath === s);\n\t\t\t\tif (!hits.length) return sp;\n\t\t\t\treturn { closed: sp.closed, anchors: sp.anchors.map((a, i) => (hits.some((r) => r.index === i) ? fn(a, { nodeId: n.id, subpath: s, index: i }) : a)) };\n\t\t\t});\n\t\t\treturn { ...n, subpaths };\n\t\t});\n\t\treturn nodes === doc.nodes ? doc : { ...doc, nodes };\n\t}\n\n\t/** Moves anchors by a document-space offset (converted into each node's own space). */\n\tprivate movedAnchors(doc: VectorDocument, refs: readonly VectorAnchorRef[], dx: number, dy: number): VectorDocument {\n\t\tconst local = new Map<string, Vec2>();\n\t\tfor (const r of refs) if (!local.has(r.nodeId)) local.set(r.nodeId, applyMatrixLinear(invertMatrix(this.worldOf(r.nodeId, doc)), { x: dx, y: dy }));\n\t\treturn this.updateAnchors(doc, refs, (a, ref) => {\n\t\t\tconst d = local.get(ref.nodeId)!;\n\t\t\treturn translateAnchor(a, d.x, d.y);\n\t\t});\n\t}\n\n\t/**\n\t * Moves one anchor to a document position. Its handles follow; with `curvature` (default:\n\t * the `curvatureMoves` setting) its two segments bend instead, as the Curvature tool does.\n\t */\n\tmoveAnchor(ref: VectorAnchorRef, to: Vec2, options: { curvature?: boolean } = {}): VectorActionResult {\n\t\tconst a = getAnchorWorld(this.doc, ref);\n\t\tif (!a) return { ok: false, message: 'There is no such anchor.' };\n\t\tconst curvature = options.curvature ?? this.editing.curvatureMoves;\n\t\tthis.change('Move Point', () => {\n\t\t\tthis.doc = curvature ? this.curvatureMoved(this.doc, ref, to) : this.movedAnchors(this.doc, [ref], to.x - a.x, to.y - a.y);\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** `doc` with anchor `ref` moved to document point `to` by a Curvature-tool move. */\n\tprivate curvatureMoved(doc: VectorDocument, ref: VectorAnchorRef, to: Vec2): VectorDocument {\n\t\tconst node = findVectorNode(doc.nodes, ref.nodeId);\n\t\tif (node?.type !== 'path' || node.locked || !node.subpaths[ref.subpath]) return doc;\n\t\tconst q = this.toLocal(ref.nodeId, to, doc);\n\t\tconst subpaths = node.subpaths.map((sp, i) => (i === ref.subpath ? curvatureMoveAnchor(sp, ref.index, q) : sp));\n\t\tconst nodes = mapTree(doc.nodes, (n) => (n.id === ref.nodeId ? { ...node, subpaths } : n));\n\t\treturn { ...doc, nodes };\n\t}\n\n\t/** `doc` with segment `ref` bent through document point `p` at parameter `t`. */\n\tprivate bent(doc: VectorDocument, ref: VectorSegmentRef, t: number, p: Vec2): VectorDocument {\n\t\tconst node = findVectorNode(doc.nodes, ref.nodeId);\n\t\tif (node?.type !== 'path' || node.locked || !node.subpaths[ref.subpath]) return doc;\n\t\tconst q = this.toLocal(ref.nodeId, p, doc);\n\t\tconst subpaths = node.subpaths.map((sp, i) => (i === ref.subpath ? bendSubpathSegment(sp, ref.segment, t, q) : sp));\n\t\treturn { ...doc, nodes: mapTree(doc.nodes, (n) => (n.id === ref.nodeId ? { ...node, subpaths } : n)) };\n\t}\n\n\t/**\n\t * Bends a segment so the curve passes through document point `to` at parameter `t`\n\t * (default 0.5, clamped to 0.05–0.95); see `bendSegment` in `pathEdit`.\n\t */\n\tbendSegment(ref: VectorSegmentRef, to: Vec2, t = 0.5): VectorActionResult {\n\t\tconst node = this.getNode(ref.nodeId);\n\t\tconst sp = node?.type === 'path' ? node.subpaths[ref.subpath] : undefined;\n\t\tif (!sp || ref.segment < 0 || ref.segment >= (sp.closed ? sp.anchors.length : sp.anchors.length - 1) || sp.anchors.length < 2) return { ok: false, message: 'There is no such segment.' };\n\t\tif (node!.locked) return { ok: false, message: 'The path is locked.' };\n\t\tthis.change('Bend Segment', () => {\n\t\t\tthis.doc = this.bent(this.doc, ref, t, to);\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** Moves one handle to a document position (see `moveAnchorHandle` for how the opposite handle follows). */\n\tmoveHandle(ref: VectorHandleRef, to: Vec2, options: { breakHandles?: boolean } = {}): VectorActionResult {\n\t\tif (!getAnchor(this.doc, ref)) return { ok: false, message: 'There is no such anchor.' };\n\t\tconst local = this.toLocal(ref.nodeId, to);\n\t\tthis.change('Move Handle', () => {\n\t\t\tthis.doc = this.updateAnchors(this.doc, [ref], (a) => moveAnchorHandle(a, ref.handle, local, options.breakHandles));\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** Converts the selected (or given) anchors between corner, smooth and symmetric. */\n\tconvertAnchors(type: VectorAnchorType, refs: readonly VectorAnchorRef[] = this.anchors): VectorActionResult {\n\t\tif (!refs.length) return { ok: false, message: 'Select points to convert.' };\n\t\tthis.change(type === 'corner' ? 'Make Corner' : 'Make Smooth', () => {\n\t\t\tconst nodes = mapTree(this.doc.nodes, (n) => {\n\t\t\t\tif (n.type !== 'path') return n;\n\t\t\t\tconst mine = refs.filter((r) => r.nodeId === n.id);\n\t\t\t\tif (!mine.length) return n;\n\t\t\t\tconst subpaths = n.subpaths.map((sp, s) => mine.filter((r) => r.subpath === s).reduce((acc, r) => convertAnchor(acc, r.index, type), sp));\n\t\t\t\treturn { ...n, subpaths };\n\t\t\t});\n\t\t\tthis.setDoc(nodes);\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** Adds an anchor on the nearest segment of `nodeId` to document point `point` (shape unchanged). */\n\tinsertAnchorAt(nodeId: string, point: Vec2): VectorAnchorRef | null {\n\t\tconst node = this.getNode(nodeId);\n\t\tif (node?.type !== 'path') return null;\n\t\tconst hit = nearestSegment(node, this.toLocal(nodeId, point));\n\t\tif (!hit) return null;\n\t\tconst { subpath, index } = insertAnchor(node.subpaths[hit.subpath], hit.segment, hit.t);\n\t\tconst ref = { nodeId, subpath: hit.subpath, index };\n\t\tthis.change('Add Point', () => {\n\t\t\tthis.replaceNode(nodeId, { ...node, subpaths: node.subpaths.map((sp, i) => (i === hit.subpath ? subpath : sp)) });\n\t\t\tthis.editingId = nodeId;\n\t\t\tthis.anchors = [ref];\n\t\t});\n\t\treturn ref;\n\t}\n\n\t/**\n\t * Deletes the selected (or given) anchors; subpaths left with one anchor and empty paths go\n\t * too. With `keepShape` (default: the `keepShapeOnDelete` setting, on) the neighbours'\n\t * handles are refitted so the path keeps its shape; off, the neighbours are joined as they are.\n\t */\n\tdeleteAnchors(refs: readonly VectorAnchorRef[] = this.anchors, options: { keepShape?: boolean } = {}): VectorActionResult {\n\t\tif (!refs.length) return { ok: false, message: 'Select points to delete.' };\n\t\tconst keepShape = options.keepShape ?? this.editing.keepShapeOnDelete;\n\t\tthis.change('Delete Points', () => {\n\t\t\tconst nodes = mapTree(this.doc.nodes, (n) => {\n\t\t\t\tif (n.type !== 'path' || n.locked) return n;\n\t\t\t\tconst mine = refs.filter((r) => r.nodeId === n.id);\n\t\t\t\tif (!mine.length) return n;\n\t\t\t\tconst subpaths = n.subpaths\n\t\t\t\t\t.map((sp, s) => {\n\t\t\t\t\t\tconst indices = mine.filter((r) => r.subpath === s).map((r) => r.index);\n\t\t\t\t\t\tif (!indices.length) return sp;\n\t\t\t\t\t\treturn keepShape ? removeAnchorsKeepingShape(sp, indices) : removeAnchors(sp, indices);\n\t\t\t\t\t})\n\t\t\t\t\t.filter((sp) => sp.anchors.length > 1);\n\t\t\t\treturn subpaths.length ? { ...n, subpaths } : null;\n\t\t\t});\n\t\t\tthis.setDoc(nodes);\n\t\t\tthis.anchors = [];\n\t\t\tthis.selection = this.selection.filter((id) => this.getNode(id));\n\t\t\tif (this.editingId && !this.getNode(this.editingId)) this.editingId = null;\n\t\t\tif (this.pen && !this.getNode(this.pen.nodeId)) this.pen = null;\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** Opens or closes the subpaths that contain selected anchors (or all subpaths of `nodeId`). */\n\tsetClosed(closed: boolean, nodeId?: string): VectorActionResult {\n\t\tconst targets = nodeId\n\t\t\t? (() => {\n\t\t\t\t\tconst n = this.getNode(nodeId);\n\t\t\t\t\treturn n?.type === 'path' ? n.subpaths.map((_, s) => ({ nodeId, subpath: s })) : [];\n\t\t\t\t})()\n\t\t\t: this.anchors.map((r) => ({ nodeId: r.nodeId, subpath: r.subpath }));\n\t\tif (!targets.length) return { ok: false, message: 'Select a path or points.' };\n\t\tthis.change(closed ? 'Close Path' : 'Open Path', () => {\n\t\t\tthis.setDoc(\n\t\t\t\tmapTree(this.doc.nodes, (n) =>\n\t\t\t\t\tn.type === 'path' && targets.some((t) => t.nodeId === n.id)\n\t\t\t\t\t\t? { ...n, subpaths: n.subpaths.map((sp, s) => (targets.some((t) => t.nodeId === n.id && t.subpath === s) ? { ...sp, closed } : sp)) }\n\t\t\t\t\t\t: n\n\t\t\t\t)\n\t\t\t);\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** Reverses the direction of every subpath of the selected paths. */\n\treversePaths(ids: readonly string[] = this.selection): VectorActionResult {\n\t\tconst set = new Set(ids.flatMap((id) => (this.getNode(id) ? collectPathNodes(this.getNode(id)!) : [])).map((p) => p.id));\n\t\tif (!set.size) return { ok: false, message: 'Select a path.' };\n\t\tthis.change('Reverse Path', () => {\n\t\t\tthis.setDoc(\n\t\t\t\tmapTree(this.doc.nodes, (n) =>\n\t\t\t\t\tn.type === 'path' && set.has(n.id)\n\t\t\t\t\t\t? { ...n, subpaths: n.subpaths.map(reverseSubpath), ...(n.strokeWidthProfile?.length ? { strokeWidthProfile: reverseWidthProfile(n.strokeWidthProfile) } : {}) }\n\t\t\t\t\t\t: n\n\t\t\t\t)\n\t\t\t);\n\t\t\tthis.anchors = [];\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Path clean-up, Scissors, Knife and Eraser\n\t// -------------------------------------------------------------------------\n\n\t/** Unlocked path nodes in the given roots (default: the paths being edited, else the selection). */\n\tprivate pathTargets(ids?: readonly string[]): VectorPathNode[] {\n\t\tconst roots = ids ?? (this.tool === 'direct' && this.anchors.length ? [...new Set(this.anchors.map((r) => r.nodeId))] : this.selection);\n\t\tconst out = new Map<string, VectorPathNode>();\n\t\tfor (const id of roots) {\n\t\t\tconst node = this.getNode(id);\n\t\t\tif (node && !node.locked) for (const p of collectPathNodes(node)) if (!p.locked) out.set(p.id, p);\n\t\t}\n\t\t// Document order, bottom to top.\n\t\tconst order: string[] = [];\n\t\twalkVectorNodes(this.doc.nodes, (n) => {\n\t\t\tif (out.has(n.id)) order.push(n.id);\n\t\t});\n\t\treturn order.map((id) => out.get(id)!);\n\t}\n\n\t/** Subpaths of node `from` mapped into node `to`'s own space. */\n\tprivate subpathsIn(from: VectorPathNode, to: string): VectorSubpath[] {\n\t\tconst m = multiplyMatrices(this.worldOf(from.id), invertMatrix(this.worldOf(to)));\n\t\tif (isRoot(m)) return from.subpaths;\n\t\tconst map = (p: Vec2) => applyMatrix(m, p);\n\t\treturn from.subpaths.map((sp) => ({\n\t\t\tclosed: sp.closed,\n\t\t\tanchors: sp.anchors.map((a) => ({ ...map(a), ...(a.in ? { in: map(a.in) } : {}), ...(a.out ? { out: map(a.out) } : {}), ...(a.type ? { type: a.type } : {}), ...(a.corner ? { corner: { ...a.corner } } : {}) }))\n\t\t}));\n\t}\n\n\t/**\n\t * Replaces a path with pieces: each entry becomes one path (the first keeps the node's id and\n\t * place, the others go just above it with the same style). No pieces removes the path.\n\t * Returns the ids of the paths now standing in for it.\n\t */\n\tprivate replaceWithPieces(node: VectorPathNode, pieces: readonly VectorSubpath[][]): string[] {\n\t\tconst nodes = pieces.filter((p) => p.length).map((subpaths, i) => ({ ...node, id: i === 0 ? node.id : this.newId(), subpaths }) as VectorPathNode);\n\t\tthis.setDoc(spliceTree(this.doc.nodes, node.id, nodes));\n\t\treturn nodes.map((n) => n.id);\n\t}\n\n\t/** Clears anchor selections, editing and pen state that no longer point at anything. */\n\tprivate dropStaleRefs() {\n\t\tthis.selection = this.selection.filter((id) => this.getNode(id));\n\t\tthis.anchors = this.anchors.filter((r) => getAnchor(this.doc, r));\n\t\tif (this.editingId && !this.getNode(this.editingId)) this.editingId = null;\n\t\tif (this.pen && !this.getNode(this.pen.nodeId)) this.pen = null;\n\t}\n\n\t/**\n\t * Join: the open subpaths of the selected (or given) paths become one open path, joined\n\t * nearest endpoints first (ends within `tolerance` document units merge into one anchor,\n\t * others get a straight segment); a single open subpath is closed. The result replaces the\n\t * lowest path, in its space and style. Default tolerance: the pick distance.\n\t */\n\tjoinPaths(options: { ids?: readonly string[]; tolerance?: number } = {}): VectorActionResult {\n\t\tconst paths = this.pathTargets(options.ids);\n\t\tif (!paths.length) return { ok: false, message: 'Select paths to join.' };\n\t\tconst base = paths[0];\n\t\tconst all = paths.flatMap((p) => this.subpathsIn(p, base.id));\n\t\tif (!all.some((sp) => !sp.closed && sp.anchors.length > 1)) return { ok: false, message: 'There are no open paths to join.' };\n\t\tconst subpaths = joinSubpaths(all, options.tolerance ?? this.tolerance());\n\t\tconst drop = new Set(paths.slice(1).map((p) => p.id));\n\t\tthis.change('Join', () => {\n\t\t\tthis.setDoc(mapTree(this.doc.nodes, (n) => (n.id === base.id ? { ...base, subpaths } : drop.has(n.id) ? null : n)));\n\t\t\tthis.selection = [base.id];\n\t\t\tthis.anchors = [];\n\t\t\tif (this.editingId && drop.has(this.editingId)) this.editingId = base.id;\n\t\t\tthis.dropStaleRefs();\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** Average: moves the selected (or given) anchors onto their average position along `axis`, in document space. */\n\taverageAnchors(axis: VectorAverageAxis = 'both', refs: readonly VectorAnchorRef[] = this.anchors): VectorActionResult {\n\t\tconst points = refs.map((r) => ({ ref: r, at: getAnchorWorld(this.doc, r) })).filter((p): p is { ref: VectorAnchorRef; at: NonNullable<ReturnType<typeof getAnchorWorld>> } => !!p.at);\n\t\tif (points.length < 2) return { ok: false, message: 'Select at least two points to average.' };\n\t\tconst cx = points.reduce((s, p) => s + p.at.x, 0) / points.length;\n\t\tconst cy = points.reduce((s, p) => s + p.at.y, 0) / points.length;\n\t\tthis.change('Average', () => {\n\t\t\tfor (const { ref, at } of points) {\n\t\t\t\tconst dx = axis === 'horizontal' ? 0 : cx - at.x;\n\t\t\t\tconst dy = axis === 'vertical' ? 0 : cy - at.y;\n\t\t\t\tif (dx || dy) this.doc = this.movedAnchors(this.doc, [ref], dx, dy);\n\t\t\t}\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** Applies `fn` to every subpath of the target paths as one undo step. */\n\tprivate editSubpaths(label: string, ids: readonly string[] | undefined, fn: (sp: VectorSubpath, node: VectorPathNode, index: number) => VectorSubpath): VectorActionResult {\n\t\tconst paths = this.pathTargets(ids);\n\t\tif (!paths.length) return { ok: false, message: 'Select a path.' };\n\t\tconst set = new Set(paths.map((p) => p.id));\n\t\tthis.change(label, () => {\n\t\t\tthis.setDoc(mapTree(this.doc.nodes, (n) => (n.type === 'path' && set.has(n.id) ? { ...n, subpaths: n.subpaths.map((sp, i) => fn(sp, n, i)) } : n)));\n\t\t\tthis.dropStaleRefs();\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/**\n\t * Remove Redundant Points: drops anchors of the selected (or given) paths that can go\n\t * without moving the outline more than `tolerance` document units (default 0.5).\n\t */\n\tremoveRedundantPoints(options: { ids?: readonly string[]; tolerance?: number } = {}): VectorActionResult {\n\t\tconst tolerance = options.tolerance ?? 0.5;\n\t\treturn this.editSubpaths('Remove Redundant Points', options.ids, (sp) => removeRedundantSubpathPoints(sp, tolerance));\n\t}\n\n\t/**\n\t * Smooth: pulls handles towards smooth tangents by `amount` (0–1, default 1). With the direct\n\t * tool and points selected, only those points change.\n\t */\n\tsmoothPaths(options: { ids?: readonly string[]; amount?: number; refs?: readonly VectorAnchorRef[] } = {}): VectorActionResult {\n\t\tconst refs = options.refs ?? (!options.ids && this.tool === 'direct' && this.anchors.length ? this.anchors : null);\n\t\tconst ids = options.ids ?? (refs ? [...new Set(refs.map((r) => r.nodeId))] : undefined);\n\t\treturn this.editSubpaths('Smooth', ids, (sp, node, i) => {\n\t\t\tconst only = refs ? new Set(refs.filter((r) => r.nodeId === node.id && r.subpath === i).map((r) => r.index)) : undefined;\n\t\t\treturn only && !only.size ? sp : smoothSubpath(sp, options.amount ?? 1, only);\n\t\t});\n\t}\n\n\t/** Splits a cut subpath into pieces: separate paths for a one-subpath node, subpaths in place for a compound one. */\n\tprivate applyCuts(node: VectorPathNode, cut: (sp: VectorSubpath, index: number) => VectorSubpath[] | null): string[] | null {\n\t\tconst results = node.subpaths.map((sp, i) => cut(sp, i));\n\t\tif (results.every((r) => !r)) return null;\n\t\tconst subpaths = node.subpaths.flatMap((sp, i) => results[i] ?? [sp]);\n\t\tif (node.subpaths.length === 1) return this.replaceWithPieces(node, subpaths.map((sp) => [sp]));\n\t\tthis.replaceNode(node.id, { ...node, subpaths });\n\t\treturn [node.id];\n\t}\n\n\t/**\n\t * Scissors: cuts path `nodeId` at the outline point nearest document point `point` (at an\n\t * anchor when within the pick distance of one). A closed subpath opens there; an open one\n\t * splits in two (a one-subpath path becomes two paths). Returns the ids of the paths.\n\t */\n\tcutPathAt(nodeId: string, point: Vec2): string[] | null {\n\t\tconst node = this.getNode(nodeId);\n\t\tif (node?.type !== 'path' || node.locked) return null;\n\t\tconst local = this.toLocal(nodeId, point);\n\t\tconst hit = nearestSegment(node, local);\n\t\tif (!hit) return null;\n\t\tconst sp = node.subpaths[hit.subpath];\n\t\tconst n = sp.anchors.length;\n\t\tconst near = [hit.segment, (hit.segment + 1) % n].find((i) => Math.hypot(sp.anchors[i].x - hit.point.x, sp.anchors[i].y - hit.point.y) <= this.tolerance());\n\t\tif (!sp.closed && n > 1 && (near === 0 || near === n - 1) && !(hit.t > 1e-6 && hit.t < 1 - 1e-6)) return null;\n\t\tlet ids: string[] | null = null;\n\t\tthis.change('Cut Path', () => {\n\t\t\tids = this.applyCuts(node, (s, i) => (i !== hit.subpath ? null : near !== undefined ? cutSubpathAt(s, [near]) : splitSubpathAt(s, hit.segment, hit.t)));\n\t\t\tif (ids) {\n\t\t\t\tthis.selection = ids;\n\t\t\t\tthis.anchors = [];\n\t\t\t\tif (this.editingId === nodeId) this.editingId = null;\n\t\t\t}\n\t\t\tthis.dropStaleRefs();\n\t\t});\n\t\treturn ids;\n\t}\n\n\t/** Scissors on the selected (or given) anchors: each cuts its subpath there (see `cutPathAt`). */\n\tcutAtAnchors(refs: readonly VectorAnchorRef[] = this.anchors): VectorActionResult {\n\t\tconst byNode = new Map<string, VectorAnchorRef[]>();\n\t\tfor (const r of refs) if (getAnchor(this.doc, r)) byNode.set(r.nodeId, [...(byNode.get(r.nodeId) ?? []), r]);\n\t\tif (!byNode.size) return { ok: false, message: 'Select points to cut at.' };\n\t\tconst created: string[] = [];\n\t\tthis.change('Cut Path', () => {\n\t\t\tfor (const [id, list] of byNode) {\n\t\t\t\tconst node = this.getNode(id) as VectorPathNode;\n\t\t\t\tif (node.locked) continue;\n\t\t\t\tconst ids = this.applyCuts(node, (sp, i) => {\n\t\t\t\t\tconst at = list.filter((r) => r.subpath === i).map((r) => r.index);\n\t\t\t\t\tconst pieces = at.length ? cutSubpathAt(sp, at) : null;\n\t\t\t\t\treturn pieces && !(pieces.length === 1 && pieces[0].closed === sp.closed) ? pieces : null;\n\t\t\t\t});\n\t\t\t\tif (ids) created.push(...ids);\n\t\t\t}\n\t\t\tif (created.length) this.selection = created;\n\t\t\tthis.anchors = [];\n\t\t\tthis.dropStaleRefs();\n\t\t});\n\t\treturn created.length ? { ok: true } : { ok: false, message: 'Points at the ends of open paths do not cut.' };\n\t}\n\n\t/** Visible, unlocked paths a stroke over `points` can reach: in `ids`, the selection, or (with neither) any whose bounds it crosses. */\n\tprivate strokeTargets(points: readonly Vec2[], ids: readonly string[] | undefined, pad: number): VectorPathNode[] {\n\t\tconst blocked = new Set<string>();\n\t\twalkVectorNodes(this.doc.nodes, (n, parent) => {\n\t\t\tif (n.visible === false || n.locked || (parent && blocked.has(parent.id))) blocked.add(n.id);\n\t\t});\n\t\tconst visible = (p: VectorPathNode) => !blocked.has(p.id);\n\t\tif (ids?.length || this.selection.length) return this.pathTargets(ids ?? this.selection).filter(visible);\n\t\tconst box = polylineBounds(points.map(toPoint), pad);\n\t\tif (!box) return [];\n\t\tconst out: VectorPathNode[] = [];\n\t\twalkVectorNodes(this.doc.nodes, (n) => {\n\t\t\tif (n.type !== 'path' || !visible(n)) return;\n\t\t\tconst b = vectorNodeBounds(n, vectorParentMatrix(this.doc.nodes, n.id) ?? IDENTITY_MATRIX);\n\t\t\tif (b && boundsIntersect(b, box)) out.push(n);\n\t\t});\n\t\treturn out;\n\t}\n\n\t/** A stroke's points in node `id`'s own space. */\n\tprivate localPoints(id: string, points: readonly Vec2[]): Point[] {\n\t\tconst inv = invertMatrix(this.worldOf(id));\n\t\treturn points.map((p) => toPoint(applyMatrix(inv, p)));\n\t}\n\n\t/**\n\t * Knife: cuts closed paths along the polyline `points` (document space) wherever it crosses\n\t * them from edge to edge, with no gap between the pieces. It cuts the given paths, the\n\t * selection, or (with neither) every path it crosses. Returns the ids of the pieces (empty\n\t * when nothing was cut).\n\t */\n\tknife(points: readonly Vec2[], options: { ids?: readonly string[] } = {}): string[] {\n\t\tconst pts = simplifyPolyline(points.map(toPoint), 0.25 / this.viewport.zoom).map(([x, y]) => ({ x, y }));\n\t\tif (pts.length < 2) return [];\n\t\tconst plan: { node: VectorPathNode; pieces: VectorSubpath[][] }[] = [];\n\t\tfor (const node of this.strokeTargets(pts, options.ids, 0)) {\n\t\t\tconst closed = node.subpaths.filter((sp) => sp.anchors.length > 1);\n\t\t\tif (!closed.length || closed.some((sp) => !sp.closed)) continue;\n\t\t\tconst result = knifePath(subpathsToBezier(closed), node.fillRule ?? 'nonzero', this.localPoints(node.id, pts));\n\t\t\tif (result) plan.push({ node, pieces: result.map((path) => bezierToSubpaths(path)) });\n\t\t}\n\t\tif (!plan.length) return [];\n\t\tconst created: string[] = [];\n\t\tthis.change('Knife', () => {\n\t\t\tfor (const { node, pieces } of plan) created.push(...this.replaceWithPieces(node, pieces));\n\t\t\tthis.selection = created;\n\t\t\tthis.anchors = [];\n\t\t\tthis.dropStaleRefs();\n\t\t});\n\t\treturn created;\n\t}\n\n\t/**\n\t * Eraser: rubs out a round stroke of diameter `size` (default: the `eraserSize` setting)\n\t * along `points` (document space). Filled closed paths lose that area (pieces that come\n\t * apart become separate paths); outlines are cut where they pass under it. It erases from\n\t * the given paths, the selection, or (with neither) every path it reaches. Returns the ids\n\t * of the paths it changed or created.\n\t */\n\terase(points: readonly Vec2[], options: { ids?: readonly string[]; size?: number } = {}): string[] {\n\t\tconst width = options.size ?? this.editing.eraserSize;\n\t\tif (!(width > 0) || !points.length) return [];\n\t\tconst pts = simplifyPolyline(points.map(toPoint), Math.min(width * 0.05, 0.25 / this.viewport.zoom));\n\t\tconst region = eraserRegion(pts, width);\n\t\tif (!region.length) return [];\n\t\tconst plan: { node: VectorPathNode; pieces: VectorSubpath[][] }[] = [];\n\t\tconst rb = bezierPathBounds(region);\n\t\tconst reach = rb ? [{ x: rb.minX, y: rb.minY }, { x: rb.maxX, y: rb.maxY }] : points;\n\t\tfor (const node of this.strokeTargets(reach, options.ids, 0)) {\n\t\t\tconst local = transformBezierPath(region, invertMatrix(this.worldOf(node.id)));\n\t\t\tconst live = node.subpaths.filter((sp) => sp.anchors.length > 1);\n\t\t\tif (!live.length) continue;\n\t\t\tif (hasPaint(node.fill) && live.every((sp) => sp.closed)) {\n\t\t\t\tconst faces = eraseFromFill(subpathsToBezier(live), node.fillRule ?? 'nonzero', local);\n\t\t\t\tif (faces) plan.push({ node, pieces: faces.map((f) => bezierToSubpaths(f)) });\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst results = live.map((sp) => eraseFromOutline(sp, local));\n\t\t\tif (results.every((r) => !r)) continue;\n\t\t\tconst subpaths = live.flatMap((sp, i) => results[i] ?? [sp]);\n\t\t\tplan.push({ node, pieces: live.length === 1 ? subpaths.map((sp) => [sp]) : [subpaths] });\n\t\t}\n\t\tif (!plan.length) return [];\n\t\tconst changed: string[] = [];\n\t\tthis.change('Erase', () => {\n\t\t\tfor (const { node, pieces } of plan) changed.push(...this.replaceWithPieces(node, pieces));\n\t\t\tthis.anchors = [];\n\t\t\tthis.dropStaleRefs();\n\t\t\tif (this.selection.length || options.ids) this.selection = [...new Set([...this.selection, ...changed.filter((id) => this.getNode(id))])];\n\t\t});\n\t\treturn changed;\n\t}\n\n\t/** Ends the pen path being drawn (a lone anchor is removed). */\n\tfinishPath(): void {\n\t\tif (!this.pen) return;\n\t\tconst node = this.getNode(this.pen.nodeId) as VectorPathNode | undefined;\n\t\tconst sp = node?.subpaths[this.pen.subpath];\n\t\tconst pen = this.pen;\n\t\tthis.pen = null;\n\t\tthis.pointer = null;\n\t\tif (node && sp && sp.anchors.length < 2) {\n\t\t\tconst subpaths = node.subpaths.filter((_, i) => i !== pen.subpath);\n\t\t\tthis.change('Delete Path', () => {\n\t\t\t\tthis.replaceNode(node.id, subpaths.length ? { ...node, subpaths } : null);\n\t\t\t\tif (!subpaths.length) {\n\t\t\t\t\tthis.selection = this.selection.filter((id) => id !== node.id);\n\t\t\t\t\tif (this.editingId === node.id) this.editingId = null;\n\t\t\t\t}\n\t\t\t\tthis.anchors = [];\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tthis.notify();\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Clipboard\n\t// -------------------------------------------------------------------------\n\n\t/** Copies the selection. Returns JSON text for the system clipboard (or null). */\n\tcopy(): string | null {\n\t\tconst nodes = this.selectedRoots()\n\t\t\t.map((id) => this.getNode(id))\n\t\t\t.filter((n): n is VectorNode => !!n)\n\t\t\t.map((n) => cloneVectorNode(n));\n\t\tif (!nodes.length) return null;\n\t\tthis.clipboard = nodes;\n\t\treturn JSON.stringify({ type: VECTOR_CLIPBOARD_TYPE, nodes });\n\t}\n\n\tcut(): string | null {\n\t\tconst text = this.copy();\n\t\tif (text) this.removeNodes(this.selectedRoots(), { label: 'Cut' });\n\t\treturn text;\n\t}\n\n\t/**\n\t * Pastes `text` (from `copy()`, or SVG markup) or the internal clipboard, offset by 10, 10.\n\t * Returns the new ids.\n\t */\n\tpaste(text?: string, options: { offset?: Vec2 } = {}): string[] {\n\t\tlet nodes: VectorNode[] | null = null;\n\t\tif (text) {\n\t\t\tconst trimmed = text.trim();\n\t\t\tif (trimmed.startsWith('{')) {\n\t\t\t\ttry {\n\t\t\t\t\tconst data = JSON.parse(trimmed);\n\t\t\t\t\tif (data?.type === VECTOR_CLIPBOARD_TYPE && Array.isArray(data.nodes)) nodes = data.nodes;\n\t\t\t\t} catch {\n\t\t\t\t\tnodes = null;\n\t\t\t\t}\n\t\t\t} else if (/<svg[\\s>]/i.test(trimmed)) {\n\t\t\t\ttry {\n\t\t\t\t\tnodes = parseVectorSvg(trimmed).nodes;\n\t\t\t\t} catch {\n\t\t\t\t\tnodes = null;\n\t\t\t\t}\n\t\t\t}\n\t\t} else nodes = this.clipboard;\n\t\tif (!nodes?.length) return [];\n\t\tconst offset = options.offset ?? (text && !text.trim().startsWith('{') ? { x: 0, y: 0 } : { x: 10, y: 10 });\n\t\tconst copies = nodes.map((n) => transformVectorNode(this.freshIds(n), translateMatrix(offset.x, offset.y)));\n\t\tthis.change('Paste', () => {\n\t\t\tthis.setDoc([...this.doc.nodes, ...copies]);\n\t\t\tthis.selection = copies.map((n) => n.id);\n\t\t\tthis.anchors = [];\n\t\t});\n\t\tif (!text) this.clipboard = copies.map((n) => cloneVectorNode(n));\n\t\treturn copies.map((n) => n.id);\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Undo\n\t// -------------------------------------------------------------------------\n\n\tundo(): boolean {\n\t\tthis.cancelDrag();\n\t\tthis.pen = null;\n\t\treturn this.history.undo();\n\t}\n\n\tredo(): boolean {\n\t\tthis.cancelDrag();\n\t\treturn this.history.redo();\n\t}\n\n\t/** Deletes selected anchors (direct tool) or nodes. */\n\tdeleteSelection(): VectorActionResult {\n\t\tif (this.tool === 'direct' && this.anchors.length) return this.deleteAnchors();\n\t\treturn this.removeNodes(this.selectedRoots());\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Pointer input\n\t// -------------------------------------------------------------------------\n\n\t/** The fields the anchor / handle helpers read, without building a full state. */\n\tprivate view() {\n\t\treturn { document: this.doc, selection: this.selection, editingId: this.editingId, tool: this.tool, pen: this.pen, selectedAnchors: this.anchors };\n\t}\n\n\tprivate tolerance() {\n\t\treturn this.hitRadius / this.viewport.zoom;\n\t}\n\n\tprivate hitTransformHandle(screen: Vec2): VectorTransformHandle | null {\n\t\tif (this.tool !== 'select' || !this.selection.length) return null;\n\t\tconst frame = this.frameOf(this.selectedRoots());\n\t\tif (!frame) return null;\n\t\tconst { box, handles } = getVectorSelectionBoxGeometry(frame, this.viewport);\n\t\tconst r = this.hitRadius + 2;\n\t\t// Tiny boxes: only corners and rotation, so the box can still be grabbed.\n\t\tconst usable = box.width < 2 * r || box.height < 2 * r ? handles.filter((h) => h.handle.length === 2 || h.handle === 'rotate') : handles;\n\t\tfor (const h of usable) if (Math.hypot(h.screen.x - screen.x, h.screen.y - screen.y) <= r * Math.SQRT2) return h.handle;\n\t\treturn null;\n\t}\n\n\tprivate hitHandle(screen: Vec2): VectorHandleRef | null {\n\t\tfor (const ref of getVisibleHandles(this.view()).reverse()) {\n\t\t\tconst a = getAnchorWorld(this.doc, ref);\n\t\t\tconst h = a?.[ref.handle];\n\t\t\tif (!h) continue;\n\t\t\tconst s = this.documentToScreen(h);\n\t\t\tif (Math.hypot(s.x - screen.x, s.y - screen.y) <= this.hitRadius) return ref;\n\t\t}\n\t\treturn null;\n\t}\n\n\tprivate cornerView() {\n\t\treturn { ...this.view(), viewport: this.viewport };\n\t}\n\n\tprivate hitCornerWidget(screen: Vec2): VectorCornerWidget | null {\n\t\tlet best: VectorCornerWidget | null = null;\n\t\tlet bestD = Infinity;\n\t\tfor (const w of getVectorCornerWidgets(this.cornerView())) {\n\t\t\tconst s = this.documentToScreen(w);\n\t\t\tconst d = Math.hypot(s.x - screen.x, s.y - screen.y);\n\t\t\tif (d <= this.hitRadius && d < bestD) {\n\t\t\t\tbest = w;\n\t\t\t\tbestD = d;\n\t\t\t}\n\t\t}\n\t\treturn best;\n\t}\n\n\tprivate hitAnchor(screen: Vec2, refs: readonly VectorAnchorRef[]): VectorAnchorRef | null {\n\t\tlet best: VectorAnchorRef | null = null;\n\t\tlet bestD = Infinity;\n\t\tfor (const ref of refs) {\n\t\t\tconst a = getAnchorWorld(this.doc, ref);\n\t\t\tif (!a) continue;\n\t\t\tconst s = this.documentToScreen(a);\n\t\t\tconst d = Math.hypot(s.x - screen.x, s.y - screen.y);\n\t\t\tif (d <= this.hitRadius && d < bestD) {\n\t\t\t\tbest = ref;\n\t\t\t\tbestD = d;\n\t\t\t}\n\t\t}\n\t\treturn best;\n\t}\n\n\t/** Deepest unlocked visible path under `p`. */\n\t/** Live groups being edited (selected, or holding the selection or the edited path): their children are hit as stored. */\n\tprivate enteredGroups(): Set<string> {\n\t\treturn vectorEnteredGroups(this.doc.nodes, this.editingId ? [...this.selection, this.editingId] : this.selection);\n\t}\n\n\tprivate hitPath(p: Vec2): VectorPathNode | null {\n\t\tconst tol = this.tolerance();\n\t\tconst entered = this.enteredGroups();\n\t\tconst search = (list: readonly VectorNode[], parent: AffineMatrix): VectorPathNode | null => {\n\t\t\tfor (let i = list.length - 1; i >= 0; i--) {\n\t\t\t\tconst n = list[i];\n\t\t\t\tif (n.visible === false || n.locked) continue;\n\t\t\t\tif (n.type === 'group') {\n\t\t\t\t\t// A live group not being edited is hit where it draws (a click in a subtracted\n\t\t\t\t\t// hole goes through), then picks the child under the pointer.\n\t\t\t\t\t// A mask group likewise, where its mask lets the contents through.\n\t\t\t\t\tif ((hasOwnVectorAppearance(n) || isVectorMaskActive(n)) && !entered.has(n.id) && !hitTestVectorNode(n, p, tol, parent, { entered })) continue;\n\t\t\t\t\tconst found = search(n.children, multiplyMatrices(nodeMatrix(n), parent));\n\t\t\t\t\tif (found) return found;\n\t\t\t\t} else if (hitTestVectorNode(n, p, tol, parent, { entered })) return n;\n\t\t\t}\n\t\t\treturn null;\n\t\t};\n\t\treturn search(this.doc.nodes, IDENTITY_MATRIX);\n\t}\n\n\t/** Endpoints of open subpaths of unlocked visible paths (pen tool resumes from them). */\n\tprivate openEndpoints(): VectorAnchorRef[] {\n\t\tconst out: VectorAnchorRef[] = [];\n\t\twalkVectorNodes(this.doc.nodes, (n) => {\n\t\t\tif (n.type !== 'path' || n.locked || n.visible === false) return;\n\t\t\tn.subpaths.forEach((sp, s) => {\n\t\t\t\tif (sp.closed || sp.anchors.length < 1) return;\n\t\t\t\tout.push({ nodeId: n.id, subpath: s, index: 0 });\n\t\t\t\tif (sp.anchors.length > 1) out.push({ nodeId: n.id, subpath: s, index: sp.anchors.length - 1 });\n\t\t\t});\n\t\t});\n\t\treturn out;\n\t}\n\n\tprivate computeHover(screen: Vec2): VectorHover | null {\n\t\tconst p = this.screenToDocument(screen);\n\t\tswitch (this.tool) {\n\t\t\tcase 'select': {\n\t\t\t\tconst handle = this.hitTransformHandle(screen);\n\t\t\t\tif (handle) return { nodeId: null, transformHandle: handle };\n\t\t\t\tconst node = hitTestVectorNodes(this.doc.nodes, p, this.tolerance(), { entered: this.enteredGroups() });\n\t\t\t\treturn node ? { nodeId: node.id } : null;\n\t\t\t}\n\t\t\tcase 'direct': {\n\t\t\t\tconst handle = this.hitHandle(screen);\n\t\t\t\tif (handle) return { nodeId: handle.nodeId, handle };\n\t\t\t\tconst anchor = this.hitAnchor(screen, getVisibleAnchors(this.view()));\n\t\t\t\tif (anchor) return { nodeId: anchor.nodeId, anchor };\n\t\t\t\tconst widget = this.hitCornerWidget(screen);\n\t\t\t\tif (widget) return { nodeId: widget.ref.nodeId, corner: widget.ref };\n\t\t\t\tfor (const id of getEditablePathIds(this.view())) {\n\t\t\t\t\tconst node = this.getNode(id) as VectorPathNode;\n\t\t\t\t\tconst seg = nearestSegment(node, this.toLocal(id, p));\n\t\t\t\t\tif (seg && seg.distance <= this.tolerance()) {\n\t\t\t\t\t\tconst point = applyMatrix(this.worldOf(id), seg.point);\n\t\t\t\t\t\treturn { nodeId: id, segment: { nodeId: id, subpath: seg.subpath, segment: seg.segment, t: seg.t, point } };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst path = this.hitPath(p);\n\t\t\t\treturn path ? { nodeId: path.id } : null;\n\t\t\t}\n\t\t\tcase 'pen': {\n\t\t\t\tconst refs = this.pen ? [{ nodeId: this.pen.nodeId, subpath: this.pen.subpath, index: 0 }] : this.openEndpoints();\n\t\t\t\tconst anchor = this.hitAnchor(screen, refs);\n\t\t\t\treturn anchor ? { nodeId: anchor.nodeId, anchor } : null;\n\t\t\t}\n\t\t\tcase 'scissors': {\n\t\t\t\tconst seg = this.nearestOutline(p);\n\t\t\t\treturn seg ? { nodeId: seg.nodeId, segment: seg } : null;\n\t\t\t}\n\t\t\tcase 'blend': {\n\t\t\t\tconst hit = this.blendHit(screen, p);\n\t\t\t\tif (!hit) return null;\n\t\t\t\treturn hit.anchor === null ? { nodeId: hit.nodeId } : { nodeId: hit.nodeId, anchor: { nodeId: hit.nodeId, subpath: 0, index: hit.anchor } };\n\t\t\t}\n\t\t\tcase 'width': {\n\t\t\t\tconst hit = this.hitWidthHandle(screen);\n\t\t\t\tif (hit) return { nodeId: hit.ref.nodeId, widthPoint: hit.ref };\n\t\t\t\tconst spot = this.strokeSpotAt(p);\n\t\t\t\treturn spot ? { nodeId: spot.nodeId, strokeSpot: spot } : null;\n\t\t\t}\n\t\t\tcase 'gradient': {\n\t\t\t\tconst a = this.getGradientAnnotator();\n\t\t\t\tconst part = a ? hitVectorGradientAnnotator(a, screen) : null;\n\t\t\t\tif (a && part) return { nodeId: a.nodeId, gradient: part };\n\t\t\t\tconst path = this.hitPath(p);\n\t\t\t\treturn path ? { nodeId: path.id } : null;\n\t\t\t}\n\t\t\tcase 'envelope': {\n\t\t\t\tconst hit = this.hitEnvelope(screen, p);\n\t\t\t\tif (hit) return { nodeId: hit.target.node.id, envelope: { nodeId: hit.target.node.id, index: hit.index } };\n\t\t\t\tconst node = hitTestVectorNodes(this.doc.nodes, p, this.tolerance(), { entered: this.enteredGroups() });\n\t\t\t\treturn node ? { nodeId: node.id } : null;\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\treturn null;\n\t\t}\n\t}\n\n\tprivate widthView(): Pick<VectorEditorState, 'document' | 'selection' | 'tool' | 'hover' | 'interaction' | 'selectedWidthPoints'> {\n\t\treturn { document: this.doc, selection: this.selection, tool: this.tool, hover: this.hover, interaction: this.interaction, selectedWidthPoints: this.widthPoints };\n\t}\n\n\t/** The width point centre or side end under `screen` (width tool). */\n\tprivate hitWidthHandle(screen: Vec2): { ref: VectorWidthPointRef & { part: 'center' | 'left' | 'right' } } | null {\n\t\tconst shown = this.hitWidthHandleIn(screen, this.widthView());\n\t\tif (shown) return shown;\n\t\t// The points of the stroke under the pointer, when no hover has shown them yet.\n\t\tconst spot = this.strokeSpotAt(this.screenToDocument(screen));\n\t\treturn spot && spot.nodeId !== this.hover?.nodeId ? this.hitWidthHandleIn(screen, { ...this.widthView(), hover: { nodeId: spot.nodeId } }) : null;\n\t}\n\n\tprivate hitWidthHandleIn(screen: Vec2, view: Pick<VectorEditorState, 'document' | 'selection' | 'tool' | 'hover' | 'interaction' | 'selectedWidthPoints'>): { ref: VectorWidthPointRef & { part: 'center' | 'left' | 'right' } } | null {\n\t\tlet best: { ref: VectorWidthPointRef & { part: 'center' | 'left' | 'right' } } | null = null;\n\t\tlet bestD = Infinity;\n\t\tfor (const h of getVectorWidthHandles(view)) {\n\t\t\tfor (const [part, q] of [['center', h], ['left', h.left], ['right', h.right]] as const) {\n\t\t\t\tconst s = this.documentToScreen(q);\n\t\t\t\tconst d = Math.hypot(s.x - screen.x, s.y - screen.y);\n\t\t\t\t// Ties go to the centre (listed first), so a zero-width point can still be moved.\n\t\t\t\tif (d <= this.hitRadius && d < bestD - 1e-9) {\n\t\t\t\t\tbest = { ref: { ...h.ref, part } };\n\t\t\t\t\tbestD = d;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn best;\n\t}\n\n\t/** The nearest stroked, unlocked visible path whose stroke is under document point `p` (width tool). */\n\tprivate strokeSpotAt(p: Vec2): VectorStrokeSpot | null {\n\t\tlet best: (VectorStrokeSpot & { distance: number }) | null = null;\n\t\tconst tol = this.tolerance();\n\t\twalkVectorNodes(this.doc.nodes, (n) => {\n\t\t\tif (n.type !== 'path' || n.visible === false || n.locked) return;\n\t\t\tif (hiddenOrLocked(this.doc.nodes, n.id)) return;\n\t\t\tconst g = vectorStrokeWidthGeometry(n);\n\t\t\tif (!g) return;\n\t\t\tconst m = this.worldOf(n.id);\n\t\t\tconst local = applyMatrix(invertMatrix(m), p);\n\t\t\tconst near = nearestStrokePosition(g.line, [local.x, local.y]);\n\t\t\tif (!near) return;\n\t\t\tconst profile = strokeWidthProfileOf(n);\n\t\t\tconst w = widthProfileAt(profile, near.t);\n\t\t\tconst reach = Math.max(near.side === 'left' ? w.left : w.right, 0) * g.unit + tol;\n\t\t\tif (near.distance > reach || near.distance > widthProfileMaxScale(profile) * g.unit + tol) return;\n\t\t\tif (best && near.distance >= best.distance) return;\n\t\t\tconst nx = near.tangent[1];\n\t\t\tconst ny = -near.tangent[0];\n\t\t\tconst [cx, cy] = near.point;\n\t\t\tbest = {\n\t\t\t\tnodeId: n.id,\n\t\t\t\tsubpath: near.contour,\n\t\t\t\tt: near.t,\n\t\t\t\tpoint: applyMatrix(m, { x: cx, y: cy }),\n\t\t\t\tleft: applyMatrix(m, { x: cx + nx * w.left * g.unit, y: cy + ny * w.left * g.unit }),\n\t\t\t\tright: applyMatrix(m, { x: cx - nx * w.right * g.unit, y: cy - ny * w.right * g.unit }),\n\t\t\t\tdistance: near.distance\n\t\t\t};\n\t\t});\n\t\tif (!best) return null;\n\t\tconst { distance: _d, ...spot } = best as VectorStrokeSpot & { distance: number };\n\t\treturn spot;\n\t}\n\n\tprivate downWidth(screen: Vec2, p: Vec2, input: VectorPointerInput) {\n\t\tconst hit = this.hitWidthHandle(screen);\n\t\tconst spot = hit ? null : this.strokeSpotAt(p);\n\t\tconst nodeId = hit?.ref.nodeId ?? spot?.nodeId;\n\t\tconst node = nodeId ? this.getNode(nodeId) : undefined;\n\t\tconst g = node?.type === 'path' ? vectorStrokeWidthGeometry(node) : null;\n\t\tif (this.widthEdit) this.widthEdit = null;\n\t\tif (!node || node.type !== 'path' || !g) {\n\t\t\t// A click away from strokes deselects (Shift keeps the selection).\n\t\t\tif (!input.shift) this.widthPoints = [];\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tconst before = this.snapshot();\n\t\tconst local = this.toLocal(node.id, p);\n\t\tlet profile: VectorWidthPoint[];\n\t\tlet ref: VectorWidthPointRef;\n\t\tlet mode: 'width' | 'move' | 'copy' = 'width';\n\t\tlet side: 'both' | 'left' | 'right' = 'both';\n\t\tlet group: number[] = [];\n\t\tlet shift = false;\n\t\tlet t: number;\n\t\tlet subpath: number;\n\t\tif (hit) {\n\t\t\tprofile = sanitizeWidthProfile(node.strokeWidthProfile);\n\t\t\tref = { nodeId: node.id, index: hit.ref.index, subpath: hit.ref.subpath };\n\t\t\tt = profile[ref.index]?.t ?? 0;\n\t\t\tsubpath = hit.ref.subpath;\n\t\t\tif (hit.ref.part === 'center') mode = input.alt && profile.length < VECTOR_WIDTH_PROFILE_MAX_POINTS ? 'copy' : 'move';\n\t\t\telse if (input.alt) side = hit.ref.part;\n\t\t\tconst same = this.widthPoints.length > 0 && this.widthPoints[0].nodeId === node.id;\n\t\t\tconst selected = same && this.widthPoints.some((r) => r.index === ref.index);\n\t\t\tif (input.shift && mode !== 'copy') {\n\t\t\t\t// Shift-click adds the point to the selection, or takes it out (and drags nothing).\n\t\t\t\tif (selected) {\n\t\t\t\t\tthis.widthPoints = this.widthPoints.filter((r) => r.index !== ref.index);\n\t\t\t\t\tthis.notify();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.widthPoints = same ? [...this.widthPoints, ref] : [ref];\n\t\t\t\tshift = true;\n\t\t\t} else if (mode === 'copy' || !selected) this.widthPoints = [ref];\n\t\t\tif (mode !== 'copy') group = this.widthPoints.filter((r) => r.index !== ref.index).map((r) => r.index);\n\t\t} else {\n\t\t\tconst near = nearestStrokePosition(g.line, [local.x, local.y])!;\n\t\t\tconst inserted = insertWidthPoint(strokeWidthProfileOf(node), near.t);\n\t\t\tprofile = inserted.points;\n\t\t\tsubpath = near.contour;\n\t\t\tt = near.t;\n\t\t\tref = { nodeId: node.id, index: inserted.index, subpath };\n\t\t\tif (input.alt) side = near.side;\n\t\t\tthis.widthPoints = [ref];\n\t\t}\n\t\tconst at = strokePositionPoint(g.line, subpath, t);\n\t\tif (!at) return;\n\t\tconst base = { ...this.doc, nodes: mapTree(this.doc.nodes, (n) => (n.id === node.id ? { ...(n as VectorPathNode), strokeWidthProfile: profile } : n)) };\n\t\tthis.doc = base;\n\t\tthis.drag = {\n\t\t\tkind: 'width',\n\t\t\tref,\n\t\t\tmode,\n\t\t\tside,\n\t\t\tsource: ref.index,\n\t\t\tgroup,\n\t\t\tshift,\n\t\t\trenumber: null,\n\t\t\torigin: { x: at.point[0], y: at.point[1] },\n\t\t\tnormal: { x: at.tangent[1], y: -at.tangent[0] },\n\t\t\tunit: g.unit,\n\t\t\tprofile,\n\t\t\tbefore,\n\t\t\tbase,\n\t\t\tstart: p,\n\t\t\tmoved: false,\n\t\t\tcreated: !hit\n\t\t};\n\t\tconst w = profile[ref.index];\n\t\tthis.interaction = { type: 'width', ref, mode, width: (w.left + w.right) * g.unit };\n\t\tthis.notify(!hit);\n\t}\n\n\t/**\n\t * A width-tool slide to `t` on the dragged point's subpath lands exactly on another width\n\t * point (not `skip`) within the pick distance on screen: dropping a point onto another makes\n\t * a discontinuous point (two points at one `t`, the width steps there).\n\t */\n\tprivate snapWidthT(drag: Extract<Drag, { kind: 'width' }>, node: VectorPathNode, line: BezierPath, t: number, skip: number): number {\n\t\tconst m = this.worldOf(node.id, drag.base);\n\t\tconst at = strokePositionPoint(line, drag.ref.subpath, t);\n\t\tif (!at) return t;\n\t\tconst here = this.documentToScreen(applyMatrix(m, { x: at.point[0], y: at.point[1] }));\n\t\tlet best = t;\n\t\tlet bestD = this.hitRadius;\n\t\tdrag.profile.forEach((w, i) => {\n\t\t\tif (i === skip || drag.group.includes(i)) return;\n\t\t\tconst q = strokePositionPoint(line, drag.ref.subpath, w.t);\n\t\t\tif (!q) return;\n\t\t\tconst s = this.documentToScreen(applyMatrix(m, { x: q.point[0], y: q.point[1] }));\n\t\t\tconst d = Math.hypot(s.x - here.x, s.y - here.y);\n\t\t\tif (d <= bestD) {\n\t\t\t\tbest = w.t;\n\t\t\t\tbestD = d;\n\t\t\t}\n\t\t});\n\t\treturn best;\n\t}\n\n\t/** The nearest outline point of any visible, unlocked path within the pick distance of document point `p`. */\n\tprivate nearestOutline(p: Vec2): NonNullable<VectorHover['segment']> | null {\n\t\tlet best: (NonNullable<VectorHover['segment']> & { distance: number }) | null = null;\n\t\tconst tol = this.tolerance();\n\t\tconst visit = (list: readonly VectorNode[], parent: AffineMatrix) => {\n\t\t\tfor (let i = list.length - 1; i >= 0; i--) {\n\t\t\t\tconst n = list[i];\n\t\t\t\tif (n.visible === false || n.locked) continue;\n\t\t\t\tconst m = multiplyMatrices(nodeMatrix(n), parent);\n\t\t\t\tif (n.type === 'group') {\n\t\t\t\t\tvisit(n.children, m);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst hit = nearestSegment(n, applyMatrix(invertMatrix(m), p));\n\t\t\t\t// Node matrices are rigid, so local distances are document distances.\n\t\t\t\tif (hit && hit.distance <= tol && (!best || hit.distance < best.distance - 1e-9)) {\n\t\t\t\t\tbest = { nodeId: n.id, subpath: hit.subpath, segment: hit.segment, t: hit.t, point: applyMatrix(m, hit.point), distance: hit.distance };\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\tvisit(this.doc.nodes, IDENTITY_MATRIX);\n\t\tif (!best) return null;\n\t\tconst { distance: _d, ...seg } = best as NonNullable<VectorHover['segment']> & { distance: number };\n\t\treturn seg;\n\t}\n\n\tprivate sameHover(a: VectorHover | null, b: VectorHover | null): boolean {\n\t\treturn JSON.stringify(a) === JSON.stringify(b);\n\t}\n\n\t/** Primary button down on the canvas at `screen` (pixels relative to the canvas). */\n\tpointerDown(screen: Vec2, input: VectorPointerInput = {}): void {\n\t\tthis.cancelDrag();\n\t\tif (input.button === 1 || this.spaceHeld) {\n\t\t\tthis.drag = { kind: 'pan', start: screen, viewport: this.viewport };\n\t\t\tthis.interaction = { type: 'pan' };\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tif (input.button !== undefined && input.button !== 0) return;\n\t\tconst p = this.screenToDocument(screen);\n\t\tthis.pointer = p;\n\t\tswitch (this.tool) {\n\t\t\tcase 'select':\n\t\t\t\treturn this.downSelect(screen, p, input);\n\t\t\tcase 'direct':\n\t\t\t\treturn this.downDirect(screen, p, input);\n\t\t\tcase 'pen':\n\t\t\t\treturn this.downPen(screen, p, input);\n\t\t\tcase 'pencil':\n\t\t\t\treturn this.downPencil(p, input);\n\t\t\tcase 'width':\n\t\t\t\treturn this.downWidth(screen, p, input);\n\t\t\tcase 'shapeBuilder':\n\t\t\tcase 'paintBucket':\n\t\t\t\treturn this.downBuilder(p, input);\n\t\t\tcase 'blend':\n\t\t\t\treturn this.downBlend(screen, p);\n\t\t\tcase 'gradient':\n\t\t\t\treturn this.downGradient(screen, p, input);\n\t\t\tcase 'envelope':\n\t\t\t\treturn this.downEnvelope(screen, p, input);\n\t\t\tcase 'scissors': {\n\t\t\t\tconst seg = this.nearestOutline(p);\n\t\t\t\tif (seg) this.cutPathAt(seg.nodeId, seg.point);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'knife':\n\t\t\tcase 'eraser':\n\t\t\t\tthis.drag = { kind: 'stroke', tool: this.tool, points: [p], before: this.snapshot() };\n\t\t\t\tthis.interaction = { type: 'stroke', tool: this.tool, points: [p], width: this.tool === 'eraser' ? this.editing.eraserSize : 0 };\n\t\t\t\tthis.notify();\n\t\t\t\treturn;\n\t\t\tdefault:\n\t\t\t\treturn this.downShape(p, input);\n\t\t}\n\t}\n\n\t/** The shape under the pointer for the Blend tool, and the anchor of its first subpath under it. */\n\tprivate blendHit(screen: Vec2, p: Vec2): VectorBlendStart | null {\n\t\tconst node = hitTestVectorNodes(this.doc.nodes, p, this.tolerance());\n\t\tif (!node) return null;\n\t\tlet anchor: number | null = null;\n\t\tif (node.type === 'path' && node.subpaths[0]) {\n\t\t\tconst refs = node.subpaths[0].anchors.map((_, index) => ({ nodeId: node.id, subpath: 0, index }));\n\t\t\tanchor = this.hitAnchor(screen, refs)?.index ?? null;\n\t\t}\n\t\treturn { nodeId: node.id, anchor };\n\t}\n\n\t/** Blend tool: the first click picks the shape to blend from, the next one blends it into the shape clicked. */\n\tprivate downBlend(screen: Vec2, p: Vec2) {\n\t\tconst hit = this.blendHit(screen, p);\n\t\tconst start = this.blendStart;\n\t\tif (!hit) {\n\t\t\tif (start) {\n\t\t\t\tthis.blendStart = null;\n\t\t\t\tthis.notify();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tif (!start || start.nodeId === hit.nodeId) {\n\t\t\tthis.blendStart = hit;\n\t\t\tthis.selection = [hit.nodeId];\n\t\t\tthis.anchors = [];\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tthis.blendStart = null;\n\t\tconst first = this.getNode(start.nodeId);\n\t\tif (isVectorBlendGroup(first)) {\n\t\t\t// Clicking on from a blend adds the shape as its next key.\n\t\t\tif (!this.addBlendKey(first.id, hit.nodeId, { start: hit.anchor }).ok) this.notify();\n\t\t\treturn;\n\t\t}\n\t\tconst starts: Record<string, number> = {};\n\t\tif (start.anchor !== null) starts[start.nodeId] = start.anchor;\n\t\tif (hit.anchor !== null) starts[hit.nodeId] = hit.anchor;\n\t\tif (!this.blend({ ids: [start.nodeId, hit.nodeId], keepOrder: true, starts })) this.notify();\n\t}\n\n\tprivate downSelect(screen: Vec2, p: Vec2, input: VectorPointerInput) {\n\t\tconst handle = this.hitTransformHandle(screen);\n\t\tif (handle) {\n\t\t\tconst frame = this.frameOf(this.selectedRoots())!;\n\t\t\tconst before = this.snapshot();\n\t\t\tconst center = applyMatrix(rotateMatrix(frame.angle), { x: frame.x + frame.width / 2, y: frame.y + frame.height / 2 });\n\t\t\tthis.drag =\n\t\t\t\thandle === 'rotate'\n\t\t\t\t\t? { kind: 'rotate', start: p, center, before, base: this.doc }\n\t\t\t\t\t: { kind: 'scale', handle, start: p, frame, before, base: this.doc };\n\t\t\tthis.interaction = handle === 'rotate' ? { type: 'rotate', angle: 0 } : { type: 'scale', handle, sx: 1, sy: 1 };\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tconst node = hitTestVectorNodes(this.doc.nodes, p, this.tolerance(), { entered: this.enteredGroups() });\n\t\tif (node) {\n\t\t\tconst selected = this.selection.includes(node.id);\n\t\t\tif (input.shift) {\n\t\t\t\tthis.selection = selected ? this.selection.filter((id) => id !== node.id) : [...this.selection, node.id];\n\t\t\t\tif (selected) {\n\t\t\t\t\tthis.notify();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} else if (!selected) this.selection = [node.id];\n\t\t\tthis.anchors = [];\n\t\t\tconst before = this.snapshot();\n\t\t\tthis.drag = {\n\t\t\t\tkind: 'move',\n\t\t\t\tstart: p,\n\t\t\t\torigin: this.boundsOf(this.selectedRoots()),\n\t\t\t\tbefore,\n\t\t\t\tbase: this.doc,\n\t\t\t\tmoved: false,\n\t\t\t\tduplicated: false,\n\t\t\t\tclickId: selected && !input.shift ? node.id : null\n\t\t\t};\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tif (!input.shift) this.selection = [];\n\t\tthis.drag = { kind: 'marquee', start: p, base: this.selection, baseAnchors: [], anchorsMode: false };\n\t\tthis.interaction = { type: 'marquee', bounds: { x: p.x, y: p.y, width: 0, height: 0 } };\n\t\tthis.notify();\n\t}\n\n\tprivate downDirect(screen: Vec2, p: Vec2, input: VectorPointerInput) {\n\t\tconst handle = this.hitHandle(screen);\n\t\tif (handle) {\n\t\t\tthis.drag = { kind: 'handle', ref: handle, before: this.snapshot(), base: this.doc };\n\t\t\tthis.interaction = { type: 'handle', ref: handle };\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tconst anchor = this.hitAnchor(screen, getVisibleAnchors(this.view()));\n\t\tif (anchor) {\n\t\t\tconst selected = this.anchors.some((a) => sameRef(a, anchor));\n\t\t\tthis.editingId = anchor.nodeId;\n\t\t\tif (input.alt) {\n\t\t\t\t// Alt-drag an anchor pulls out fresh symmetric handles.\n\t\t\t\tthis.anchors = [anchor];\n\t\t\t\tthis.drag = { kind: 'pull', ref: anchor, before: this.snapshot(), base: this.doc, label: 'Make Smooth', outOnly: false, closing: false, altBreaks: false };\n\t\t\t\tthis.notify();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (input.shift) {\n\t\t\t\tthis.anchors = selected ? this.anchors.filter((a) => !sameRef(a, anchor)) : [...this.anchors, anchor];\n\t\t\t\tif (selected) {\n\t\t\t\t\tthis.notify();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} else if (!selected) this.anchors = [anchor];\n\t\t\tthis.drag = { kind: 'anchors', start: p, before: this.snapshot(), base: this.doc, moved: false, clickRef: selected && !input.shift ? anchor : null, lead: anchor };\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tconst widget = this.hitCornerWidget(screen);\n\t\tif (widget) {\n\t\t\tconst refs = getVectorCornerWidgets(this.cornerView()).map((w) => w.ref);\n\t\t\tthis.drag = { kind: 'corner', widget, refs, start: p, before: this.snapshot(), base: this.doc, moved: false, alt: !!input.alt, shift: !!input.shift };\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tfor (const id of getEditablePathIds(this.view())) {\n\t\t\tconst node = this.getNode(id) as VectorPathNode;\n\t\t\tconst seg = nearestSegment(node, this.toLocal(id, p));\n\t\t\tif (!seg || seg.distance > this.tolerance()) continue;\n\t\t\tconst sp = node.subpaths[seg.subpath];\n\t\t\tif (input.alt) {\n\t\t\t\tconst before = this.snapshot();\n\t\t\t\tconst { subpath, index } = insertAnchor(sp, seg.segment, seg.t);\n\t\t\t\tthis.replaceNode(id, { ...node, subpaths: node.subpaths.map((s, i) => (i === seg.subpath ? subpath : s)) });\n\t\t\t\tconst ref = { nodeId: id, subpath: seg.subpath, index };\n\t\t\t\tthis.anchors = [ref];\n\t\t\t\tthis.editingId = id;\n\t\t\t\tthis.drag = { kind: 'anchors', start: p, before, base: this.doc, moved: true, clickRef: null, lead: ref };\n\t\t\t\tthis.notify(true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// A click selects the segment's anchors; a drag bends it through the pointer.\n\t\t\tconst a = { nodeId: id, subpath: seg.subpath, index: seg.segment };\n\t\t\tconst b = { nodeId: id, subpath: seg.subpath, index: (seg.segment + 1) % sp.anchors.length };\n\t\t\tthis.anchors = input.shift ? [...this.anchors.filter((r) => !sameRef(r, a) && !sameRef(r, b)), a, b] : [a, b];\n\t\t\tthis.editingId = id;\n\t\t\tthis.drag = { kind: 'bend', ref: { nodeId: id, subpath: seg.subpath, segment: seg.segment }, t: seg.t, start: p, before: this.snapshot(), base: this.doc, moved: false };\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tconst path = this.hitPath(p);\n\t\tif (path) {\n\t\t\tthis.editingId = path.id;\n\t\t\tif (!input.shift) this.selection = [path.id];\n\t\t\telse if (!this.selection.includes(path.id)) this.selection = [...this.selection, path.id];\n\t\t\tthis.anchors = path.subpaths.flatMap((sp, s) => sp.anchors.map((_, i) => ({ nodeId: path.id, subpath: s, index: i })));\n\t\t\tthis.drag = { kind: 'anchors', start: p, before: this.snapshot(), base: this.doc, moved: false, clickRef: null, lead: null };\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tif (!input.shift) {\n\t\t\tthis.anchors = [];\n\t\t}\n\t\tthis.drag = { kind: 'marquee', start: p, base: this.selection, baseAnchors: this.anchors, anchorsMode: getEditablePathIds(this.view()).length > 0 };\n\t\tthis.interaction = { type: 'marquee', bounds: { x: p.x, y: p.y, width: 0, height: 0 } };\n\t\tthis.notify();\n\t}\n\n\tprivate downPen(screen: Vec2, p: Vec2, input: VectorPointerInput) {\n\t\tconst before = this.snapshot();\n\t\tconst sp = this.pen ? (this.getNode(this.pen.nodeId) as VectorPathNode | undefined)?.subpaths[this.pen.subpath] : undefined;\n\t\tif (this.pen && sp) {\n\t\t\tconst pen = this.pen;\n\t\t\tconst first = { nodeId: pen.nodeId, subpath: pen.subpath, index: 0 };\n\t\t\tif (sp.anchors.length >= 2 && this.hitAnchor(screen, [first])) {\n\t\t\t\tconst node = this.getNode(pen.nodeId) as VectorPathNode;\n\t\t\t\tthis.replaceNode(pen.nodeId, { ...node, subpaths: node.subpaths.map((s, i) => (i === pen.subpath ? { ...s, closed: true } : s)) });\n\t\t\t\tthis.drag = { kind: 'pull', ref: first, before, base: this.doc, label: 'Close Path', outOnly: false, closing: true, altBreaks: true };\n\t\t\t\tthis.interaction = { type: 'pen', nodeId: pen.nodeId };\n\t\t\t\tthis.notify(true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst last = getAnchorWorld(this.doc, { nodeId: pen.nodeId, subpath: pen.subpath, index: sp.anchors.length - 1 })!;\n\t\t\tconst leave = { at: { x: last.x, y: last.y }, constrain: input.shift ? ('angle' as const) : null };\n\t\t\tlet pt = this.snapDrawn(p, leave, input, () => this.drawingTargets(pen.nodeId));\n\t\t\tpt = this.toLocal(pen.nodeId, pt);\n\t\t\tconst node = this.getNode(pen.nodeId) as VectorPathNode;\n\t\t\tconst anchors = [...sp.anchors, { x: pt.x, y: pt.y }];\n\t\t\tthis.replaceNode(pen.nodeId, { ...node, subpaths: node.subpaths.map((s, i) => (i === pen.subpath ? { ...s, anchors } : s)) });\n\t\t\tconst ref = { nodeId: pen.nodeId, subpath: pen.subpath, index: anchors.length - 1 };\n\t\t\tthis.anchors = [ref];\n\t\t\tthis.drag = { kind: 'pull', ref, before, base: this.doc, label: 'Add Point', outOnly: false, closing: false, altBreaks: true };\n\t\t\tthis.interaction = { type: 'pen', nodeId: pen.nodeId };\n\t\t\tthis.notify(true);\n\t\t\treturn;\n\t\t}\n\t\tconst endpoint = this.hitAnchor(screen, this.openEndpoints());\n\t\tif (endpoint) {\n\t\t\tlet node = this.getNode(endpoint.nodeId) as VectorPathNode;\n\t\t\tlet index = endpoint.index;\n\t\t\tif (index === 0 && node.subpaths[endpoint.subpath].anchors.length > 1) {\n\t\t\t\tnode = { ...node, subpaths: node.subpaths.map((s, i) => (i === endpoint.subpath ? reverseSubpath(s) : s)) };\n\t\t\t\tthis.replaceNode(node.id, node);\n\t\t\t\tindex = node.subpaths[endpoint.subpath].anchors.length - 1;\n\t\t\t}\n\t\t\tthis.pen = { nodeId: node.id, subpath: endpoint.subpath };\n\t\t\tthis.selection = [node.id];\n\t\t\tthis.editingId = node.id;\n\t\t\tconst ref = { nodeId: node.id, subpath: endpoint.subpath, index };\n\t\t\tthis.anchors = [ref];\n\t\t\tthis.drag = { kind: 'pull', ref, before, base: this.doc, label: 'Continue Path', outOnly: true, closing: false, altBreaks: true };\n\t\t\tthis.interaction = { type: 'pen', nodeId: node.id };\n\t\t\tthis.notify(this.doc !== before.doc);\n\t\t\treturn;\n\t\t}\n\t\tconst pt = this.snapDrawn(p, null, input, () => this.drawingTargets(null));\n\t\tconst node: VectorPathNode = { id: this.newId(), type: 'path', name: 'Path', ...this.styleFor(true), subpaths: [{ anchors: [{ x: pt.x, y: pt.y }], closed: false }] };\n\t\tthis.setDoc([...this.doc.nodes, node]);\n\t\tthis.pen = { nodeId: node.id, subpath: 0 };\n\t\tthis.selection = [node.id];\n\t\tthis.editingId = node.id;\n\t\tconst ref = { nodeId: node.id, subpath: 0, index: 0 };\n\t\tthis.anchors = [ref];\n\t\tthis.drag = { kind: 'pull', ref, before, base: this.doc, label: 'Start Path', outOnly: false, closing: false, altBreaks: true };\n\t\tthis.interaction = { type: 'pen', nodeId: node.id };\n\t\tthis.notify(true);\n\t}\n\n\tprivate downPencil(p: Vec2, input: VectorPointerInput = {}) {\n\t\tconst before = this.snapshot();\n\t\tconst node: VectorPathNode = {\n\t\t\tid: this.newId(),\n\t\t\ttype: 'path',\n\t\t\tname: 'Pencil',\n\t\t\t...this.styleFor(true),\n\t\t\tstrokeLinecap: this.defaults.strokeLinecap ?? 'round',\n\t\t\tstrokeLinejoin: this.defaults.strokeLinejoin ?? 'round',\n\t\t\tsubpaths: [{ anchors: [{ x: p.x, y: p.y }], closed: false }]\n\t\t};\n\t\tthis.setDoc([...this.doc.nodes, node]);\n\t\tconst lazy = new LazyMouse((this.editing.pencilSmoothing * VECTOR_PENCIL_STRING_PX) / this.viewport.zoom);\n\t\tlazy.push([p.x, p.y]);\n\t\tconst mode = this.editing.pencilPressure;\n\t\tconst hasPressure = typeof input.pressure === 'number' && Number.isFinite(input.pressure);\n\t\tconst record = hasPressure && (mode === 'all' || (mode === 'pen' && input.pointerType === 'pen'));\n\t\tconst pressure = hasPressure ? input.pressure! : 0.5;\n\t\tthis.drag = { kind: 'pencil', nodeId: node.id, points: [[p.x, p.y]], before, lazy, pressures: record ? [pressure] : null, pressure };\n\t\tthis.interaction = { type: 'draw', tool: 'pencil', nodeId: node.id, bounds: { x: p.x, y: p.y, width: 0, height: 0 } };\n\t\tthis.notify(true);\n\t}\n\n\tprivate downShape(p: Vec2, input: VectorPointerInput) {\n\t\tconst before = this.snapshot();\n\t\t// The targets are gathered before the new shape joins the document, and kept for the drag.\n\t\tconst targets = this.drawingTargets(null);\n\t\tconst start = this.snapDrawn(p, null, input, () => targets);\n\t\tconst kind = this.tool as VectorShapeInput['kind'];\n\t\tconst node = this.createShapeNode({ kind, x: start.x, y: start.y, width: 0, height: 0 });\n\t\tthis.setDoc([...this.doc.nodes, node]);\n\t\tthis.drag = { kind: 'shape', tool: this.tool, start, nodeId: node.id, before, targets };\n\t\tthis.interaction = { type: 'draw', tool: this.tool, nodeId: node.id, bounds: { x: start.x, y: start.y, width: 0, height: 0 } };\n\t\tthis.notify(true);\n\t}\n\n\t/** Pointer moved over the canvas (with or without a button down). */\n\tpointerMove(screen: Vec2, input: VectorPointerInput = {}): void {\n\t\tconst p = this.screenToDocument(screen);\n\t\tconst drag = this.drag;\n\t\tif (!drag && (this.tool === 'shapeBuilder' || this.tool === 'paintBucket')) {\n\t\t\tthis.pointer = p;\n\t\t\tif (this.hoverBuilder(p, !!input.alt)) this.notify();\n\t\t\treturn;\n\t\t}\n\t\tif (!drag) {\n\t\t\tconst hover = this.computeHover(screen);\n\t\t\tconst pointerMatters = this.tool === 'pen' && !!this.pen;\n\t\t\tconst guides = this.guides;\n\t\t\tthis.hover = hover;\n\t\t\tconst pointer = this.hoverSnap(p, input);\n\t\t\tif (this.sameHover(hover, this.state.hover) && !pointerMatters && JSON.stringify(guides) === JSON.stringify(this.guides)) return;\n\t\t\tthis.pointer = pointer;\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tthis.pointer = p;\n\t\tswitch (drag.kind) {\n\t\t\tcase 'pan':\n\t\t\t\tthis.viewport = { ...drag.viewport, x: drag.viewport.x + screen.x - drag.start.x, y: drag.viewport.y + screen.y - drag.start.y };\n\t\t\t\tthis.notify();\n\t\t\t\treturn;\n\t\t\tcase 'gradient':\n\t\t\t\treturn this.moveGradient(drag, screen, p, input);\n\t\t\tcase 'envelope':\n\t\t\t\treturn this.moveEnvelope(drag, p);\n\t\t\tcase 'move': {\n\t\t\t\tlet dx = p.x - drag.start.x;\n\t\t\t\tlet dy = p.y - drag.start.y;\n\t\t\t\tif (!drag.moved && Math.hypot(dx, dy) * this.viewport.zoom < 2) return;\n\t\t\t\tdrag.moved = true;\n\t\t\t\tif (input.shift) Math.abs(dx) > Math.abs(dy) ? (dy = 0) : (dx = 0);\n\t\t\t\tif (input.alt && !drag.duplicated) {\n\t\t\t\t\tdrag.duplicated = true;\n\t\t\t\t\tconst created: string[] = [];\n\t\t\t\t\tdrag.base = this.withDuplicates(drag.base, this.selectedRoots(), { x: 0, y: 0 }, created);\n\t\t\t\t\tthis.selection = created;\n\t\t\t\t\t// The originals stay behind as targets.\n\t\t\t\t\tdrag.targets = undefined;\n\t\t\t\t}\n\t\t\t\t({ dx, dy } = this.snapMove(drag, dx, dy, input));\n\t\t\t\tthis.doc = this.transformed(drag.base, new Set(this.selectedRoots()), translateMatrix(dx, dy));\n\t\t\t\tthis.interaction = { type: 'move', dx, dy };\n\t\t\t\tthis.notify(true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'scale': {\n\t\t\t\tif (this.freeDistort && drag.handle !== 'rotate') return this.moveDistort(drag, p, input);\n\t\t\t\t// Scale in the box's own (possibly rotated) frame.\n\t\t\t\tconst angle = drag.frame.angle;\n\t\t\t\tconst q = angle ? applyMatrix(rotateMatrix(-angle), p) : p;\n\t\t\t\tlet { sx, sy, origin } = scaleFor(drag.handle, drag.frame, q, input);\n\t\t\t\tif (this.snap.objects && !input.mod && drag.handle !== 'rotate') {\n\t\t\t\t\tdrag.targets ??= VectorSnapTargets.forResize(drag.base, this.selectedRoots());\n\t\t\t\t\tconst res = drag.targets.snapScale(drag.frame, drag.handle, sx, sy, input, this.snapTolerance());\n\t\t\t\t\t({ sx, sy } = res);\n\t\t\t\t\tthis.setGuides(res);\n\t\t\t\t} else this.setGuides(null);\n\t\t\t\tconst ids = new Set(this.selectedRoots());\n\t\t\t\tthis.doc = this.transformed(drag.base, ids, this.inFrame(angle, scaleMatrix(sx, sy, origin)));\n\t\t\t\tthis.interaction = { type: 'scale', handle: drag.handle, sx, sy };\n\t\t\t\tthis.notify(true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'rotate': {\n\t\t\t\tconst a0 = Math.atan2(drag.start.y - drag.center.y, drag.start.x - drag.center.x);\n\t\t\t\tconst a1 = Math.atan2(p.y - drag.center.y, p.x - drag.center.x);\n\t\t\t\tlet angle = ((a1 - a0) * 180) / Math.PI;\n\t\t\t\tif (input.shift) angle = Math.round(angle / 15) * 15;\n\t\t\t\tthis.doc = this.transformed(drag.base, new Set(this.selectedRoots()), rotateMatrix(angle, drag.center));\n\t\t\t\tthis.interaction = { type: 'rotate', angle };\n\t\t\t\tthis.notify(true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'marquee': {\n\t\t\t\tconst bounds = boundsFromPoints(drag.start, p);\n\t\t\t\tthis.interaction = { type: 'marquee', bounds };\n\t\t\t\tif (drag.anchorsMode) {\n\t\t\t\t\tconst inside = getVisibleAnchors(this.view()).filter((r) => {\n\t\t\t\t\t\tconst a = getAnchorWorld(this.doc, r)!;\n\t\t\t\t\t\treturn a.x >= bounds.x && a.x <= bounds.x + bounds.width && a.y >= bounds.y && a.y <= bounds.y + bounds.height;\n\t\t\t\t\t});\n\t\t\t\t\tthis.anchors = [...drag.baseAnchors, ...inside.filter((r) => !drag.baseAnchors.some((b) => sameRef(b, r)))];\n\t\t\t\t} else {\n\t\t\t\t\tconst hits = this.doc.nodes\n\t\t\t\t\t\t.filter((n) => !n.locked && n.visible !== false)\n\t\t\t\t\t\t.filter((n) => {\n\t\t\t\t\t\t\tconst b = vectorNodeBounds(n);\n\t\t\t\t\t\t\treturn b && boundsIntersect(b, bounds);\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.map((n) => n.id);\n\t\t\t\t\tthis.selection = [...new Set([...drag.base, ...hits])];\n\t\t\t\t}\n\t\t\t\tthis.notify();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'anchors': {\n\t\t\t\tlet dx = p.x - drag.start.x;\n\t\t\t\tlet dy = p.y - drag.start.y;\n\t\t\t\tif (!drag.moved && Math.hypot(dx, dy) * this.viewport.zoom < 2) return;\n\t\t\t\tdrag.moved = true;\n\t\t\t\tconst lead = this.leadAnchor(drag);\n\t\t\t\tif (lead) {\n\t\t\t\t\t// The grabbed (or nearest) anchor snaps; the others move with it.\n\t\t\t\t\tconst leave = input.shift ? { at: lead, constrain: 'axis' as const, curves: false } : null;\n\t\t\t\t\tconst q = this.snapDrawn({ x: lead.x + dx, y: lead.y + dy }, leave, input, () => (drag.targets ??= VectorSnapTargets.forAnchorDrag(drag.base, this.anchors)));\n\t\t\t\t\tdx = q.x - lead.x;\n\t\t\t\t\tdy = q.y - lead.y;\n\t\t\t\t} else if (input.shift) Math.abs(dx) > Math.abs(dy) ? (dy = 0) : (dx = 0);\n\t\t\t\tconst single = this.anchors.length === 1 ? this.anchors[0] : null;\n\t\t\t\tconst from = single ? getAnchorWorld(drag.base, single) : undefined;\n\t\t\t\t// A lone point bends its segments with the Curvature setting (Alt held flips it).\n\t\t\t\tif (single && from && this.editing.curvatureMoves !== !!input.alt) this.doc = this.curvatureMoved(drag.base, single, { x: from.x + dx, y: from.y + dy });\n\t\t\t\telse this.doc = this.movedAnchors(drag.base, this.anchors, dx, dy);\n\t\t\t\tthis.interaction = { type: 'anchors', dx, dy };\n\t\t\t\tthis.notify(true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'handle': {\n\t\t\t\tconst anchor = getAnchorWorld(drag.base, drag.ref);\n\t\t\t\tconst leave = anchor ? { at: { x: anchor.x, y: anchor.y }, constrain: input.shift ? ('angle' as const) : null } : null;\n\t\t\t\tconst pt = this.snapDrawn(p, leave, input, () => (drag.targets ??= VectorSnapTargets.forHandle(drag.base, drag.ref.nodeId)));\n\t\t\t\tconst local = this.toLocal(drag.ref.nodeId, pt, drag.base);\n\t\t\t\tthis.doc = this.updateAnchors(drag.base, [drag.ref], (a) => moveAnchorHandle(a, drag.ref.handle, local, input.alt));\n\t\t\t\tthis.interaction = { type: 'handle', ref: drag.ref };\n\t\t\t\tthis.notify(true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'corner': {\n\t\t\t\tif (!drag.moved && Math.hypot(p.x - drag.start.x, p.y - drag.start.y) * this.viewport.zoom < 3) return;\n\t\t\t\tdrag.moved = true;\n\t\t\t\tconst c = drag.widget.corner;\n\t\t\t\tconst inv = invertMatrix(this.worldOf(drag.widget.ref.nodeId, drag.base));\n\t\t\t\tconst d = applyMatrixLinear(inv, { x: p.x - drag.start.x, y: p.y - drag.start.y });\n\t\t\t\tconst radius = Math.max(0, Math.min(c.maxRadius, drag.widget.drawnRadius + cornerRadiusChange(c, d)));\n\t\t\t\tthis.doc = this.updateAnchors(drag.base, drag.refs, (a) => setAnchorCorner(a, radius));\n\t\t\t\tthis.interaction = { type: 'corner', ref: drag.widget.ref, radius };\n\t\t\t\tthis.notify(true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'bend': {\n\t\t\t\tif (!drag.moved && Math.hypot(p.x - drag.start.x, p.y - drag.start.y) * this.viewport.zoom < 2) return;\n\t\t\t\tdrag.moved = true;\n\t\t\t\tthis.doc = this.bent(drag.base, drag.ref, drag.t, p);\n\t\t\t\tthis.interaction = { type: 'bend', ref: drag.ref };\n\t\t\t\tthis.notify(true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'builder':\n\t\t\t\treturn this.moveBuilder(drag, p);\n\t\t\tcase 'stroke': {\n\t\t\t\tconst last = drag.points[drag.points.length - 1];\n\t\t\t\tif (Math.hypot(p.x - last.x, p.y - last.y) * this.viewport.zoom < 1.5) return;\n\t\t\t\tdrag.points.push(p);\n\t\t\t\tthis.interaction = { type: 'stroke', tool: drag.tool, points: drag.points.slice(), width: drag.tool === 'eraser' ? this.editing.eraserSize : 0 };\n\t\t\t\tthis.notify();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'pull': {\n\t\t\t\tconst anchor = getAnchorWorld(drag.base, drag.ref);\n\t\t\t\tif (!anchor) return;\n\t\t\t\tif (Math.hypot(p.x - anchor.x, p.y - anchor.y) * this.viewport.zoom < 2) {\n\t\t\t\t\tthis.doc = drag.base;\n\t\t\t\t\tthis.notify(true);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.guides = null;\n\t\t\t\tlet pt = p;\n\t\t\t\tif (input.shift) pt = constrain45(anchor, pt);\n\t\t\t\tconst side = drag.closing ? 'in' : 'out';\n\t\t\t\t// Dragging out of the closing anchor pulls its incoming handle toward the pointer's mirror.\n\t\t\t\tconst pos = this.toLocal(drag.ref.nodeId, drag.closing ? { x: 2 * anchor.x - pt.x, y: 2 * anchor.y - pt.y } : pt, drag.base);\n\t\t\t\tthis.doc = this.updateAnchors(drag.base, [drag.ref], (a) => {\n\t\t\t\t\tif (drag.outOnly || (drag.altBreaks && input.alt)) return moveAnchorHandle(a, side, pos, true);\n\t\t\t\t\treturn moveAnchorHandle({ ...a, type: 'symmetric' }, side, pos);\n\t\t\t\t});\n\t\t\t\tthis.notify(true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'shape': {\n\t\t\t\tconst node = this.getNode(drag.nodeId);\n\t\t\t\tif (node?.type !== 'path') return;\n\t\t\t\tconst leave = { at: drag.start, constrain: input.shift ? (drag.tool === 'line' ? ('angle' as const) : ('square' as const)) : null, curves: false };\n\t\t\t\tlet end = this.snapDrawn(p, leave, input, () => (drag.targets ??= this.drawingTargets(null)));\n\t\t\t\tif (!this.snappedToObject && drag.tool !== 'line' && !input.shift && !input.mod && this.snap.objects && drag.targets) end = this.matchShapeSize(drag.targets, drag.start, end, input.alt);\n\t\t\t\tlet start = drag.start;\n\t\t\t\tlet bounds: VectorBounds;\n\t\t\t\tif (drag.tool === 'line') {\n\t\t\t\t\tif (input.shift) end = constrain45(start, end);\n\t\t\t\t\tif (input.alt) start = { x: 2 * start.x - end.x, y: 2 * start.y - end.y };\n\t\t\t\t\tbounds = { x: start.x, y: start.y, width: end.x - start.x, height: end.y - start.y };\n\t\t\t\t} else {\n\t\t\t\t\tlet w = end.x - start.x;\n\t\t\t\t\tlet h = end.y - start.y;\n\t\t\t\t\tif (input.shift) {\n\t\t\t\t\t\tconst s = Math.max(Math.abs(w), Math.abs(h));\n\t\t\t\t\t\tw = Math.sign(w || 1) * s;\n\t\t\t\t\t\th = Math.sign(h || 1) * s;\n\t\t\t\t\t}\n\t\t\t\t\tif (input.alt) start = { x: start.x - w, y: start.y - h };\n\t\t\t\t\tconst x2 = drag.start.x + w;\n\t\t\t\t\tconst y2 = drag.start.y + h;\n\t\t\t\t\tbounds = boundsFromPoints(start, { x: x2, y: y2 });\n\t\t\t\t}\n\t\t\t\tconst shaped = this.createShapeNode({ kind: drag.tool as VectorShapeInput['kind'], ...bounds });\n\t\t\t\tthis.replaceNode(drag.nodeId, { ...node, subpaths: shaped.subpaths });\n\t\t\t\tthis.interaction = { type: 'draw', tool: drag.tool, nodeId: drag.nodeId, bounds };\n\t\t\t\tthis.notify(true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'width': {\n\t\t\t\tif (!drag.moved && Math.hypot(p.x - drag.start.x, p.y - drag.start.y) * this.viewport.zoom < 2) return;\n\t\t\t\tdrag.moved = true;\n\t\t\t\tconst node = findVectorNode(drag.base.nodes, drag.ref.nodeId) as VectorPathNode;\n\t\t\t\tconst local = this.toLocal(node.id, p, drag.base);\n\t\t\t\tconst src = drag.profile[drag.source];\n\t\t\t\tlet profile: VectorWidthPoint[];\n\t\t\t\tlet index = drag.source;\n\t\t\t\tif (drag.mode !== 'width') {\n\t\t\t\t\tconst g = vectorStrokeWidthGeometry(node);\n\t\t\t\t\tconst near = g ? nearestStrokePosition(g.line, [local.x, local.y], drag.ref.subpath) : null;\n\t\t\t\t\tif (!g || !near) return;\n\t\t\t\t\tconst t = drag.group.length ? near.t : this.snapWidthT(drag, node, g.line, near.t, drag.source);\n\t\t\t\t\tif (drag.mode === 'copy') {\n\t\t\t\t\t\t// The source stays; a copy of it slides (and may pass other points).\n\t\t\t\t\t\t({ points: profile, index } = insertWidthPoint(drag.profile, t, { left: src.left, right: src.right }, { step: true }));\n\t\t\t\t\t} else if (drag.group.length) {\n\t\t\t\t\t\tconst shifted = shiftWidthPoints(drag.profile, [drag.source, ...drag.group], t - src.t);\n\t\t\t\t\t\tprofile = shifted.points;\n\t\t\t\t\t\tdrag.renumber = shifted.renumber;\n\t\t\t\t\t\tindex = shifted.renumber.get(drag.source) ?? drag.source;\n\t\t\t\t\t} else profile = setWidthPoint(drag.profile, drag.source, { t });\n\t\t\t\t} else {\n\t\t\t\t\tconst off = ((local.x - drag.origin.x) * drag.normal.x + (local.y - drag.origin.y) * drag.normal.y) / drag.unit;\n\t\t\t\t\tconst patch =\n\t\t\t\t\t\tdrag.side === 'both' ? { left: Math.abs(off), right: Math.abs(off) } : drag.side === 'left' ? { left: Math.max(0, off) } : { right: Math.max(0, -off) };\n\t\t\t\t\tprofile = setWidthPoint(drag.profile, drag.source, patch);\n\t\t\t\t\tif (drag.group.length) {\n\t\t\t\t\t\t// Every selected point widens by as much as the dragged one.\n\t\t\t\t\t\tconst dl = profile[drag.source].left - src.left;\n\t\t\t\t\t\tconst dr = profile[drag.source].right - src.right;\n\t\t\t\t\t\tfor (const i of drag.group) profile = setWidthPoint(profile, i, { left: Math.max(0, drag.profile[i].left + dl), right: Math.max(0, drag.profile[i].right + dr) });\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tthis.doc = { ...drag.base, nodes: mapTree(drag.base.nodes, (n) => (n.id === node.id ? { ...(n as VectorPathNode), strokeWidthProfile: profile } : n)) };\n\t\t\t\tdrag.ref = { ...drag.ref, index };\n\t\t\t\tconst w = profile[index];\n\t\t\t\tthis.interaction = { type: 'width', ref: drag.ref, mode: drag.mode, width: (w.left + w.right) * drag.unit };\n\t\t\t\tthis.notify(true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'pencil': {\n\t\t\t\tconst last = drag.points[drag.points.length - 1];\n\t\t\t\tlet moved = false;\n\t\t\t\tif (typeof input.pressure === 'number' && Number.isFinite(input.pressure)) drag.pressure = input.pressure;\n\t\t\t\tfor (const q of drag.lazy.push([p.x, p.y])) {\n\t\t\t\t\tif (Math.hypot(q[0] - last[0], q[1] - last[1]) * this.viewport.zoom < 1.5) continue;\n\t\t\t\t\tdrag.points.push(q);\n\t\t\t\t\tdrag.pressures?.push(drag.pressure);\n\t\t\t\t\tmoved = true;\n\t\t\t\t}\n\t\t\t\tconst string = drag.lazy.string();\n\t\t\t\tconst node = this.getNode(drag.nodeId);\n\t\t\t\tif (node?.type !== 'path') return;\n\t\t\t\tif (!moved) {\n\t\t\t\t\tif (string && this.interaction?.type === 'draw') {\n\t\t\t\t\t\tthis.interaction = { ...this.interaction, string: [{ x: string[0][0], y: string[0][1] }, { x: string[1][0], y: string[1][1] }] };\n\t\t\t\t\t\tthis.notify();\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.replaceNode(drag.nodeId, { ...node, subpaths: [{ anchors: drag.points.map(([x, y]) => ({ x, y })), closed: false }] });\n\t\t\t\tconst xs = drag.points.map((q) => q[0]);\n\t\t\t\tconst ys = drag.points.map((q) => q[1]);\n\t\t\t\tthis.interaction = {\n\t\t\t\t\ttype: 'draw',\n\t\t\t\t\ttool: 'pencil',\n\t\t\t\t\tnodeId: drag.nodeId,\n\t\t\t\t\tbounds: { x: Math.min(...xs), y: Math.min(...ys), width: Math.max(...xs) - Math.min(...xs), height: Math.max(...ys) - Math.min(...ys) },\n\t\t\t\t\t...(string ? { string: [{ x: string[0][0], y: string[0][1] }, { x: string[1][0], y: string[1][1] }] as [Vec2, Vec2] } : {})\n\t\t\t\t};\n\t\t\t\tthis.notify(true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Snaps a move of the selection by (dx, dy): its bounds line up with targets or space evenly, else go to the grid. */\n\tprivate snapMove(drag: Extract<Drag, { kind: 'move' }>, dx: number, dy: number, input: VectorModifiers): { dx: number; dy: number } {\n\t\tconst origin = drag.origin;\n\t\tif (input.mod || !origin) {\n\t\t\tthis.setGuides(null);\n\t\t\treturn { dx, dy };\n\t\t}\n\t\t// A Shift-locked axis stays put.\n\t\tconst axes = { x: !input.shift || dx !== 0, y: !input.shift || dy !== 0 };\n\t\tlet res = null;\n\t\tif (this.snap.objects) {\n\t\t\tdrag.targets ??= VectorSnapTargets.forMove(drag.base, this.selectedRoots());\n\t\t\tres = drag.targets.snapRect({ ...origin, x: origin.x + dx, y: origin.y + dy }, this.snapTolerance(), { spacing: true, axes });\n\t\t}\n\t\tthis.setGuides(res);\n\t\treturn {\n\t\t\tdx: res?.x ? dx + res.dx : axes.x ? this.gridValue(origin.x + dx) - origin.x : dx,\n\t\t\tdy: res?.y ? dy + res.dy : axes.y ? this.gridValue(origin.y + dy) - origin.y : dy\n\t\t};\n\t}\n\n\t/** The anchor a point drag snaps by, where it started: the grabbed one, else the moving one nearest the press. */\n\tprivate leadAnchor(drag: Extract<Drag, { kind: 'anchors' }>): Vec2 | null {\n\t\tconst ref = drag.lead && this.anchors.some((a) => sameRef(a, drag.lead!)) ? drag.lead : null;\n\t\tif (ref) {\n\t\t\tconst a = getAnchorWorld(drag.base, ref);\n\t\t\treturn a ? { x: a.x, y: a.y } : null;\n\t\t}\n\t\tlet best: Vec2 | null = null;\n\t\tfor (const r of this.anchors) {\n\t\t\tconst a = getAnchorWorld(drag.base, r);\n\t\t\tif (a && (!best || Math.hypot(a.x - drag.start.x, a.y - drag.start.y) < Math.hypot(best.x - drag.start.x, best.y - drag.start.y))) best = { x: a.x, y: a.y };\n\t\t}\n\t\treturn best;\n\t}\n\n\t/** A shape's dragged corner moved so its width or height matches another object's, when one is within reach. */\n\tprivate matchShapeSize(targets: VectorSnapTargets, start: Vec2, end: Vec2, fromCentre = false): Vec2 {\n\t\t// Dragged from the centre, the shape is twice as large as the drag.\n\t\tconst k = fromCentre ? 2 : 1;\n\t\tconst tol = this.snapTolerance() * k;\n\t\tconst out = { ...end };\n\t\tconst guides: VectorSnapGuides = { lines: [], labels: [] };\n\t\tfor (const axis of ['x', 'y'] as const) {\n\t\t\tconst d = end[axis] - start[axis];\n\t\t\tconst len = Math.abs(d) > 1e-9 ? targets.matchSize(Math.abs(d) * k, tol) : null;\n\t\t\tif (len === null || len <= 0) continue;\n\t\t\tout[axis] = start[axis] + (Math.sign(d) * len) / k;\n\t\t\tconst from = { ...out, [axis]: fromCentre ? 2 * start[axis] - out[axis] : start[axis] };\n\t\t\tguides.lines.push({ kind: 'size', x1: from.x, y1: from.y, x2: out.x, y2: out.y });\n\t\t\tguides.labels.push({ kind: 'size', x: (from.x + out.x) / 2, y: (from.y + out.y) / 2, text: `${axis === 'x' ? 'W' : 'H'} ${Math.round(len * 100) / 100}` });\n\t\t}\n\t\tthis.setGuides(guides);\n\t\treturn out;\n\t}\n\n\t/** Primary button released. */\n\tpointerUp(screen?: Vec2, input: VectorPointerInput = {}): void {\n\t\t// A pen reports no pressure as it lifts: keep the last pressure for the final move.\n\t\tif (screen && this.drag && this.drag.kind !== 'pan') this.pointerMove(screen, { ...input, pressure: undefined });\n\t\tconst drag = this.drag;\n\t\tthis.drag = null;\n\t\tthis.interaction = null;\n\t\tthis.guides = null;\n\t\tif (!drag) return;\n\t\tswitch (drag.kind) {\n\t\t\tcase 'pan':\n\t\t\tcase 'marquee':\n\t\t\t\tthis.notify();\n\t\t\t\treturn;\n\t\t\tcase 'move':\n\t\t\t\tif (!drag.moved) {\n\t\t\t\t\tif (drag.clickId) this.selection = [drag.clickId];\n\t\t\t\t\tthis.notify();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\treturn this.commit(drag.duplicated ? 'Duplicate' : 'Move', drag.before);\n\t\t\tcase 'scale':\n\t\t\t\treturn this.commit(drag.op ?? 'Scale', drag.before);\n\t\t\tcase 'envelope':\n\t\t\t\tif (!drag.moved) {\n\t\t\t\t\tthis.notify();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\treturn this.commit('Edit Envelope', drag.before);\n\t\t\tcase 'rotate':\n\t\t\t\treturn this.commit('Rotate', drag.before);\n\t\t\tcase 'anchors':\n\t\t\t\tif (!drag.moved) {\n\t\t\t\t\tif (drag.clickRef) this.anchors = [drag.clickRef];\n\t\t\t\t\tif (drag.before.doc !== this.doc) return this.commit('Add Point', drag.before);\n\t\t\t\t\tthis.notify();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\treturn this.commit(drag.before.doc !== drag.base ? 'Add Point' : 'Move Points', drag.before);\n\t\t\tcase 'handle':\n\t\t\t\treturn this.commit('Move Handle', drag.before);\n\t\t\tcase 'bend':\n\t\t\t\tif (!drag.moved) {\n\t\t\t\t\tthis.notify();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\treturn this.commit('Bend Segment', drag.before);\n\t\t\tcase 'corner': {\n\t\t\t\tif (drag.moved) return this.commit('Corner Radius', drag.before);\n\t\t\t\tif (drag.alt) {\n\t\t\t\t\tconst kind = nextVectorCornerKind(drag.widget.kind);\n\t\t\t\t\tthis.doc = this.updateAnchors(drag.base, drag.refs, (a) => setAnchorCorner(a, a.corner?.radius ?? 0, kind));\n\t\t\t\t\treturn this.commit('Corner Kind', drag.before);\n\t\t\t\t}\n\t\t\t\t// A click selects the widget's corner, so the next drag rounds that corner alone.\n\t\t\t\tconst ref = drag.widget.ref;\n\t\t\t\tconst has = this.anchors.some((a) => sameRef(a, ref));\n\t\t\t\tthis.editingId = ref.nodeId;\n\t\t\t\tthis.anchors = drag.shift ? (has ? this.anchors.filter((a) => !sameRef(a, ref)) : [...this.anchors, ref]) : [ref];\n\t\t\t\tthis.notify();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'width': {\n\t\t\t\tif (drag.moved) {\n\t\t\t\t\tconst several = drag.group.length > 0;\n\t\t\t\t\tif (drag.mode === 'copy') this.widthPoints = [drag.ref];\n\t\t\t\t\telse if (drag.mode === 'move' && several) {\n\t\t\t\t\t\t// Points moved together stay selected (re-sorting may renumber them).\n\t\t\t\t\t\tthis.widthPoints = [drag.source, ...drag.group].map((i) => ({ ...drag.ref, index: drag.renumber?.get(i) ?? i }));\n\t\t\t\t\t} else if (!several) this.widthPoints = [drag.ref];\n\t\t\t\t\tconst label = drag.mode === 'copy' ? 'Copy Width Point' : drag.mode === 'move' ? (several ? 'Move Width Points' : 'Move Width Point') : several ? 'Width Points' : 'Width Point';\n\t\t\t\t\treturn this.commit(label, drag.before);\n\t\t\t\t}\n\t\t\t\t// A click adds nothing; on a width point it selects it (just it, among several selected).\n\t\t\t\tif (drag.created) {\n\t\t\t\t\tthis.doc = drag.before.doc;\n\t\t\t\t\tthis.widthPoints = [];\n\t\t\t\t\tthis.notify(true);\n\t\t\t\t} else {\n\t\t\t\t\tif (!drag.shift && drag.group.length) this.widthPoints = [drag.ref];\n\t\t\t\t\tthis.notify();\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'builder':\n\t\t\t\treturn this.upBuilder(drag);\n\t\t\tcase 'gradient':\n\t\t\t\treturn this.upGradient(drag);\n\t\t\tcase 'stroke': {\n\t\t\t\tthis.notify();\n\t\t\t\tif (drag.tool === 'knife') this.knife(drag.points);\n\t\t\t\telse this.erase(drag.points);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'pull':\n\t\t\t\tif (drag.closing) this.pen = null;\n\t\t\t\treturn this.commit(drag.label, drag.before);\n\t\t\tcase 'shape': {\n\t\t\t\tconst node = this.getNode(drag.nodeId);\n\t\t\t\tconst b = node ? vectorNodeBounds(node) : null;\n\t\t\t\tconst min = 2 / this.viewport.zoom;\n\t\t\t\tif (!b || (b.width < min && b.height < min)) {\n\t\t\t\t\tthis.doc = drag.before.doc;\n\t\t\t\t\tthis.notify(true);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.selection = [drag.nodeId];\n\t\t\t\tthis.anchors = [];\n\t\t\t\tthis.tool = 'select';\n\t\t\t\treturn this.commit(`Draw ${TOOL_LABELS[drag.tool]}`, drag.before);\n\t\t\t}\n\t\t\tcase 'pencil': {\n\t\t\t\tconst node = this.getNode(drag.nodeId);\n\t\t\t\tif (node?.type !== 'path' || drag.points.length < 2) {\n\t\t\t\t\tthis.doc = drag.before.doc;\n\t\t\t\t\tthis.notify(true);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tfor (const q of drag.lazy.finish()) {\n\t\t\t\t\tdrag.points.push(q);\n\t\t\t\t\tdrag.pressures?.push(drag.pressure);\n\t\t\t\t}\n\t\t\t\tconst shape = this.editing.recognizeShapes ? recognizeInkShape(drag.points, { minSize: 6 / this.viewport.zoom }) : null;\n\t\t\t\tif (shape) {\n\t\t\t\t\tthis.replaceNode(drag.nodeId, { ...node, name: inkShapeLabel(shape), subpaths: [inkShapeSubpath(shape)] });\n\t\t\t\t\tthis.selection = [drag.nodeId];\n\t\t\t\t\treturn this.commit(`Draw ${inkShapeLabel(shape)}`, drag.before);\n\t\t\t\t}\n\t\t\t\tconst fitted = fitCubics(drag.points, 1.5 / this.viewport.zoom, { cornerAngleDeg: 60 });\n\t\t\t\tconst anchors: VectorAnchor[] = [];\n\t\t\t\tfor (const c of fitted) {\n\t\t\t\t\tif (!anchors.length) anchors.push({ x: c.p0[0], y: c.p0[1] });\n\t\t\t\t\tanchors[anchors.length - 1].out = { x: c.p1[0], y: c.p1[1] };\n\t\t\t\t\tanchors.push({ x: c.p3[0], y: c.p3[1], in: { x: c.p2[0], y: c.p2[1] } });\n\t\t\t\t}\n\t\t\t\tconst subpaths = anchors.length > 1 ? bezierToSubpaths(subpathsToBezier([{ anchors, closed: false }])) : node.subpaths;\n\t\t\t\tconst profile = drag.pressures ? pencilPressureProfile(drag.points, drag.pressures) : [];\n\t\t\t\tthis.replaceNode(drag.nodeId, { ...node, subpaths, ...(profile.length ? { strokeWidthProfile: profile } : {}) });\n\t\t\t\tthis.selection = [drag.nodeId];\n\t\t\t\treturn this.commit('Draw', drag.before);\n\t\t\t}\n\t\t}\n\t}\n\n\t/** The pointer left the canvas. */\n\tpointerLeave(): void {\n\t\tif (this.drag) return;\n\t\tif (!this.hover && !this.pointer && !this.guides) return;\n\t\tthis.hover = null;\n\t\tthis.pointer = null;\n\t\tthis.guides = null;\n\t\tthis.notify();\n\t}\n\n\t/** Abandons the drag in progress (Escape, lost capture). */\n\tcancelDrag(): void {\n\t\tconst drag = this.drag;\n\t\tif (!drag) return;\n\t\tthis.drag = null;\n\t\tthis.interaction = null;\n\t\tthis.guides = null;\n\t\tif (drag.kind === 'pan' || drag.kind === 'builder') {\n\t\t\tif (drag.kind === 'builder') this.builderPreview = null;\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tif (drag.kind === 'marquee') {\n\t\t\tthis.selection = drag.base;\n\t\t\tthis.anchors = drag.baseAnchors;\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tthis.restore(drag.before);\n\t}\n\n\t/**\n\t * Double click: select tool enters path editing, direct tool toggles an anchor or adds one,\n\t * width tool opens the width-point editor (`widthPointEdit`).\n\t */\n\tdoubleClick(screen: Vec2): void {\n\t\tconst p = this.screenToDocument(screen);\n\t\tif (this.tool === 'select') {\n\t\t\tconst path = this.hitPath(p);\n\t\t\tif (!path) return;\n\t\t\tthis.tool = 'direct';\n\t\t\tthis.selection = [path.id];\n\t\t\tthis.editingId = path.id;\n\t\t\tthis.anchors = [];\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tif (this.tool === 'direct') {\n\t\t\tconst anchor = this.hitAnchor(screen, getVisibleAnchors(this.view()));\n\t\t\tif (anchor) {\n\t\t\t\tconst a = getAnchor(this.doc, anchor)!;\n\t\t\t\tthis.convertAnchors(a.in || a.out ? 'corner' : 'smooth', [anchor]);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst id = this.hover?.segment?.nodeId ?? this.editingId;\n\t\t\tif (id) {\n\t\t\t\tconst node = this.getNode(id) as VectorPathNode | undefined;\n\t\t\t\tconst seg = node ? nearestSegment(node, this.toLocal(id, p)) : null;\n\t\t\t\tif (seg && seg.distance <= this.tolerance()) this.insertAnchorAt(id, p);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tif (this.tool === 'width') {\n\t\t\t// Width Point Edit: on a width point, or on a stroke (applying it adds a point there).\n\t\t\tconst hit = this.hitWidthHandle(screen);\n\t\t\tif (hit) {\n\t\t\t\tthis.openWidthPointEdit(hit.ref);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst spot = this.strokeSpotAt(p);\n\t\t\tif (spot) this.openWidthPointEdit({ nodeId: spot.nodeId, subpath: spot.subpath, t: spot.t });\n\t\t\treturn;\n\t\t}\n\t\tif (this.tool === 'pen') this.finishPath();\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Keyboard input\n\t// -------------------------------------------------------------------------\n\n\t/**\n\t * Handles editor shortcuts. Returns true when the key was used (call `preventDefault`).\n\t * Do not forward keys typed into text inputs.\n\t */\n\tkeyDown(input: VectorKeyInput): boolean {\n\t\tconst key = input.key;\n\t\tconst lower = key.length === 1 ? key.toLowerCase() : key;\n\t\tif (key === ' ') {\n\t\t\tif (!this.spaceHeld) {\n\t\t\t\tthis.spaceHeld = true;\n\t\t\t\tthis.notify();\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t\tif (input.mod) {\n\t\t\tswitch (lower) {\n\t\t\t\tcase 'z':\n\t\t\t\t\tinput.shift ? this.redo() : this.undo();\n\t\t\t\t\treturn true;\n\t\t\t\tcase 'y':\n\t\t\t\t\tthis.redo();\n\t\t\t\t\treturn true;\n\t\t\t\tcase 'a':\n\t\t\t\t\tthis.selectAll();\n\t\t\t\t\treturn true;\n\t\t\t\tcase 'd':\n\t\t\t\t\tthis.duplicate();\n\t\t\t\t\treturn true;\n\t\t\t\tcase 'g':\n\t\t\t\t\tinput.shift ? this.ungroup() : this.group();\n\t\t\t\t\treturn true;\n\t\t\t\tcase 'j':\n\t\t\t\t\treturn input.alt ? this.averageAnchors('both').ok : this.joinPaths().ok;\n\t\t\t\t// Make Clipping Mask; with Alt, Release (Option-7 types ¶ on a Mac).\n\t\t\t\tcase '7':\n\t\t\t\tcase '¶':\n\t\t\t\t\treturn input.alt || key === '¶' ? this.releaseMask().length > 0 : this.makeMask('clip').ok;\n\t\t\t\tcase ']':\n\t\t\t\t\tthis.arrange(input.alt ? 'front' : 'forward');\n\t\t\t\t\treturn true;\n\t\t\t\tcase '[':\n\t\t\t\t\tthis.arrange(input.alt ? 'back' : 'backward');\n\t\t\t\t\treturn true;\n\t\t\t\tcase '=':\n\t\t\t\tcase '+':\n\t\t\t\t\tthis.setViewport({ zoom: this.viewport.zoom * 1.25 });\n\t\t\t\t\treturn true;\n\t\t\t\tcase '-':\n\t\t\t\t\tthis.setViewport({ zoom: this.viewport.zoom / 1.25 });\n\t\t\t\t\treturn true;\n\t\t\t\tcase '0':\n\t\t\t\t\tthis.setViewport({ zoom: 1 });\n\t\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t\tswitch (key) {\n\t\t\tcase 'Escape':\n\t\t\t\tif (this.drag) this.cancelDrag();\n\t\t\t\telse if (this.widthEdit) this.closeWidthPointEdit();\n\t\t\t\telse if (this.tool === 'width' && this.widthPoints.length) {\n\t\t\t\t\tthis.widthPoints = [];\n\t\t\t\t\tthis.notify();\n\t\t\t\t}\n\t\t\t\telse if (this.pen) this.finishPath();\n\t\t\t\telse if (this.blendStart) {\n\t\t\t\t\tthis.blendStart = null;\n\t\t\t\t\tthis.notify();\n\t\t\t\t} else if (this.tool === 'direct' && this.anchors.length) {\n\t\t\t\t\tthis.anchors = [];\n\t\t\t\t\tthis.notify();\n\t\t\t\t} else if (this.tool !== 'select') this.setTool('select');\n\t\t\t\telse this.clearSelection();\n\t\t\t\treturn true;\n\t\t\tcase 'Enter':\n\t\t\t\tif (this.pen) {\n\t\t\t\t\tthis.finishPath();\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\tif (this.tool === 'select' && this.selection.length === 1 && this.getNode(this.selection[0])?.type === 'path') {\n\t\t\t\t\tthis.tool = 'direct';\n\t\t\t\t\tthis.editingId = this.selection[0];\n\t\t\t\t\tthis.notify();\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\tcase 'Delete':\n\t\t\tcase 'Backspace':\n\t\t\t\tif (this.tool === 'width' && this.widthPoints.length) return this.removeWidthPoints().ok;\n\t\t\t\tif (this.tool === 'gradient' && this.gradientEdit.stop !== null) return this.removeGradientStop(this.gradientEdit.stop).ok;\n\t\t\t\tif (this.pen) {\n\t\t\t\t\tconst node = this.getNode(this.pen.nodeId) as VectorPathNode;\n\t\t\t\t\tconst sp = node.subpaths[this.pen.subpath];\n\t\t\t\t\tif (sp.anchors.length > 2) this.deleteAnchors([{ nodeId: node.id, subpath: this.pen.subpath, index: sp.anchors.length - 1 }]);\n\t\t\t\t\telse {\n\t\t\t\t\t\tthis.removeNodes([node.id], { label: 'Delete Path' });\n\t\t\t\t\t\tthis.pen = null;\n\t\t\t\t\t}\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\treturn this.deleteSelection().ok;\n\t\t\tcase 'ArrowLeft':\n\t\t\tcase 'ArrowRight':\n\t\t\tcase 'ArrowUp':\n\t\t\tcase 'ArrowDown': {\n\t\t\t\tconst step = input.shift ? 10 : 1;\n\t\t\t\tconst dx = key === 'ArrowLeft' ? -step : key === 'ArrowRight' ? step : 0;\n\t\t\t\tconst dy = key === 'ArrowUp' ? -step : key === 'ArrowDown' ? step : 0;\n\t\t\t\treturn this.translate(dx, dy).ok;\n\t\t\t}\n\t\t}\n\t\tif (key.length === 1 && !input.alt) {\n\t\t\tconst tool = TOOL_KEYS[lower];\n\t\t\tif (tool) {\n\t\t\t\tthis.setTool(tool);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\tkeyUp(input: VectorKeyInput): void {\n\t\tif (input.key === ' ' && this.spaceHeld) {\n\t\t\tthis.spaceHeld = false;\n\t\t\tthis.notify();\n\t\t}\n\t}\n}\n\n/** \"Rectangle\", \"Square\", \"Ellipse\", \"Circle\", \"Line\", \"Triangle\" or \"Polygon\". */\nfunction inkShapeLabel(shape: VectorInkShape): string {\n\tconst name = vectorInkShapeName(shape);\n\treturn name[0].toUpperCase() + name.slice(1);\n}\n\n/** The subpath of a recognised pencil shape. */\nfunction inkShapeSubpath(shape: VectorInkShape): VectorSubpath {\n\tswitch (shape.kind) {\n\t\tcase 'line':\n\t\t\treturn lineSubpath({ x: shape.a[0], y: shape.a[1] }, { x: shape.b[0], y: shape.b[1] });\n\t\tcase 'rect':\n\t\t\treturn rectSubpath(shape);\n\t\tcase 'ellipse':\n\t\t\treturn ellipseSubpath(shape);\n\t\tcase 'polygon':\n\t\t\treturn { anchors: shape.points.map(([x, y]) => ({ x, y })), closed: true };\n\t}\n}\n\n/** Snaps `p` to the nearest 45° direction from `origin`, keeping its distance. */\nfunction constrain45(origin: Vec2, p: Vec2): Vec2 {\n\tconst dx = p.x - origin.x;\n\tconst dy = p.y - origin.y;\n\tconst len = Math.hypot(dx, dy);\n\tconst a = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);\n\treturn { x: origin.x + Math.cos(a) * len, y: origin.y + Math.sin(a) * len };\n}\n\n/** Scale factors and origin for dragging a selection-box handle to `p`. */\nexport function scaleFor(handle: VectorTransformHandle, b: VectorBounds, p: Vec2, mods: VectorModifiers = {}): { sx: number; sy: number; origin: Vec2 } {\n\tconst west = handle.includes('w');\n\tconst east = handle.includes('e');\n\tconst north = handle.startsWith('n');\n\tconst south = handle.startsWith('s');\n\tconst center = { x: b.x + b.width / 2, y: b.y + b.height / 2 };\n\tconst origin = mods.alt ? center : { x: west ? b.x + b.width : east ? b.x : center.x, y: north ? b.y + b.height : south ? b.y : center.y };\n\tconst reachX = mods.alt ? b.width / 2 : b.width;\n\tconst reachY = mods.alt ? b.height / 2 : b.height;\n\tlet sx = west || east ? (reachX > 1e-9 ? ((p.x - origin.x) * (west ? -1 : 1)) / reachX : 1) : 1;\n\tlet sy = north || south ? (reachY > 1e-9 ? ((p.y - origin.y) * (north ? -1 : 1)) / reachY : 1) : 1;\n\tif (mods.shift) {\n\t\tif ((west || east) && (north || south)) {\n\t\t\tconst s = Math.max(Math.abs(sx), Math.abs(sy));\n\t\t\tsx = Math.sign(sx || 1) * s;\n\t\t\tsy = Math.sign(sy || 1) * s;\n\t\t} else if (west || east) sy = Math.abs(sx);\n\t\telse sx = Math.abs(sy);\n\t}\n\treturn { sx, sy, origin };\n}\n\n/** A style applied to a path; a `null` (or empty) width profile removes it. */\nfunction withStyle(node: VectorPathNode, style: VectorStyle & { opacity?: number }): VectorNode {\n\tconst { fills, strokes, ...shorthand } = style;\n\tlet next = { ...node, ...shorthand } as VectorPathNode;\n\tif ('strokeWidthProfile' in style) {\n\t\tconst p = style.strokeWidthProfile ? sanitizeWidthProfile(style.strokeWidthProfile) : [];\n\t\tif (p.length) next.strokeWidthProfile = p;\n\t\telse delete next.strokeWidthProfile;\n\t}\n\t// Shorthands edit the top layer of a stack; whole stacks given replace them.\n\tnext = normalizeVectorPaintStacks(next, 'shorthand');\n\tif (fills !== undefined) next = withVectorFills(next, sanitizeVectorFills(fills ?? []));\n\tif (strokes !== undefined) next = withVectorStrokes(next, sanitizeVectorStrokes(strokes ?? []));\n\treturn next;\n}\n\n/** True when node `id` or one of its ancestors is locked or hidden. */\nfunction hiddenOrLocked(nodes: readonly VectorNode[], id: string): boolean {\n\tconst loc = locateVectorNode(nodes as VectorNode[], id);\n\tif (!loc) return false;\n\tconst n = loc.list[loc.index];\n\tif (n.locked || n.visible === false) return true;\n\treturn loc.parent ? hiddenOrLocked(nodes, loc.parent.id) : false;\n}\n\n/** The width profile of pencil samples and their pressures (positions by length along the samples). */\nfunction pencilPressureProfile(points: readonly Point[], pressures: readonly number[]): VectorWidthPoint[] {\n\tconst n = Math.min(points.length, pressures.length);\n\tif (n < 2) return [];\n\tconst s = [0];\n\tfor (let i = 1; i < n; i++) s.push(s[i - 1] + Math.hypot(points[i][0] - points[i - 1][0], points[i][1] - points[i - 1][1]));\n\tconst total = s[n - 1];\n\tif (!(total > 0)) return [];\n\treturn pressureWidthProfile(s.map((d, i) => ({ t: d / total, pressure: pressures[i] })));\n}\n","/**\n * Framework-neutral render data for a vector editor: the scene (document-space SVG paths and\n * gradient defs) and the overlay (screen-space selection box, anchors, handles, marquee and\n * pen preview). React, Svelte or plain DOM code turns these into `<svg>` elements.\n */\nimport { bezierPathToSvg, transformBezierPath, type BezierPath } from '../canvas/pathops/bezier.js';\nimport { hatchPath } from '../canvas/pathops/hatch.js';\nimport type { SvgGradientDef } from '../canvas/gradient.js';\nimport { expandVectorGradientStops, isCentredVectorGradient, isVectorGradient, parseVectorColor, sortedVectorGradientStops, type VectorGradientHandle, type VectorPaintDef } from './gradient.js';\nimport { getVectorGradientAnnotator, VECTOR_GRADIENT_STOP_GAP } from './gradientTool.js';\n\nexport type { SvgGradientDef };\nimport {\n\tgetAnchorWorld,\n\tgetVectorCornerWidgets,\n\tgetVectorEnvelopeTarget,\n\tgetVectorSelectionBoxGeometry,\n\tgetVectorWidthHandles,\n\tgetVisibleAnchors,\n\tgetVisibleHandles,\n\ttype VectorEditorController,\n\ttype VectorEditorState,\n\ttype VectorSelectionBoxGeometry,\n\ttype VectorWidthPointRef\n} from './controller.js';\nimport { sanitizeWidthProfile, widthProfilePresetId, type VectorWidthPreset } from './width.js';\nimport {\n\tfindVectorNode,\n\twalkVectorNodes,\n\tlocateVectorNode,\n\tsubpathsToBezier,\n\tvectorWorldMatrix,\n\tIDENTITY_MATRIX,\n\tinferAnchorType,\n\tmultiplyMatrices,\n\tnodeMatrix,\n\tvectorNodeBounds,\n\tvectorParentMatrix,\n\tvectorPathData,\n\tvectorPathWorldBezier,\n\ttype AffineMatrix\n} from './geometry.js';\nimport { vectorNodeFilterSvg, vectorPaintToSvg, vectorStrokeSvgParts, type VectorRenderStrokeParts } from './svg.js';\nimport { needsVectorPaintLayers, vectorBlendModeCss, vectorFills, vectorPaintLayerNodes, vectorStrokes, vectorTargetPaint, VECTOR_BLEND_MODE_LABELS } from './paintLayers.js';\nimport { VECTOR_FILTER_LABELS, VECTOR_FILTER_PARAM_SPECS, type VectorFilterParamSpec, type VectorRenderFilter } from './filters.js';\nimport { vectorCorners } from './corners.js';\nimport {\n\texpandVectorAppearance,\n\thasOwnVectorAppearance,\n\thasVectorAppearance,\n\tvectorAppearanceOutline,\n\tvectorEnteredGroups,\n\tVECTOR_EFFECT_LABELS,\n\tVECTOR_EFFECT_PARAM_SPECS,\n\ttype VectorEffectParamSpec,\n\ttype VectorEnvelopeParams\n} from './appearance.js';\nimport { isVectorMaskActive, isVectorMaskGroup, vectorMaskRenderData, vectorMaskSummary, type VectorMaskRenderData, type VectorMaskSummary } from './mask.js';\nimport { vectorMeshEditPoints, vectorMeshGridCurves, VECTOR_WARP_STYLES, VECTOR_WARP_STYLE_LABELS } from './warp.js';\nimport { VECTOR_EFFECT_TYPES, type VectorEffectType } from './types.js';\nimport { isVectorBlendGroup, resolveVectorBlendOptions, vectorBlendRailAt, vectorBlendSpine } from './blendGroup.js';\nimport type { VectorGuideKind, VectorSnapKind } from './snap.js';\nimport { VECTOR_BLEND_MODES } from './types.js';\nimport type {\n\tVectorMaskType,\n\tVectorAnchorRef,\n\tVectorBlendMode,\n\tVectorFilterType,\n\tVectorStrokeLayer,\n\tVectorAnchorType,\n\tVectorArrowAlign,\n\tVectorArrowhead,\n\tVectorBlendOptions,\n\tVectorBlendSpacing,\n\tVectorBounds,\n\tVectorCornerKind,\n\tVectorGradient,\n\tVectorGradientInterpolation,\n\tVectorGradientSpread,\n\tVectorGradientStop,\n\tVectorGradientType,\n\tVectorStrokeGradientMode,\n\tVectorGroupNode,\n\tVectorHandleRef,\n\tVectorNode,\n\tVectorPaint,\n\tVectorPathfinderOperation,\n\tVectorPathNode,\n\tVectorStrokeAlign,\n\tVectorTransformHandle,\n\tVectorWidthPoint,\n\tVec2\n} from './types.js';\n\nexport interface VectorRenderPath {\n\tkind: 'path';\n\tid: string;\n\t/** SVG `transform` (`matrix(…)`) for a rotated node. */\n\ttransform?: string;\n\td: string;\n\t/** `none`, a colour, or `url(#id)` of a gradient in `defs`. */\n\tfill: string;\n\tstroke: string;\n\tstrokeWidth: number;\n\tstrokeLinecap?: string;\n\tstrokeLinejoin?: string;\n\tstrokeDasharray?: string;\n\tfillRule: 'nonzero' | 'evenodd';\n\topacity: number;\n\t/**\n\t * Set when the stroke takes more than one element: an inside / outside alignment or\n\t * arrowheads. Draw `d` with the fill only, then `stroke.d` with the stroke (at\n\t * `stroke.width`, clipped or masked as `stroke.clip` says), then each head filled with\n\t * `stroke`; group them under `id`, `transform` and `opacity`.\n\t */\n\tstrokeParts?: VectorRenderStrokeParts;\n\t/**\n\t * Set when the path paints through its fill and stroke stacks (several layers, or a layer with\n\t * its own opacity, blend mode or visibility): draw a `<g>` with `id`, `transform`, `opacity`,\n\t * `blendMode` and `filter`, holding each layer in order, bottom to top, as a path of `d` (see\n\t * `VectorRenderPaintLayer`). `fill`, `stroke` and `strokeParts` above then describe the top\n\t * drawn layers only, for readers that draw one paint.\n\t */\n\tlayers?: VectorRenderPaintLayer[];\n\t/** CSS `mix-blend-mode` of the whole node (absent: normal). */\n\tblendMode?: string;\n\t/** `url(#id)` of the node's filter in `filters` (shadows, glows, blurs, feather). */\n\tfilter?: string;\n}\n\n/**\n * One drawn layer of a path's paint stack: a fill (`fill` set, `stroke` none) or a stroke with its\n * own settings and parts (`strokeParts` as on `VectorRenderPath`). Draw it with `opacity` and\n * `mix-blend-mode: blendMode` on its element (or on the group of its parts).\n */\nexport interface VectorRenderPaintLayer {\n\tkind: 'fill' | 'stroke';\n\t/** Index in its stack (bottom to top). */\n\tindex: number;\n\tfill: string;\n\tstroke: string;\n\tstrokeWidth: number;\n\tstrokeLinecap?: string;\n\tstrokeLinejoin?: string;\n\tstrokeDasharray?: string;\n\topacity: number;\n\tblendMode?: string;\n\tstrokeParts?: VectorRenderStrokeParts;\n}\n\nexport interface VectorRenderGroup {\n\tkind: 'group';\n\tid: string;\n\t/** SVG `transform` (`matrix(…)`) for a rotated group. */\n\ttransform?: string;\n\topacity: number;\n\t/** CSS `mix-blend-mode` of the group (absent: normal). */\n\tblendMode?: string;\n\t/** `url(#id)` of the group's filter in `filters`. */\n\tfilter?: string;\n\t/**\n\t * Set on a mask group whose mask is on: `children` are then the masked contents only. Draw them\n\t * in an inner `<g>` with `clip-path` / `mask` referencing the `<clipPath>` / `<mask>` that\n\t * `mask` describes (`mask.nodes`: the mask drawn into a `<mask>`). The group's own transform,\n\t * opacity, blend mode and filter stay on the outer `<g>`, so they apply to the masked result.\n\t */\n\tmask?: VectorRenderMask;\n\tchildren: VectorRenderNode[];\n}\n\n/** A mask group's mask as render data (see `VectorMaskRenderData`), with the mask drawn as nodes for opacity masks. */\nexport interface VectorRenderMask extends Omit<VectorMaskRenderData, 'invertFilter'> {\n\t/** The mask as drawn, for the `<mask>` of an opacity mask (in the group's own space). */\n\tnodes?: VectorRenderNode[];\n}\n\nexport type VectorRenderNode = VectorRenderPath | VectorRenderGroup;\n\nexport interface VectorSceneRenderData {\n\t/** SVG `transform` for the document layer: `translate(x y) scale(zoom)`. */\n\ttransform: string;\n\twidth: number;\n\theight: number;\n\tbackground?: string;\n\t/**\n\t * Paint servers referenced by `fill` / `stroke` values: `<linearGradient>` /\n\t * `<radialGradient>` descriptors and `pattern`s (conic, diamond and stroke-mode gradients:\n\t * draw each of `gradients`, then each tile filled, inside a `<pattern>`).\n\t */\n\tdefs: VectorPaintDef[];\n\t/** Filters referenced by nodes' `filter` (draw each as an SVG `<filter>`, see `VectorRenderFilter`). */\n\tfilters: VectorRenderFilter[];\n\t/** Visible nodes, bottom to top. */\n\tnodes: VectorRenderNode[];\n}\n\nexport interface VectorRenderAnchor {\n\tref: VectorAnchorRef;\n\t/** Screen position. */\n\tx: number;\n\ty: number;\n\ttype: VectorAnchorType;\n\tselected: boolean;\n\thovered: boolean;\n\t/** The pen tool closes the path when this anchor is clicked. */\n\tcloses: boolean;\n}\n\nexport interface VectorRenderHandle {\n\tref: VectorHandleRef;\n\tx: number;\n\ty: number;\n\t/** Screen position of the handle's anchor (draw a line to it). */\n\tanchorX: number;\n\tanchorY: number;\n\thovered: boolean;\n}\n\n/** A live corner widget in screen pixels. */\nexport interface VectorRenderCornerWidget {\n\tref: VectorAnchorRef;\n\tx: number;\n\ty: number;\n\tradius: number;\n\tkind: VectorCornerKind;\n\thovered: boolean;\n\t/** Its corners are being dragged. */\n\tactive: boolean;\n}\n\n/**\n * A width point of the width tool in screen pixels: a diamond at (`x`, `y`) on the path and a\n * line across the stroke from `left` to `right`, with a handle at each end.\n */\nexport interface VectorRenderWidthPoint {\n\tref: VectorWidthPointRef;\n\tx: number;\n\ty: number;\n\tleft: Vec2;\n\tright: Vec2;\n\tselected: boolean;\n\t/** The part under the pointer, if any. */\n\thovered: 'center' | 'left' | 'right' | null;\n\t/** Being dragged. */\n\tactive: boolean;\n}\n\n/** A smart guide line in screen pixels. */\nexport interface VectorRenderGuideLine {\n\tkind: VectorGuideKind;\n\tx1: number;\n\ty1: number;\n\tx2: number;\n\ty2: number;\n}\n\n/** A smart guide label (\"anchor\", \"intersect\", \"align\", \"centre\", a gap or a size) at a screen position. */\nexport interface VectorRenderGuideLabel {\n\tkind: VectorSnapKind;\n\tx: number;\n\ty: number;\n\ttext: string;\n}\n\nexport interface VectorOverlayRenderData {\n\t/** Outlines (screen-space path data) of hovered and selected nodes. */\n\toutlines: { id: string; d: string; selected: boolean }[];\n\t/**\n\t * Selection box with resize and rotate handles (select tool). `x / y / width / height` and\n\t * each handle's `x / y` are the unrotated box in screen pixels: draw them inside\n\t * `<g transform={transform}>`. `corners` and each handle's `screen` are true positions.\n\t */\n\tselectionBox:\n\t\t| (VectorBounds & { angle: number; transform: string; corners: Vec2[]; handles: VectorSelectionBoxGeometry['handles']; freeDistort: boolean })\n\t\t| null;\n\t/**\n\t * A Free Distort drag (screen space): the quad the selection box's corners make so far (`d`,\n\t * closed path data) and its corners clockwise from the top left. The box is hidden meanwhile.\n\t */\n\tdistort: { mode: 'distort' | 'perspective' | 'skew'; d: string; corners: Vec2[] } | null;\n\t/**\n\t * The Envelope tool's mesh of the selected envelope (screen space): `grid` is the grid lines as\n\t * path data, `anchors` the points where they cross and `handles` the handles along them, each\n\t * with the anchor it belongs to (`index` is the point's index in the mesh). Draw anchors as\n\t * squares and handles as dots joined to their anchors.\n\t */\n\tenvelope: {\n\t\tnodeId: string;\n\t\tgrid: string;\n\t\tanchors: { index: number; x: number; y: number; hovered: boolean; active: boolean }[];\n\t\thandles: { index: number; x: number; y: number; anchorX: number; anchorY: number; hovered: boolean; active: boolean }[];\n\t} | null;\n\tanchors: VectorRenderAnchor[];\n\thandles: VectorRenderHandle[];\n\t/** Marquee rectangle, screen space. */\n\tmarquee: VectorBounds | null;\n\t/** Rubber-band segment from the pen path's last anchor to the pointer, screen space. */\n\tpenPreview: string | null;\n\t/** Artboard rectangle, screen space. */\n\tartboard: VectorBounds;\n\t/** Smart guides of the snap in progress, screen space (empty when nothing snapped). */\n\tguides: { lines: VectorRenderGuideLine[]; labels: VectorRenderGuideLabel[] };\n\t/**\n\t * The knife or eraser stroke being drawn, screen space: `d` is an open polyline; draw the\n\t * eraser's with a round-capped stroke `width` pixels wide (0 for the knife: a thin line).\n\t */\n\ttoolStroke: { tool: 'knife' | 'eraser'; d: string; width: number } | null;\n\t/** The pencil's lazy-mouse string from the pen to the pointer, screen space. */\n\tpencilString: { x1: number; y1: number; x2: number; y2: number } | null;\n\t/** Where the scissors would cut (screen space). */\n\tcutPoint: Vec2 | null;\n\t/**\n\t * Live corner widgets of the direct tool (screen space): drag one to round its corners,\n\t * Alt-click to cycle round / inverted / chamfer, click to pick its corner alone.\n\t */\n\tcornerWidgets: VectorRenderCornerWidget[];\n\t/** Width points of the width tool (screen space). */\n\twidthPoints: VectorRenderWidthPoint[];\n\t/**\n\t * Where a width-tool drag would add a width point (screen space): draw a dashed line from\n\t * `left` to `right` and a hollow diamond at (`x`, `y`).\n\t */\n\twidthSpot: { x: number; y: number; left: Vec2; right: Vec2 } | null;\n\t/**\n\t * Shape Builder and paint bucket highlight (screen space): `faces` are filled paths (fill\n\t * them translucent and outline them), `hatches` the 45° hatch lines over each face\n\t * (`hatchPath`, a few pixels apart; merge and erase only, empty strings for the paint\n\t * bucket: stroke them over the translucent fill), `lines` are the line pieces and outline\n\t * edges an erase deletes, `trail` is the drag's polyline. `mode` is `merge`, `erase` (Alt)\n\t * or `fill` (paint bucket).\n\t */\n\tbuilder: { mode: 'merge' | 'erase' | 'fill'; faces: string[]; hatches: string[]; lines: string[]; trail: string | null } | null;\n\t/**\n\t * Blends (screen space): the spine of each selected blend (`d`, draw it dashed; `keys` are\n\t * where its keys sit on it; `replaced` when it is a replaced spine) and, with the Blend tool,\n\t * the outline of the shape it blends from (`start`, with the clicked anchor) and the anchors\n\t * of the first subpath under the pointer (click one to start the blend there).\n\t */\n\tblend: {\n\t\tspines: { id: string; d: string; keys: Vec2[]; replaced: boolean }[];\n\t\tstart: { d: string; anchor: Vec2 | null } | null;\n\t\tanchors: { x: number; y: number; hovered: boolean }[];\n\t};\n\t/** The gradient tool's annotator over the selection's gradient (screen space), or null. */\n\tgradient: VectorRenderGradientAnnotator | null;\n\t/**\n\t * Mask indicators (screen space): the outline of the mask of each mask group that is selected\n\t * or holds the selection (`d`, draw it dashed), with its type, whether it is inverted, and\n\t * whether the mask itself is selected (`editing`).\n\t */\n\tmasks: { id: string; maskId: string; d: string; type: VectorMaskType; invert: boolean; enabled: boolean; editing: boolean }[];\n\t/** A short status, e.g. \"W 120 × H 80\" or \"45°\". */\n\tstatus: string | null;\n}\n\n/**\n * The gradient annotator in screen pixels: draw the `extent` outline (dashed), the bar from\n * `start` to `end`, the handles, each stop's chip (a swatch at `x, y` with a tick to `barX, barY`;\n * `removing`: the dragged stop is off the bar and will be deleted, draw it faded) and the\n * midpoint diamonds.\n */\nexport interface VectorRenderGradientAnnotator {\n\tnodeId: string;\n\ttype: VectorGradientType;\n\tstart: { x: number; y: number; hovered: boolean };\n\t/** `radius` for radial, conic and diamond gradients. */\n\tend: { x: number; y: number; hovered: boolean; handle: 'end' | 'radius' };\n\taspect: { x: number; y: number; hovered: boolean } | null;\n\tfocal: { x: number; y: number; hovered: boolean } | null;\n\tstops: { index: number; x: number; y: number; barX: number; barY: number; color: string; opacity: number; selected: boolean; hovered: boolean }[];\n\tmidpoints: { index: number; x: number; y: number; hovered: boolean }[];\n\textent: string | null;\n\tremoving: boolean;\n}\n\nconst fmt = (n: number) => Math.round(n * 100) / 100;\nconst fmtMatrix = (m: AffineMatrix) => `matrix(${m.map((n) => Math.round(n * 1e6) / 1e6).join(' ')})`;\n/** Distance between the Shape Builder's hatch lines over a face (screen pixels). */\nexport const VECTOR_BUILDER_HATCH_GAP = 6;\n\n/** The blend mode and filter of a node, as render data (its filter pushed to `filters`). */\nfunction nodeFinish(node: VectorNode, filters: VectorRenderFilter[], idPrefix: string): { blendMode?: string; filter?: string } {\n\tconst out: { blendMode?: string; filter?: string } = {};\n\tconst blend = vectorBlendModeCss(node.blendMode);\n\tif (blend) out.blendMode = blend;\n\tconst filter = vectorNodeFilterSvg(node, `${idPrefix}filter-${node.id}`);\n\tif (filter) {\n\t\tfilters.push(filter);\n\t\tout.filter = `url(#${filter.id})`;\n\t}\n\treturn out;\n}\n\n/** The SVG line attributes of a (layer) node's stroke. */\nfunction lineStyle(node: VectorPathNode) {\n\treturn {\n\t\tstrokeWidth: node.strokeWidth ?? 1,\n\t\t...(node.strokeLinecap ? { strokeLinecap: node.strokeLinecap } : {}),\n\t\t...(node.strokeLinejoin ? { strokeLinejoin: node.strokeLinejoin } : {}),\n\t\t...(node.strokeDasharray?.length ? { strokeDasharray: node.strokeDasharray.join(' ') } : {})\n\t};\n}\n\n/** The drawn layers of a path's stacks as render data (paint servers pushed to `defs`). */\nfunction renderLayers(node: VectorPathNode, defs: VectorPaintDef[], idPrefix: string): VectorRenderPaintLayer[] {\n\tconst out: VectorRenderPaintLayer[] = [];\n\tfor (const l of vectorPaintLayerNodes(node)) {\n\t\tif (!l.drawn) continue;\n\t\tconst role = l.kind;\n\t\tconst paint = vectorPaintToSvg(l.layer.paint, `${idPrefix}${role}-${node.id}-${l.index}`, { node: l.node, ...(role === 'stroke' ? { role } : {}) });\n\t\tdefs.push(...paint.defs);\n\t\tconst blend = vectorBlendModeCss(l.layer.blendMode);\n\t\tconst base = { kind: role, index: l.index, opacity: l.layer.opacity ?? 1, ...(blend ? { blendMode: blend } : {}) };\n\t\tif (role === 'fill') out.push({ ...base, fill: paint.value, stroke: 'none', strokeWidth: 0 });\n\t\telse {\n\t\t\tconst strokeParts = vectorStrokeSvgParts(l.node, `${idPrefix}clip-${node.id}-${l.index}`);\n\t\t\tout.push({ ...base, fill: 'none', stroke: paint.value, ...lineStyle(l.node), ...(strokeParts ? { strokeParts } : {}) });\n\t\t}\n\t}\n\treturn out;\n}\n\nfunction renderNode(stored: VectorNode, defs: VectorPaintDef[], filters: VectorRenderFilter[], idPrefix: string): VectorRenderNode | null {\n\tif (stored.visible === false) return null;\n\t// Path effects and live booleans draw as their expanded shapes.\n\tconst node = expandVectorAppearance(stored);\n\tif (node.type === 'group') {\n\t\tconst finish = nodeFinish(node, filters, idPrefix);\n\t\tconst masked = isVectorMaskGroup(node) ? renderMask(node, defs, filters, idPrefix) : null;\n\t\t// A mask group without a working mask draws its contents whole (a hidden mask child stays hidden).\n\t\tconst list = isVectorMaskGroup(node) ? node.children.slice(1) : node.children;\n\t\tconst children = list.map((c) => renderNode(c, defs, filters, idPrefix)).filter((c): c is VectorRenderNode => !!c);\n\t\treturn {\n\t\t\tkind: 'group',\n\t\t\tid: node.id,\n\t\t\t...(node.transform ? { transform: fmtMatrix(node.transform) } : {}),\n\t\t\topacity: node.opacity ?? 1,\n\t\t\t...finish,\n\t\t\t...(masked ? { mask: masked } : {}),\n\t\t\tchildren\n\t\t};\n\t}\n\tconst finish = nodeFinish(node, filters, idPrefix);\n\tconst common = {\n\t\tkind: 'path' as const,\n\t\tid: node.id,\n\t\t...(node.transform ? { transform: fmtMatrix(node.transform) } : {}),\n\t\td: vectorPathData(node),\n\t\tfillRule: node.fillRule ?? ('nonzero' as const),\n\t\topacity: node.opacity ?? 1,\n\t\t...finish\n\t};\n\tif (needsVectorPaintLayers(node)) {\n\t\tconst layers = renderLayers(node, defs, idPrefix);\n\t\tconst fill = layers.filter((l) => l.kind === 'fill').at(-1);\n\t\tconst stroke = layers.filter((l) => l.kind === 'stroke').at(-1);\n\t\treturn {\n\t\t\t...common,\n\t\t\tfill: fill?.fill ?? 'none',\n\t\t\tstroke: stroke?.stroke ?? 'none',\n\t\t\tstrokeWidth: stroke?.strokeWidth ?? node.strokeWidth ?? 1,\n\t\t\t...(stroke?.strokeLinecap ? { strokeLinecap: stroke.strokeLinecap } : {}),\n\t\t\t...(stroke?.strokeLinejoin ? { strokeLinejoin: stroke.strokeLinejoin } : {}),\n\t\t\t...(stroke?.strokeDasharray ? { strokeDasharray: stroke.strokeDasharray } : {}),\n\t\t\t...(stroke?.strokeParts ? { strokeParts: stroke.strokeParts } : {}),\n\t\t\tlayers\n\t\t};\n\t}\n\tconst fill = vectorPaintToSvg(node.fill, `${idPrefix}fill-${node.id}`, { node });\n\tconst stroke = vectorPaintToSvg(node.stroke, `${idPrefix}stroke-${node.id}`, { node, role: 'stroke' });\n\tdefs.push(...fill.defs, ...stroke.defs);\n\tconst strokeParts = stroke.value !== 'none' ? vectorStrokeSvgParts(node, `${idPrefix}clip-${node.id}`) : undefined;\n\treturn {\n\t\t...common,\n\t\t...(strokeParts ? { strokeParts } : {}),\n\t\tfill: fill.value,\n\t\tstroke: stroke.value,\n\t\t...lineStyle(node)\n\t};\n}\n\n/** A mask group's mask as render data (its inverting filter pushed to `filters`), or null while it is off. */\nfunction renderMask(group: VectorGroupNode, defs: VectorPaintDef[], filters: VectorRenderFilter[], idPrefix: string): VectorRenderMask | null {\n\tif (!isVectorMaskActive(group)) return null;\n\tconst data = vectorMaskRenderData(group, `${idPrefix}mask-${group.id}`, `${idPrefix}mask-filter-${group.id}`);\n\tif (!data) return null;\n\tconst { invertFilter, ...mask } = data;\n\tif (invertFilter) filters.push(invertFilter);\n\tif (mask.type === 'clip') return mask;\n\t// The mask draws as itself (shown even when stored hidden would have turned the mask off: it is on here).\n\tconst source = renderNode(group.children[0], defs, filters, idPrefix);\n\treturn { ...mask, nodes: source ? [source] : [] };\n}\n\n/**\n * The document layer. Render the nodes inside `<g transform={transform}>` and `defs` in a\n * `<defs>` block. Pass a unique `idPrefix` when several editors share a page.\n */\nexport function getVectorSceneRenderData(state: Pick<VectorEditorState, 'document' | 'viewport'>, options: { idPrefix?: string } = {}): VectorSceneRenderData {\n\tconst defs: VectorPaintDef[] = [];\n\tconst filters: VectorRenderFilter[] = [];\n\tconst prefix = options.idPrefix ?? 've-';\n\tconst nodes = state.document.nodes.map((n) => renderNode(n, defs, filters, prefix)).filter((n): n is VectorRenderNode => !!n);\n\tconst v = state.viewport;\n\treturn {\n\t\ttransform: `translate(${fmt(v.x)} ${fmt(v.y)}) scale(${v.zoom})`,\n\t\twidth: state.document.width,\n\t\theight: state.document.height,\n\t\t...(state.document.background ? { background: state.document.background } : {}),\n\t\tdefs,\n\t\tfilters,\n\t\tnodes\n\t};\n}\n\nconst toScreen = (state: Pick<VectorEditorState, 'viewport'>, p: Vec2) => ({ x: p.x * state.viewport.zoom + state.viewport.x, y: p.y * state.viewport.zoom + state.viewport.y });\n\nfunction screenOutline(state: VectorEditorState, node: VectorNode, entered: ReadonlySet<string>): string {\n\tconst v = state.viewport;\n\tconst view: AffineMatrix = [v.zoom, 0, 0, v.zoom, v.x, v.y];\n\tconst parent = vectorParentMatrix(state.document.nodes, node.id) ?? IDENTITY_MATRIX;\n\treturn outlinePaths(node, parent, entered)\n\t\t.map((path) => bezierPathToSvg(transformBezierPath(path, view), { precision: 2 }))\n\t\t.join(' ');\n}\n\n/**\n * Document-space outlines of a subtree: paths as stored (their anchors are what is edited; the\n * scene shows their effects), live groups as drawn unless they are being edited (then their\n * children, as figcraft outlines a boolean group's operands).\n */\nfunction outlinePaths(node: VectorNode, parent: AffineMatrix, entered: ReadonlySet<string>): BezierPath[] {\n\tif (node.type === 'path') return [vectorPathWorldBezier(node, parent, { effects: false })];\n\tconst m = multiplyMatrices(nodeMatrix(node), parent);\n\t// Live and mask groups outline as drawn (masked) unless they are being edited.\n\tif ((hasOwnVectorAppearance(node) || isVectorMaskActive(node)) && !entered.has(node.id)) return [transformBezierPath(vectorAppearanceOutline(node), m)];\n\treturn node.children.filter((c) => c.visible !== false).flatMap((c) => outlinePaths(c, m, entered));\n}\n\n/** Live groups being edited in `state` (see `vectorEnteredGroups`). */\nexport function getVectorEnteredGroups(state: Pick<VectorEditorState, 'document' | 'selection' | 'editingId'>): Set<string> {\n\treturn vectorEnteredGroups(state.document.nodes, state.editingId ? [...state.selection, state.editingId] : state.selection);\n}\n\nfunction collect(node: VectorNode): VectorPathNode[] {\n\tif (node.type === 'path') return [node];\n\treturn node.children.flatMap(collect);\n}\n\n/** The editing overlay in screen pixels. Render it on top of the scene, untransformed. */\nexport function getVectorOverlayRenderData(state: VectorEditorState): VectorOverlayRenderData {\n\tconst doc = state.document;\n\tconst z = state.viewport.zoom;\n\tconst outlines: VectorOverlayRenderData['outlines'] = [];\n\tconst entered = getVectorEnteredGroups(state);\n\tconst seen = new Set<string>();\n\tconst outline = (id: string, selected: boolean) => {\n\t\tif (seen.has(id)) return;\n\t\tconst node = findVectorNode(doc.nodes, id);\n\t\tif (!node || node.visible === false) return;\n\t\tseen.add(id);\n\t\toutlines.push({ id, d: screenOutline(state, node, entered), selected });\n\t};\n\tfor (const id of state.selection) outline(id, true);\n\tif (state.editingId) outline(state.editingId, true);\n\tif (state.hover?.nodeId && !state.interaction) outline(state.hover.nodeId, false);\n\n\tlet selectionBox: VectorOverlayRenderData['selectionBox'] = null;\n\tif (state.tool === 'select' && state.selectionFrame && state.interaction?.type !== 'marquee' && state.interaction?.type !== 'distort') {\n\t\tconst g = getVectorSelectionBoxGeometry(state.selectionFrame, state.viewport);\n\t\tselectionBox = { ...g.box, angle: g.angle, transform: g.transform, corners: g.corners, handles: g.handles, freeDistort: state.freeDistort };\n\t}\n\n\tconst selected = (r: VectorAnchorRef) => state.selectedAnchors.some((a) => a.nodeId === r.nodeId && a.subpath === r.subpath && a.index === r.index);\n\tconst sameAnchor = (a: VectorAnchorRef | undefined, b: VectorAnchorRef) => !!a && a.nodeId === b.nodeId && a.subpath === b.subpath && a.index === b.index;\n\tconst anchors: VectorRenderAnchor[] = getVisibleAnchors(state).map((ref) => {\n\t\tconst a = getAnchorWorld(doc, ref)!;\n\t\tconst s = toScreen(state, a);\n\t\tconst closes = !!state.pen && state.pen.nodeId === ref.nodeId && state.pen.subpath === ref.subpath && ref.index === 0;\n\t\treturn { ref, x: s.x, y: s.y, type: a.type ?? inferAnchorType(a), selected: selected(ref), hovered: sameAnchor(state.hover?.anchor, ref), closes };\n\t});\n\tif (state.tool === 'pen' && !state.pen && state.hover?.anchor) {\n\t\tconst a = getAnchorWorld(doc, state.hover.anchor);\n\t\tif (a) {\n\t\t\tconst s = toScreen(state, a);\n\t\t\tanchors.push({ ref: state.hover.anchor, x: s.x, y: s.y, type: a.type ?? 'corner', selected: false, hovered: true, closes: false });\n\t\t}\n\t}\n\n\tconst handles: VectorRenderHandle[] = getVisibleHandles(state).flatMap((ref) => {\n\t\tconst a = getAnchorWorld(doc, ref);\n\t\tconst h = a?.[ref.handle];\n\t\tif (!a || !h) return [];\n\t\tconst s = toScreen(state, h);\n\t\tconst sa = toScreen(state, a);\n\t\tconst hovered = !!state.hover?.handle && sameAnchor(state.hover.handle, ref) && state.hover.handle.handle === ref.handle;\n\t\treturn [{ ref, x: s.x, y: s.y, anchorX: sa.x, anchorY: sa.y, hovered }];\n\t});\n\n\tlet marquee: VectorBounds | null = null;\n\tif (state.interaction?.type === 'marquee') {\n\t\tconst b = state.interaction.bounds;\n\t\tconst tl = toScreen(state, b);\n\t\tmarquee = { x: tl.x, y: tl.y, width: b.width * z, height: b.height * z };\n\t}\n\n\tlet penPreview: string | null = null;\n\tif (state.tool === 'pen' && state.pen && state.pointer && !state.interaction) {\n\t\tconst node = findVectorNode(doc.nodes, state.pen.nodeId);\n\t\tconst sp = node?.type === 'path' ? node.subpaths[state.pen.subpath] : undefined;\n\t\tconst last = sp ? getAnchorWorld(doc, { nodeId: state.pen.nodeId, subpath: state.pen.subpath, index: sp.anchors.length - 1 }) : undefined;\n\t\tif (last) {\n\t\t\tconst a = toScreen(state, last);\n\t\t\tconst p = toScreen(state, state.pointer);\n\t\t\tconst c = last.out ? toScreen(state, last.out) : null;\n\t\t\tpenPreview = c ? `M${fmt(a.x)} ${fmt(a.y)}Q${fmt(c.x)} ${fmt(c.y)} ${fmt(p.x)} ${fmt(p.y)}` : `M${fmt(a.x)} ${fmt(a.y)}L${fmt(p.x)} ${fmt(p.y)}`;\n\t\t}\n\t}\n\n\tconst origin = toScreen(state, { x: 0, y: 0 });\n\tconst artboard = { x: origin.x, y: origin.y, width: doc.width * z, height: doc.height * z };\n\n\tlet status: string | null = null;\n\tconst it = state.interaction;\n\tif (it?.type === 'rotate') status = `${Math.round(it.angle)}°`;\n\telse if (it?.type === 'move' || it?.type === 'anchors') status = `Δ ${fmt(it.dx)}, ${fmt(it.dy)}`;\n\telse if (it?.type === 'scale' || it?.type === 'draw') {\n\t\tconst b = it.type === 'draw' ? (findVectorNode(doc.nodes, it.nodeId) ? vectorNodeBounds(findVectorNode(doc.nodes, it.nodeId)!) : null) : state.selectionFrame;\n\t\tif (b) status = `W ${fmt(b.width)} × H ${fmt(b.height)}`;\n\t}\n\n\tconst guides: VectorOverlayRenderData['guides'] = { lines: [], labels: [] };\n\tif (state.guides) {\n\t\tfor (const l of state.guides.lines) {\n\t\t\tconst a = toScreen(state, { x: l.x1, y: l.y1 });\n\t\t\tconst b = toScreen(state, { x: l.x2, y: l.y2 });\n\t\t\tguides.lines.push({ kind: l.kind, x1: fmt(a.x), y1: fmt(a.y), x2: fmt(b.x), y2: fmt(b.y) });\n\t\t}\n\t\tfor (const l of state.guides.labels) {\n\t\t\tconst p = toScreen(state, l);\n\t\t\tguides.labels.push({ kind: l.kind, x: fmt(p.x), y: fmt(p.y), text: l.text });\n\t\t}\n\t}\n\n\tlet toolStroke: VectorOverlayRenderData['toolStroke'] = null;\n\tif (it?.type === 'stroke') {\n\t\tconst pts = it.points.map((q) => toScreen(state, q));\n\t\tconst d = pts.map((q, i) => `${i ? 'L' : 'M'}${fmt(q.x)} ${fmt(q.y)}`).join('') + (pts.length === 1 ? `L${fmt(pts[0].x)} ${fmt(pts[0].y)}` : '');\n\t\ttoolStroke = { tool: it.tool, d, width: fmt(it.width * z) };\n\t}\n\tlet pencilString: VectorOverlayRenderData['pencilString'] = null;\n\tif (it?.type === 'draw' && it.string) {\n\t\tconst a = toScreen(state, it.string[0]);\n\t\tconst b = toScreen(state, it.string[1]);\n\t\tpencilString = { x1: fmt(a.x), y1: fmt(a.y), x2: fmt(b.x), y2: fmt(b.y) };\n\t}\n\tconst dragged = it?.type === 'corner' ? it.ref : null;\n\tconst cornerWidgets: VectorRenderCornerWidget[] = getVectorCornerWidgets(state).map((w) => {\n\t\tconst s = toScreen(state, w);\n\t\treturn { ref: w.ref, x: fmt(s.x), y: fmt(s.y), radius: w.radius, kind: w.kind, hovered: sameAnchor(state.hover?.corner, w.ref), active: !!dragged };\n\t});\n\tif (it?.type === 'corner') status = `R ${fmt(it.radius)}`;\n\tconst cutPoint = state.tool === 'scissors' && state.hover?.segment && !it ? (({ x, y }) => ({ x: fmt(x), y: fmt(y) }))(toScreen(state, state.hover.segment.point)) : null;\n\n\tconst sc = (p: Vec2) => {\n\t\tconst q = toScreen(state, p);\n\t\treturn { x: fmt(q.x), y: fmt(q.y) };\n\t};\n\tconst sameWidth = (a: VectorWidthPointRef | null | undefined, b: VectorWidthPointRef) => !!a && a.nodeId === b.nodeId && a.index === b.index;\n\tconst widthPoints: VectorRenderWidthPoint[] = getVectorWidthHandles(state).map((h) => {\n\t\tconst c = sc(h);\n\t\tconst hw = state.hover?.widthPoint;\n\t\tconst hovered = hw && sameWidth(hw, h.ref) && hw.subpath === h.ref.subpath ? hw.part : null;\n\t\treturn { ref: h.ref, x: c.x, y: c.y, left: sc(h.left), right: sc(h.right), selected: h.selected, hovered, active: it?.type === 'width' && sameWidth(it.ref, h.ref) };\n\t});\n\tconst spot = state.tool === 'width' && !it ? state.hover?.strokeSpot : null;\n\tconst widthSpot = spot ? { ...sc(spot.point), left: sc(spot.left), right: sc(spot.right) } : null;\n\tif (it?.type === 'width') status = it.mode === 'move' ? 'Move width point' : it.mode === 'copy' ? 'Copy width point' : `W ${fmt(it.width)}`;\n\n\tlet builder: VectorOverlayRenderData['builder'] = null;\n\tif (state.builder) {\n\t\tconst v = state.viewport;\n\t\tconst view: AffineMatrix = [v.zoom, 0, 0, v.zoom, v.x, v.y];\n\t\tconst svg = (path: BezierPath) => bezierPathToSvg(path, { precision: 2 });\n\t\tconst screen = state.builder.faces.map((f) => transformBezierPath(f, view));\n\t\tconst hatch = state.builder.mode !== 'fill';\n\t\tconst trail = state.builder.trail && state.builder.trail.length > 1 ? state.builder.trail.map((q, i) => `${i ? 'L' : 'M'}${sc(q).x} ${sc(q).y}`).join('') : null;\n\t\tbuilder = {\n\t\t\tmode: state.builder.mode,\n\t\t\tfaces: screen.map(svg),\n\t\t\thatches: screen.map((f) => (hatch ? svg(hatchPath(f, { gap: VECTOR_BUILDER_HATCH_GAP })) : '')),\n\t\t\tlines: state.builder.lines.map((l) => svg(transformBezierPath(l, view))),\n\t\t\ttrail\n\t\t};\n\t\tif (state.builder.mode !== 'fill' && it?.type === 'builder') {\n\t\t\tconst n = state.builder.faces.length;\n\t\t\tstatus = state.builder.mode === 'erase' ? `Delete ${n} ${n === 1 ? 'face' : 'faces'}` : `Merge ${n} ${n === 1 ? 'face' : 'faces'}`;\n\t\t}\n\t}\n\n\tconst blend = blendOverlay(state);\n\tif (state.tool === 'blend' && !it) status = state.blendStart ? 'Click another shape to blend into' : 'Click a shape to blend from';\n\n\tlet gradient: VectorRenderGradientAnnotator | null = null;\n\tif (state.tool === 'gradient') {\n\t\tgradient = gradientOverlay(state);\n\t\tif (!gradient && !it) status = 'Drag across a shape to set a gradient';\n\t\telse if (it?.type === 'gradient' && it.removing) status = 'Release to delete the stop';\n\t}\n\n\tlet distort: VectorOverlayRenderData['distort'] = null;\n\tif (it?.type === 'distort') {\n\t\tconst corners = it.corners.map(sc);\n\t\tdistort = { mode: it.mode, corners, d: corners.map((c, i) => `${i ? 'L' : 'M'}${c.x} ${c.y}`).join('') + 'Z' };\n\t\tstatus = it.mode === 'skew' ? 'Skew' : it.mode === 'perspective' ? 'Perspective' : 'Distort';\n\t}\n\n\tconst envelope = state.tool === 'envelope' ? envelopeOverlay(state) : null;\n\tif (state.tool === 'envelope' && !envelope && !it) status = 'Select an object with an envelope to edit its mesh';\n\n\treturn { outlines, selectionBox, distort, anchors, handles, marquee, penPreview, artboard, guides, status, toolStroke, pencilString, cutPoint, cornerWidgets, widthPoints, widthSpot, builder, blend, gradient, envelope, masks: maskOverlay(state) };\n}\n\n/** Mask indicators of the mask groups that are selected or hold the selection (see `VectorOverlayRenderData.masks`). */\nfunction maskOverlay(state: VectorEditorState): VectorOverlayRenderData['masks'] {\n\tconst ids = state.editingId ? [...state.selection, state.editingId] : state.selection;\n\tif (!ids.length) return [];\n\tconst want = new Set(ids);\n\tconst v = state.viewport;\n\tconst view: AffineMatrix = [v.zoom, 0, 0, v.zoom, v.x, v.y];\n\tconst out: VectorOverlayRenderData['masks'] = [];\n\tconst visit = (list: readonly VectorNode[], parent: AffineMatrix): boolean => {\n\t\tlet any = false;\n\t\tfor (const n of list) {\n\t\t\tif (n.type !== 'group') {\n\t\t\t\tif (want.has(n.id)) any = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst m = multiplyMatrices(nodeMatrix(n), parent);\n\t\t\tconst inside = visit(n.children, m);\n\t\t\tconst hit = want.has(n.id) || inside;\n\t\t\tif (hit) any = true;\n\t\t\tif (!hit || !isVectorMaskGroup(n)) continue;\n\t\t\tconst summary = vectorMaskSummary(n)!;\n\t\t\tconst d = outlinePaths(n.children[0], m, new Set())\n\t\t\t\t.map((path) => bezierPathToSvg(transformBezierPath(path, view), { precision: 2 }))\n\t\t\t\t.join(' ');\n\t\t\tout.push({ id: n.id, maskId: summary.maskId, d, type: summary.type, invert: summary.invert, enabled: summary.enabled, editing: want.has(summary.maskId) });\n\t\t}\n\t\treturn any;\n\t};\n\tvisit(state.document.nodes, IDENTITY_MATRIX);\n\treturn out;\n}\n\nfunction envelopeOverlay(state: VectorEditorState): VectorOverlayRenderData['envelope'] {\n\tconst target = getVectorEnvelopeTarget(state.document, state.selection);\n\tif (!target) return null;\n\tconst v = state.viewport;\n\tconst m = multiplyMatrices(target.world, [v.zoom, 0, 0, v.zoom, v.x, v.y]);\n\tconst sc = (p: Vec2) => ({ x: fmt(m[0] * p.x + m[2] * p.y + m[4]), y: fmt(m[1] * p.x + m[3] * p.y + m[5]) });\n\tconst { mesh } = target.frame;\n\tconst grid = vectorMeshGridCurves(mesh)\n\t\t.map((c) => {\n\t\t\tconst [a, b, cc, d] = c.map(sc);\n\t\t\treturn `M${a.x} ${a.y}C${b.x} ${b.y} ${cc.x} ${cc.y} ${d.x} ${d.y}`;\n\t\t})\n\t\t.join('');\n\tconst hover = state.hover?.envelope?.nodeId === target.node.id ? state.hover.envelope.index : undefined;\n\tconst it = state.interaction;\n\tconst active = it?.type === 'envelope' && it.nodeId === target.node.id ? it.index : undefined;\n\tconst pts = vectorMeshEditPoints(mesh);\n\treturn {\n\t\tnodeId: target.node.id,\n\t\tgrid,\n\t\tanchors: pts.anchors.map((index) => ({ index, ...sc(mesh.points[index]), hovered: !it && hover === index, active: active === index })),\n\t\thandles: pts.handles.map((h) => {\n\t\t\tconst a = sc(mesh.points[h.anchor]);\n\t\t\treturn { index: h.index, ...sc(mesh.points[h.index]), anchorX: a.x, anchorY: a.y, hovered: !it && hover === h.index, active: active === h.index };\n\t\t})\n\t};\n}\n\n/** Warp choices of an envelope: the fifteen presets, then Mesh. */\nexport const VECTOR_ENVELOPE_STYLE_OPTIONS: readonly { value: VectorEnvelopeParams['style']; label: string }[] = [\n\t...VECTOR_WARP_STYLES.map((value) => ({ value, label: VECTOR_WARP_STYLE_LABELS[value] })),\n\t{ value: 'mesh', label: 'Mesh' }\n];\n\n/** What a Distort / Envelope panel shows for the selection. */\nexport interface VectorEnvelopePanelData {\n\t/** The one selected node, or null when several are selected. */\n\tnodeId: string | null;\n\tcount: number;\n\t/** The selected node's envelope (the first one in its stack), if any. */\n\tenvelope: { effectId: string; enabled: boolean; params: VectorEnvelopeParams } | null;\n\t/** Some selected node can take an envelope (Make with Warp / Mesh). */\n\tcanMake: boolean;\n\t/** Make with Top Object: several objects selected and the topmost is a closed path. */\n\tcanMakeWithTopObject: boolean;\n\t/** Some selected node has an envelope (Release, Expand). */\n\tcanRelease: boolean;\n}\n\n/** The Distort / Envelope panel of the selection, or null when nothing is selected. */\nexport function getVectorEnvelopePanel(state: Pick<VectorEditorState, 'document' | 'selection'>): VectorEnvelopePanelData | null {\n\tconst nodes = state.selection.map((id) => findVectorNode(state.document.nodes, id)).filter((n): n is VectorNode => !!n && !n.blendStep);\n\tif (!nodes.length) return null;\n\tconst one = nodes.length === 1 ? nodes[0] : null;\n\tconst first = one?.effects?.find((e) => e.type === 'envelope');\n\tconst order = new Map<string, number>();\n\twalkVectorNodes(state.document.nodes, (n) => order.set(n.id, order.size));\n\tconst top = nodes.length > 1 ? nodes.slice().sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0))[nodes.length - 1] : null;\n\treturn {\n\t\tnodeId: one?.id ?? null,\n\t\tcount: nodes.length,\n\t\tenvelope: first?.type === 'envelope' ? { effectId: first.id, enabled: first.enabled !== false, params: first.params } : null,\n\t\tcanMake: nodes.some((n) => !n.locked),\n\t\tcanMakeWithTopObject: top?.type === 'path' && top.subpaths.some((sp) => sp.closed && sp.anchors.length > 1),\n\t\tcanRelease: nodes.some((n) => !n.locked && !!n.effects?.some((e) => e.type === 'envelope'))\n\t};\n}\n\nfunction gradientOverlay(state: VectorEditorState): VectorRenderGradientAnnotator | null {\n\tconst a = getVectorGradientAnnotator(state.document, state.selection, state.viewport, state.gradientEdit.target, state.gradientEdit.layer ?? null);\n\tif (!a) return null;\n\tconst hover = state.interaction ? null : state.hover?.gradient;\n\tconst handleHovered = (h: VectorGradientHandle) => hover?.kind === 'handle' && hover.handle === h;\n\tconst pt = (p: Vec2) => ({ x: fmt(p.x), y: fmt(p.y) });\n\tconst centred = isCentredVectorGradient(a.gradient.type);\n\tconst removing = state.interaction?.type === 'gradient' && !!state.interaction.removing;\n\treturn {\n\t\tnodeId: a.nodeId,\n\t\ttype: a.gradient.type,\n\t\tstart: { ...pt(a.start), hovered: handleHovered('start') },\n\t\tend: { ...pt(a.end), hovered: handleHovered(centred ? 'radius' : 'end'), handle: centred ? 'radius' : 'end' },\n\t\taspect: a.aspect ? { ...pt(a.aspect), hovered: handleHovered('aspect') } : null,\n\t\tfocal: a.focal ? { ...pt(a.focal), hovered: handleHovered('focal') } : null,\n\t\tstops: a.stops.map((s) => ({\n\t\t\tindex: s.index,\n\t\t\t...pt(s),\n\t\t\tbarX: fmt(s.x - a.normal.x * VECTOR_GRADIENT_STOP_GAP),\n\t\t\tbarY: fmt(s.y - a.normal.y * VECTOR_GRADIENT_STOP_GAP),\n\t\t\tcolor: s.color,\n\t\t\topacity: s.opacity ?? 1,\n\t\t\tselected: state.gradientEdit.stop === s.index,\n\t\t\thovered: hover?.kind === 'stop' && hover.index === s.index\n\t\t})),\n\t\tmidpoints: a.midpoints.map((m) => ({ index: m.index, ...pt(m), hovered: hover?.kind === 'midpoint' && hover.index === m.index })),\n\t\textent: a.extent,\n\t\tremoving\n\t};\n}\n\n/** Blend groups the selection covers (a selected key counts as its blend). */\nfunction selectedBlends(state: Pick<VectorEditorState, 'document' | 'selection'>): VectorGroupNode[] {\n\tconst out = new Map<string, VectorGroupNode>();\n\tfor (const id of state.selection) {\n\t\tconst node = findVectorNode(state.document.nodes, id);\n\t\tconst group = isVectorBlendGroup(node) ? node : locateVectorNode(state.document.nodes, id)?.parent;\n\t\tif (isVectorBlendGroup(group)) out.set(group.id, group);\n\t}\n\treturn [...out.values()];\n}\n\nfunction blendOverlay(state: VectorEditorState): VectorOverlayRenderData['blend'] {\n\tconst v = state.viewport;\n\tconst view: AffineMatrix = [v.zoom, 0, 0, v.zoom, v.x, v.y];\n\tconst doc = state.document;\n\tconst sc = (p: Vec2, m: AffineMatrix) => {\n\t\tconst q = { x: m[0] * p.x + m[2] * p.y + m[4], y: m[1] * p.x + m[3] * p.y + m[5] };\n\t\treturn { x: fmt(q.x), y: fmt(q.y) };\n\t};\n\tconst spines = selectedBlends(state).flatMap((g) => {\n\t\tconst s = vectorBlendSpine(g);\n\t\tif (!s) return [];\n\t\tconst m = multiplyMatrices(vectorWorldMatrix(doc.nodes, g.id) ?? IDENTITY_MATRIX, view);\n\t\tconst d = bezierPathToSvg(transformBezierPath(subpathsToBezier([s.spine], { corners: false }), m), { precision: 2 });\n\t\tconst keys = s.stops.map((stop) => {\n\t\t\tconst p = vectorBlendRailAt(s.rail, stop).point;\n\t\t\treturn sc({ x: p[0], y: p[1] }, m);\n\t\t});\n\t\treturn [{ id: g.id, d, keys, replaced: s.replaced }];\n\t});\n\tlet start: VectorOverlayRenderData['blend']['start'] = null;\n\tconst anchors: VectorOverlayRenderData['blend']['anchors'] = [];\n\tif (state.tool === 'blend') {\n\t\tconst from = state.blendStart ? findVectorNode(doc.nodes, state.blendStart.nodeId) : undefined;\n\t\tif (from && state.blendStart) {\n\t\t\tconst a = state.blendStart.anchor === null ? undefined : getAnchorWorld(doc, { nodeId: from.id, subpath: 0, index: state.blendStart.anchor });\n\t\t\tstart = { d: screenOutline(state, from, new Set()), anchor: a ? sc(a, view) : null };\n\t\t}\n\t\tconst hovered = state.hover?.nodeId ? findVectorNode(doc.nodes, state.hover.nodeId) : undefined;\n\t\tif (hovered?.type === 'path' && hovered.subpaths[0]) {\n\t\t\thovered.subpaths[0].anchors.forEach((_, index) => {\n\t\t\t\tconst a = getAnchorWorld(doc, { nodeId: hovered.id, subpath: 0, index });\n\t\t\t\tif (a) anchors.push({ ...sc(a, view), hovered: state.hover?.anchor?.index === index });\n\t\t\t});\n\t\t}\n\t}\n\treturn { spines, start, anchors };\n}\n\n/** What the Blend controls of a toolbar or style panel show. */\nexport interface VectorBlendControls {\n\t/** Ids of the selected blends (a selected key counts as its blend). */\n\tids: string[];\n\t/** The first selected blend's options, or the options new blends get. */\n\toptions: Required<VectorBlendOptions>;\n\t/** Two or more shapes that share a parent are selected: Blend can run. */\n\tcanBlend: boolean;\n\t/** One blend and one path are selected: Replace Spine can run. */\n\tcanReplaceSpine: boolean;\n\t/** The first selected blend follows a replaced spine. */\n\thasSpine: boolean;\n}\n\n/** State of the Blend controls for the current selection. */\nexport function getVectorBlendControls(state: Pick<VectorEditorState, 'document' | 'selection' | 'blendDefaults'>): VectorBlendControls {\n\tconst blends = selectedBlends(state);\n\tconst doc = state.document;\n\tconst roots = state.selection.filter((id) => {\n\t\tconst node = findVectorNode(doc.nodes, id);\n\t\treturn !!node && !node.blendStep && !node.locked && !state.selection.includes(locateVectorNode(doc.nodes, id)?.parent?.id ?? '');\n\t});\n\tconst parents = new Set(roots.map((id) => locateVectorNode(doc.nodes, id)?.parent?.id ?? null));\n\tconst paths = roots.filter((id) => findVectorNode(doc.nodes, id)?.type === 'path' && !isVectorBlendGroup(locateVectorNode(doc.nodes, id)?.parent));\n\tconst first = blends[0];\n\treturn {\n\t\tids: blends.map((g) => g.id),\n\t\toptions: resolveVectorBlendOptions(first ? first.blend : state.blendDefaults),\n\t\tcanBlend: roots.length >= 2 && parents.size === 1,\n\t\tcanReplaceSpine: blends.length === 1 && paths.length === 1,\n\t\thasSpine: !!first?.blend?.spine\n\t};\n}\n\n/**\n * Applies blend options from a toolbar: to the selected blends (one undo step) and to the\n * options new blends get, so the controls keep showing what was picked.\n */\nexport function applyVectorBlendOptions(controller: VectorEditorController, patch: Partial<VectorBlendOptions>): void {\n\tcontroller.setBlendDefaults(patch);\n\tif (getVectorBlendControls(controller.getState()).ids.length) controller.setBlendOptions(patch);\n}\n\n/** Labels of the blend spacing modes, for toolbars. */\nexport const VECTOR_BLEND_SPACING_LABELS: Readonly<Record<VectorBlendSpacing, string>> = { steps: 'Specified Steps', distance: 'Specified Distance', smoothColor: 'Smooth Color' };\n\n// ---------------------------------------------------------------------------\n// Event adapters (duck-typed, so they work with DOM, React and Svelte events)\n// ---------------------------------------------------------------------------\n\n/** Colours and sizes (screen pixels) the bundled canvases draw the overlay with. */\nexport const VECTOR_OVERLAY_THEME = {\n\taccent: '#0ea5e9',\n\t/** Smart guide lines and labels. */\n\tguide: '#ff3dfc',\n\thandleFill: '#ffffff',\n\tmarqueeFill: 'rgba(14, 165, 233, 0.08)',\n\t/** The eraser's swath while it is dragged. */\n\teraserFill: 'rgba(239, 68, 68, 0.25)',\n\ttransformHandleSize: 8,\n\tanchorSize: 7,\n\thandleRadius: 3.5,\n\t/** Radius of a live corner widget (a ring with a dot). */\n\tcornerWidgetRadius: 4,\n\t/** Half the diagonal of a width point's diamond. */\n\twidthPointSize: 5,\n\t/** Shape Builder faces to merge and paint bucket regions. */\n\tbuilderFill: 'rgba(14, 165, 233, 0.22)',\n\t/** Shape Builder faces and line pieces an Alt gesture deletes. */\n\tbuilderErase: '#ef4444',\n\tbuilderEraseFill: 'rgba(239, 68, 68, 0.22)',\n\t/** Opacity of the hatch lines over Shape Builder faces (drawn in the face's outline colour). */\n\tbuilderHatchOpacity: 0.6,\n\t/** Mask indicators: the outline of a selected mask group's mask (dashed; solid while the mask is selected). */\n\tmask: '#8b5cf6',\n\tmaskDash: '5 3'\n} as const;\n\n/** SVG path data of the diamond a width point is drawn with (screen pixels). */\nexport function vectorDiamondPath(x: number, y: number, r: number = VECTOR_OVERLAY_THEME.widthPointSize): string {\n\treturn `M${fmt(x)} ${fmt(y - r)}L${fmt(x + r)} ${fmt(y)}L${fmt(x)} ${fmt(y + r)}L${fmt(x - r)} ${fmt(y)}Z`;\n}\n\ninterface ModifierEventLike {\n\tshiftKey?: boolean;\n\taltKey?: boolean;\n\tmetaKey?: boolean;\n\tctrlKey?: boolean;\n}\n\n/** Modifiers of a mouse / pointer / key event (`mod` is Cmd or Ctrl). */\nexport function toVectorModifiers(event: ModifierEventLike): { shift: boolean; alt: boolean; mod: boolean } {\n\treturn { shift: !!event.shiftKey, alt: !!event.altKey, mod: !!(event.metaKey || event.ctrlKey) };\n}\n\n/**\n * A pointer event as `VectorEditorController.pointerDown/Move/Up` input: modifiers, button,\n * and the pressure and pointer type the pencil records as a width profile.\n */\nexport function toVectorPointerInput(event: ModifierEventLike & { button?: number; pressure?: number; pointerType?: string }) {\n\treturn {\n\t\t...toVectorModifiers(event),\n\t\tbutton: event.button,\n\t\t...(typeof event.pressure === 'number' ? { pressure: event.pressure } : {}),\n\t\t...(event.pointerType ? { pointerType: event.pointerType } : {})\n\t};\n}\n\n/** A key event as `VectorEditorController.keyDown/keyUp` input. */\nexport function toVectorKeyInput(event: ModifierEventLike & { key: string }) {\n\treturn { key: event.key, ...toVectorModifiers(event) };\n}\n\n/** True for targets that should keep their own keys (inputs, textareas, contenteditable). */\nexport function isVectorTextTarget(target: unknown): boolean {\n\tconst el = target as { tagName?: string; isContentEditable?: boolean; getAttribute?: (name: string) => string | null } | null;\n\tif (!el || typeof el !== 'object') return false;\n\tconst tag = (el.tagName ?? '').toUpperCase();\n\tif (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true;\n\treturn !!el.isContentEditable || el.getAttribute?.('contenteditable') === 'true';\n}\n\n// ---------------------------------------------------------------------------\n// Style panel helpers\n// ---------------------------------------------------------------------------\n\n/** Style shown in a style panel: the first selected path's, with `mixed` flags when paths differ. */\nexport interface VectorSelectionStyle {\n\tfill: VectorPaint;\n\tstroke: VectorPaint;\n\tstrokeWidth: number;\n\topacity: number;\n\tstrokeAlign: VectorStrokeAlign;\n\tstartArrow: VectorArrowhead | null;\n\tendArrow: VectorArrowhead | null;\n\tarrowAlign: VectorArrowAlign;\n\tstartArrowScale: number;\n\tendArrowScale: number;\n\t/** The first path's width profile (null when uniform) and the preset it matches. */\n\tstrokeWidthProfile: VectorWidthPoint[] | null;\n\twidthProfile: VectorWidthPreset | 'custom';\n\t/**\n\t * Live corners of the selected anchors (direct tool) or of every corner of the selected\n\t * paths: the first one's radius and kind, and how many corners there are (0: none to round).\n\t */\n\tcornerRadius: number;\n\tcornerKind: VectorCornerKind;\n\tcorners: number;\n\t/** Some selected path is open, so arrowheads apply. */\n\thasOpenPath: boolean;\n\tmixed: {\n\t\tfill: boolean;\n\t\tstroke: boolean;\n\t\tstrokeWidth: boolean;\n\t\topacity: boolean;\n\t\tstrokeAlign: boolean;\n\t\tstartArrow: boolean;\n\t\tendArrow: boolean;\n\t\tarrowAlign: boolean;\n\t\tstartArrowScale: boolean;\n\t\tendArrowScale: boolean;\n\t\twidthProfile: boolean;\n\t\tcornerRadius: boolean;\n\t\tcornerKind: boolean;\n\t};\n\t/** Ids of the paths the style applies to. */\n\tpathIds: string[];\n}\n\nexport function getVectorSelectionStyle(\n\tstate: Pick<VectorEditorState, 'document' | 'selection'> & Partial<Pick<VectorEditorState, 'selectedAnchors' | 'tool'>>\n): VectorSelectionStyle | null {\n\tconst paths: VectorPathNode[] = [];\n\tfor (const id of state.selection) {\n\t\tconst node = findVectorNode(state.document.nodes, id);\n\t\tif (node) paths.push(...collect(node));\n\t}\n\tconst anchorMode = state.tool === 'direct' && !!state.selectedAnchors?.length;\n\tif (anchorMode) {\n\t\tfor (const r of state.selectedAnchors!) {\n\t\t\tif (paths.some((p) => p.id === r.nodeId)) continue;\n\t\t\tconst node = findVectorNode(state.document.nodes, r.nodeId);\n\t\t\tif (node?.type === 'path') paths.push(node);\n\t\t}\n\t}\n\tif (!paths.length) return null;\n\tconst first = paths[0];\n\tconst same = (get: (p: VectorPathNode) => unknown) => paths.every((p) => JSON.stringify(get(p) ?? null) === JSON.stringify(get(first) ?? null));\n\t// Corners: those of the selected anchors in the direct tool, else every corner of the paths.\n\tconst corners: { radius: number; kind: VectorCornerKind }[] = [];\n\tfor (const p of paths) {\n\t\tfor (const c of vectorCorners(p.subpaths)) {\n\t\t\tif (anchorMode && !state.selectedAnchors!.some((r) => r.nodeId === p.id && r.subpath === c.subpath && r.index === c.index)) continue;\n\t\t\tconst corner = p.subpaths[c.subpath].anchors[c.index].corner;\n\t\t\tcorners.push({ radius: corner?.radius ?? 0, kind: corner?.kind ?? 'round' });\n\t\t}\n\t}\n\tconst c0 = corners[0] ?? { radius: 0, kind: 'round' as const };\n\treturn {\n\t\tfill: first.fill ?? null,\n\t\tstroke: first.stroke ?? null,\n\t\tstrokeWidth: first.strokeWidth ?? 1,\n\t\topacity: first.opacity ?? 1,\n\t\tstrokeAlign: first.strokeAlign ?? 'center',\n\t\tstartArrow: first.startArrow ?? null,\n\t\tendArrow: first.endArrow ?? null,\n\t\tarrowAlign: first.arrowAlign ?? 'extend',\n\t\tstartArrowScale: first.startArrowScale ?? 1,\n\t\tendArrowScale: first.endArrowScale ?? 1,\n\t\tstrokeWidthProfile: profileOf(first),\n\t\twidthProfile: widthProfilePresetId(profileOf(first)),\n\t\tcornerRadius: c0.radius,\n\t\tcornerKind: c0.kind,\n\t\tcorners: corners.length,\n\t\thasOpenPath: paths.some((p) => p.subpaths.some((sp) => !sp.closed && sp.anchors.length > 1)),\n\t\tmixed: {\n\t\t\tfill: !same((p) => p.fill),\n\t\t\tstroke: !same((p) => p.stroke),\n\t\t\tstrokeWidth: !same((p) => p.strokeWidth ?? 1),\n\t\t\topacity: !same((p) => p.opacity ?? 1),\n\t\t\tstrokeAlign: !same((p) => p.strokeAlign ?? 'center'),\n\t\t\tstartArrow: !same((p) => p.startArrow ?? null),\n\t\t\tendArrow: !same((p) => p.endArrow ?? null),\n\t\t\tarrowAlign: !same((p) => p.arrowAlign ?? 'extend'),\n\t\t\tstartArrowScale: !same((p) => p.startArrowScale ?? 1),\n\t\t\tendArrowScale: !same((p) => p.endArrowScale ?? 1),\n\t\t\twidthProfile: !same(profileOf),\n\t\t\tcornerRadius: corners.some((c) => c.radius !== c0.radius),\n\t\t\tcornerKind: corners.some((c) => c.kind !== c0.kind)\n\t\t},\n\t\tpathIds: paths.map((p) => p.id)\n\t};\n}\n\nfunction profileOf(p: VectorPathNode): VectorWidthPoint[] | null {\n\tconst points = sanitizeWidthProfile(p.strokeWidthProfile);\n\treturn points.length && widthProfilePresetId(points) !== 'uniform' ? points : null;\n}\n\n/** Labels of the stroke alignments, corner kinds and arrowheads, for style panels. */\nexport const VECTOR_STROKE_ALIGN_LABELS: Readonly<Record<VectorStrokeAlign, string>> = { center: 'Center', inside: 'Inside', outside: 'Outside' };\nexport const VECTOR_CORNER_KIND_LABELS: Readonly<Record<VectorCornerKind, string>> = { round: 'Round', inverted: 'Inverted', chamfer: 'Chamfer' };\nexport const VECTOR_ARROWHEAD_LABELS: Readonly<Record<VectorArrowhead, string>> = {\n\ttriangle: 'Triangle',\n\tarrow: 'Arrow',\n\topen: 'Open arrow',\n\tcircle: 'Circle',\n\tsquare: 'Square',\n\tbar: 'Bar'\n};\n\n/** A paint as `#rrggbb` for `<input type=\"color\">`, or null for none, gradients and other CSS colours. */\nexport function vectorPaintToHex(paint: VectorPaint | undefined): string | null {\n\tif (typeof paint !== 'string') return null;\n\tconst m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(paint.trim());\n\tif (!m) return null;\n\tconst hex = m[1].length === 3 ? m[1].replace(/./g, (c) => c + c) : m[1];\n\treturn `#${hex.toLowerCase()}`;\n}\n\n// ---------------------------------------------------------------------------\n// Gradient editor\n// ---------------------------------------------------------------------------\n\nexport const VECTOR_GRADIENT_TYPE_LABELS: Readonly<Record<VectorGradientType, string>> = { linear: 'Linear', radial: 'Radial', conic: 'Conic', reflected: 'Reflected', diamond: 'Diamond' };\nexport const VECTOR_GRADIENT_SPREAD_LABELS: Readonly<Record<VectorGradientSpread, string>> = { pad: 'Pad', reflect: 'Reflect', repeat: 'Repeat' };\nexport const VECTOR_GRADIENT_INTERPOLATION_LABELS: Readonly<Record<VectorGradientInterpolation, string>> = { srgb: 'sRGB', linear: 'Linear light', oklab: 'Oklab' };\nexport const VECTOR_STROKE_GRADIENT_MODE_LABELS: Readonly<Record<VectorStrokeGradientMode, string>> = { within: 'Within stroke', along: 'Along stroke', across: 'Across stroke' };\n\n/** What a gradient editor shows for the selection's `target` paint (see `getVectorGradientEditor`). */\nexport interface VectorGradientEditorData {\n\ttarget: 'fill' | 'stroke';\n\t/** The layer of the `target` stack edited (null: the top one). */\n\tlayer: number | null;\n\t/** The first selected path's paint is a gradient. */\n\tgradient: VectorGradient | null;\n\t/** The paint as a `#rrggbb` colour when it is one (what a new gradient starts from). */\n\tcolor: string | null;\n\t/** Sorted stops; `index` is what `setGradientStop` takes. */\n\tstops: (VectorGradientStop & { index: number; selected: boolean })[];\n\tselected: (VectorGradientStop & { index: number; last: boolean }) | null;\n\t/** A CSS `linear-gradient(to right, …)` of the ramp (midpoints and colour space expanded). */\n\tpreview: string | null;\n\ttype: VectorGradientType | null;\n\tspread: VectorGradientSpread;\n\tinterpolation: VectorGradientInterpolation;\n\tsmoothness: number;\n\tstrokeMode: VectorStrokeGradientMode;\n\t/** The selected paths' gradients differ. */\n\tmixed: boolean;\n}\n\n/** The gradient editor of the selection's fill or stroke (`state.gradientEdit.target`), or null with nothing selected. */\nexport function getVectorGradientEditor(state: Pick<VectorEditorState, 'document' | 'selection' | 'gradientEdit'>): VectorGradientEditorData | null {\n\tconst paths: VectorPathNode[] = [];\n\tfor (const id of state.selection) {\n\t\tconst node = findVectorNode(state.document.nodes, id);\n\t\tif (node) paths.push(...collect(node));\n\t}\n\tif (!paths.length) return null;\n\tconst { target, layer } = state.gradientEdit;\n\tconst paint = vectorTargetPaint(paths[0], target, layer ?? null);\n\tconst gradient = isVectorGradient(paint) ? paint : null;\n\tconst sorted = gradient ? sortedVectorGradientStops(gradient) : [];\n\tconst stopIndex = state.gradientEdit.stop;\n\tconst stops = sorted.map((s, index) => ({ ...s, index, selected: index === stopIndex }));\n\tconst sel = stopIndex !== null && sorted[stopIndex] ? { ...sorted[stopIndex], index: stopIndex, last: stopIndex === sorted.length - 1 } : null;\n\tconst preview = gradient\n\t\t? `linear-gradient(to right, ${expandVectorGradientStops(gradient)\n\t\t\t\t.map((s) => {\n\t\t\t\t\tconst c = parseVectorColor(s.color);\n\t\t\t\t\tconst a = (c?.[3] ?? 1) * (s.opacity ?? 1);\n\t\t\t\t\tconst color = c ? `rgba(${Math.round(c[0] * 255)}, ${Math.round(c[1] * 255)}, ${Math.round(c[2] * 255)}, ${Math.round(a * 1000) / 1000})` : s.color;\n\t\t\t\t\treturn `${color} ${Math.round(s.offset * 10000) / 100}%`;\n\t\t\t\t})\n\t\t\t\t.join(', ')})`\n\t\t: null;\n\treturn {\n\t\ttarget,\n\t\tlayer: layer ?? null,\n\t\tgradient,\n\t\tcolor: vectorPaintToHex(paint),\n\t\tstops,\n\t\tselected: sel,\n\t\tpreview,\n\t\ttype: gradient?.type ?? null,\n\t\tspread: gradient?.spread ?? 'pad',\n\t\tinterpolation: gradient?.interpolation ?? 'srgb',\n\t\tsmoothness: gradient?.smoothness ?? 0,\n\t\tstrokeMode: gradient?.strokeMode ?? 'within',\n\t\tmixed: paths.some((p) => JSON.stringify(vectorTargetPaint(p, target, layer ?? null) ?? null) !== JSON.stringify(paint ?? null))\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Toolbar helpers\n// ---------------------------------------------------------------------------\n\n/** A path-editing button of the bundled toolbars. */\nexport interface VectorPathAction {\n\tid: 'join' | 'average' | 'cut' | 'clean' | 'smooth' | 'outline';\n\tlabel: string;\n\ttitle: string;\n\t/** Shortcut after Cmd / Ctrl, if any. */\n\tshortcut?: string;\n\t/** Whether it can run with `selected` nodes and `points` selected anchors (direct tool). */\n\tenabled: (selected: number, points: number) => boolean;\n\trun: (controller: VectorEditorController) => unknown;\n}\n\n/** Join, Average, Cut at Points, Remove Redundant Points, Smooth and Outline Stroke, as the toolbars show them. */\nexport const VECTOR_PATH_ACTIONS: readonly VectorPathAction[] = [\n\t{ id: 'join', label: '⋈', title: 'Join', shortcut: 'J', enabled: (s, p) => s + p > 0, run: (c) => c.joinPaths() },\n\t{ id: 'average', label: '⊹', title: 'Average Points', shortcut: 'Alt+J', enabled: (_s, p) => p > 1, run: (c) => c.averageAnchors('both') },\n\t{ id: 'cut', label: '✂·', title: 'Cut at Points', enabled: (_s, p) => p > 0, run: (c) => c.cutAtAnchors() },\n\t{ id: 'clean', label: '⌁', title: 'Remove Redundant Points', enabled: (s, p) => s + p > 0, run: (c) => c.removeRedundantPoints() },\n\t{ id: 'smooth', label: '∿', title: 'Smooth', enabled: (s, p) => s + p > 0, run: (c) => c.smoothPaths() },\n\t{ id: 'outline', label: '⊡', title: 'Outline Stroke', enabled: (s) => s > 0, run: (c) => c.outlineStroke() }\n];\n\n/** A Pathfinder button of the bundled toolbars. */\nexport interface VectorPathfinderAction {\n\tid: VectorPathfinderOperation;\n\tlabel: string;\n\ttitle: string;\n\t/** Paths (or groups) it needs selected. */\n\tmin: number;\n}\n\n/** Divide, Trim, Merge, Crop, Outline and Minus Back, as the toolbars show them. */\nexport const VECTOR_PATHFINDER_ACTIONS: readonly VectorPathfinderAction[] = [\n\t{ id: 'divide', label: '⊞', title: 'Divide', min: 1 },\n\t{ id: 'trim', label: '⧉', title: 'Trim', min: 2 },\n\t{ id: 'merge', label: '⧈', title: 'Merge', min: 2 },\n\t{ id: 'crop', label: '⌗', title: 'Crop', min: 2 },\n\t{ id: 'outline', label: '⬚', title: 'Outline', min: 1 },\n\t{ id: 'minusBack', label: '⊖', title: 'Minus Back', min: 2 }\n];\n\n/** The on / off path-editing settings the toolbars toggle. */\nexport const VECTOR_EDIT_TOGGLES: readonly { key: 'keepShapeOnDelete' | 'curvatureMoves' | 'recognizeShapes'; label: string; title: string }[] = [\n\t{ key: 'keepShapeOnDelete', label: 'Keep', title: 'Keep shape when deleting points' },\n\t{ key: 'curvatureMoves', label: 'Curv', title: 'Curvature point moves (Alt flips while dragging)' },\n\t{ key: 'recognizeShapes', label: 'Shapes', title: 'Turn pencil scribbles into shapes' }\n];\n\n\n// ---------------------------------------------------------------------------\n// Effects panel\n// ---------------------------------------------------------------------------\n\n/** One effect of the listed stack, with what a panel needs to edit it. */\nexport interface VectorEffectsPanelItem {\n\tid: string;\n\ttype: VectorEffectType;\n\tlabel: string;\n\tenabled: boolean;\n\tparams: Record<string, unknown>;\n\t/** The parameters to edit (none for an envelope: `getVectorEnvelopePanel` and the Envelope panels edit it). */\n\tspecs: readonly VectorEffectParamSpec[];\n}\n\n/** What an Effects panel shows for the selection. */\nexport interface VectorEffectsPanelData {\n\t/** The node whose stack is listed (when one node is selected), else null. */\n\tnodeId: string | null;\n\t/** Selected nodes an added effect goes to. */\n\tcount: number;\n\t/** The listed stack, applied top to bottom. */\n\teffects: VectorEffectsPanelItem[];\n\t/** Something selected is live (Expand Appearance applies). */\n\texpandable: boolean;\n}\n\n/** Effect types in menu order, with their names. */\nexport const VECTOR_EFFECT_MENU: readonly { type: VectorEffectType; label: string }[] = VECTOR_EFFECT_TYPES.map((type) => ({ type, label: VECTOR_EFFECT_LABELS[type] }));\n\n/** The Effects panel of the selection, or null when nothing that takes effects is selected. */\nexport function getVectorEffectsPanel(state: Pick<VectorEditorState, 'document' | 'selection'>): VectorEffectsPanelData | null {\n\tconst nodes = state.selection.map((id) => findVectorNode(state.document.nodes, id)).filter((n): n is VectorNode => !!n && !n.blendStep);\n\tif (!nodes.length) return null;\n\tconst one = nodes.length === 1 ? nodes[0] : null;\n\treturn {\n\t\tnodeId: one?.id ?? null,\n\t\tcount: nodes.length,\n\t\teffects: (one?.effects ?? []).map((e) => ({\n\t\t\tid: e.id,\n\t\t\ttype: e.type,\n\t\t\tlabel: VECTOR_EFFECT_LABELS[e.type] ?? e.type,\n\t\t\tenabled: e.enabled !== false,\n\t\t\tparams: e.params as unknown as Record<string, unknown>,\n\t\t\t// The Envelope panel (`getVectorEnvelopePanel`) edits an envelope's warp and mesh.\n\t\t\tspecs: e.type === 'envelope' ? [] : (VECTOR_EFFECT_PARAM_SPECS[e.type] ?? [])\n\t\t})),\n\t\texpandable: nodes.some(hasVectorAppearance)\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Appearance panel: fill and stroke stacks, blend mode, filters\n// ---------------------------------------------------------------------------\n\n/** One fill or stroke layer as an appearance panel lists it. */\nexport interface VectorAppearancePanelLayer {\n\tkind: 'fill' | 'stroke';\n\t/** Index in its stack (0 is the bottom); what `updatePaintLayer` and friends take. */\n\tindex: number;\n\tpaint: VectorPaint;\n\t/** The paint as `#rrggbb` when it is one (for a colour input). */\n\tcolor: string | null;\n\t/** A CSS `background` showing the paint (a colour, or the gradient's ramp). */\n\tswatch: string;\n\tgradient: boolean;\n\topacity: number;\n\tblendMode: VectorBlendMode;\n\tvisible: boolean;\n\t/** Stroke layers: width and alignment (for the compact row). */\n\tstrokeWidth?: number;\n\tstrokeAlign?: VectorStrokeAlign;\n\t/** A stroke of a kind this version cannot draw (kept, shown read-only). */\n\tunknownKind?: string;\n\t/** The gradient editor and tool work on this layer. */\n\tediting: boolean;\n}\n\n/** What an appearance panel shows for the selection. */\nexport interface VectorAppearancePanelData {\n\t/** Selected nodes (blend mode and filters apply to them). */\n\tnodeIds: string[];\n\t/** Paths the stacks belong to (groups give every path inside); the first one's stacks are listed. */\n\tpathIds: string[];\n\tfills: VectorAppearancePanelLayer[];\n\tstrokes: VectorAppearancePanelLayer[];\n\t/** The first selected node's blend mode. */\n\tblendMode: VectorBlendMode;\n\tmixed: { fills: boolean; strokes: boolean; blendMode: boolean };\n\t/** The filter stack, listed when one node is selected (`filterNodeId`). */\n\tfilterNodeId: string | null;\n\tfilters: VectorFiltersPanelItem[];\n}\n\n/** One filter of the listed stack, with what a panel needs to edit it. */\nexport interface VectorFiltersPanelItem {\n\tid: string;\n\ttype: VectorFilterType;\n\tlabel: string;\n\tenabled: boolean;\n\tparams: Record<string, unknown>;\n\tspecs: readonly VectorFilterParamSpec[];\n}\n\n/** A CSS background for a paint: the colour, or a left-to-right ramp of a gradient's stops. */\nexport function vectorPaintSwatch(paint: VectorPaint | undefined): string {\n\tif (typeof paint === 'string') return paint;\n\tif (isVectorGradient(paint)) {\n\t\treturn `linear-gradient(to right, ${expandVectorGradientStops(paint)\n\t\t\t.map((s) => {\n\t\t\t\tconst c = parseVectorColor(s.color);\n\t\t\t\tconst a = (c?.[3] ?? 1) * (s.opacity ?? 1);\n\t\t\t\tconst color = c ? `rgba(${Math.round(c[0] * 255)}, ${Math.round(c[1] * 255)}, ${Math.round(c[2] * 255)}, ${Math.round(a * 1000) / 1000})` : s.color;\n\t\t\t\treturn `${color} ${Math.round(s.offset * 10000) / 100}%`;\n\t\t\t})\n\t\t\t.join(', ')})`;\n\t}\n\treturn 'transparent';\n}\n\n/** The appearance panel of the selection, or null with nothing selected. */\nexport function getVectorAppearancePanel(state: Pick<VectorEditorState, 'document' | 'selection' | 'gradientEdit'>): VectorAppearancePanelData | null {\n\tconst nodes = state.selection.map((id) => findVectorNode(state.document.nodes, id)).filter((n): n is VectorNode => !!n && !n.blendStep);\n\tif (!nodes.length) return null;\n\tconst paths: VectorPathNode[] = [];\n\tfor (const n of nodes) for (const p of collect(n)) if (!p.blendStep && !paths.some((q) => q.id === p.id)) paths.push(p);\n\tconst first = paths[0];\n\tconst edit = state.gradientEdit;\n\tconst layer = (kind: 'fill' | 'stroke', l: VectorStrokeLayer, index: number, count: number): VectorAppearancePanelLayer => ({\n\t\tkind,\n\t\tindex,\n\t\tpaint: l.paint,\n\t\tcolor: vectorPaintToHex(l.paint),\n\t\tswatch: vectorPaintSwatch(l.paint),\n\t\tgradient: isVectorGradient(l.paint),\n\t\topacity: l.opacity ?? 1,\n\t\tblendMode: l.blendMode ?? 'normal',\n\t\tvisible: l.visible !== false,\n\t\t...(kind === 'stroke' ? { strokeWidth: l.strokeWidth ?? 1, strokeAlign: l.strokeAlign ?? 'center' } : {}),\n\t\t...((l.kind ?? 'paint') !== 'paint' ? { unknownKind: String(l.kind) } : {}),\n\t\tediting: edit.target === kind && (edit.layer === null ? index === count - 1 : edit.layer === index)\n\t});\n\tconst fills = first ? vectorFills(first) : [];\n\tconst strokes = first ? vectorStrokes(first) : [];\n\tconst same = (get: (p: VectorPathNode) => unknown) => paths.every((p) => JSON.stringify(get(p)) === JSON.stringify(get(first)));\n\tconst one = nodes.length === 1 ? nodes[0] : null;\n\treturn {\n\t\tnodeIds: nodes.map((n) => n.id),\n\t\tpathIds: paths.map((p) => p.id),\n\t\tfills: fills.map((l, i) => layer('fill', l, i, fills.length)),\n\t\tstrokes: strokes.map((l, i) => layer('stroke', l, i, strokes.length)),\n\t\tblendMode: nodes[0].blendMode ?? 'normal',\n\t\tmixed: {\n\t\t\tfills: !!first && !same(vectorFills),\n\t\t\tstrokes: !!first && !same(vectorStrokes),\n\t\t\tblendMode: nodes.some((n) => (n.blendMode ?? 'normal') !== (nodes[0].blendMode ?? 'normal'))\n\t\t},\n\t\tfilterNodeId: one?.id ?? null,\n\t\tfilters: (one?.filters ?? []).map((f) => ({\n\t\t\tid: f.id,\n\t\t\ttype: f.type,\n\t\t\tlabel: VECTOR_FILTER_LABELS[f.type] ?? f.type,\n\t\t\tenabled: f.enabled !== false,\n\t\t\tparams: f.params as unknown as Record<string, unknown>,\n\t\t\tspecs: VECTOR_FILTER_PARAM_SPECS[f.type] ?? []\n\t\t}))\n\t};\n}\n\n/** Blend modes in menu order, with their names. */\nexport const VECTOR_BLEND_MODE_MENU: readonly { mode: VectorBlendMode; label: string }[] = VECTOR_BLEND_MODES.map((mode) => ({ mode, label: VECTOR_BLEND_MODE_LABELS[mode] }));\n\n// ---------------------------------------------------------------------------\n// Masks panel\n// ---------------------------------------------------------------------------\n\nexport const VECTOR_MASK_TYPE_LABELS: Readonly<Record<VectorMaskType, string>> = { clip: 'Clip', alpha: 'Alpha', luminance: 'Luminance' };\n/** Mask types for a menu, with a hint each. */\nexport const VECTOR_MASK_TYPE_OPTIONS: readonly { value: VectorMaskType; label: string; title: string }[] = [\n\t{ value: 'clip', label: 'Clip', title: 'Clipping mask: the contents show inside the mask\\'s outline' },\n\t{ value: 'luminance', label: 'Luminance', title: 'Opacity mask by brightness: white shows, black hides' },\n\t{ value: 'alpha', label: 'Alpha', title: 'Opacity mask by opacity: opaque shows, transparent hides' }\n];\n\n/** What the mask panel shows for the selection. */\nexport interface VectorMaskPanelData {\n\t/** Make Clipping Mask / Make Opacity Mask would work: two or more unlocked objects sharing a parent. */\n\tcanMake: boolean;\n\t/** Why the make actions are off, for a tooltip (null when they are on). */\n\tmakeHint: string | null;\n\t/**\n\t * The mask group the selection is or is directly inside, with its settings and what of it is\n\t * selected (`group` itself, its `mask` or its `contents`); null otherwise.\n\t */\n\tgroup: (VectorMaskSummary & { id: string; name?: string; editing: 'group' | 'mask' | 'contents' }) | null;\n}\n\n/** The mask panel's data for the selection, or null with nothing selected. */\nexport function getVectorMaskPanel(state: Pick<VectorEditorState, 'document' | 'selection'>): VectorMaskPanelData | null {\n\tconst doc = state.document;\n\tif (!state.selection.length) return null;\n\tconst locs = state.selection.map((id) => locateVectorNode(doc.nodes, id));\n\tlet makeHint: string | null = null;\n\tif (state.selection.length < 2) makeHint = 'Select two or more objects: the topmost becomes the mask.';\n\telse if (locs.some((l) => !l)) makeHint = 'Select objects in the document.';\n\telse if (locs.some((l) => l!.list[l!.index].locked || l!.list[l!.index].blendStep)) makeHint = 'Unlock the selected objects first.';\n\telse if (locs.some((l) => (l!.parent?.id ?? null) !== (locs[0]!.parent?.id ?? null))) makeHint = 'Select objects at one level (sharing a parent).';\n\tlet group: VectorMaskPanelData['group'] = null;\n\tconst first = locs[0];\n\tconst node = first ? first.list[first.index] : null;\n\tconst owner = isVectorMaskGroup(node) ? node : isVectorMaskGroup(first?.parent) ? first!.parent : null;\n\tif (owner && state.selection.every((id) => id === owner.id || owner.children.some((c) => c.id === id))) {\n\t\tconst summary = vectorMaskSummary(owner)!;\n\t\tconst editing = state.selection.includes(owner.id) ? 'group' : state.selection.includes(summary.maskId) ? 'mask' : 'contents';\n\t\tgroup = { ...summary, id: owner.id, ...(owner.name ? { name: owner.name } : {}), editing };\n\t}\n\treturn { canMake: makeHint === null, makeHint, group };\n}\n","import type { CommandSpec } from '../commands/registry.js';\nimport type { VectorEditorController, VectorShapeInput } from './controller.js';\nimport { vectorNodeBounds, vectorNodeRotation, vectorParentMatrix, vectorPathData, IDENTITY_MATRIX } from './geometry.js';\nimport type { VectorAlign, VectorAnchorRef, VectorArrange, VectorAverageAxis, VectorBooleanOperation, VectorCornerKind, VectorNode, VectorPathfinderOperation, VectorStyle, VectorTool } from './types.js';\nimport { VECTOR_ARROWHEADS, VECTOR_CORNER_KINDS, VECTOR_EFFECT_TYPES, VECTOR_PATHFINDER_OPERATIONS, VECTOR_TOOLS, type VectorEffect, type VectorEffectType, type VectorWidthPoint } from './types.js';\nimport { VECTOR_WIDTH_PRESET_IDS, type VectorTaperOptions, type VectorWaveOptions, type VectorWidthPreset } from './width.js';\nimport { resolveVectorBlendOptions, vectorBlendKeys } from './blendGroup.js';\nimport { VECTOR_BLEND_SPACINGS, VECTOR_GRADIENT_INTERPOLATIONS, VECTOR_GRADIENT_SPREADS, VECTOR_GRADIENT_TYPES, VECTOR_STROKE_GRADIENT_MODES, type VectorBlendOptions } from './types.js';\nimport { sanitizeVectorGradient, type VectorGradientPatch } from './gradient.js';\nimport { isVectorBlendMode, vectorFills, vectorStrokes } from './paintLayers.js';\nimport { vectorFilterParamKeys } from './filters.js';\nimport { VECTOR_BLEND_MODES, VECTOR_FILTER_TYPES, type VectorBlendMode, type VectorFilter, type VectorFilterType, type VectorPaint, type VectorPaintLayer, type VectorStrokeLayer } from './types.js';\nimport { isValidVectorMesh, VECTOR_WARP_STYLES } from './warp.js';\nimport { isVectorMaskGroup, vectorMaskSummary, type VectorMaskSummary } from './mask.js';\nimport type { VectorMaskType } from './types.js';\nimport type { VectorEnvelopeParams } from './appearance.js';\n\nexport interface VectorCommandOptions {\n\t/** Id prefix (default `vector.`); use one per editor when several are registered. */\n\tprefix?: string;\n\t/** Palette group (default `Vector`). */\n\tgroup?: string;\n\t/** Command scope. Omit for global commands. */\n\tscope?: string;\n}\n\n/** A node as listed for agents. */\nexport interface VectorNodeListing {\n\tid: string;\n\ttype: 'path' | 'group';\n\tname?: string;\n\tdepth: number;\n\t/** Axis-aligned bounds in document units. */\n\tbounds: { x: number; y: number; width: number; height: number } | null;\n\t/** Rotation in degrees, clockwise (ancestors' rotations included). */\n\trotation: number;\n\tvisible: boolean;\n\tlocked: boolean;\n\tselected: boolean;\n\tfill?: unknown;\n\tstroke?: unknown;\n\tstrokeWidth?: number;\n\t/** SVG path data (paths only). */\n\td?: string;\n\tchildren?: number;\n\t/** A blend group's options (its steps are listed as children with `blendStep`). */\n\tblend?: { spacing: string; steps: number; distance: number; orientToSpine: boolean; linear: boolean; spine: boolean; keys: string[] };\n\t/** An in-between generated by a blend (locked; changes come from its keys). */\n\tblendStep?: boolean;\n\t/** The node's live path effects, applied in order. */\n\teffects?: VectorEffect[];\n\t/** A live boolean group's operation (its children are the operands). */\n\tliveBoolean?: VectorBooleanOperation;\n\t/** An operand's own operation in a live boolean group. */\n\tbooleanOperation?: VectorBooleanOperation;\n\t/** The fill stack, bottom to top, when the path has more than its `fill` says. */\n\tfills?: VectorPaintLayer[];\n\t/** The stroke stack, bottom to top, when the path has more than its `stroke` says. */\n\tstrokes?: VectorStrokeLayer[];\n\t/** The node's blend mode, when not normal. */\n\tblendMode?: VectorBlendMode;\n\t/** The node's visual filters (shadows, glows, blurs, feather). */\n\tfilters?: VectorFilter[];\n\t/** A mask group's mask: type, inversion, the mask's id (its first child) and the masked contents. */\n\tmask?: VectorMaskSummary;\n}\n\nconst EFFECT_PARAMS_DESCRIPTION =\n\t'Effect parameters (missing ones keep their value or default). trim: start, end (percent), offset (degrees), mode simultaneous|individual. roundCorners: radius. puckerBloat: amount (−100…100). twist: angle, center {x, y} (offset from the middle). zigzag: size, ridges, smooth. wiggle: size, detail, smooth, speed, correlation, phase, seed, time (seconds). offset: amount, join miter|round|bevel, miterLimit, copies, copyOffset. merge: mode merge|add|subtract|intersect|exclude. repeater: copies (fractions fade the last), offset, position {x, y}, anchor {x, y} (offset from the middle), rotation, scale {x, y} (percent), startOpacity, endOpacity (percent), composite below|above. envelope: style (a warp preset: arc|arcLower|arcUpper|arch|bulge|shellLower|shellUpper|flag|wave|fish|rise|fisheye|inflate|squeeze|twist, or mesh), bend, horizontal, vertical (percent, −100…100), orientation horizontal|vertical, rows, columns, mesh { us, vs, points } (node space; prefer vector.envelope).';\n\nconst fail = (message: string): never => {\n\tthrow new Error(message);\n};\n\nconst IDS = { type: 'array', items: { type: 'string' }, description: 'Node ids (default: the selection).' } as const;\n\nconst ANCHORS = {\n\ttype: 'array',\n\titems: {\n\t\ttype: 'object',\n\t\tproperties: { nodeId: { type: 'string' }, subpath: { type: 'integer', minimum: 0 }, index: { type: 'integer', minimum: 0 } },\n\t\trequired: ['nodeId', 'subpath', 'index']\n\t},\n\tdescription: 'Anchors as { nodeId, subpath, index } (default: the selected points).'\n} as const;\n\nconst POINTS = {\n\ttype: 'array',\n\titems: { type: 'array', items: { type: 'number' }, minItems: 2, maxItems: 2 },\n\tminItems: 1,\n\tdescription: 'Polyline [[x, y], …] in document units.'\n} as const;\n\nconst WIDTH_POINTS = {\n\ttype: ['array', 'null'],\n\titems: {\n\t\ttype: 'object',\n\t\tproperties: {\n\t\t\tt: { type: 'number', minimum: 0, maximum: 1, description: 'Position along each subpath by length (0 start, 1 end).' },\n\t\t\tleft: { type: 'number', minimum: 0, description: 'Left side as a multiple of half the stroke width (1 = uniform).' },\n\t\t\tright: { type: 'number', minimum: 0, description: 'Right side (looking along the path, y down).' }\n\t\t},\n\t\trequired: ['t', 'left', 'right']\n\t},\n\tdescription: 'Variable-width profile points, interpolated smoothly (monotone cubic); two points at one `t` make a step. `null` or `[]` strokes at a uniform width.'\n} as const;\n\nconst BLEND_PROPS = {\n\tspacing: { type: 'string', enum: ['steps', 'distance', 'smoothColor'], description: 'How in-betweens are spaced (default steps).' },\n\tsteps: { type: 'integer', minimum: 1, maximum: 1000, description: 'In-betweens between each pair of keys (steps spacing; default 5).' },\n\tdistance: { type: 'number', exclusiveMinimum: 0, description: 'Distance between in-betweens along the spine (distance spacing; default 20).' },\n\torientToSpine: { type: 'boolean', description: 'Turn the in-betweens with a replaced spine.' },\n\tlinear: { type: 'boolean', description: 'Move anchors in straight lines instead of the rotation-aware morph.' }\n} as const;\n\nconst checkBlendOptions = (params: Partial<VectorBlendOptions> | undefined) => {\n\tif (params?.spacing !== undefined && !VECTOR_BLEND_SPACINGS.includes(params.spacing)) fail(`Unknown blend spacing \"${params.spacing}\".`);\n\tif (params?.steps !== undefined && !(Number.isFinite(params.steps) && params.steps >= 1)) fail('steps must be at least 1.');\n\tif (params?.distance !== undefined && !(Number.isFinite(params.distance) && params.distance > 0)) fail('distance must be positive.');\n};\n\nconst POINT = { type: 'object', properties: { x: { type: 'number' }, y: { type: 'number' } }, required: ['x', 'y'] } as const;\n\n/** Gradient fields shared by paints and `setGradient`. */\nconst GRADIENT_PROPS = {\n\ttype: { type: 'string', enum: VECTOR_GRADIENT_TYPES, description: 'linear, radial, conic (an angle sweep), reflected (linear mirrored about the start) or diamond.' },\n\tstops: {\n\t\ttype: 'array',\n\t\tminItems: 1,\n\t\titems: {\n\t\t\ttype: 'object',\n\t\t\tproperties: {\n\t\t\t\toffset: { type: 'number', minimum: 0, maximum: 1 },\n\t\t\t\tcolor: { type: 'string', description: 'A CSS colour.' },\n\t\t\t\topacity: { type: 'number', minimum: 0, maximum: 1 },\n\t\t\t\tmidpoint: { type: 'number', minimum: 0.05, maximum: 0.95, description: 'Where between this stop and the next the colours are half-mixed (default 0.5).' }\n\t\t\t},\n\t\t\trequired: ['offset', 'color']\n\t\t}\n\t},\n\tangle: { type: 'number', description: 'Shorthand direction in degrees (0 up, clockwise): linear / reflected direction across the box, conic start angle, diamond turn.' },\n\tcx: { type: 'number', description: 'Shorthand centre x of radial / conic / diamond gradients, a fraction of the box (default 0.5).' },\n\tcy: { type: 'number' },\n\tr: { type: 'number', description: 'Shorthand radius, a fraction of the box (default 0.5).' },\n\tstart: { ...POINT, description: 'Explicit start (centre of radial / conic / diamond), in `units`. Wins over the shorthand.' },\n\tend: { ...POINT, description: 'Explicit end of the main axis (the radius of radial / conic / diamond).' },\n\twidth: { ...POINT, description: 'End of the secondary axis (aspect and turn of radial / conic / diamond); default square to the main axis, as long.' },\n\tfocal: { ...POINT, description: 'Radial focal point (where t = 0), in `units`.' },\n\tfocalRadius: { type: 'number', minimum: 0, maximum: 0.99, description: 'Radial focal circle radius as a fraction of the radius (SVG fr).' },\n\tunits: { type: 'string', enum: ['objectBoundingBox', 'userSpaceOnUse'], description: 'Space of start / end / width / focal: fractions of the shape box (default) or the node\\'s own coordinates.' },\n\tspread: { type: 'string', enum: VECTOR_GRADIENT_SPREADS, description: 'What paints past the ends: pad (default), reflect or repeat.' },\n\tinterpolation: { type: 'string', enum: VECTOR_GRADIENT_INTERPOLATIONS, description: 'Colour mixing space: srgb (default), linear (light) or oklab.' },\n\tsmoothness: { type: 'number', minimum: 0, maximum: 1, description: 'Bends the ramp along a smooth spline through the stops (0 = straight).' },\n\tstrokeMode: { type: 'string', enum: VECTOR_STROKE_GRADIENT_MODES, description: 'On strokes: within (default, like a fill), along the path or across it.' }\n} as const;\n\nconst PAINT = {\n\tdescription:\n\t\t'A CSS colour, `null` for none, or a gradient object: `{ type: \"linear\" | \"radial\" | \"conic\" | \"reflected\" | \"diamond\", stops: [{ offset, color, opacity?, midpoint? }] }` plus the shorthand `angle` / `cx, cy, r` or explicit `start`, `end`, `width`, `focal`, `focalRadius`, `units`, and `spread`, `interpolation`, `smoothness`, `strokeMode` (see vector.setGradient).',\n\tanyOf: [{ type: 'string' }, { type: 'null' }, { type: 'object', properties: GRADIENT_PROPS, required: ['type', 'stops'] }]\n} as const;\n\nconst BLEND_MODE = {\n\ttype: 'string',\n\tenum: VECTOR_BLEND_MODES,\n\tdescription: 'How it mixes with what is under it (CSS mix-blend-mode names; linear-dodge draws as plus-lighter). Default normal.'\n} as const;\n\n/** A fill layer's own fields. */\nconst FILL_LAYER_PROPS = {\n\tpaint: PAINT,\n\topacity: { type: 'number', minimum: 0, maximum: 1, description: 'Layer opacity 0 to 1 (default 1), multiplying the paint\\'s own alpha.' },\n\tblendMode: BLEND_MODE,\n\tvisible: { type: 'boolean', description: 'false hides the layer (kept, not drawn or hit). Default true.' }\n} as const;\n\n/** Stroke settings, each stroke layer's own (and the stroke shorthand's). */\nconst STROKE_SETTING_PROPS = {\n\tstrokeWidth: { type: 'number', minimum: 0, description: 'Stroke width in document units (default 1).' },\n\tstrokeLinecap: { type: 'string', enum: ['butt', 'round', 'square'], description: 'Line cap (default butt).' },\n\tstrokeLinejoin: { type: 'string', enum: ['miter', 'round', 'bevel'], description: 'Line join (default miter).' },\n\tstrokeDasharray: { type: ['array', 'null'], items: { type: 'number', minimum: 0 }, description: 'Dash and gap lengths in document units; null or [] for a solid line.' },\n\tstrokeAlign: { type: 'string', enum: ['center', 'inside', 'outside'], description: 'Where the stroke sits on the outline (closed shapes only; open paths stroke centred). Default center.' },\n\tstartArrow: { type: ['string', 'null'], enum: [...VECTOR_ARROWHEADS, null], description: 'Head at the start of an open path, or null for none.' },\n\tendArrow: { type: ['string', 'null'], enum: [...VECTOR_ARROWHEADS, null], description: 'Head at the end of an open path, or null for none.' },\n\tarrowAlign: { type: 'string', enum: ['extend', 'tip'], description: '`extend`: the tip sits past the end point; `tip`: on it, the stroke shortened under the head.' },\n\tstartArrowScale: { type: 'number', exclusiveMinimum: 0, description: 'Start head size as a multiple of the stroke width (default 1).' },\n\tendArrowScale: { type: 'number', exclusiveMinimum: 0, description: 'End head size as a multiple of the stroke width (default 1).' },\n\tstrokeWidthProfile: WIDTH_POINTS\n} as const;\n\nconst FILL_LAYER = { type: 'object', properties: FILL_LAYER_PROPS, required: ['paint'], additionalProperties: false } as const;\nconst STROKE_LAYER = { type: 'object', properties: { ...FILL_LAYER_PROPS, ...STROKE_SETTING_PROPS }, required: ['paint'], additionalProperties: false } as const;\n\nconst STYLE_PROPS = {\n\tfills: { type: 'array', items: FILL_LAYER, description: 'Replace the fill stack, bottom to top (fills draw under strokes). `fill` alone edits the top fill.' },\n\tstrokes: { type: 'array', items: STROKE_LAYER, description: 'Replace the stroke stack, bottom to top, each stroke with its own settings. `stroke` and the stroke settings alone edit the top stroke.' },\n\tfill: PAINT,\n\tstroke: PAINT,\n\t...STROKE_SETTING_PROPS,\n\tfillRule: { type: 'string', enum: ['nonzero', 'evenodd'], description: 'Which areas count as inside (default nonzero).' }\n} as const;\n\nconst checkPaint = (paint: unknown, where: string): VectorPaint => {\n\tif (paint && typeof paint === 'object') return sanitizeVectorGradient(paint) ?? fail(`${where} is not a valid gradient: give a type and at least one stop { offset, color }.`);\n\treturn paint as VectorPaint;\n};\n\nconst pickStyle = (params: Record<string, unknown>): VectorStyle => {\n\tconst out: Record<string, unknown> = {};\n\tfor (const key of Object.keys(STYLE_PROPS)) if (params[key] !== undefined) out[key] = params[key];\n\tfor (const key of ['fill', 'stroke'] as const) if (out[key] !== undefined) out[key] = checkPaint(out[key], key);\n\tfor (const key of ['fills', 'strokes'] as const) {\n\t\tconst stack = out[key];\n\t\tif (!Array.isArray(stack)) continue;\n\t\tstack.forEach((l, i) => {\n\t\t\tif (!l || typeof l !== 'object') fail(`${key}[${i}] must be a layer object { paint, … }.`);\n\t\t\tconst layer = l as Record<string, unknown>;\n\t\t\tcheckPaint(layer.paint, `${key}[${i}].paint`);\n\t\t\tif (layer.blendMode !== undefined && !isVectorBlendMode(layer.blendMode)) fail(`${key}[${i}].blendMode \"${String(layer.blendMode)}\" is not one of: ${VECTOR_BLEND_MODES.join(', ')}.`);\n\t\t});\n\t}\n\treturn out as VectorStyle;\n};\n\nconst check = (result: { ok: boolean; message?: string }) => {\n\tif (!result.ok) fail(result.message ?? 'The command failed.');\n};\n\n/**\n * Palette and agent commands for a vector editor: list, select, add shapes and paths, style,\n * transform, align, arrange, group, boolean operations and SVG import / export. Register them\n * in a `CommandRegistry` and agents get them through `createCommandToolProvider`. Failures\n * throw sentences, which the registry returns as `{ ok: false, code: 'failed', message }`.\n */\nexport function createVectorCommands(controller: VectorEditorController, options: VectorCommandOptions = {}): CommandSpec[] {\n\tconst prefix = options.prefix ?? 'vector.';\n\tconst group = options.group ?? 'Vector';\n\tconst base = { group, ...(options.scope ? { scope: options.scope } : {}) };\n\tconst hasSelection = () => (controller.getState().selection.length ? true : 'Nothing is selected.');\n\tconst ids = (params: { ids?: string[] } | undefined) => {\n\t\tconst list = params?.ids ?? controller.getState().selection;\n\t\tfor (const id of list) if (!controller.getNode(id)) fail(`There is no node \"${id}\".`);\n\t\tif (!list.length) fail('Nothing is selected; pass ids.');\n\t\treturn list;\n\t};\n\tconst enabledFor = (_: unknown, params: { ids?: string[] } | undefined) => (params?.ids?.length ? true : hasSelection());\n\tconst anchorsOf = (refs: VectorAnchorRef[] | undefined): VectorAnchorRef[] => {\n\t\tif (!refs) return [...controller.getState().selectedAnchors];\n\t\tfor (const r of refs) {\n\t\t\tconst node = controller.getNode(r.nodeId);\n\t\t\tif (node?.type !== 'path' || !node.subpaths[r.subpath]?.anchors[r.index]) fail(`There is no anchor ${r.index} on subpath ${r.subpath} of \"${r.nodeId}\".`);\n\t\t}\n\t\treturn refs;\n\t};\n\n\tconst list = (): { width: number; height: number; nodes: VectorNodeListing[]; selection: readonly string[]; tool: VectorTool } => {\n\t\tconst state = controller.getState();\n\t\tconst out: VectorNodeListing[] = [];\n\t\tconst visit = (nodes: readonly VectorNode[], depth: number) => {\n\t\t\tfor (const n of nodes) {\n\t\t\t\tconst parent = vectorParentMatrix(state.document.nodes, n.id) ?? IDENTITY_MATRIX;\n\t\t\t\tconst b = vectorNodeBounds(n, parent);\n\t\t\t\tout.push({\n\t\t\t\t\tid: n.id,\n\t\t\t\t\ttype: n.type,\n\t\t\t\t\t...(n.name ? { name: n.name } : {}),\n\t\t\t\t\tdepth,\n\t\t\t\t\trotation: Math.round(vectorNodeRotation(n, parent) * 100) / 100,\n\t\t\t\t\tbounds: b && { x: Math.round(b.x * 100) / 100, y: Math.round(b.y * 100) / 100, width: Math.round(b.width * 100) / 100, height: Math.round(b.height * 100) / 100 },\n\t\t\t\t\tvisible: n.visible !== false,\n\t\t\t\t\tlocked: !!n.locked,\n\t\t\t\t\tselected: state.selection.includes(n.id),\n\t\t\t\t\t...(n.type === 'path'\n\t\t\t\t\t\t? { fill: n.fill ?? null, stroke: n.stroke ?? null, strokeWidth: n.strokeWidth ?? 1, d: vectorPathData(n, 2) }\n\t\t\t\t\t\t: { children: n.children.length }),\n\t\t\t\t\t...(n.type === 'group' && n.blend\n\t\t\t\t\t\t? { blend: { ...resolveVectorBlendOptions(n.blend), spine: !!n.blend.spine, keys: vectorBlendKeys(n).map((k) => k.id) } }\n\t\t\t\t\t\t: {}),\n\t\t\t\t\t...(n.blendStep ? { blendStep: true } : {}),\n\t\t\t\t\t...(n.effects?.length ? { effects: n.effects } : {}),\n\t\t\t\t\t...(n.type === 'group' && n.liveBoolean ? { liveBoolean: n.liveBoolean } : {}),\n\t\t\t\t\t...(n.booleanOperation ? { booleanOperation: n.booleanOperation } : {}),\n\t\t\t\t\t...(n.type === 'path' && Array.isArray(n.fills) ? { fills: n.fills } : {}),\n\t\t\t\t\t...(n.type === 'path' && Array.isArray(n.strokes) ? { strokes: n.strokes } : {}),\n\t\t\t\t\t...(n.blendMode && n.blendMode !== 'normal' ? { blendMode: n.blendMode } : {}),\n\t\t\t\t\t...(n.filters?.length ? { filters: n.filters } : {}),\n\t\t\t\t\t...(isVectorMaskGroup(n) ? { mask: vectorMaskSummary(n)! } : {})\n\t\t\t\t});\n\t\t\t\tif (n.type === 'group') visit(n.children, depth + 1);\n\t\t\t}\n\t\t};\n\t\tvisit(state.document.nodes, 0);\n\t\treturn { width: state.document.width, height: state.document.height, nodes: out, selection: state.selection, tool: state.tool };\n\t};\n\n\t/** A node's appearance as agents read it: stacks (paths), blend mode, opacity and filters. */\n\tconst appearanceOf = (id: string) => {\n\t\tconst n = controller.getNode(id) ?? fail(`There is no node \"${id}\".`);\n\t\treturn {\n\t\t\tid: n.id,\n\t\t\ttype: n.type,\n\t\t\t...(n.type === 'path' ? { fills: vectorFills(n), strokes: vectorStrokes(n), fillRule: n.fillRule ?? 'nonzero' } : {}),\n\t\t\tblendMode: n.blendMode ?? 'normal',\n\t\t\topacity: n.opacity ?? 1,\n\t\t\tfilters: n.filters ?? []\n\t\t};\n\t};\n\tconst pathOf = (id: string) => {\n\t\tconst n = controller.getNode(id) ?? fail(`There is no node \"${id}\". Call vector.list for ids.`);\n\t\tif (n.type !== 'path') fail(`\"${id}\" is a group; fills and strokes belong to paths (pass a path id, or use ids on vector.addFill / vector.addStroke for every path inside).`);\n\t\treturn n as Extract<VectorNode, { type: 'path' }>;\n\t};\n\tconst layerIndex = (id: string, kind: 'fill' | 'stroke', index: number, key = 'index') => {\n\t\tconst stack = kind === 'fill' ? vectorFills(pathOf(id)) : vectorStrokes(pathOf(id));\n\t\tif (!Number.isInteger(index) || !stack[index]) fail(`${key} ${index} is out of range: \"${id}\" has ${stack.length} ${kind}${stack.length === 1 ? '' : 's'} (0 is the bottom). Read them with vector.getAppearance.`);\n\t};\n\tconst stackResult = (id: string, kind: 'fill' | 'stroke') => (kind === 'fill' ? { id, fills: vectorFills(pathOf(id)) } : { id, strokes: vectorStrokes(pathOf(id)) });\n\tconst layerPatchOf = (params: Record<string, unknown>, keys: readonly string[]) => {\n\t\tconst out: Record<string, unknown> = {};\n\t\tfor (const key of keys) if (params[key] !== undefined) out[key] = key === 'paint' ? checkPaint(params[key], 'paint') : params[key];\n\t\treturn out;\n\t};\n\tconst pathsUnder = (list: readonly string[]) => {\n\t\tconst out: string[] = [];\n\t\tconst walk = (n: VectorNode) => (n.type === 'path' ? out.push(n.id) : n.children.forEach(walk));\n\t\tfor (const id of list) walk(controller.getNode(id)!);\n\t\treturn out;\n\t};\n\tconst FILL_KEYS = Object.keys(FILL_LAYER_PROPS);\n\tconst STROKE_KEYS = [...FILL_KEYS, ...Object.keys(STROKE_SETTING_PROPS)];\n\tconst layerCommands = (kind: 'fill' | 'stroke'): CommandSpec[] => {\n\t\tconst Kind = kind === 'fill' ? 'Fill' : 'Stroke';\n\t\tconst props = kind === 'fill' ? FILL_LAYER_PROPS : { ...FILL_LAYER_PROPS, ...STROKE_SETTING_PROPS };\n\t\tconst keys = kind === 'fill' ? FILL_KEYS : STROKE_KEYS;\n\t\tconst NODE = { type: 'string', description: 'Id of the path (from vector.list).' } as const;\n\t\tconst INDEX = { type: 'integer', minimum: 0, description: `Position of the ${kind} in the path's ${kind} stack, 0 at the bottom (from vector.getAppearance).` } as const;\n\t\tconst listKey = `${kind}s`;\n\t\treturn [\n\t\t\t{\n\t\t\t\t...base,\n\t\t\t\tid: `${prefix}add${Kind}`,\n\t\t\t\tlabel: `Add ${Kind}`,\n\t\t\t\tkeywords: ['appearance', 'layer', kind, 'stack', 'multiple', 'paint'],\n\t\t\t\tdescription:\n\t\t\t\t\tkind === 'fill'\n\t\t\t\t\t\t? 'Adds a fill layer to paths (groups: every path inside), on top of the fill stack or at `index`. Fills draw bottom to top, all under the strokes. Returns each path\\'s new fill stack.'\n\t\t\t\t\t\t: 'Adds a stroke layer to paths (groups: every path inside), on top of the stroke stack or at `index`, with its own width, alignment, dashes, caps, joins, arrowheads and profile (unset settings come from the stack\\'s current top stroke). Strokes draw bottom to top, over every fill. Returns each path\\'s new stroke stack.',\n\t\t\t\tparams: {\n\t\t\t\t\ttype: 'object',\n\t\t\t\t\tproperties: {\n\t\t\t\t\t\tids: { ...IDS, description: 'Paths or groups to add it to (default: the selection; groups give every path inside).' },\n\t\t\t\t\t\t...props,\n\t\t\t\t\t\tpaint: { ...PAINT, description: `${PAINT.description} Default ${kind === 'fill' ? '#808080' : '#000000'}.` },\n\t\t\t\t\t\tindex: { type: 'integer', minimum: 0, description: `Where in the stack to put it, 0 at the bottom (default: on top).` }\n\t\t\t\t\t},\n\t\t\t\t\tadditionalProperties: false\n\t\t\t\t},\n\t\t\t\tundoable: true,\n\t\t\t\tenabled: enabledFor,\n\t\t\t\trun: (_ctx: unknown, params: ({ ids?: string[]; index?: number } & Record<string, unknown>) | undefined) => {\n\t\t\t\t\tconst targets = ids(params);\n\t\t\t\t\tcheck(controller.addPaintLayer(kind, layerPatchOf(params ?? {}, keys), { ids: targets, index: params?.index }));\n\t\t\t\t\treturn { nodes: pathsUnder(targets).map((id) => stackResult(id, kind)) };\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\t...base,\n\t\t\t\tid: `${prefix}update${Kind}`,\n\t\t\t\tlabel: `Edit ${Kind}`,\n\t\t\t\tkeywords: ['appearance', 'layer', kind, 'opacity', 'blend', 'hide', 'show'],\n\t\t\t\tdescription: `Changes ${kind} \\`index\\` of path \\`nodeId\\`: ${kind === 'fill' ? 'paint, opacity, blend mode, visibility' : 'paint, opacity, blend mode, visibility and any stroke setting (null removes an arrowhead, dashes or a profile)'}. Missing fields keep their value. Returns the path's ${kind} stack.`,\n\t\t\t\tparams: { type: 'object', properties: { nodeId: NODE, index: INDEX, ...props }, required: ['nodeId', 'index'], additionalProperties: false },\n\t\t\t\tundoable: true,\n\t\t\t\trun: (_ctx: unknown, params: { nodeId: string; index: number } & Record<string, unknown>) => {\n\t\t\t\t\tlayerIndex(params.nodeId, kind, params.index);\n\t\t\t\t\tconst patch = layerPatchOf(params, keys);\n\t\t\t\t\tif (!Object.keys(patch).length) fail(`Pass at least one of: ${keys.join(', ')}.`);\n\t\t\t\t\tcheck(controller.updatePaintLayer(kind, params.index, patch, { ids: [params.nodeId] }));\n\t\t\t\t\treturn stackResult(params.nodeId, kind);\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\t...base,\n\t\t\t\tid: `${prefix}remove${Kind}`,\n\t\t\t\tlabel: `Remove ${Kind}`,\n\t\t\t\tkeywords: ['appearance', 'layer', kind, 'delete'],\n\t\t\t\tdescription: `Removes ${kind} \\`index\\` from path \\`nodeId\\`. Returns the path's remaining ${kind} stack.`,\n\t\t\t\tparams: { type: 'object', properties: { nodeId: NODE, index: INDEX }, required: ['nodeId', 'index'], additionalProperties: false },\n\t\t\t\tundoable: true,\n\t\t\t\trun: (_ctx: unknown, params: { nodeId: string; index: number }) => {\n\t\t\t\t\tlayerIndex(params.nodeId, kind, params.index);\n\t\t\t\t\tcheck(controller.removePaintLayer(kind, params.index, { ids: [params.nodeId] }));\n\t\t\t\t\treturn stackResult(params.nodeId, kind);\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\t...base,\n\t\t\t\tid: `${prefix}move${Kind}`,\n\t\t\t\tlabel: `Reorder ${Kind}s`,\n\t\t\t\tkeywords: ['appearance', 'layer', kind, 'order', 'reorder', 'stack'],\n\t\t\t\tdescription: `Moves ${kind} \\`index\\` of path \\`nodeId\\` to position \\`to\\` in its ${kind} stack (0 is the bottom). Returns the reordered ${listKey}.`,\n\t\t\t\tparams: {\n\t\t\t\t\ttype: 'object',\n\t\t\t\t\tproperties: { nodeId: NODE, index: INDEX, to: { type: 'integer', minimum: 0, description: `New position, 0 at the bottom (clamped to the stack).` } },\n\t\t\t\t\trequired: ['nodeId', 'index', 'to'],\n\t\t\t\t\tadditionalProperties: false\n\t\t\t\t},\n\t\t\t\tundoable: true,\n\t\t\t\trun: (_ctx: unknown, params: { nodeId: string; index: number; to: number }) => {\n\t\t\t\t\tlayerIndex(params.nodeId, kind, params.index);\n\t\t\t\t\tcheck(controller.movePaintLayer(kind, params.index, params.to, { ids: [params.nodeId] }));\n\t\t\t\t\treturn stackResult(params.nodeId, kind);\n\t\t\t\t}\n\t\t\t}\n\t\t];\n\t};\n\tconst FILTER_PARAMS = {\n\t\ttype: 'object',\n\t\tdescription:\n\t\t\t'Filter parameters (missing ones keep their value or default; lengths in document units). dropShadow / innerShadow: color (CSS), opacity (0 to 1), offsetX, offsetY, blur (radius; reaches about 1.5 times it), spread (inner: choke). outerGlow / innerGlow: color, opacity, blur, spread. layerBlur: radius. feather: radius (edge fade width).',\n\t\tproperties: {\n\t\t\tcolor: { type: 'string', description: 'Shadow or glow colour (CSS).' },\n\t\t\topacity: { type: 'number', minimum: 0, maximum: 1, description: 'Shadow or glow opacity, 0 to 1.' },\n\t\t\toffsetX: { type: 'number', description: 'Shadow offset to the right, document units.' },\n\t\t\toffsetY: { type: 'number', description: 'Shadow offset downwards, document units.' },\n\t\t\tblur: { type: 'number', minimum: 0, description: 'Shadow or glow blur radius, document units.' },\n\t\t\tspread: { type: 'number', minimum: 0, description: 'Grows the silhouette before blurring (inner shadow and glow: chokes inwards).' },\n\t\t\tradius: { type: 'number', minimum: 0, description: 'Layer blur radius, or feather width, document units.' }\n\t\t},\n\t\tadditionalProperties: false\n\t} as const;\n\tconst checkFilterParams = (type: VectorFilterType, params: Record<string, unknown> | undefined) => {\n\t\tconst allowed = vectorFilterParamKeys(type);\n\t\tfor (const key of Object.keys(params ?? {})) if (!allowed.includes(key)) fail(`${type} has no parameter \"${key}\"; it takes ${allowed.join(', ')}.`);\n\t};\n\tconst filtersResult = (id: string) => ({ id, filters: controller.getNode(id)?.filters ?? [] });\n\tconst filterOf = (nodeId: string, filterId: string) => {\n\t\tconst node = controller.getNode(nodeId) ?? fail(`There is no node \"${nodeId}\".`);\n\t\treturn node.filters?.find((f) => f.id === filterId) ?? fail(`Node \"${nodeId}\" has no filter \"${filterId}\"; it has ${(node.filters ?? []).map((f) => `\"${f.id}\" (${f.type})`).join(', ') || 'none'}.`);\n\t};\n\tconst maskResult = (id: string) => ({ id, mask: vectorMaskSummary(controller.getNode(id)) ?? fail(`\"${id}\" is not a mask group.`) });\n\t/** Checks the objects a mask is made of before anything changes. */\n\tconst maskInputs = (params: { ids?: string[]; maskId?: string } | undefined) => {\n\t\tconst list = [...new Set(ids(params))];\n\t\tif (list.length < 2) fail(`Masking needs at least two objects (the mask and what it masks); got ${list.length}. Pass ids, or select more.`);\n\t\tfor (const id of list) if (controller.getNode(id)!.locked) fail(`\"${id}\" is locked; ask the user to unlock it, or leave it out of ids.`);\n\t\tif (params?.maskId !== undefined && !list.includes(params.maskId)) fail(`maskId \"${params.maskId}\" must be one of ids (${list.map((id) => `\"${id}\"`).join(', ')}).`);\n\t\treturn list;\n\t};\n\tconst maskTargets = (params: { ids?: string[] } | undefined) => {\n\t\tconst list = ids(params);\n\t\tconst groups = list.map((id) => controller.getNode(id)).filter((n) => isVectorMaskGroup(n));\n\t\tif (!groups.length) fail(`None of ${list.map((id) => `\"${id}\"`).join(', ')} is a mask group. Mask groups have a \\`mask\\` in vector.list; make one with vector.makeClippingMask or vector.makeOpacityMask.`);\n\t\treturn groups.map((g) => g!.id);\n\t};\n\tconst MASK_INPUT = {\n\t\tids: { ...IDS, description: 'The objects to mask with, two or more sharing a parent (default: the selection). The topmost becomes the mask unless maskId says otherwise.' },\n\t\tmaskId: { type: 'string', description: 'Which of ids is the mask (default: the topmost).' },\n\t\tinvert: { type: 'boolean', description: 'Show the contents only where the mask hides them (default false).' }\n\t} as const;\n\tconst maskCommands = (): CommandSpec[] => [\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}makeClippingMask`,\n\t\t\tlabel: 'Make Clipping Mask',\n\t\t\tkeywords: ['mask', 'clip', 'clipping', 'clip path', 'crop', 'inside'],\n\t\t\tshortcut: 'Mod+7',\n\t\t\tdescription:\n\t\t\t\t'Groups objects into a clipping mask group: the mask (the topmost object, or maskId) moves to the bottom of the new group and the others show only inside its outline (its paint is not drawn; every path in it counts, by its fill rule). Hit-testing, bounds, snapping and booleans use the clipped result. One undo step. Returns { id, mask: { type, invert, maskId, contents, enabled } }.',\n\t\t\tparams: { type: 'object', properties: MASK_INPUT, additionalProperties: false },\n\t\t\tundoable: true,\n\t\t\tenabled: enabledFor,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[]; maskId?: string; invert?: boolean } | undefined) => {\n\t\t\t\tconst list = maskInputs(params);\n\t\t\t\tconst r = controller.makeMask('clip', { ids: list, ...(params?.maskId ? { maskId: params.maskId } : {}), invert: params?.invert });\n\t\t\t\tif (!r.ok) fail(r.message);\n\t\t\t\treturn maskResult((r as { id: string }).id);\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}makeOpacityMask`,\n\t\t\tlabel: 'Make Opacity Mask',\n\t\t\tkeywords: ['mask', 'opacity mask', 'alpha', 'luminance', 'fade', 'gradient mask', 'soft mask'],\n\t\t\tdescription:\n\t\t\t\t'Groups objects into an opacity mask group: the mask (the topmost object, or maskId) moves to the bottom of the new group and hides the others where it is dark (luminance: white shows, black hides) or transparent (alpha). A gradient mask fades the contents. One undo step. Returns { id, mask: { type, invert, maskId, contents, enabled } }.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: { ...MASK_INPUT, type: { type: 'string', enum: ['luminance', 'alpha'], description: 'luminance (by brightness, the default) or alpha (by opacity).' } },\n\t\t\t\tadditionalProperties: false\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\tenabled: enabledFor,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[]; maskId?: string; invert?: boolean; type?: 'alpha' | 'luminance' } | undefined) => {\n\t\t\t\tconst list = maskInputs(params);\n\t\t\t\tconst r = controller.makeMask(params?.type ?? 'luminance', { ids: list, ...(params?.maskId ? { maskId: params.maskId } : {}), invert: params?.invert });\n\t\t\t\tif (!r.ok) fail(r.message);\n\t\t\t\treturn maskResult((r as { id: string }).id);\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}setMaskOptions`,\n\t\t\tlabel: 'Mask Options…',\n\t\t\tkeywords: ['mask', 'clip', 'invert', 'alpha', 'luminance', 'opacity mask'],\n\t\t\tdescription:\n\t\t\t\t'Changes how mask groups mask: `type` clip (by outline), alpha or luminance, `invert`, and `enabled` (false hides the mask so the contents draw whole; true shows it again). Pass at least one. One undo step. Returns { groups: [{ id, mask }] }.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tids: { ...IDS, description: 'Mask group ids (default: the selection).' },\n\t\t\t\t\ttype: { type: 'string', enum: ['clip', 'alpha', 'luminance'], description: 'clip (the mask\\'s outline), alpha (its opacity) or luminance (its brightness).' },\n\t\t\t\t\tinvert: { type: 'boolean', description: 'true: show the contents where the mask hides them.' },\n\t\t\t\t\tenabled: { type: 'boolean', description: 'false: turn the mask off (it is hidden and the contents draw whole); true: on again.' }\n\t\t\t\t},\n\t\t\t\tadditionalProperties: false\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\tenabled: enabledFor,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[]; type?: VectorMaskType; invert?: boolean; enabled?: boolean } | undefined) => {\n\t\t\t\tif (params?.type === undefined && params?.invert === undefined && params?.enabled === undefined) fail('Pass at least one of type, invert, enabled.');\n\t\t\t\tconst groups = maskTargets(params);\n\t\t\t\tconst patch = { ...(params?.type ? { type: params.type } : {}), ...(params?.invert !== undefined ? { invert: params.invert } : {}), ...(params?.enabled !== undefined ? { enabled: params.enabled } : {}) };\n\t\t\t\tcheck(controller.setMaskOptions(patch, { ids: groups }));\n\t\t\t\treturn { groups: groups.map(maskResult) };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}releaseMask`,\n\t\t\tlabel: 'Release Mask',\n\t\t\tkeywords: ['mask', 'clip', 'release', 'unmask', 'remove mask'],\n\t\t\tshortcut: 'Mod+Alt+7',\n\t\t\tdescription:\n\t\t\t\t'Releases mask groups: the mask goes back on top of the contents and draws as itself again; the group is replaced by its children unless it has filters, a blend mode or path effects (then it stays a plain group). One undo step. Returns { ids } of the released objects (the mask last).',\n\t\t\tparams: { type: 'object', properties: { ids: { ...IDS, description: 'Mask group ids (default: the selection).' } }, additionalProperties: false },\n\t\t\tundoable: true,\n\t\t\tenabled: enabledFor,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[] } | undefined) => {\n\t\t\t\tconst groups = maskTargets(params);\n\t\t\t\treturn { ids: controller.releaseMask({ ids: groups }) };\n\t\t\t}\n\t\t}\n\t];\n\tconst appearanceCommands = (): CommandSpec[] => [\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}getAppearance`,\n\t\t\tlabel: 'Get Appearance',\n\t\t\treadOnly: true,\n\t\t\tkeywords: ['appearance', 'fills', 'strokes', 'blend mode', 'filters', 'shadow'],\n\t\t\tdescription:\n\t\t\t\t'Reads a node\\'s appearance: for paths the fill and stroke stacks (bottom to top, each with paint, opacity, blendMode, visible and, for strokes, their settings) and fill rule; for any node its blend mode, opacity and filters (with ids). Use the indexes and ids with the fill, stroke and filter commands.',\n\t\t\tparams: { type: 'object', properties: { id: { type: 'string', description: 'Node id (from vector.list).' } }, required: ['id'], additionalProperties: false },\n\t\t\trun: (_ctx: unknown, params: { id: string }) => appearanceOf(params.id)\n\t\t},\n\t\t...layerCommands('fill'),\n\t\t...layerCommands('stroke'),\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}setBlendMode`,\n\t\t\tlabel: 'Blend Mode…',\n\t\t\tkeywords: ['blend', 'multiply', 'screen', 'overlay', 'mix-blend-mode', 'composite'],\n\t\t\tdescription: 'Sets how whole nodes (paths or groups, with everything they draw) mix with what is under them. `normal` removes it. Returns each node\\'s blend mode.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: { ids: IDS, blendMode: { ...BLEND_MODE, description: 'The blend mode (CSS mix-blend-mode names; linear-dodge draws as plus-lighter).' } },\n\t\t\t\trequired: ['blendMode'],\n\t\t\t\tadditionalProperties: false\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\tenabled: enabledFor,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[]; blendMode: VectorBlendMode }) => {\n\t\t\t\tconst targets = ids(params);\n\t\t\t\tcheck(controller.setBlendMode(params.blendMode, { ids: targets }));\n\t\t\t\treturn { nodes: targets.map((id) => ({ id, blendMode: controller.getNode(id)?.blendMode ?? 'normal' })) };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}addFilter`,\n\t\t\tlabel: 'Add Filter…',\n\t\t\tkeywords: ['filter', 'drop shadow', 'inner shadow', 'glow', 'blur', 'feather', 'effect', 'appearance'],\n\t\t\tdescription:\n\t\t\t\t'Adds a visual filter `type` (dropShadow, innerShadow, outerGlow, innerGlow, layerBlur, feather) to nodes (paths or groups), at the end of each filter stack or at `index`. Filters never change geometry (bounds and hit-testing ignore them) and export as SVG filters. Returns each node\\'s new filter id and its filter stack.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\ttype: { type: 'string', enum: VECTOR_FILTER_TYPES, description: 'The filter type.' },\n\t\t\t\t\tparams: FILTER_PARAMS,\n\t\t\t\t\tindex: { type: 'integer', minimum: 0, description: 'Position in the stack (default: last).' },\n\t\t\t\t\tids: IDS\n\t\t\t\t},\n\t\t\t\trequired: ['type'],\n\t\t\t\tadditionalProperties: false\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\tenabled: enabledFor,\n\t\t\trun: (_ctx: unknown, params: { type: VectorFilterType; params?: Record<string, unknown>; index?: number; ids?: string[] }) => {\n\t\t\t\tif (!VECTOR_FILTER_TYPES.includes(params.type)) fail(`Unknown filter type \"${params.type}\"; use one of ${VECTOR_FILTER_TYPES.join(', ')}.`);\n\t\t\t\tcheckFilterParams(params.type, params.params);\n\t\t\t\tconst targets = ids(params);\n\t\t\t\tconst created = controller.addFilter(params.type, { ids: targets, params: params.params, index: params.index });\n\t\t\t\tif (!created.length) fail('Nothing to add the filter to (blend steps take no filters).');\n\t\t\t\treturn { nodes: targets.map((id, i) => ({ filterId: created[i], ...filtersResult(id) })) };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}updateFilter`,\n\t\t\tlabel: 'Edit Filter…',\n\t\t\tkeywords: ['filter', 'shadow', 'glow', 'blur', 'feather', 'enable', 'disable'],\n\t\t\tdescription: 'Changes filter `filterId` of node `nodeId`: its `params` (merged; only the keys its type takes) and whether it is `enabled`. Returns the node\\'s filter stack.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tnodeId: { type: 'string', description: 'Node id.' },\n\t\t\t\t\tfilterId: { type: 'string', description: 'Filter id (from vector.addFilter or vector.getAppearance).' },\n\t\t\t\t\tparams: FILTER_PARAMS,\n\t\t\t\t\tenabled: { type: 'boolean', description: 'false turns the filter off (kept in the stack).' }\n\t\t\t\t},\n\t\t\t\trequired: ['nodeId', 'filterId'],\n\t\t\t\tadditionalProperties: false\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { nodeId: string; filterId: string; params?: Record<string, unknown>; enabled?: boolean }) => {\n\t\t\t\tconst filter = filterOf(params.nodeId, params.filterId);\n\t\t\t\tcheckFilterParams(filter.type, params.params);\n\t\t\t\tif (params.params === undefined && params.enabled === undefined) fail('Pass params or enabled.');\n\t\t\t\tcheck(controller.updateFilter(params.nodeId, params.filterId, { params: params.params, enabled: params.enabled }));\n\t\t\t\treturn filtersResult(params.nodeId);\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}removeFilter`,\n\t\t\tlabel: 'Remove Filter',\n\t\t\tkeywords: ['filter', 'delete', 'shadow', 'blur'],\n\t\t\tdescription: 'Removes filter `filterId` from node `nodeId`. Returns the node\\'s remaining filters.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: { nodeId: { type: 'string', description: 'Node id.' }, filterId: { type: 'string', description: 'Filter id.' } },\n\t\t\t\trequired: ['nodeId', 'filterId'],\n\t\t\t\tadditionalProperties: false\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { nodeId: string; filterId: string }) => {\n\t\t\t\tfilterOf(params.nodeId, params.filterId);\n\t\t\t\tcheck(controller.removeFilter(params.nodeId, params.filterId));\n\t\t\t\treturn filtersResult(params.nodeId);\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}moveFilter`,\n\t\t\tlabel: 'Reorder Filters',\n\t\t\tkeywords: ['filter', 'order', 'reorder'],\n\t\t\tdescription: 'Moves filter `filterId` of node `nodeId` to position `index` of its stack. Order matters within a kind: later drop shadows draw over earlier ones, later inner effects over earlier ones. Returns the reordered filters.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tnodeId: { type: 'string', description: 'Node id.' },\n\t\t\t\t\tfilterId: { type: 'string', description: 'Filter id.' },\n\t\t\t\t\tindex: { type: 'integer', minimum: 0, description: 'New position, 0 first (clamped to the stack).' }\n\t\t\t\t},\n\t\t\t\trequired: ['nodeId', 'filterId', 'index'],\n\t\t\t\tadditionalProperties: false\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { nodeId: string; filterId: string; index: number }) => {\n\t\t\t\tfilterOf(params.nodeId, params.filterId);\n\t\t\t\tcheck(controller.moveFilter(params.nodeId, params.filterId, params.index));\n\t\t\t\treturn filtersResult(params.nodeId);\n\t\t\t}\n\t\t}\n\t];\n\n\treturn [\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}list`,\n\t\t\tlabel: 'List Vector Nodes',\n\t\t\treadOnly: true,\n\t\t\tkeywords: ['layers', 'shapes', 'document', 'outline'],\n\t\t\tdescription: 'Lists every node (paths and groups, depth first) with bounds, style, path data and selection.',\n\t\t\trun: list\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}select`,\n\t\t\tlabel: 'Select Nodes',\n\t\t\tkeywords: ['pick', 'choose'],\n\t\t\tdescription: 'Selects nodes by id (empty list clears the selection).',\n\t\t\tparams: { type: 'object', properties: { ids: { type: 'array', items: { type: 'string' } }, add: { type: 'boolean' } }, required: ['ids'] },\n\t\t\trun: (_ctx: unknown, params: { ids: string[]; add?: boolean }) => {\n\t\t\t\tfor (const id of params.ids) if (!controller.getNode(id)) fail(`There is no node \"${id}\".`);\n\t\t\t\tif (!params.ids.length && !params.add) controller.clearSelection();\n\t\t\t\telse controller.select(params.ids, { add: params.add });\n\t\t\t\treturn { selection: controller.getState().selection };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}addShape`,\n\t\t\tlabel: 'Add Shape…',\n\t\t\tkeywords: ['rectangle', 'ellipse', 'circle', 'polygon', 'star', 'line', 'draw'],\n\t\t\tdescription: 'Adds a rectangle, ellipse, polygon, star or line in document units, using the current default style unless overridden.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tkind: { type: 'string', enum: ['rect', 'ellipse', 'polygon', 'star', 'line'] },\n\t\t\t\t\tx: { type: 'number' },\n\t\t\t\t\ty: { type: 'number' },\n\t\t\t\t\twidth: { type: 'number', description: 'For a line: the x offset of its end.' },\n\t\t\t\t\theight: { type: 'number', description: 'For a line: the y offset of its end.' },\n\t\t\t\t\tcornerRadius: { type: 'number', minimum: 0, description: 'Live corner radius of a rect, polygon or star.' },\n\t\t\t\t\tcornerKind: { type: 'string', enum: VECTOR_CORNER_KINDS },\n\t\t\t\t\tsides: { type: 'integer', minimum: 3 },\n\t\t\t\t\tpoints: { type: 'integer', minimum: 3 },\n\t\t\t\t\tinnerRatio: { type: 'number', minimum: 0, maximum: 1 },\n\t\t\t\t\tname: { type: 'string' },\n\t\t\t\t\t...STYLE_PROPS\n\t\t\t\t},\n\t\t\t\trequired: ['kind', 'x', 'y', 'width', 'height']\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: VectorShapeInput) => ({ id: controller.addShape(params) })\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}addPath`,\n\t\t\tlabel: 'Add Path…',\n\t\t\tkeywords: ['svg', 'path data', 'bezier', 'curve'],\n\t\t\tdescription: 'Adds a path from SVG path data (`d`), in document units.',\n\t\t\tparams: { type: 'object', properties: { d: { type: 'string' }, name: { type: 'string' }, ...STYLE_PROPS }, required: ['d'] },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { d: string; name?: string } & Record<string, unknown>) => {\n\t\t\t\tconst id = controller.addPath(params.d, { ...pickStyle(params), ...(params.name ? { name: params.name } : {}) });\n\t\t\t\tconst node = controller.getNode(id);\n\t\t\t\tif (node?.type === 'path' && !node.subpaths.length) {\n\t\t\t\t\tcontroller.undo();\n\t\t\t\t\tfail('The path data has no points.');\n\t\t\t\t}\n\t\t\t\treturn { id };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}setStyle`,\n\t\t\tlabel: 'Set Style…',\n\t\t\tkeywords: ['fill', 'stroke', 'colour', 'color', 'gradient', 'width'],\n\t\t\tdescription: 'Sets fill, stroke and stroke settings of nodes (groups apply to every path inside).',\n\t\t\tparams: { type: 'object', properties: { ids: IDS, ...STYLE_PROPS } },\n\t\t\tenabled: enabledFor,\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[] } & Record<string, unknown>) => {\n\t\t\t\tconst style = pickStyle(params);\n\t\t\t\tif (!Object.keys(style).length) fail('Pass at least one style property.');\n\t\t\t\tcheck(controller.setStyle(style, ids(params)));\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}setGradient`,\n\t\t\tlabel: 'Gradient…',\n\t\t\tkeywords: ['gradient', 'linear', 'radial', 'conic', 'angle', 'reflected', 'diamond', 'stops', 'midpoint', 'oklab', 'spread', 'focal', 'along stroke', 'across stroke', 'reverse'],\n\t\t\tdescription:\n\t\t\t\t'Edits the gradient of the fill (default) or stroke of paths (groups apply to every path inside): any gradient field (missing ones keep their value; a colour becomes a gradient from it to white), `aspect` (secondary axis over main axis), `reverse` (flip the stops and midpoints), `focal` / `width` null to remove them. Midpoints, smoothness and oklab / linear interpolation export to SVG as extra stops; conic and diamond gradients as patterns.',\n\t\t\tparams: { type: 'object', properties: { ids: IDS, target: { type: 'string', enum: ['fill', 'stroke'] }, ...GRADIENT_PROPS, aspect: { type: 'number' }, reverse: { type: 'boolean' } } },\n\t\t\tenabled: enabledFor,\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: ({ ids?: string[]; target?: 'fill' | 'stroke' } & VectorGradientPatch) | undefined) => {\n\t\t\t\tconst { ids: _i, target, ...patch } = params ?? {};\n\t\t\t\tif (patch.type !== undefined && !VECTOR_GRADIENT_TYPES.includes(patch.type)) fail(`Unknown gradient type \"${patch.type}\".`);\n\t\t\t\tif (patch.stops !== undefined && !(Array.isArray(patch.stops) && patch.stops.length)) fail('stops needs at least one { offset, color }.');\n\t\t\t\tcheck(controller.setGradient(patch, { ids: ids(params), target: target ?? 'fill' }));\n\t\t\t\tconst id = ids(params)[0];\n\t\t\t\tconst node = id ? controller.getNode(id) : undefined;\n\t\t\t\tconst path = node?.type === 'path' ? node : undefined;\n\t\t\t\treturn { ok: true, ...(path ? { gradient: path[target ?? 'fill'] } : {}) };\n\t\t\t}\n\t\t},\n\t\t...appearanceCommands(),\n\t\t...maskCommands(),\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}setCorners`,\n\t\t\tlabel: 'Corner Radius…',\n\t\t\tkeywords: ['round', 'rounded', 'corners', 'radius', 'fillet', 'chamfer', 'live corners'],\n\t\t\tdescription:\n\t\t\t\t'Sets the live corner radius (and optionally `kind`: round, inverted or chamfer) of `anchors`, else the selected points (direct tool), else every corner of `ids` / the selection. Radius 0 makes corners sharp; radii shrink to fit short sides. The anchors stay editable.',\n\t\t\tparams: { type: 'object', properties: { radius: { type: 'number', minimum: 0 }, kind: { type: 'string', enum: VECTOR_CORNER_KINDS }, ids: IDS, anchors: ANCHORS } },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { radius?: number; kind?: VectorCornerKind; ids?: string[]; anchors?: VectorAnchorRef[] } | undefined) => {\n\t\t\t\tconst refs = params?.anchors ? anchorsOf(params.anchors) : undefined;\n\t\t\t\tconst target = { refs, ids: params?.ids ? ids(params) : undefined };\n\t\t\t\tif (params?.radius !== undefined) check(controller.setCornerRadius(params.radius, { ...target, kind: params.kind }));\n\t\t\t\telse if (params?.kind) check(controller.setCornerKind(params.kind, target));\n\t\t\t\telse fail('Pass `radius` and / or `kind`.');\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}setWidthProfile`,\n\t\t\tlabel: 'Width Profile…',\n\t\t\tkeywords: ['variable width', 'taper', 'tapered', 'pressure', 'profile', 'brush', 'calligraphy', 'bulge', 'thick', 'thin'],\n\t\t\tdescription:\n\t\t\t\t'Gives the stroke of paths a variable width: a `preset` (`uniform` removes the profile), explicit `points` ({ t, left, right }: side scales along each subpath), or a parametric `taper` (startLength / endLength as fractions of the path, startWidth / endWidth 0–1, startEase / endEase 0–1) and / or `wave` (amount 0–1, cycles, phase in degrees). Profiled strokes draw as filled outlines; Outline Stroke bakes them.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tids: IDS,\n\t\t\t\t\tpreset: { type: 'string', enum: VECTOR_WIDTH_PRESET_IDS },\n\t\t\t\t\tpoints: WIDTH_POINTS,\n\t\t\t\t\ttaper: {\n\t\t\t\t\t\ttype: 'object',\n\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\tstartLength: { type: 'number', minimum: 0, maximum: 1 },\n\t\t\t\t\t\t\tendLength: { type: 'number', minimum: 0, maximum: 1 },\n\t\t\t\t\t\t\tstartWidth: { type: 'number', minimum: 0, maximum: 1 },\n\t\t\t\t\t\t\tendWidth: { type: 'number', minimum: 0, maximum: 1 },\n\t\t\t\t\t\t\tstartEase: { type: 'number', minimum: 0, maximum: 1 },\n\t\t\t\t\t\t\tendEase: { type: 'number', minimum: 0, maximum: 1 }\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\twave: { type: 'object', properties: { amount: { type: 'number', minimum: 0, maximum: 1 }, cycles: { type: 'number', exclusiveMinimum: 0 }, phase: { type: 'number' } } }\n\t\t\t\t}\n\t\t\t},\n\t\t\tenabled: enabledFor,\n\t\t\tundoable: true,\n\t\t\trun: (\n\t\t\t\t_ctx: unknown,\n\t\t\t\tparams: { ids?: string[]; preset?: VectorWidthPreset; points?: VectorWidthPoint[] | null; taper?: VectorTaperOptions; wave?: VectorWaveOptions } | undefined\n\t\t\t) => {\n\t\t\t\tconst target = { ids: ids(params) };\n\t\t\t\tif (params?.preset) check(controller.setWidthProfile(params.preset, target));\n\t\t\t\telse if (params?.points !== undefined) check(controller.setWidthProfile(params.points ?? null, target));\n\t\t\t\telse if (params?.taper || params?.wave) check(controller.setWidthProfile({ taper: params.taper, wave: params.wave }, target));\n\t\t\t\telse fail('Pass `preset`, `points`, `taper` or `wave`.');\n\t\t\t\tconst node = controller.getNode(target.ids[0]);\n\t\t\t\treturn { ok: true, points: node?.type === 'path' ? (node.strokeWidthProfile ?? []) : [] };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}setWidthPoint`,\n\t\t\tlabel: 'Width Point…',\n\t\t\tkeywords: ['width point', 'variable width', 'stroke width at', 'taper', 'profile'],\n\t\t\tdescription:\n\t\t\t\t'Sets one width point of a path\\'s stroke profile numerically: `index` names an existing point (see `vector.setWidthProfile`\\'s result), leave it out to add one at `t`. `left` / `right` are side widths in document units (how far the stroke reaches on each side), `t` the position along each subpath (0–1); a point moved onto another\\'s `t` makes a step. Returns the profile.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tid: { type: 'string', description: 'The path.' },\n\t\t\t\t\tindex: { type: 'integer', minimum: 0 },\n\t\t\t\t\tt: { type: 'number', minimum: 0, maximum: 1 },\n\t\t\t\t\tleft: { type: 'number', minimum: 0 },\n\t\t\t\t\tright: { type: 'number', minimum: 0 }\n\t\t\t\t},\n\t\t\t\trequired: ['id']\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { id: string; index?: number; t?: number; left?: number; right?: number } | undefined) => {\n\t\t\t\tif (!params?.id) fail('Pass the path `id`.');\n\t\t\t\tcheck(controller.setWidthPoint({ nodeId: params!.id, index: params!.index ?? null }, { t: params!.t, left: params!.left, right: params!.right }));\n\t\t\t\tconst node = controller.getNode(params!.id);\n\t\t\t\treturn { ok: true, points: node?.type === 'path' ? (node.strokeWidthProfile ?? []) : [] };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}outlineStroke`,\n\t\t\tlabel: 'Outline Stroke',\n\t\t\tkeywords: ['expand', 'stroke to path', 'outline', 'convert stroke'],\n\t\t\tdescription: 'Turns the stroke of paths into filled shapes of the area it paints (alignment, caps, joins, dashes and arrowheads included). Returns the new ids.',\n\t\t\tparams: { type: 'object', properties: { ids: IDS } },\n\t\t\tenabled: enabledFor,\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[] } | undefined) => {\n\t\t\t\tconst created = controller.outlineStroke({ ids: params?.ids ? ids(params) : undefined });\n\t\t\t\tif (!created.length) fail('No selected path has a stroke.');\n\t\t\t\treturn { ids: created };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}distort`,\n\t\t\tlabel: 'Free Distort…',\n\t\t\tkeywords: ['distort', 'perspective', 'free distort', 'corners', 'projective', 'keystone'],\n\t\t\tdescription:\n\t\t\t\t'Free Distort: moves the four corners of the selection box (clockwise from the top left, in the box’s own rotation) to `corners` in document units, distorting the nodes in perspective (straight lines stay straight; curves are followed within `tolerance`). The corners must make a convex quadrilateral. One undo step.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tids: IDS,\n\t\t\t\t\tcorners: { type: 'array', items: POINT, minItems: 4, maxItems: 4, description: 'New top-left, top-right, bottom-right and bottom-left corners.' },\n\t\t\t\t\ttolerance: { type: 'number', exclusiveMinimum: 0, description: 'How closely curves follow the distortion (document units, default 0.05).' }\n\t\t\t\t},\n\t\t\t\trequired: ['corners']\n\t\t\t},\n\t\t\tenabled: enabledFor,\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[]; corners: { x: number; y: number }[]; tolerance?: number }) => {\n\t\t\t\tcheck(controller.distort(params.corners, { ids: ids(params), tolerance: params.tolerance }));\n\t\t\t\tconst frame = controller.getState().selectionFrame;\n\t\t\t\treturn { ok: true, bounds: frame ? { x: frame.x, y: frame.y, width: frame.width, height: frame.height } : null };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}skew`,\n\t\t\tlabel: 'Skew…',\n\t\t\tkeywords: ['skew', 'shear', 'slant', 'oblique'],\n\t\t\tdescription:\n\t\t\t\t'Skews nodes along the selection box’s axes: `ax` degrees horizontally (vertical lines lean right for positive angles) and `ay` degrees vertically, about the box centre or `origin`. One undo step.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tids: IDS,\n\t\t\t\t\tax: { type: 'number', minimum: -89.9, maximum: 89.9 },\n\t\t\t\t\tay: { type: 'number', minimum: -89.9, maximum: 89.9 },\n\t\t\t\t\torigin: POINT\n\t\t\t\t}\n\t\t\t},\n\t\t\tenabled: enabledFor,\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[]; ax?: number; ay?: number; origin?: { x: number; y: number } }) => {\n\t\t\t\tcheck(controller.skew(params.ax ?? 0, params.ay ?? 0, { ids: ids(params), origin: params.origin }));\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}envelope`,\n\t\t\tlabel: 'Envelope Distort…',\n\t\t\tkeywords: ['envelope', 'warp', 'mesh', 'arc', 'arch', 'bulge', 'flag', 'wave', 'fish', 'rise', 'fisheye', 'inflate', 'squeeze', 'twist', 'shell', 'bend', 'distort'],\n\t\t\tdescription:\n\t\t\t\t'Envelope distortion that stays editable (a live `envelope` effect). Give `preset` (with `bend`, `horizontal`, `vertical` in percent, −100…100, and `orientation`) to warp, or `mesh` ({ rows, columns } and optionally `points`, the control grid row by row in the node’s own space) for an editable mesh; a node that already has an envelope is changed. `topObject: true` uses the topmost selected closed path as the envelope of the others; `release: true` removes the envelope and gives back its shape as a path; `expand: true` bakes it (Expand Appearance). One undo step.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tids: IDS,\n\t\t\t\t\tpreset: { type: 'string', enum: VECTOR_WARP_STYLES },\n\t\t\t\t\tbend: { type: 'number', minimum: -100, maximum: 100 },\n\t\t\t\t\thorizontal: { type: 'number', minimum: -100, maximum: 100 },\n\t\t\t\t\tvertical: { type: 'number', minimum: -100, maximum: 100 },\n\t\t\t\t\torientation: { type: 'string', enum: ['horizontal', 'vertical'] },\n\t\t\t\t\tmesh: {\n\t\t\t\t\t\ttype: 'object',\n\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\trows: { type: 'integer', minimum: 1, maximum: 16 },\n\t\t\t\t\t\t\tcolumns: { type: 'integer', minimum: 1, maximum: 16 },\n\t\t\t\t\t\t\tpoints: { type: 'array', items: POINT, description: '(3·columns + 1) × (3·rows + 1) control points, row by row (node space).' }\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\ttopObject: { type: 'boolean' },\n\t\t\t\t\trelease: { type: 'boolean' },\n\t\t\t\t\texpand: { type: 'boolean' }\n\t\t\t\t}\n\t\t\t},\n\t\t\tenabled: enabledFor,\n\t\t\tundoable: true,\n\t\t\trun: (\n\t\t\t\t_ctx: unknown,\n\t\t\t\tparams: {\n\t\t\t\t\tids?: string[];\n\t\t\t\t\tpreset?: string;\n\t\t\t\t\tbend?: number;\n\t\t\t\t\thorizontal?: number;\n\t\t\t\t\tvertical?: number;\n\t\t\t\t\torientation?: 'horizontal' | 'vertical';\n\t\t\t\t\tmesh?: { rows?: number; columns?: number; points?: { x: number; y: number }[] };\n\t\t\t\t\ttopObject?: boolean;\n\t\t\t\t\trelease?: boolean;\n\t\t\t\t\texpand?: boolean;\n\t\t\t\t}\n\t\t\t) => {\n\t\t\t\tconst target = ids(params);\n\t\t\t\tif (params.release) return { released: target, shapes: controller.releaseEnvelope({ ids: target }) };\n\t\t\t\tif (params.expand) return { expanded: controller.expandAppearance({ ids: target }) };\n\t\t\t\tif (params.topObject) {\n\t\t\t\t\tconst r = controller.makeEnvelopeWithTopObject({ ids: target, rows: params.mesh?.rows, columns: params.mesh?.columns });\n\t\t\t\t\tcheck(r);\n\t\t\t\t\treturn { nodeId: r.nodeId };\n\t\t\t\t}\n\t\t\t\tif (params.preset !== undefined && !(VECTOR_WARP_STYLES as readonly string[]).includes(params.preset)) fail(`Unknown warp preset \"${params.preset}\".`);\n\t\t\t\tconst patch: Partial<VectorEnvelopeParams> = {};\n\t\t\t\tif (params.preset) patch.style = params.preset as VectorEnvelopeParams['style'];\n\t\t\t\tfor (const k of ['bend', 'horizontal', 'vertical'] as const) if (params[k] !== undefined) patch[k] = params[k];\n\t\t\t\tif (params.orientation) patch.orientation = params.orientation;\n\t\t\t\tif (params.mesh) {\n\t\t\t\t\tif (params.preset) fail('Give either `preset` or `mesh`, not both.');\n\t\t\t\t\tpatch.style = 'mesh';\n\t\t\t\t\tconst columns = params.mesh.columns ?? 2;\n\t\t\t\t\tconst rows = params.mesh.rows ?? 2;\n\t\t\t\t\tpatch.columns = columns;\n\t\t\t\t\tpatch.rows = rows;\n\t\t\t\t\tif (params.mesh.points) {\n\t\t\t\t\t\tconst even = (n: number) => Array.from({ length: n + 1 }, (_, i) => i / n);\n\t\t\t\t\t\tconst mesh = { us: even(columns), vs: even(rows), points: params.mesh.points.map((p) => ({ x: p.x, y: p.y })) };\n\t\t\t\t\t\tif (!isValidVectorMesh(mesh)) fail(`A ${columns} × ${rows} mesh needs ${(3 * columns + 1) * (3 * rows + 1)} finite points.`);\n\t\t\t\t\t\tpatch.mesh = mesh;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst made = controller.envelope(patch, { ids: target });\n\t\t\t\tif (!made.length) fail('Nothing to envelope.');\n\t\t\t\treturn { envelopes: made };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}transform`,\n\t\t\tlabel: 'Transform…',\n\t\t\tkeywords: ['move', 'translate', 'scale', 'resize', 'rotate', 'flip'],\n\t\t\tdescription:\n\t\t\t\t'Moves (dx, dy), scales (sx, sy, along the selection box axes) and/or rotates (`rotate`: by degrees clockwise around the combined centre; `angle`: to an absolute rotation, each node around its own centre), in that order.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tids: IDS,\n\t\t\t\t\tdx: { type: 'number' },\n\t\t\t\t\tdy: { type: 'number' },\n\t\t\t\t\tsx: { type: 'number' },\n\t\t\t\t\tsy: { type: 'number' },\n\t\t\t\t\trotate: { type: 'number' },\n\t\t\t\t\tangle: { type: 'number' }\n\t\t\t\t}\n\t\t\t},\n\t\t\tenabled: enabledFor,\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[]; dx?: number; dy?: number; sx?: number; sy?: number; rotate?: number; angle?: number }) => {\n\t\t\t\tconst target = ids(params);\n\t\t\t\tif (params.dx || params.dy) check(controller.translate(params.dx ?? 0, params.dy ?? 0, { ids: target }));\n\t\t\t\tif (params.sx !== undefined || params.sy !== undefined) check(controller.scale(params.sx ?? 1, params.sy ?? params.sx ?? 1, { ids: target }));\n\t\t\t\tif (params.rotate) check(controller.rotate(params.rotate, { ids: target }));\n\t\t\t\tif (params.angle !== undefined) check(controller.setRotation(params.angle, { ids: target }));\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}delete`,\n\t\t\tlabel: 'Delete',\n\t\t\tkeywords: ['remove'],\n\t\t\tdescription: 'Deletes nodes.',\n\t\t\tparams: { type: 'object', properties: { ids: IDS } },\n\t\t\tenabled: enabledFor,\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[] } | undefined) => {\n\t\t\t\tcheck(controller.removeNodes(ids(params)));\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}boolean`,\n\t\t\tlabel: 'Boolean Operation…',\n\t\t\tkeywords: ['union', 'unite', 'subtract', 'intersect', 'exclude', 'combine', 'pathfinder'],\n\t\t\tdescription:\n\t\t\t\t'Combines two or more paths with union, subtract (upper shapes from the lowest), intersect or exclude. With `live`, the shapes stay editable operands of a live boolean group instead (a lone live group gets the operation; its operands get their own).',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\toperation: { type: 'string', enum: ['union', 'subtract', 'intersect', 'exclude'] },\n\t\t\t\t\tids: IDS,\n\t\t\t\t\tlive: { type: 'boolean', description: 'Keep the operands editable in a live boolean group (default false: one new path).' }\n\t\t\t\t},\n\t\t\t\trequired: ['operation']\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { operation: VectorBooleanOperation; ids?: string[]; live?: boolean }) => {\n\t\t\t\tconst id = controller.booleanOp(params.operation, { ids: ids(params), live: params.live });\n\t\t\t\tif (!id) fail(params.live ? 'Select two or more nodes with one parent, a live boolean group or its operands.' : 'Select at least two overlapping paths (the result was empty).');\n\t\t\t\treturn { id };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}addEffect`,\n\t\t\tlabel: 'Add Path Effect…',\n\t\t\tkeywords: ['effect', 'trim', 'round corners', 'pucker', 'bloat', 'twist', 'zig zag', 'wiggle', 'offset', 'merge', 'repeater', 'appearance', 'live'],\n\t\t\tdescription: 'Adds a live path effect to the bottom of the effect stack of nodes (paths or groups). Effects apply top to bottom; effects after a repeater act on its copies. Returns the new effect ids.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\ttype: { type: 'string', enum: VECTOR_EFFECT_TYPES },\n\t\t\t\t\tparams: { type: 'object', description: EFFECT_PARAMS_DESCRIPTION },\n\t\t\t\t\tindex: { type: 'integer', minimum: 0, description: 'Position in the stack (default: last).' },\n\t\t\t\t\tids: IDS\n\t\t\t\t},\n\t\t\t\trequired: ['type']\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\tenabled: enabledFor,\n\t\t\trun: (_ctx: unknown, params: { type: VectorEffectType; params?: Record<string, unknown>; index?: number; ids?: string[] }) => {\n\t\t\t\tif (!VECTOR_EFFECT_TYPES.includes(params.type)) fail(`Unknown effect type \"${params.type}\".`);\n\t\t\t\tconst targets = ids(params);\n\t\t\t\tconst created = controller.addEffect(params.type, { ids: targets, params: params.params, index: params.index });\n\t\t\t\tif (!created.length) fail('Nothing to add the effect to.');\n\t\t\t\treturn { effects: targets.map((id, i) => ({ nodeId: id, effectId: created[i] })) };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}updateEffect`,\n\t\t\tlabel: 'Edit Path Effect…',\n\t\t\tkeywords: ['effect', 'parameters', 'enable', 'disable', 'reorder'],\n\t\t\tdescription: 'Changes a path effect: its parameters, whether it is enabled, and its position in the stack.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tnodeId: { type: 'string' },\n\t\t\t\t\teffectId: { type: 'string' },\n\t\t\t\t\tparams: { type: 'object', description: EFFECT_PARAMS_DESCRIPTION },\n\t\t\t\t\tenabled: { type: 'boolean' },\n\t\t\t\t\tindex: { type: 'integer', minimum: 0, description: 'Move it to this position in the stack (0 applies first).' }\n\t\t\t\t},\n\t\t\t\trequired: ['nodeId', 'effectId']\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { nodeId: string; effectId: string; params?: Record<string, unknown>; enabled?: boolean; index?: number }) => {\n\t\t\t\tif (params.params !== undefined || params.enabled !== undefined) check(controller.updateEffect(params.nodeId, params.effectId, { params: params.params, enabled: params.enabled }));\n\t\t\t\tif (params.index !== undefined) check(controller.moveEffect(params.nodeId, params.effectId, params.index));\n\t\t\t\treturn { effects: controller.getNode(params.nodeId)?.effects ?? [] };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}removeEffect`,\n\t\t\tlabel: 'Remove Path Effect',\n\t\t\tkeywords: ['effect', 'delete'],\n\t\t\tdescription: 'Removes a path effect from a node.',\n\t\t\tparams: { type: 'object', properties: { nodeId: { type: 'string' }, effectId: { type: 'string' } }, required: ['nodeId', 'effectId'] },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { nodeId: string; effectId: string }) => {\n\t\t\t\tcheck(controller.removeEffect(params.nodeId, params.effectId));\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}expandAppearance`,\n\t\t\tlabel: 'Expand Appearance',\n\t\t\tkeywords: ['effects', 'bake', 'flatten', 'live boolean', 'expand'],\n\t\t\tdescription: 'Bakes the path effects and live booleans of nodes (and everything inside them) into plain paths.',\n\t\t\tparams: { type: 'object', properties: { ids: IDS } },\n\t\t\tundoable: true,\n\t\t\tenabled: enabledFor,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[] } | undefined) => {\n\t\t\t\tconst expanded = controller.expandAppearance({ ids: ids(params) });\n\t\t\t\tif (!expanded.length) fail('Nothing selected has path effects or a live boolean.');\n\t\t\t\treturn { ids: expanded };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}blend`,\n\t\t\tlabel: 'Make Blend…',\n\t\t\tkeywords: ['blend', 'morph', 'tween', 'interpolate', 'in-between', 'steps', 'transition'],\n\t\t\tdescription:\n\t\t\t\t'Blends two or more paths or groups (sharing a parent; bottom to top, or in the order given with keepOrder) into a live blend group: the in-betweens morph shape (anchors matched by arc length, the start and winding that twist least, rotation-aware so turning shapes turn) and paint (OKLab colours, gradients, stroke width, opacity, width profiles), and regenerate whenever a key changes. Spacing: `steps` (count between each pair of keys), `distance` (along the spine) or `smoothColor`. `starts` maps key ids to the anchor of their first subpath the blend starts from. Returns the blend id.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tids: IDS,\n\t\t\t\t\t...BLEND_PROPS,\n\t\t\t\t\tkeepOrder: { type: 'boolean', description: 'Blend in the order of `ids` instead of stacking order.' },\n\t\t\t\t\tstarts: { type: 'object', additionalProperties: { type: 'integer', minimum: 0 }, description: 'Start anchor per key id.' }\n\t\t\t\t}\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: (VectorBlendOptions & { ids?: string[]; keepOrder?: boolean; starts?: Record<string, number> }) | undefined) => {\n\t\t\t\tcheckBlendOptions(params);\n\t\t\t\tconst id = controller.blend({ ...params, ids: ids(params) });\n\t\t\t\tif (!id) fail('Select two or more shapes that share a parent.');\n\t\t\t\treturn { id };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}blendOptions`,\n\t\t\tlabel: 'Blend Options…',\n\t\t\tkeywords: ['blend', 'steps', 'spacing', 'distance', 'smooth color', 'orientation', 'align to path'],\n\t\t\tdescription: 'Changes the spacing (steps, distance or smoothColor), step count, distance, orient-to-spine and linear morph of the given (or selected) blends; their steps regenerate.',\n\t\t\tparams: { type: 'object', properties: { ids: IDS, ...BLEND_PROPS } },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: (VectorBlendOptions & { ids?: string[] }) | undefined) => {\n\t\t\t\tcheckBlendOptions(params);\n\t\t\t\tconst r = controller.setBlendOptions(params ?? {}, { ids: params?.ids });\n\t\t\t\tif (!r.ok) fail(r.message);\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}expandBlend`,\n\t\t\tlabel: 'Expand Blend',\n\t\t\tkeywords: ['blend', 'expand', 'flatten', 'bake'],\n\t\t\tdescription: 'Expands the given (or selected) blends into plain groups of their keys and in-betweens (editable paths with fresh ids). Returns the group ids.',\n\t\t\tparams: { type: 'object', properties: { ids: IDS } },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[] } | undefined) => {\n\t\t\t\tconst groups = controller.expandBlend({ ids: params?.ids });\n\t\t\t\tif (!groups.length) fail('Select a blend to expand.');\n\t\t\t\treturn { ids: groups };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}releaseBlend`,\n\t\t\tlabel: 'Release Blend',\n\t\t\tkeywords: ['blend', 'release', 'undo blend'],\n\t\t\tdescription: 'Releases the given (or selected) blends: the in-betweens go and the keys (plus a replaced spine, as an unpainted path) take the blend\\'s place. Returns their ids.',\n\t\t\tparams: { type: 'object', properties: { ids: IDS } },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[] } | undefined) => {\n\t\t\t\tconst released = controller.releaseBlend({ ids: params?.ids });\n\t\t\t\tif (!released.length) fail('Select a blend to release.');\n\t\t\t\treturn { ids: released };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}replaceSpine`,\n\t\t\tlabel: 'Replace Spine',\n\t\t\tkeywords: ['blend', 'spine', 'path', 'follow', 'along path'],\n\t\t\tdescription: 'With one blend and one path given (or selected): the path becomes the blend\\'s spine (it is removed) and the keys move onto it, spread evenly by arc length; the in-betweens follow it.',\n\t\t\tparams: { type: 'object', properties: { ids: IDS } },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[] } | undefined) => {\n\t\t\t\tconst r = controller.replaceBlendSpine({ ids: params?.ids });\n\t\t\t\tif (!r.ok) fail(r.message);\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}pathfinder`,\n\t\t\tlabel: 'Pathfinder…',\n\t\t\tkeywords: ['divide', 'trim', 'merge', 'crop', 'outline', 'minus back', 'split', 'faces'],\n\t\t\tdescription:\n\t\t\t\t'Pathfinder over the given paths (bottom to top): divide (every face its own shape, painted like the top-most shape over it; open unfilled paths cut), trim (hidden parts removed), merge (trim, then touching parts of one fill joined), crop (the top shape crops the others), outline (outlines cut into open strokes where they meet) or minusBack (the top shape minus everything below). Results replace the paths, grouped when there are several.',\n\t\t\tparams: { type: 'object', properties: { operation: { type: 'string', enum: [...VECTOR_PATHFINDER_OPERATIONS] }, ids: IDS }, required: ['operation'] },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { operation: VectorPathfinderOperation; ids?: string[] }) => {\n\t\t\t\tif (!VECTOR_PATHFINDER_OPERATIONS.includes(params.operation)) fail(`Unknown Pathfinder operation \"${params.operation}\".`);\n\t\t\t\tconst created = controller.pathfinder(params.operation, { ids: ids(params) });\n\t\t\t\tif (!created.length) fail('Nothing came out: select paths that overlap (two or more, except for divide and outline).');\n\t\t\t\treturn { ids: created, selection: [...controller.getState().selection] };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}shapeBuilderFaces`,\n\t\t\tlabel: 'List Shape Builder Faces',\n\t\t\tkeywords: ['shape builder', 'regions', 'faces', 'planar'],\n\t\t\tdescription: 'Lists the faces the given paths divide the plane into: path data, area, a point inside and the ids of the paths filling each (empty when only outlines enclose it).',\n\t\t\tparams: { type: 'object', properties: { ids: IDS } },\n\t\t\trun: (_ctx: unknown, params: { ids?: string[] } | undefined) => ({ faces: controller.getShapeBuilderFaces({ ids: ids(params) }) })\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}shapeBuilder`,\n\t\t\tlabel: 'Shape Builder',\n\t\t\tkeywords: ['merge faces', 'build shape', 'combine regions', 'delete region'],\n\t\t\tdescription:\n\t\t\t\t'Shape Builder gesture over the faces of the given paths: `points` is the pointer path in document units (one point is a click; [[x, y], …]; use the points from shapeBuilderFaces). Merges the faces it touches into one shape, or with `erase: true` deletes them, the pieces of unfilled open paths and the edges of other outlines (cut where paths meet) within `tolerance`; a deleted edge opens its path there, and edges bounding a deleted face go with it. Shapes covering those faces lose that area. Returns the counts of faces, line pieces and edges.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: { points: POINTS, erase: { type: 'boolean' }, tolerance: { type: 'number', minimum: 0 }, ids: IDS },\n\t\t\t\trequired: ['points']\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { points: [number, number][]; erase?: boolean; tolerance?: number; ids?: string[] }) => {\n\t\t\t\tconst result = controller.shapeBuilder(params.points.map(([x, y]) => ({ x, y })), { ids: ids(params), erase: params.erase, tolerance: params.tolerance });\n\t\t\t\tcheck(result);\n\t\t\t\treturn { merged: result.merged, faces: result.faces, lines: result.lines, edges: result.edges };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}fillRegion`,\n\t\t\tlabel: 'Fill Region',\n\t\t\tkeywords: ['paint bucket', 'live paint', 'fill area', 'enclosed'],\n\t\t\tdescription:\n\t\t\t\t'Paint bucket: fills the area the visible outlines enclose around `point` (crossings split, nested shapes left as holes) with a new path in the current fill, below the selection or on top. `gap` closes gaps up to that size between loose ends.',\n\t\t\tparams: { type: 'object', properties: { point: { type: 'array', items: { type: 'number' }, minItems: 2, maxItems: 2, description: '[x, y] in document units.' }, gap: { type: 'number', minimum: 0 } }, required: ['point'] },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { point: [number, number]; gap?: number }) => {\n\t\t\t\tconst id = controller.fillRegion({ x: params.point[0], y: params.point[1] }, { gap: params.gap });\n\t\t\t\tif (!id) fail('No closed region surrounds that point.');\n\t\t\t\treturn { id };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}align`,\n\t\t\tlabel: 'Align…',\n\t\t\tkeywords: ['left', 'right', 'center', 'top', 'bottom', 'middle'],\n\t\t\tdescription: 'Aligns nodes to their combined bounds, or to the artboard (`to: \"artboard\"`, or when only one node is given).',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: { edge: { type: 'string', enum: ['left', 'center', 'right', 'top', 'middle', 'bottom'] }, to: { type: 'string', enum: ['selection', 'artboard'] }, ids: IDS },\n\t\t\t\trequired: ['edge']\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { edge: VectorAlign; to?: 'selection' | 'artboard'; ids?: string[] }) => {\n\t\t\t\tcheck(controller.align(params.edge, { ids: ids(params), to: params.to }));\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}distribute`,\n\t\t\tlabel: 'Distribute…',\n\t\t\tkeywords: ['space', 'evenly', 'gaps'],\n\t\t\tdescription: 'Spaces three or more nodes with equal gaps.',\n\t\t\tparams: { type: 'object', properties: { direction: { type: 'string', enum: ['horizontal', 'vertical'] }, ids: IDS }, required: ['direction'] },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { direction: 'horizontal' | 'vertical'; ids?: string[] }) => {\n\t\t\t\tcheck(controller.distribute(params.direction, { ids: ids(params) }));\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}arrange`,\n\t\t\tlabel: 'Arrange…',\n\t\t\tkeywords: ['order', 'front', 'back', 'forward', 'backward', 'z-index', 'stack'],\n\t\t\tdescription: 'Changes stacking order within each node’s parent.',\n\t\t\tparams: { type: 'object', properties: { order: { type: 'string', enum: ['front', 'forward', 'backward', 'back'] }, ids: IDS }, required: ['order'] },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { order: VectorArrange; ids?: string[] }) => {\n\t\t\t\tcheck(controller.arrange(params.order, { ids: ids(params) }));\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}group`,\n\t\t\tlabel: 'Group',\n\t\t\tshortcut: 'Mod+G',\n\t\t\tdescription: 'Groups nodes that share a parent.',\n\t\t\tparams: { type: 'object', properties: { ids: IDS, name: { type: 'string' } } },\n\t\t\tenabled: enabledFor,\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[]; name?: string } | undefined) => {\n\t\t\t\tconst id = controller.group({ ids: ids(params), name: params?.name });\n\t\t\t\tif (!id) fail('Only nodes that share a parent can be grouped.');\n\t\t\t\treturn { id };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}ungroup`,\n\t\t\tlabel: 'Ungroup',\n\t\t\tshortcut: 'Mod+Shift+G',\n\t\t\tdescription: 'Replaces groups with their children.',\n\t\t\tparams: { type: 'object', properties: { ids: IDS } },\n\t\t\tenabled: enabledFor,\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[] } | undefined) => {\n\t\t\t\tcheck(controller.ungroup({ ids: ids(params) }));\n\t\t\t\treturn { selection: controller.getState().selection };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}deleteAnchors`,\n\t\t\tlabel: 'Delete Points',\n\t\t\tkeywords: ['remove', 'anchor', 'point', 'node'],\n\t\t\tdescription:\n\t\t\t\t'Deletes anchors. With `keepShape` (default: the editor setting, on) the neighbouring handles are refitted so the outline keeps its shape; off, the neighbours are joined as they are.',\n\t\t\tparams: { type: 'object', properties: { anchors: ANCHORS, keepShape: { type: 'boolean' } } },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { anchors?: VectorAnchorRef[]; keepShape?: boolean } | undefined) => {\n\t\t\t\tcheck(controller.deleteAnchors(anchorsOf(params?.anchors), { keepShape: params?.keepShape }));\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}moveAnchor`,\n\t\t\tlabel: 'Move Point…',\n\t\t\tkeywords: ['anchor', 'drag', 'curvature'],\n\t\t\tdescription: 'Moves one anchor to (x, y) in document units. `curvature: true` bends its two segments like the Curvature tool instead of carrying its handles along.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: { nodeId: { type: 'string' }, subpath: { type: 'integer', minimum: 0 }, index: { type: 'integer', minimum: 0 }, x: { type: 'number' }, y: { type: 'number' }, curvature: { type: 'boolean' } },\n\t\t\t\trequired: ['nodeId', 'subpath', 'index', 'x', 'y']\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: VectorAnchorRef & { x: number; y: number; curvature?: boolean }) => {\n\t\t\t\tcheck(controller.moveAnchor({ nodeId: params.nodeId, subpath: params.subpath, index: params.index }, { x: params.x, y: params.y }, { curvature: params.curvature }));\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}bendSegment`,\n\t\t\tlabel: 'Bend Segment…',\n\t\t\tkeywords: ['curve', 'segment', 'drag'],\n\t\t\tdescription: 'Bends segment `segment` (from anchor `segment` to the next) so the curve passes through (x, y) at parameter `t` (default 0.5).',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tnodeId: { type: 'string' },\n\t\t\t\t\tsubpath: { type: 'integer', minimum: 0 },\n\t\t\t\t\tsegment: { type: 'integer', minimum: 0 },\n\t\t\t\t\tx: { type: 'number' },\n\t\t\t\t\ty: { type: 'number' },\n\t\t\t\t\tt: { type: 'number', minimum: 0, maximum: 1 }\n\t\t\t\t},\n\t\t\t\trequired: ['nodeId', 'subpath', 'segment', 'x', 'y']\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { nodeId: string; subpath: number; segment: number; x: number; y: number; t?: number }) => {\n\t\t\t\tcheck(controller.bendSegment({ nodeId: params.nodeId, subpath: params.subpath, segment: params.segment }, { x: params.x, y: params.y }, params.t));\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}join`,\n\t\t\tlabel: 'Join',\n\t\t\tshortcut: 'Mod+J',\n\t\t\tkeywords: ['connect', 'close', 'merge', 'endpoints'],\n\t\t\tdescription: 'Joins the open subpaths of paths nearest endpoints first (ends within `tolerance` merge); a single open path is closed.',\n\t\t\tparams: { type: 'object', properties: { ids: IDS, tolerance: { type: 'number', minimum: 0 } } },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[]; tolerance?: number } | undefined) => {\n\t\t\t\tcheck(controller.joinPaths({ ids: params?.ids ? ids(params) : undefined, tolerance: params?.tolerance }));\n\t\t\t\treturn { selection: controller.getState().selection };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}average`,\n\t\t\tlabel: 'Average Points…',\n\t\t\tshortcut: 'Mod+Alt+J',\n\t\t\tkeywords: ['align', 'points', 'anchors', 'collapse'],\n\t\t\tdescription: 'Moves anchors onto their average position: the same y (`horizontal`), the same x (`vertical`) or the same point (`both`, default).',\n\t\t\tparams: { type: 'object', properties: { axis: { type: 'string', enum: ['horizontal', 'vertical', 'both'] }, anchors: ANCHORS } },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { axis?: VectorAverageAxis; anchors?: VectorAnchorRef[] } | undefined) => {\n\t\t\t\tcheck(controller.averageAnchors(params?.axis ?? 'both', anchorsOf(params?.anchors)));\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}removeRedundantPoints`,\n\t\t\tlabel: 'Remove Redundant Points',\n\t\t\tkeywords: ['simplify', 'clean up', 'anchors', 'collinear'],\n\t\t\tdescription: 'Drops anchors that can go without moving the outline more than `tolerance` document units (default 0.5).',\n\t\t\tparams: { type: 'object', properties: { ids: IDS, tolerance: { type: 'number', minimum: 0 } } },\n\t\t\tenabled: enabledFor,\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[]; tolerance?: number } | undefined) => {\n\t\t\t\tcheck(controller.removeRedundantPoints({ ids: params?.ids ? ids(params) : undefined, tolerance: params?.tolerance }));\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}smooth`,\n\t\t\tlabel: 'Smooth Path',\n\t\t\tkeywords: ['round', 'soften', 'handles', 'curves'],\n\t\t\tdescription: 'Pulls the handles of paths (or of the selected points) towards smooth tangents by `amount` (0–1, default 1).',\n\t\t\tparams: { type: 'object', properties: { ids: IDS, amount: { type: 'number', minimum: 0, maximum: 1 }, anchors: ANCHORS } },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[]; amount?: number; anchors?: VectorAnchorRef[] } | undefined) => {\n\t\t\t\tcheck(controller.smoothPaths({ ids: params?.ids ? ids(params) : undefined, amount: params?.amount, refs: params?.anchors }));\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}cut`,\n\t\t\tlabel: 'Cut Path (Scissors)…',\n\t\t\tkeywords: ['scissors', 'split', 'open', 'break'],\n\t\t\tdescription: 'Scissors: cuts path `nodeId` at the outline point nearest (x, y), or cuts at `anchors`. A closed path opens; an open one splits in two.',\n\t\t\tparams: { type: 'object', properties: { nodeId: { type: 'string' }, x: { type: 'number' }, y: { type: 'number' }, anchors: ANCHORS } },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { nodeId?: string; x?: number; y?: number; anchors?: VectorAnchorRef[] } | undefined) => {\n\t\t\t\tif (params?.nodeId !== undefined) {\n\t\t\t\t\tif (!controller.getNode(params.nodeId)) fail(`There is no node \"${params.nodeId}\".`);\n\t\t\t\t\tif (params.x === undefined || params.y === undefined) fail('Pass x and y with nodeId.');\n\t\t\t\t\tconst created = controller.cutPathAt(params.nodeId, { x: params.x!, y: params.y! });\n\t\t\t\t\tif (!created) fail('Nothing to cut there (the end of an open path, or a locked path).');\n\t\t\t\t\treturn { ids: created };\n\t\t\t\t}\n\t\t\t\tcheck(controller.cutAtAnchors(anchorsOf(params?.anchors)));\n\t\t\t\treturn { ids: controller.getState().selection };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}knife`,\n\t\t\tlabel: 'Knife…',\n\t\t\tkeywords: ['slice', 'cut', 'split', 'divide'],\n\t\t\tdescription: 'Cuts closed paths along a polyline wherever it crosses them edge to edge (no gap between pieces). Cuts `ids`, else the selection, else every path the line crosses.',\n\t\t\tparams: { type: 'object', properties: { points: { ...POINTS, minItems: 2 }, ids: IDS }, required: ['points'] },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { points: [number, number][]; ids?: string[] }) => {\n\t\t\t\tconst created = controller.knife(params.points.map(([x, y]) => ({ x, y })), { ids: params.ids ? ids(params) : undefined });\n\t\t\t\tif (!created.length) fail('The knife did not cross any closed path from edge to edge.');\n\t\t\t\treturn { ids: created };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}erase`,\n\t\t\tlabel: 'Erase…',\n\t\t\tkeywords: ['eraser', 'rub out', 'remove'],\n\t\t\tdescription:\n\t\t\t\t'Erases a round stroke of diameter `size` (default: the eraser size setting) along a polyline: filled shapes lose the area, outlines are cut under it. Erases from `ids`, else the selection, else every path it reaches.',\n\t\t\tparams: { type: 'object', properties: { points: POINTS, size: { type: 'number', exclusiveMinimum: 0 }, ids: IDS }, required: ['points'] },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { points: [number, number][]; size?: number; ids?: string[] }) => {\n\t\t\t\tconst changed = controller.erase(params.points.map(([x, y]) => ({ x, y })), { size: params.size, ids: params.ids ? ids(params) : undefined });\n\t\t\t\tif (!changed.length) fail('The eraser did not touch any path.');\n\t\t\t\treturn { ids: changed.filter((id) => controller.getNode(id)) };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}setEditing`,\n\t\t\tlabel: 'Path Editing Options…',\n\t\t\tkeywords: ['settings', 'curvature', 'pencil', 'smoothing', 'lazy mouse', 'shapes', 'eraser'],\n\t\t\tdescription:\n\t\t\t\t'Sets how editing behaves: `keepShapeOnDelete`, `curvatureMoves` (anchor drags bend like the Curvature tool), `pencilSmoothing` (lazy mouse, 0–1), `recognizeShapes` (pencil scribbles become lines, rectangles, ellipses, polygons), `eraserSize` and `pencilPressure` (`pen`: record pen pressure on pencil strokes as a width profile, `all`: any pointer reporting pressure, `off`).',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tkeepShapeOnDelete: { type: 'boolean' },\n\t\t\t\t\tcurvatureMoves: { type: 'boolean' },\n\t\t\t\t\tpencilSmoothing: { type: 'number', minimum: 0, maximum: 1 },\n\t\t\t\t\trecognizeShapes: { type: 'boolean' },\n\t\t\t\t\teraserSize: { type: 'number', exclusiveMinimum: 0 },\n\t\t\t\t\tpencilPressure: { type: 'string', enum: ['pen', 'all', 'off'] }\n\t\t\t\t}\n\t\t\t},\n\t\t\trun: (_ctx: unknown, params: Record<string, unknown> | undefined) => {\n\t\t\t\tconst patch: Record<string, unknown> = {};\n\t\t\t\tfor (const key of ['keepShapeOnDelete', 'curvatureMoves', 'pencilSmoothing', 'recognizeShapes', 'eraserSize', 'pencilPressure']) if (params?.[key] !== undefined) patch[key] = params[key];\n\t\t\t\tcontroller.setEditing(patch);\n\t\t\t\treturn { editing: controller.getState().editing };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}setTool`,\n\t\t\tlabel: 'Choose Tool…',\n\t\t\tkeywords: ['pen', 'pencil', 'select', 'rectangle', 'ellipse'],\n\t\t\tdescription: 'Switches the canvas tool.',\n\t\t\tparams: { type: 'object', properties: { tool: { type: 'string', enum: [...VECTOR_TOOLS] } }, required: ['tool'] },\n\t\t\trun: (_ctx: unknown, params: { tool: VectorTool }) => {\n\t\t\t\tcontroller.setTool(params.tool);\n\t\t\t\treturn { tool: params.tool };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}exportSvg`,\n\t\t\tlabel: 'Export SVG',\n\t\t\treadOnly: true,\n\t\t\tkeywords: ['download', 'save', 'markup'],\n\t\t\tdescription: 'Returns the document as SVG markup.',\n\t\t\tparams: { type: 'object', properties: { precision: { type: 'integer', minimum: 0, maximum: 8 } } },\n\t\t\trun: (_ctx: unknown, params: { precision?: number } | undefined) => ({ svg: controller.toSvg({ precision: params?.precision }) })\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}importSvg`,\n\t\t\tlabel: 'Import SVG…',\n\t\t\tkeywords: ['open', 'load', 'paste', 'markup'],\n\t\t\tdescription: 'Adds the shapes of SVG markup on top of the document (or replaces it with `replace: true`).',\n\t\t\tparams: { type: 'object', properties: { svg: { type: 'string' }, replace: { type: 'boolean' } }, required: ['svg'] },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { svg: string; replace?: boolean }) => {\n\t\t\t\tcheck(params.replace ? controller.loadSvg(params.svg) : controller.importSvg(params.svg));\n\t\t\t\treturn { nodes: controller.getState().document.nodes.length, selection: controller.getState().selection };\n\t\t\t}\n\t\t}\n\t];\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/editor/editor/packages/core/dist/vector/index.cjs","../../src/vector/gradientTool.ts","../../src/vector/blendGroup.ts","../../src/vector/constraints/lm.ts","../../src/vector/constraints/expr.ts","../../src/vector/constraints/solve.ts","../../src/vector/constraints/auto.ts","../../src/vector/distort.ts","../../src/vector/snap.ts","../../src/vector/pathEdit.ts","../../src/vector/ink.ts","../../src/vector/builder.ts","../../src/vector/controller.ts","../../src/vector/constraints/glyphs.ts","../../src/vector/render.ts","../../src/vector/commands.ts","../../src/vector/textPanel.ts","../../src/vector/symbolsPanel.ts"],"names":["len","t","v","e","lines","cross","r","key","node","finish","stroke","fmt","corners","pt","list","STROKE_KEYS"],"mappings":"AAAA;AACE;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B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AAA;AACU,IAAA;AACA,IAAA;AACD,IAAA;AACE,EAAA;AACtD,EAAA;AAC0B,IAAA;AACkC,IAAA;AACjE,EAAA;AACD;AAG4I;AACzG,EAAA;AACI,EAAA;AAC1B,EAAA;AACqB,EAAA;AACqB,IAAA;AACrD,IAAA;AACD,EAAA;AACyD,EAAA;AAC1D;AhBs3b6D;AACA;AiB/8bZ;AAG4E;AAC/E,EAAA;AACN,EAAA;AACQ,EAAA;AACxB,IAAA;AAC8B,MAAA;AACX,MAAA;AACzC,IAAA;AACD,EAAA;AACgB,EAAA;AACiC,EAAA;AACd,EAAA;AACqC,EAAA;AAChB,IAAA;AACN,IAAA;AACI,IAAA;AAC9C,IAAA;AACA,MAAA;AACE,MAAA;AACuB,MAAA;AACqB,MAAA;AACpD,MAAA;AAC0C,MAAA;AAC3C,IAAA;AACA,EAAA;AACyD,EAAA;AACY,EAAA;AAC/D,EAAA;AACN,IAAA;AACyD,IAAA;AAEd,IAAA;AAEc,IAAA;AAC1D,EAAA;AACD;AAOgK;AAC/I,EAAA;AACmB,EAAA;AAC8C,EAAA;AACvB,EAAA;AAC9C,EAAA;AAC+C,EAAA;AAC5D;AjBu8b6D;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/editor/editor/packages/core/dist/vector/index.cjs","sourcesContent":[null,"/**\n * The Gradient tool's on-canvas annotator: a bar from the start (or centre) handle to the end (or\n * radius) handle with the colour stops under it and midpoint diamonds above it, plus a radial,\n * conic or diamond gradient's extent outline, aspect handle and (radial) focal point. All in\n * screen pixels, for hit-testing and the overlay.\n *\n * After vectorcraft `crates/tools/src/xform/gradient.rs` (`Annotator`: stop chips below the bar,\n * midpoints above it, a stop dragged off the bar is removed, a click on the bar adds one), ArtCraft,\n * MIT OR Apache-2.0 — see THIRD_PARTY_NOTICES.md.\n */\nimport { vectorTargetPaint } from './paintLayers.js';\nimport { collectPathNodes, findVectorNode, vectorWorldMatrix, IDENTITY_MATRIX, multiplyMatrices, type AffineMatrix } from './geometry.js';\nimport { vectorPathLocalBounds } from './svg.js';\nimport {\n\tisCentredVectorGradient,\n\tisVectorGradient,\n\tsortedVectorGradientStops,\n\tvectorGradientHandles,\n\tvectorGradientMatrix,\n\tvectorGradientMidpointPosition,\n\ttype VectorGradientHandle\n} from './gradient.js';\nimport type { Vec2, VectorBounds, VectorDocument, VectorGradient, VectorGradientStop, VectorPathNode, VectorViewport } from './types.js';\n\n/** Which paint of the selection the Gradient tool and the gradient editor work on. */\nexport type VectorGradientTarget = 'fill' | 'stroke';\n\n/** A part of the annotator under the pointer. */\nexport type VectorGradientPart =\n\t| { kind: 'handle'; handle: VectorGradientHandle }\n\t| { kind: 'stop'; index: number }\n\t| { kind: 'midpoint'; index: number }\n\t/** The bar at `offset` (0–1): a click adds a stop there, a drag moves the gradient. */\n\t| { kind: 'bar'; offset: number };\n\n/** Distance of the stop chips below the bar, the midpoint diamonds above it (screen pixels). */\nexport const VECTOR_GRADIENT_STOP_GAP = 12;\nexport const VECTOR_GRADIENT_MIDPOINT_GAP = 9;\n/** How far off the bar a dragged stop goes before it is removed (screen pixels). */\nexport const VECTOR_GRADIENT_OFF_BAR = 24;\nconst STOP_HIT = 6;\nconst MIDPOINT_HIT = 5;\nconst HANDLE_HIT = 6;\nconst BAR_HIT = 5;\n\nexport interface VectorGradientAnnotator {\n\tnodeId: string;\n\ttarget: VectorGradientTarget;\n\tgradient: VectorGradient;\n\t/** The path's box in its own space. */\n\tbounds: VectorBounds | null;\n\t/** The path's own space → document. */\n\tworld: AffineMatrix;\n\t/** Screen positions of the handles. */\n\tstart: Vec2;\n\tend: Vec2;\n\t/** Radial, conic and diamond gradients. */\n\taspect: Vec2 | null;\n\t/** Radial gradients. */\n\tfocal: Vec2 | null;\n\t/** Unit normal to the bar towards the stop chips (screen). */\n\tnormal: Vec2;\n\t/** Stops (sorted, as the gradient stores them) with their chips' screen positions. */\n\tstops: (VectorGradientStop & { index: number; x: number; y: number })[];\n\t/** The midpoint diamond after each stop but the last (screen). */\n\tmidpoints: { index: number; x: number; y: number; midpoint: number }[];\n\t/** Screen path of the extent: an ellipse (radial, conic) or a rhombus (diamond). */\n\textent: string | null;\n}\n\nconst fmt = (n: number) => Math.round(n * 100) / 100;\nconst apply = (m: AffineMatrix, p: Vec2): Vec2 => ({ x: m[0] * p.x + m[2] * p.y + m[4], y: m[1] * p.x + m[3] * p.y + m[5] });\n\n/** The path whose `target` paint the annotator shows: the first path of the selection with a gradient there. */\nexport function vectorGradientTargetPath(doc: VectorDocument, selection: readonly string[], target: VectorGradientTarget, layer: number | null = null): VectorPathNode | null {\n\tfor (const id of selection) {\n\t\tconst node = findVectorNode(doc.nodes, id);\n\t\tif (!node) continue;\n\t\tconst path = collectPathNodes(node).find((p) => isVectorGradient(vectorTargetPaint(p, target, layer)));\n\t\tif (path) return path;\n\t}\n\treturn null;\n}\n\n/** The annotator of the selection's gradient (`target` paint), in screen pixels, or null when it has none. */\nexport function getVectorGradientAnnotator(\n\tdoc: VectorDocument,\n\tselection: readonly string[],\n\tviewport: VectorViewport,\n\ttarget: VectorGradientTarget,\n\tlayer: number | null = null\n): VectorGradientAnnotator | null {\n\tconst path = vectorGradientTargetPath(doc, selection, target, layer);\n\tif (!path) return null;\n\tconst gradient = vectorTargetPaint(path, target, layer) as VectorGradient;\n\tconst world = vectorWorldMatrix(doc.nodes, path.id) ?? IDENTITY_MATRIX;\n\tconst screen = multiplyMatrices(world, [viewport.zoom, 0, 0, viewport.zoom, viewport.x, viewport.y]);\n\tconst bounds = vectorPathLocalBounds(path);\n\tconst h = vectorGradientHandles(gradient, bounds, screen);\n\tconst v = { x: h.end.x - h.start.x, y: h.end.y - h.start.y };\n\tconst len = Math.hypot(v.x, v.y);\n\tconst normal = len > 1e-9 ? { x: -v.y / len, y: v.x / len } : { x: 0, y: 1 };\n\tconst at = (t: number, gap: number) => ({ x: h.start.x + v.x * t + normal.x * gap, y: h.start.y + v.y * t + normal.y * gap });\n\tconst sorted = sortedVectorGradientStops(gradient);\n\tconst stops = sorted.map((s, index) => ({ ...s, index, ...at(s.offset, VECTOR_GRADIENT_STOP_GAP) }));\n\tconst midpoints = sorted.slice(0, -1).flatMap((s, index) => {\n\t\tconst pos = vectorGradientMidpointPosition(sorted, index);\n\t\treturn pos === null || sorted[index + 1].offset - s.offset < 1e-6 ? [] : [{ index, midpoint: s.midpoint ?? 0.5, ...at(pos, -VECTOR_GRADIENT_MIDPOINT_GAP) }];\n\t});\n\tlet extent: string | null = null;\n\tif (isCentredVectorGradient(gradient.type)) {\n\t\tconst frame = multiplyMatrices(vectorGradientMatrix(gradient, bounds), screen);\n\t\tconst pts =\n\t\t\tgradient.type === 'diamond'\n\t\t\t\t? [{ x: 1, y: 0 }, { x: 0, y: 1 }, { x: -1, y: 0 }, { x: 0, y: -1 }]\n\t\t\t\t: Array.from({ length: 48 }, (_, k) => ({ x: Math.cos((k * Math.PI) / 24), y: Math.sin((k * Math.PI) / 24) }));\n\t\textent = `M${pts.map((p) => apply(frame, p)).map((p) => `${fmt(p.x)} ${fmt(p.y)}`).join('L')}Z`;\n\t}\n\treturn { nodeId: path.id, target, gradient, bounds, world, start: h.start, end: h.end, aspect: h.aspect, focal: h.focal, normal, stops, midpoints, extent };\n}\n\n/** Where `p` lies relative to the bar: offset along it (0 at the start, 1 at the end) and distance towards the stop chips. */\nexport function vectorGradientBarPosition(a: Pick<VectorGradientAnnotator, 'start' | 'end' | 'normal'>, p: Vec2): { offset: number; across: number } {\n\tconst v = { x: a.end.x - a.start.x, y: a.end.y - a.start.y };\n\tconst l2 = v.x * v.x + v.y * v.y;\n\tconst d = { x: p.x - a.start.x, y: p.y - a.start.y };\n\tif (l2 < 1e-12) return { offset: 0, across: Math.hypot(d.x, d.y) };\n\treturn { offset: (d.x * v.x + d.y * v.y) / l2, across: d.x * a.normal.x + d.y * a.normal.y };\n}\n\n/** The annotator part under screen point `p`: stops, then midpoints, the handles and the bar. */\nexport function hitVectorGradientAnnotator(a: VectorGradientAnnotator, p: Vec2): VectorGradientPart | null {\n\tconst near = (q: Vec2 | null, r: number) => !!q && Math.hypot(q.x - p.x, q.y - p.y) <= r;\n\tfor (let i = a.stops.length - 1; i >= 0; i--) if (near(a.stops[i], STOP_HIT)) return { kind: 'stop', index: a.stops[i].index };\n\tfor (const m of a.midpoints) if (near(m, MIDPOINT_HIT)) return { kind: 'midpoint', index: m.index };\n\tconst centred = isCentredVectorGradient(a.gradient.type);\n\tif (near(a.end, HANDLE_HIT)) return { kind: 'handle', handle: centred ? 'radius' : 'end' };\n\t// The focal point sits on the centre until moved: grab it with Alt there (see the controller).\n\tif (a.focal && near(a.focal, HANDLE_HIT) && !near(a.start, 1)) return { kind: 'handle', handle: 'focal' };\n\tif (near(a.start, HANDLE_HIT)) return { kind: 'handle', handle: 'start' };\n\tif (near(a.aspect, HANDLE_HIT)) return { kind: 'handle', handle: 'aspect' };\n\tconst { offset, across } = vectorGradientBarPosition(a, p);\n\tif (offset >= 0 && offset <= 1 && across >= -BAR_HIT && across <= VECTOR_GRADIENT_STOP_GAP + STOP_HIT) return { kind: 'bar', offset };\n\treturn null;\n}\n","// Portions ported from vectorcraft-main/crates/doc/src/blend.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n/**\n * Live blend groups: a group whose `blend` is set keeps its key shapes as children and\n * regenerates the in-betweens (`blendStep` children, locked) whenever a key or the options\n * change. Steps follow the straight lines between the keys' centres, or a replaced spine.\n *\n * - **Spacing** (`vectorBlendStepCount`): a number of steps per pair of keys, a fixed distance\n * along the spine, or smooth colour — one step per two levels of the largest colour channel\n * difference, by distance when the colours match.\n * - **Spine**: with a replaced spine the keys sit along it, spread evenly by arc length; each\n * step is moved by how far the spine bends away from the chord between its keys, so a step\n * lands on the spine and the keys stay where they are. Orient to spine also turns the steps\n * by how much the spine's direction differs from the keys' (keys keep their orientation);\n * without a replaced spine the steps run straight and keep their orientation.\n */\nimport type { Point } from '../canvas/types.js';\nimport { hexToRgba } from '../tokens/color.js';\nimport { segmentPoint, segmentTangent } from '../canvas/pathops/bezier.js';\nimport { createVectorMorph, type VectorMorph } from './blend.js';\nimport {\n\tcollectPathNodes,\n\tIDENTITY_MATRIX,\n\trotateMatrix,\n\tsubpathSegment,\n\tsubpathSegmentCount,\n\ttransformVectorNode,\n\ttranslateMatrix,\n\tmultiplyMatrices,\n\tnodeMatrix,\n\tvectorNodeBounds,\n\ttype AffineMatrix\n} from './geometry.js';\nimport type { VectorBlend, VectorBlendOptions, VectorGroupNode, VectorNode, VectorPaint, VectorPathNode, VectorSubpath } from './types.js';\n\nexport const DEFAULT_VECTOR_BLEND_STEPS = 5;\nexport const DEFAULT_VECTOR_BLEND_DISTANCE = 20;\n/** Most steps between two keys. */\nexport const VECTOR_BLEND_MAX_STEPS = 1000;\n/** Most steps smooth colour makes between two keys. */\nexport const VECTOR_BLEND_MAX_SMOOTH_STEPS = 256;\n\n/** Whether a node is a live blend group. */\nexport function isVectorBlendGroup(node: VectorNode | null | undefined): node is VectorGroupNode & { blend: VectorBlend } {\n\treturn !!node && node.type === 'group' && !!node.blend;\n}\n\n/** The keys of a blend group (its children that are not generated steps), in stacking order. */\nexport function vectorBlendKeys(group: VectorGroupNode): VectorNode[] {\n\treturn group.children.filter((c) => !c.blendStep);\n}\n\n/** The full options of a blend, defaults filled in. */\nexport function resolveVectorBlendOptions(options: VectorBlendOptions | null | undefined): Required<VectorBlendOptions> {\n\treturn {\n\t\tspacing: options?.spacing ?? 'steps',\n\t\tsteps: options?.steps ?? DEFAULT_VECTOR_BLEND_STEPS,\n\t\tdistance: options?.distance ?? DEFAULT_VECTOR_BLEND_DISTANCE,\n\t\torientToSpine: options?.orientToSpine ?? false,\n\t\tlinear: options?.linear ?? false\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Step counts\n// ---------------------------------------------------------------------------\n\nfunction paintColors(p: VectorPaint | undefined): string[] {\n\tif (typeof p === 'string') return [p];\n\tif (p && typeof p === 'object') return p.stops.map((s) => s.color);\n\treturn [];\n}\n\n/** Largest channel difference (0–1) between the colours of two shapes' fills and strokes. */\nexport function vectorBlendColorDistance(a: VectorNode, b: VectorNode): number {\n\tconst pa = collectPathNodes(a)[0];\n\tconst pb = collectPathNodes(b)[0];\n\tif (!pa || !pb) return 0;\n\tlet d = 0;\n\tfor (const key of ['fill', 'stroke'] as const) {\n\t\tconst ca = paintColors(pa[key]);\n\t\tconst cb = paintColors(pb[key]);\n\t\tca.forEach((c, i) => {\n\t\t\tconst other = cb[i] ?? cb[0];\n\t\t\tif (other === undefined) return;\n\t\t\tconst x = hexToRgba(c);\n\t\t\tconst y = hexToRgba(other);\n\t\t\tif (!x || !y) return;\n\t\t\td = Math.max(d, Math.abs(x.r - y.r), Math.abs(x.g - y.g), Math.abs(x.b - y.b), Math.abs(x.a - y.a));\n\t\t});\n\t}\n\treturn d;\n}\n\nconst centreOf = (n: VectorNode): Point => {\n\tconst b = vectorNodeBounds(n);\n\treturn b ? [b.x + b.width / 2, b.y + b.height / 2] : [0, 0];\n};\n\n/** Number of in-betweens between two keys `length` apart along the spine. */\nexport function vectorBlendStepCount(a: VectorNode, b: VectorNode, options: VectorBlendOptions | null | undefined, length: number): number {\n\tconst o = resolveVectorBlendOptions(options);\n\tif (o.spacing === 'distance') {\n\t\tconst d = Math.max(0.01, o.distance);\n\t\treturn Math.min(VECTOR_BLEND_MAX_STEPS, Math.max(0, Math.round(length / d) - 1));\n\t}\n\tif (o.spacing === 'smoothColor') {\n\t\tconst cd = vectorBlendColorDistance(a, b);\n\t\tif (cd > 1 / 255) return Math.min(VECTOR_BLEND_MAX_SMOOTH_STEPS, Math.max(1, Math.ceil((cd * 255) / 2)));\n\t\t// Same colours: base the count on how far the shapes are apart.\n\t\tconst ba = vectorNodeBounds(a);\n\t\tconst bb = vectorNodeBounds(b);\n\t\tconst dist =\n\t\t\tba && bb\n\t\t\t\t? Math.max(Math.abs(ba.x - bb.x), Math.abs(ba.y - bb.y), Math.abs(ba.x + ba.width - bb.x - bb.width), Math.abs(ba.y + ba.height - bb.y - bb.height))\n\t\t\t\t: length;\n\t\treturn Math.min(VECTOR_BLEND_MAX_SMOOTH_STEPS, Math.max(1, Math.ceil(dist / 2)));\n\t}\n\treturn Math.min(VECTOR_BLEND_MAX_STEPS, Math.max(1, Math.round(Number.isFinite(o.steps) ? o.steps : DEFAULT_VECTOR_BLEND_STEPS)));\n}\n\n// ---------------------------------------------------------------------------\n// Spine\n// ---------------------------------------------------------------------------\n\n/** A subpath flattened to a polyline with cumulative arc lengths and tangent angles. */\nexport interface VectorBlendRail {\n\tpts: Point[];\n\tcum: number[];\n\tangles: number[];\n\ttotal: number;\n\tclosed: boolean;\n}\n\n/** Flattens a spine subpath (32 samples per curve). Null with fewer than two distinct points. */\nexport function vectorBlendRail(sp: VectorSubpath): VectorBlendRail | null {\n\tconst pts: Point[] = [];\n\tconst angles: number[] = [];\n\tconst push = (p: Point, tangent: Point) => {\n\t\tconst last = pts[pts.length - 1];\n\t\tif (last && Math.hypot(p[0] - last[0], p[1] - last[1]) < 1e-9) return;\n\t\tpts.push(p);\n\t\tangles.push(Math.atan2(tangent[1], tangent[0]));\n\t};\n\tconst count = subpathSegmentCount(sp);\n\tfor (let i = 0; i < count; i++) {\n\t\tconst seg = subpathSegment(sp, i);\n\t\tconst samples = seg.length === 2 ? 1 : 32;\n\t\tif (i === 0) push(seg[0], segmentTangent(seg, 0));\n\t\tfor (let k = 1; k <= samples; k++) push(segmentPoint(seg, k / samples), segmentTangent(seg, k / samples));\n\t}\n\tif (pts.length < 2) return null;\n\tconst cum = [0];\n\tfor (let i = 1; i < pts.length; i++) cum.push(cum[i - 1] + Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]));\n\treturn { pts, cum, angles, total: cum[cum.length - 1], closed: sp.closed };\n}\n\n/** Point and direction (radians) at arc length `s` (clamped; a closed rail wraps). */\nexport function vectorBlendRailAt(rail: VectorBlendRail, s: number): { point: Point; angle: number } {\n\tlet len = Number.isFinite(s) ? s : 0;\n\tif (rail.closed && rail.total > 0) len = ((len % rail.total) + rail.total) % rail.total;\n\tlen = Math.min(rail.total, Math.max(0, len));\n\tlet lo = 0;\n\tlet hi = rail.cum.length - 1;\n\twhile (hi - lo > 1) {\n\t\tconst mid = (lo + hi) >> 1;\n\t\tif (rail.cum[mid] <= len) lo = mid;\n\t\telse hi = mid;\n\t}\n\tconst span = rail.cum[hi] - rail.cum[lo];\n\tconst u = span > 1e-12 ? (len - rail.cum[lo]) / span : 0;\n\tconst a = rail.pts[lo];\n\tconst b = rail.pts[hi];\n\treturn { point: [a[0] + (b[0] - a[0]) * u, a[1] + (b[1] - a[1]) * u], angle: Math.atan2(b[1] - a[1], b[0] - a[0]) };\n}\n\n/**\n * The spine of a blend in the group's space and where along it each key sits: the replaced\n * spine (keys spread evenly by arc length) or the straight lines between the keys' centres.\n */\nexport function vectorBlendSpine(group: VectorGroupNode): { spine: VectorSubpath; rail: VectorBlendRail; stops: number[]; replaced: boolean } | null {\n\tconst keys = vectorBlendKeys(group);\n\tif (keys.length < 2) return null;\n\tconst stored = group.blend?.spine;\n\tif (stored && stored.anchors.length >= 2) {\n\t\tconst rail = vectorBlendRail(stored);\n\t\tif (rail) {\n\t\t\tconst end = rail.closed ? rail.total * (1 - 1 / keys.length) : rail.total;\n\t\t\treturn { spine: stored, rail, stops: keys.map((_, i) => (end * i) / (keys.length - 1)), replaced: true };\n\t\t}\n\t}\n\tconst centres = keys.map(centreOf);\n\tconst spine: VectorSubpath = { closed: false, anchors: centres.map((c) => ({ x: c[0], y: c[1] })) };\n\tconst stops = [0];\n\tfor (let i = 1; i < centres.length; i++) stops.push(stops[i - 1] + Math.hypot(centres[i][0] - centres[i - 1][0], centres[i][1] - centres[i - 1][1]));\n\tconst rail = vectorBlendRail(spine) ?? { pts: centres, cum: stops, angles: centres.map(() => 0), total: stops[stops.length - 1], closed: false };\n\treturn { spine, rail, stops, replaced: false };\n}\n\n/** Where key `i` of a blend with spine `spine` should sit: its stop on the spine. */\nexport function vectorBlendKeyTargets(group: VectorGroupNode): Point[] {\n\tconst s = vectorBlendSpine(group);\n\treturn s ? s.stops.map((stop) => vectorBlendRailAt(s.rail, stop).point) : [];\n}\n\n// ---------------------------------------------------------------------------\n// Generating steps\n// ---------------------------------------------------------------------------\n\nconst morphCache = new WeakMap<VectorNode, WeakMap<VectorNode, Map<string, VectorMorph>>>();\n\nfunction morphFor(a: VectorNode, b: VectorNode, blend: VectorBlend): VectorMorph {\n\tconst starts = [blend.starts?.[a.id] ?? null, blend.starts?.[b.id] ?? null] as const;\n\tconst key = JSON.stringify([!!blend.linear, starts]);\n\tlet inner = morphCache.get(a);\n\tif (!inner) morphCache.set(a, (inner = new WeakMap()));\n\tlet byKey = inner.get(b);\n\tif (!byKey) inner.set(b, (byKey = new Map()));\n\tlet morph = byKey.get(key);\n\tif (!morph) byKey.set(key, (morph = createVectorMorph(a, b, { linear: blend.linear, starts })));\n\treturn morph;\n}\n\nconst wrapAngle = (a: number) => Math.atan2(Math.sin(a), Math.cos(a));\n\n/** A step marked as generated: locked, flagged, ids derived from `id`. */\nfunction asStep(node: VectorNode, id: string): VectorNode {\n\tconst rename = (n: VectorNode, nid: string): VectorNode =>\n\t\tn.type === 'group' ? { ...n, id: nid, children: n.children.map((c, i) => rename(c, `${nid}.${i}`)) } : { ...n, id: nid };\n\treturn { ...rename(node, id), blendStep: true, locked: true };\n}\n\n/**\n * A blend group's children with fresh steps: each key followed by the in-betweens to the next,\n * bottom to top. Groups with fewer than two keys keep just their keys.\n */\nexport function generateVectorBlendChildren(group: VectorGroupNode): VectorNode[] {\n\tconst keys = vectorBlendKeys(group);\n\tconst blend = group.blend;\n\tif (!blend || keys.length < 2) return keys;\n\tconst spine = vectorBlendSpine(group);\n\tif (!spine) return keys;\n\tconst out: VectorNode[] = [];\n\tlet counter = 0;\n\tkeys.forEach((a, i) => {\n\t\tout.push(a);\n\t\tconst b = keys[i + 1];\n\t\tif (!b) return;\n\t\tconst s0 = spine.stops[i];\n\t\tconst s1 = spine.stops[i + 1];\n\t\tconst n = vectorBlendStepCount(a, b, blend, Math.abs(s1 - s0));\n\t\tconst morph = morphFor(a, b, blend);\n\t\tconst p0 = vectorBlendRailAt(spine.rail, s0);\n\t\tconst p1 = vectorBlendRailAt(spine.rail, s1);\n\t\tfor (let j = 1; j <= n; j++) {\n\t\t\tconst t = j / (n + 1);\n\t\t\tlet step = morph.at(t);\n\t\t\tif (spine.replaced) {\n\t\t\t\tconst p = vectorBlendRailAt(spine.rail, s0 + (s1 - s0) * t);\n\t\t\t\tconst dx = p.point[0] - (p0.point[0] + (p1.point[0] - p0.point[0]) * t);\n\t\t\t\tconst dy = p.point[1] - (p0.point[1] + (p1.point[1] - p0.point[1]) * t);\n\t\t\t\tlet m: AffineMatrix = Math.hypot(dx, dy) > 1e-9 ? translateMatrix(dx, dy) : IDENTITY_MATRIX;\n\t\t\t\tif (blend.orientToSpine) {\n\t\t\t\t\tconst turn = wrapAngle(p.angle - (p0.angle + wrapAngle(p1.angle - p0.angle) * t));\n\t\t\t\t\tif (Math.abs(turn) > 1e-9) {\n\t\t\t\t\t\tconst c = centreOf(step);\n\t\t\t\t\t\tconst origin = { x: c[0] + m[4], y: c[1] + m[5] };\n\t\t\t\t\t\tm = multiplyMatrices(m, rotateMatrix((turn * 180) / Math.PI, origin));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (m !== IDENTITY_MATRIX) step = transformVectorNode(step, m);\n\t\t\t}\n\t\t\tout.push(asStep(step, `${group.id}~${++counter}`));\n\t\t}\n\t});\n\treturn out;\n}\n\nconst childrenCache = new WeakMap<VectorNode, { sig: string; keys: VectorNode[]; children: VectorNode[] }>();\n\nconst sameList = (a: readonly VectorNode[], b: readonly VectorNode[]) => a.length === b.length && a.every((n, i) => n === b[i]);\n\nfunction blendChildren(group: VectorGroupNode): VectorNode[] {\n\tconst keys = vectorBlendKeys(group);\n\tconst sig = `${group.id}|${JSON.stringify(group.blend)}`;\n\tconst cached = keys[0] ? childrenCache.get(keys[0]) : undefined;\n\tlet children: VectorNode[];\n\tif (cached && cached.sig === sig && sameList(cached.keys, keys)) children = cached.children;\n\telse {\n\t\tchildren = generateVectorBlendChildren(group);\n\t\tif (keys[0]) childrenCache.set(keys[0], { sig, keys, children });\n\t}\n\treturn sameList(children, group.children) ? group.children : children;\n}\n\n/**\n * Nodes with every blend group's steps brought up to date (nested blends first). Returns the\n * same array when nothing changed; steps are cached per set of keys, so calling this after\n * every edit is cheap.\n */\nexport function refreshVectorBlends(nodes: readonly VectorNode[]): VectorNode[] {\n\tlet changed = false;\n\tconst out = nodes.map((n) => {\n\t\tif (n.type !== 'group') return n;\n\t\tconst inner = refreshVectorBlends(n.children);\n\t\tlet next: VectorGroupNode = inner === n.children ? n : { ...n, children: inner };\n\t\tif (next.blend) {\n\t\t\tconst children = blendChildren(next);\n\t\t\tif (children !== next.children) next = { ...next, children };\n\t\t}\n\t\tif (next !== n) changed = true;\n\t\treturn next;\n\t});\n\treturn changed ? out : (nodes as VectorNode[]);\n}\n\n/** A step or key with the generated flags dropped and fresh ids. */\nfunction plain(node: VectorNode, newId: () => string): VectorNode {\n\tconst { blendStep: _s, locked: _l, ...rest } = node;\n\tconst renamed = { ...rest, id: newId() } as VectorNode;\n\tif (renamed.type === 'group') renamed.children = renamed.children.map((c) => plain(c, newId));\n\treturn renamed;\n}\n\n/** Expand: the blend as a plain group of its keys and steps (steps get fresh ids). */\nexport function expandVectorBlend(group: VectorGroupNode, newId: () => string): VectorGroupNode {\n\tconst { blend: _b, ...rest } = group;\n\treturn { ...rest, children: group.children.map((c) => (c.blendStep ? plain(c, newId) : c)) };\n}\n\n/**\n * Release: the blend's keys in the group's parent space (the group's transform and opacity\n * applied), followed by the replaced spine as an unpainted path when there is one.\n */\nexport function releaseVectorBlend(group: VectorGroupNode, newId: () => string): VectorNode[] {\n\tconst m = nodeMatrix(group);\n\tconst opacity = group.opacity;\n\tconst keys = vectorBlendKeys(group)\n\t\t.map((c) => (group.transform ? transformVectorNode(c, m) : c))\n\t\t.map((c) => (opacity !== undefined && opacity < 1 ? { ...c, opacity: (c.opacity ?? 1) * opacity } : c));\n\tconst spine = group.blend?.spine;\n\tif (!spine || spine.anchors.length < 2) return keys;\n\tconst path: VectorPathNode = { id: newId(), type: 'path', name: 'Spine', fill: null, stroke: null, subpaths: [{ closed: spine.closed, anchors: spine.anchors.map((a) => ({ ...a })) }] };\n\treturn [...keys, group.transform ? transformVectorNode(path, m) : path];\n}\n","// Portions ported from cadcraft/crates/constraints/src/lm.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * Damped, weighted, minimum-norm Gauss–Newton (Levenberg–Marquardt style) with a numerical\n * Jacobian, and the rank / null-space tools the degrees-of-freedom analysis uses.\n *\n * Each step solves `min ‖W^{1/2} Δx‖` subject to `J Δx = −r` (damped):\n * `Δx = −W⁻¹ Jᵀ (J W⁻¹ Jᵀ + μ I)⁻¹ r`. Under-constrained systems therefore move as little as\n * possible, and heavily weighted parameters (geometry just edited) move least.\n */\n\n/** Most residual rows one solve may have (the normal matrix is rows × rows). */\nexport const MAX_ROWS = 800;\n\nexport type ResidualFn = (x: readonly number[], out: number[]) => void;\n\nexport const maxAbs = (v: readonly number[]): number => v.reduce((m, a) => (Number.isFinite(a) ? Math.max(m, Math.abs(a)) : Infinity), 0);\nconst norm2 = (v: readonly number[]): number => v.reduce((s, a) => s + a * a, 0);\n\n/** Numerical Jacobian over the free parameters (row-major, rows × free.length). */\nexport function jacobian(f: ResidualFn, x: readonly number[], free: readonly number[], rows: number): Float64Array {\n\tconst nf = free.length;\n\tconst jac = new Float64Array(rows * nf);\n\tconst xp = x.slice();\n\tconst rp: number[] = [];\n\tconst rm: number[] = [];\n\tfree.forEach((j, k) => {\n\t\tconst xj = x[j];\n\t\tconst h = 1e-7 * Math.max(Math.abs(xj), 1);\n\t\txp[j] = xj + h;\n\t\trp.length = 0;\n\t\tf(xp, rp);\n\t\txp[j] = xj - h;\n\t\trm.length = 0;\n\t\tf(xp, rm);\n\t\txp[j] = xj;\n\t\tfor (let i = 0; i < rows; i++) {\n\t\t\tconst d = ((rp[i] ?? 0) - (rm[i] ?? 0)) / (2 * h);\n\t\t\tjac[i * nf + k] = Number.isFinite(d) ? d : 0;\n\t\t}\n\t});\n\treturn jac;\n}\n\n/** Solves the dense `n × n` system `a y = b` by Gaussian elimination with partial pivoting; null when singular. */\nfunction solveDense(a: Float64Array, b: Float64Array, n: number): Float64Array | null {\n\tlet scale = 0;\n\tfor (const v of a) scale = Math.max(scale, Math.abs(v));\n\tscale = Math.max(scale, 1e-300);\n\tfor (let col = 0; col < n; col++) {\n\t\tlet piv = col;\n\t\tlet best = 0;\n\t\tfor (let r = col; r < n; r++) {\n\t\t\tconst v = Math.abs(a[r * n + col]);\n\t\t\tif (v > best) {\n\t\t\t\tbest = v;\n\t\t\t\tpiv = r;\n\t\t\t}\n\t\t}\n\t\tif (best <= scale * 1e-15) return null;\n\t\tif (piv !== col) {\n\t\t\tfor (let c = 0; c < n; c++) {\n\t\t\t\tconst t = a[piv * n + c];\n\t\t\t\ta[piv * n + c] = a[col * n + c];\n\t\t\t\ta[col * n + c] = t;\n\t\t\t}\n\t\t\tconst t = b[piv];\n\t\t\tb[piv] = b[col];\n\t\t\tb[col] = t;\n\t\t}\n\t\tconst p = a[col * n + col];\n\t\tfor (let r = col + 1; r < n; r++) {\n\t\t\tconst f = a[r * n + col] / p;\n\t\t\tif (f === 0) continue;\n\t\t\tfor (let c = col; c < n; c++) a[r * n + c] -= f * a[col * n + c];\n\t\t\tb[r] -= f * b[col];\n\t\t}\n\t}\n\tconst y = new Float64Array(n);\n\tfor (let r = n - 1; r >= 0; r--) {\n\t\tlet s = b[r];\n\t\tfor (let c = r + 1; c < n; c++) s -= a[r * n + c] * y[c];\n\t\ty[r] = s / a[r * n + r];\n\t\tif (!Number.isFinite(y[r])) return null;\n\t}\n\treturn y;\n}\n\nfunction reflect(v: readonly number[], vv: number, c: number[]): void {\n\tlet d = 0;\n\tfor (let i = 0; i < v.length; i++) d += v[i] * c[i];\n\tconst f = (2 * d) / vv;\n\tif (f !== 0) for (let i = 0; i < v.length; i++) c[i] -= f * v[i];\n}\n\n/**\n * Householder QR with column pivoting of the columns `cols` (all the same length). Stops when the\n * largest remaining column norm drops to `rel` times the first pivot's (or below `abs`). Returns\n * the numerical rank and the Householder vectors.\n */\nexport function qr(cols: number[][], rel: number, abs: number): { rank: number; reflectors: number[][] } {\n\tconst m = cols[0]?.length ?? 0;\n\tconst n = cols.length;\n\tconst refl: number[][] = [];\n\tlet first: number | null = null;\n\tfor (let k = 0; k < Math.min(m, n); k++) {\n\t\tlet best = k;\n\t\tlet bestNorm = -1;\n\t\tfor (let j = k; j < n; j++) {\n\t\t\tlet v = 0;\n\t\t\tfor (let i = k; i < m; i++) v += cols[j][i] * cols[j][i];\n\t\t\tif (Number.isFinite(v) && v > bestNorm) {\n\t\t\t\tbestNorm = v;\n\t\t\t\tbest = j;\n\t\t\t}\n\t\t}\n\t\tconst norm = Math.sqrt(Math.max(bestNorm, 0));\n\t\tconst lim = first === null ? abs : Math.max(first * rel, abs);\n\t\tif (!(norm > lim)) break;\n\t\tif (first === null) first = norm;\n\t\t[cols[k], cols[best]] = [cols[best], cols[k]];\n\t\tconst col = cols[k];\n\t\tconst v = new Array<number>(m).fill(0);\n\t\tfor (let i = k; i < m; i++) v[i] = col[i];\n\t\tconst alpha = v[k] >= 0 ? -norm : norm;\n\t\tv[k] -= alpha;\n\t\tconst vv = norm2(v);\n\t\tif (!(vv > 0)) break;\n\t\tfor (let j = k; j < n; j++) reflect(v, vv, cols[j]);\n\t\trefl.push(v);\n\t}\n\treturn { rank: refl.length, reflectors: refl };\n}\n\n/** Column norm (after row balancing) below which a column is difference noise. */\nconst COL_NOISE = 1e-6;\n/** Relative threshold below which a pivot counts as zero (the Jacobian is numerical). */\nconst RANK_TOL = 1e-7;\n\n/** Rows and then columns scaled to unit norm, as the rows of the `rows × cols` matrix `m` (columns of its transpose). */\nfunction balancedRows(m: Float64Array, rows: number, cols: number): number[][] {\n\tconst r: number[][] = [];\n\tfor (let i = 0; i < rows; i++) {\n\t\tconst row = Array.from(m.subarray(i * cols, (i + 1) * cols));\n\t\tconst n = Math.sqrt(norm2(row));\n\t\tconst f = n > 1e-12 && Number.isFinite(n) ? 1 / n : 0;\n\t\tr.push(row.map((v) => v * f));\n\t}\n\tfor (let c = 0; c < cols; c++) {\n\t\tlet n = 0;\n\t\tfor (const row of r) n += row[c] * row[c];\n\t\tn = Math.sqrt(n);\n\t\t// An exactly zero derivative evaluated by differences is noise, not a direction.\n\t\tconst f = n > COL_NOISE && Number.isFinite(n) ? 1 / n : 0;\n\t\tfor (const row of r) row[c] *= f;\n\t}\n\treturn r;\n}\n\n/** Numerical rank of a row-major `rows × cols` Jacobian (balanced, then QR with column pivoting). */\nexport function rankQr(m: Float64Array, rows: number, cols: number): number {\n\tif (rows === 0 || cols === 0) return 0;\n\treturn qr(balancedRows(m, rows, cols), RANK_TOL, 1e-12).rank;\n}\n\n/** Rank and an orthonormal basis of the null space of a row-major `rows × cols` Jacobian (balanced columns). */\nexport function nullSpace(m: Float64Array, rows: number, cols: number): { rank: number; basis: number[][] } {\n\tconst { rank, reflectors } = rows === 0 ? { rank: 0, reflectors: [] as number[][] } : qr(balancedRows(m, rows, cols), RANK_TOL, 1e-12);\n\tconst basis: number[][] = [];\n\tfor (let j = rank; j < cols; j++) {\n\t\tconst e = new Array<number>(cols).fill(0);\n\t\te[j] = 1;\n\t\tfor (let i = reflectors.length - 1; i >= 0; i--) {\n\t\t\tconst v = reflectors[i];\n\t\t\tconst vv = norm2(v);\n\t\t\tif (vv > 0) reflect(v, vv, e);\n\t\t}\n\t\tbasis.push(e);\n\t}\n\treturn { rank, basis };\n}\n\nexport interface SolveOutcome {\n\tx: number[];\n\tconverged: boolean;\n}\n\n/** Runs the solver from `x0`; only `free` parameters change. */\nexport function solveLm(f: ResidualFn, x0: readonly number[], free: readonly number[], weights: readonly number[], tol: number, maxIter: number): SolveOutcome {\n\tlet x = x0.slice();\n\tlet r: number[] = [];\n\tf(x, r);\n\tconst rows = r.length;\n\tif (rows === 0 || maxAbs(r) <= tol) return { x, converged: true };\n\tif (!free.length || rows > MAX_ROWS) return { x, converged: false };\n\tconst nf = free.length;\n\tconst winv = free.map((j) => 1 / Math.max(weights[j] ?? 1, 1e-12));\n\tlet mu = 1e-10;\n\tlet cost = norm2(r);\n\tlet rn: number[] = [];\n\tfor (let iter = 0; iter < maxIter; iter++) {\n\t\tconst jac = jacobian(f, x, free, rows);\n\t\t// A = J W⁻¹ Jᵀ\n\t\tconst a = new Float64Array(rows * rows);\n\t\tfor (let i = 0; i < rows; i++) {\n\t\t\tfor (let k = i; k < rows; k++) {\n\t\t\t\tlet s = 0;\n\t\t\t\tfor (let c = 0; c < nf; c++) s += jac[i * nf + c] * jac[k * nf + c] * winv[c];\n\t\t\t\ta[i * rows + k] = s;\n\t\t\t\ta[k * rows + i] = s;\n\t\t\t}\n\t\t}\n\t\tlet diagMax = 1e-12;\n\t\tfor (let i = 0; i < rows; i++) diagMax = Math.max(diagMax, a[i * rows + i]);\n\t\tlet improved = false;\n\t\tfor (let attempt = 0; attempt < 12; attempt++) {\n\t\t\tconst ad = a.slice();\n\t\t\tfor (let i = 0; i < rows; i++) ad[i * rows + i] += mu * diagMax;\n\t\t\tconst y = solveDense(ad, Float64Array.from(r), rows);\n\t\t\tif (!y) {\n\t\t\t\tmu *= 10;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst xn = x.slice();\n\t\t\tfree.forEach((j, c) => {\n\t\t\t\tlet s = 0;\n\t\t\t\tfor (let i = 0; i < rows; i++) s += jac[i * nf + c] * y[i];\n\t\t\t\txn[j] -= winv[c] * s;\n\t\t\t});\n\t\t\trn = [];\n\t\t\tf(xn, rn);\n\t\t\tconst cn = norm2(rn);\n\t\t\tif (Number.isFinite(cn) && cn < cost) {\n\t\t\t\tx = xn;\n\t\t\t\tr = rn;\n\t\t\t\tcost = cn;\n\t\t\t\tmu = Math.max(mu * 0.1, 1e-15);\n\t\t\t\timproved = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tmu *= 10;\n\t\t}\n\t\tif (maxAbs(r) <= tol) return { x, converged: true };\n\t\tif (!improved) break;\n\t}\n\treturn { x, converged: maxAbs(r) <= tol };\n}\n","// Portions ported from cadcraft/crates/constraints/src/expr.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * A tiny expression language for dimensional constraints and named parameters: numbers, names,\n * `+ - * /`, unary minus, parentheses and the constant `pi`.\n */\n\n/** Longest expression accepted (characters). */\nexport const VECTOR_EXPR_MAX_LENGTH = 1024;\n/** Deepest nesting (parentheses, unary minus) and name reference chain. */\nconst MAX_DEPTH = 64;\n\nexport class VectorExprError extends Error {}\n\ntype Expr = { k: 'num'; v: number } | { k: 'var'; n: string } | { k: 'neg'; a: Expr } | { k: 'bin'; op: '+' | '-' | '*' | '/'; a: Expr; b: Expr };\n\n/** A valid name: a letter or `_`, then letters, digits or `_` (not `pi`), at most 64 characters. */\nexport function isValidVectorParameterName(s: string): boolean {\n\treturn /^[A-Za-z_][A-Za-z0-9_]*$/.test(s) && s.length <= 64 && s.toLowerCase() !== 'pi';\n}\n\nfunction parse(text: string): Expr {\n\tif (text.length > VECTOR_EXPR_MAX_LENGTH) throw new VectorExprError('expression too long or too deeply nested');\n\tif (!text.trim()) throw new VectorExprError('syntax error: empty expression');\n\tlet i = 0;\n\tlet depth = 0;\n\tconst ws = () => {\n\t\twhile (i < text.length && /\\s/.test(text[i])) i++;\n\t};\n\tconst peek = () => {\n\t\tws();\n\t\treturn text[i];\n\t};\n\tconst enter = () => {\n\t\tif (++depth > MAX_DEPTH) throw new VectorExprError('expression too long or too deeply nested');\n\t};\n\tconst sum = (): Expr => {\n\t\tenter();\n\t\tlet e = product();\n\t\tfor (let c = peek(); c === '+' || c === '-'; c = peek()) {\n\t\t\ti++;\n\t\t\te = { k: 'bin', op: c, a: e, b: product() };\n\t\t}\n\t\tdepth--;\n\t\treturn e;\n\t};\n\tconst product = (): Expr => {\n\t\tlet e = unary();\n\t\tfor (let c = peek(); c === '*' || c === '/'; c = peek()) {\n\t\t\ti++;\n\t\t\te = { k: 'bin', op: c, a: e, b: unary() };\n\t\t}\n\t\treturn e;\n\t};\n\tconst unary = (): Expr => {\n\t\tconst c = peek();\n\t\tif (c === '-' || c === '+') {\n\t\t\ti++;\n\t\t\tenter();\n\t\t\tconst e = unary();\n\t\t\tdepth--;\n\t\t\treturn c === '-' ? { k: 'neg', a: e } : e;\n\t\t}\n\t\treturn atom();\n\t};\n\tconst atom = (): Expr => {\n\t\tconst c = peek();\n\t\tif (c === '(') {\n\t\t\ti++;\n\t\t\tconst e = sum();\n\t\t\tif (peek() !== ')') throw new VectorExprError('syntax error: missing `)`');\n\t\t\ti++;\n\t\t\treturn e;\n\t\t}\n\t\tif (c !== undefined && /[0-9.]/.test(c)) {\n\t\t\tconst m = /^(\\d*\\.?\\d*)([eE][+-]?\\d+)?/.exec(text.slice(i))!;\n\t\t\ti += m[0].length;\n\t\t\tconst v = Number(m[0]);\n\t\t\tif (!m[1] || m[1] === '.' || !Number.isFinite(v) || /\\..*\\./.test(m[1])) throw new VectorExprError(`syntax error: bad number \\`${m[0]}\\``);\n\t\t\tif (text[i] === '.') throw new VectorExprError(`syntax error: bad number \\`${m[0]}.\\``);\n\t\t\treturn { k: 'num', v };\n\t\t}\n\t\tif (c !== undefined && /[A-Za-z_]/.test(c)) {\n\t\t\tconst m = /^[A-Za-z0-9_]+/.exec(text.slice(i))!;\n\t\t\ti += m[0].length;\n\t\t\treturn m[0].toLowerCase() === 'pi' ? { k: 'num', v: Math.PI } : { k: 'var', n: m[0] };\n\t\t}\n\t\tif (c === undefined) throw new VectorExprError('syntax error: unexpected end');\n\t\tthrow new VectorExprError(`syntax error: unexpected \\`${c}\\``);\n\t};\n\tconst e = sum();\n\tif (peek() !== undefined) throw new VectorExprError(`syntax error: unexpected \\`${text.slice(i).trim()}\\``);\n\treturn e;\n}\n\nfunction evalWith(e: Expr, lookup: (name: string) => number): number {\n\tlet v: number;\n\tswitch (e.k) {\n\t\tcase 'num':\n\t\t\tv = e.v;\n\t\t\tbreak;\n\t\tcase 'var':\n\t\t\tv = lookup(e.n);\n\t\t\tbreak;\n\t\tcase 'neg':\n\t\t\tv = -evalWith(e.a, lookup);\n\t\t\tbreak;\n\t\tcase 'bin': {\n\t\t\tconst x = evalWith(e.a, lookup);\n\t\t\tconst y = evalWith(e.b, lookup);\n\t\t\tif (e.op === '/' && y === 0) throw new VectorExprError('division by zero');\n\t\t\tv = e.op === '+' ? x + y : e.op === '-' ? x - y : e.op === '*' ? x * y : x / y;\n\t\t}\n\t}\n\tif (!Number.isFinite(v)) throw new VectorExprError('result is not a finite number');\n\treturn v;\n}\n\n/** Checks an expression's syntax (throws a `VectorExprError` saying what is wrong). */\nexport function checkVectorExpression(text: string): void {\n\tparse(text);\n}\n\n/** Names an expression refers to. */\nexport function vectorExpressionNames(text: string): string[] {\n\tconst out: string[] = [];\n\tconst walk = (e: Expr) => {\n\t\tif (e.k === 'var') {\n\t\t\tif (!out.includes(e.n)) out.push(e.n);\n\t\t} else if (e.k === 'neg') walk(e.a);\n\t\telse if (e.k === 'bin') {\n\t\t\twalk(e.a);\n\t\t\twalk(e.b);\n\t\t}\n\t};\n\twalk(parse(text));\n\treturn out;\n}\n\n/** Named expressions (parameters and dimensional constraint names), evaluated lazily with cycle detection. */\nexport class VectorExprEnv {\n\tprivate readonly exprs = new Map<string, string>();\n\tprivate readonly cache = new Map<string, number>();\n\n\tset(name: string, expr: string): void {\n\t\tthis.exprs.set(name, expr);\n\t\tthis.cache.clear();\n\t}\n\n\thas(name: string): boolean {\n\t\treturn this.exprs.has(name);\n\t}\n\n\t/** The value of a name. */\n\tvalue(name: string, stack: string[] = []): number {\n\t\tconst hit = this.cache.get(name);\n\t\tif (hit !== undefined) return hit;\n\t\tif (stack.includes(name)) throw new VectorExprError(`parameter \\`${name}\\` refers to itself`);\n\t\tif (stack.length >= MAX_DEPTH) throw new VectorExprError('expression too long or too deeply nested');\n\t\tconst text = this.exprs.get(name);\n\t\tif (text === undefined) throw new VectorExprError(`unknown parameter \\`${name}\\``);\n\t\tconst e = parse(text);\n\t\tstack.push(name);\n\t\tconst v = evalWith(e, (n) => this.value(n, stack));\n\t\tstack.pop();\n\t\tthis.cache.set(name, v);\n\t\treturn v;\n\t}\n\n\t/** Evaluates a free expression against these names. */\n\teval(text: string): number {\n\t\treturn evalWith(parse(text), (n) => this.value(n, []));\n\t}\n}\n\n/** Evaluates an expression with no names. */\nexport function evalVectorExpression(text: string): number {\n\treturn evalWith(parse(text), (n) => {\n\t\tthrow new VectorExprError(`unknown parameter \\`${n}\\``);\n\t});\n}\n\n/** A value as an expression: up to 6 decimals, trailing zeros trimmed. */\nexport function formatVectorValue(v: number): string {\n\tconst s = v.toFixed(6).replace(/0+$/, '').replace(/\\.$/, '');\n\treturn !s || s === '-0' ? '0' : s;\n}\n","// Portions ported from cadcraft/crates/constraints/src/lib.rs, residual.rs and dof.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * The constraint solver over a vector document: anchors, handles, segments and circles referenced\n * by constraints become a parameter vector (document space), each constraint becomes residual\n * equations, and the damped minimum-norm Gauss–Newton solver (`lm.ts`) drives them to zero per\n * connected component, moving geometry as little as possible and what was just edited least.\n *\n * Adapted from cadcraft's solver: its lines, circles and polyline vertices become our anchors,\n * segments (the line between two anchors, or the cubic with its handles for G1 / G2) and round\n * closed paths; its entity handles become anchor ids.\n */\n\nimport { applyMatrix, invertMatrix, multiplyMatrices, IDENTITY_MATRIX } from '../../geom/affine.js';\nimport type { AffineMatrix } from '../../geom/types.js';\nimport { MAX_ROWS, jacobian, maxAbs, nullSpace, qr, rankQr, solveLm, type ResidualFn } from './lm.js';\nimport { formatVectorValue, isValidVectorParameterName, VectorExprEnv, VectorExprError } from './expr.js';\nimport type { Vec2, VectorAnchor, VectorConstraint, VectorConstraintKind, VectorDocument, VectorGeomRef, VectorNode, VectorPathNode } from '../types.js';\n\n/** Solver iterations. */\nconst MAX_ITER = 200;\n/** Constraints tried one by one when locating a conflict. */\nconst MAX_CONFLICT_PROBES = 48;\n/** Most constraints one document may hold. */\nexport const VECTOR_MAX_CONSTRAINTS = 2000;\n\nexport const VECTOR_DIMENSIONAL_KINDS: ReadonlySet<VectorConstraintKind> = new Set(['distance', 'angle', 'radius', 'diameter']);\n\nexport class VectorConstraintError extends Error {\n\tconstructor(\n\t\tmessage: string,\n\t\t/** Constraints involved (conflicting or duplicated), when known. */\n\t\treadonly ids: string[] = []\n\t) {\n\t\tsuper(message);\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Locating references\n// ---------------------------------------------------------------------------\n\ninterface Located {\n\tnode: VectorPathNode;\n\t/** Node space → document. */\n\tworld: AffineMatrix;\n}\n\nfunction locateNodes(doc: VectorDocument): Map<string, Located> {\n\tconst out = new Map<string, Located>();\n\tconst visit = (list: readonly VectorNode[], parent: AffineMatrix) => {\n\t\tfor (const n of list) {\n\t\t\tconst m = n.transform ? multiplyMatrices(n.transform as AffineMatrix, parent) : parent;\n\t\t\tif (n.type === 'path') out.set(n.id, { node: n, world: m });\n\t\t\telse if (n.type === 'group') visit(n.children, m);\n\t\t}\n\t};\n\tvisit(doc.nodes, IDENTITY_MATRIX);\n\treturn out;\n}\n\n/** Subpath and index of the anchor with `id` in a path, or null. */\nexport function findVectorAnchorById(node: VectorPathNode, id: string): { subpath: number; index: number } | null {\n\tfor (let s = 0; s < node.subpaths.length; s++) {\n\t\tconst i = node.subpaths[s].anchors.findIndex((a) => a.id === id);\n\t\tif (i >= 0) return { subpath: s, index: i };\n\t}\n\treturn null;\n}\n\n// ---------------------------------------------------------------------------\n// Model: parameters\n// ---------------------------------------------------------------------------\n\ntype Part = 'anchor' | 'in' | 'out';\n\ninterface PointSlot {\n\tkind: 'point';\n\tnodeId: string;\n\tsubpath: number;\n\tindex: number;\n\tpart: Part;\n\toff: number;\n}\n\ninterface CircleSlot {\n\tkind: 'circle';\n\tnodeId: string;\n\toff: number;\n\t/** Centre and radius the path had (document space). */\n\tc0: Vec2;\n\tr0: number;\n}\n\ntype Slot = PointSlot | CircleSlot;\n\nconst sub = (a: Vec2, b: Vec2): Vec2 => ({ x: a.x - b.x, y: a.y - b.y });\nconst len = (v: Vec2) => Math.hypot(v.x, v.y);\nconst cross = (a: Vec2, b: Vec2) => a.x * b.y - a.y * b.x;\nconst dot = (a: Vec2, b: Vec2) => a.x * b.x + a.y * b.y;\nconst unit = (v: Vec2): Vec2 => {\n\tconst l = len(v);\n\treturn l > 1e-12 ? { x: v.x / l, y: v.y / l } : v;\n};\nconst wrap = (a: number) => {\n\tconst r = ((((a + Math.PI) % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI)) - Math.PI;\n\treturn Number.isFinite(r) ? r : 0;\n};\n\n/** The round closed path's centre and radius (document space), or null when it is not round. */\nexport function vectorPathCircle(node: VectorPathNode, world: AffineMatrix = IDENTITY_MATRIX): { c: Vec2; r: number } | null {\n\tconst sp = node.subpaths[0];\n\tif (!sp || !sp.closed || sp.anchors.length < 3) return null;\n\tconst pts = sp.anchors.map((a) => applyMatrix(world, a));\n\tconst c = { x: pts.reduce((s, p) => s + p.x, 0) / pts.length, y: pts.reduce((s, p) => s + p.y, 0) / pts.length };\n\tconst ds = pts.map((p) => len(sub(p, c)));\n\tconst r = ds.reduce((s, d) => s + d, 0) / ds.length;\n\tif (!(r > 1e-9) || ds.some((d) => Math.abs(d - r) > r * 0.05)) return null;\n\t// The middle of every segment too (a polygon's corners lie on a circle, its sides do not).\n\tconst n = sp.anchors.length;\n\tfor (let i = 0; i < n; i++) {\n\t\tconst a = sp.anchors[i];\n\t\tconst b = sp.anchors[(i + 1) % n];\n\t\tconst p1 = a.out ?? a;\n\t\tconst p2 = b.in ?? b;\n\t\tconst mid = applyMatrix(world, { x: (a.x + 3 * p1.x + 3 * p2.x + b.x) / 8, y: (a.y + 3 * p1.y + 3 * p2.y + b.y) / 8 });\n\t\tif (Math.abs(len(sub(mid, c)) - r) > r * 0.05) return null;\n\t}\n\treturn { c, r };\n}\n\nclass Model {\n\treadonly x0: number[] = [];\n\treadonly slots = new Map<string, Slot>();\n\tconstructor(readonly located: Map<string, Located>) {}\n\n\tprivate anchorOf(ref: VectorGeomRef): { loc: Located; subpath: number; index: number; a: VectorAnchor } | null {\n\t\tconst loc = this.located.get(ref.nodeId);\n\t\tif (!loc || !ref.anchor) return null;\n\t\tconst at = findVectorAnchorById(loc.node, ref.anchor);\n\t\tif (!at) return null;\n\t\treturn { loc, ...at, a: loc.node.subpaths[at.subpath].anchors[at.index] };\n\t}\n\n\t/** The slot of a point of a path (anchor or handle), allocating it. */\n\tpointSlot(loc: Located, subpath: number, index: number, part: Part): PointSlot | null {\n\t\tconst sp = loc.node.subpaths[subpath];\n\t\tconst a = sp?.anchors[index];\n\t\tif (!a) return null;\n\t\tconst key = `${loc.node.id}#${subpath}:${index}#${part}`;\n\t\tconst hit = this.slots.get(key);\n\t\tif (hit) return hit as PointSlot;\n\t\tconst p = part === 'anchor' ? a : (a[part] ?? a);\n\t\tconst d = applyMatrix(loc.world, p);\n\t\tconst slot: PointSlot = { kind: 'point', nodeId: loc.node.id, subpath, index, part, off: this.x0.length };\n\t\tthis.x0.push(d.x, d.y);\n\t\tthis.slots.set(key, slot);\n\t\treturn slot;\n\t}\n\n\tcircleSlot(nodeId: string): CircleSlot | null {\n\t\tconst key = `${nodeId}#circle`;\n\t\tconst hit = this.slots.get(key);\n\t\tif (hit) return hit as CircleSlot;\n\t\tconst loc = this.located.get(nodeId);\n\t\tif (!loc) return null;\n\t\tconst circle = vectorPathCircle(loc.node, loc.world);\n\t\tif (!circle) return null;\n\t\tconst slot: CircleSlot = { kind: 'circle', nodeId, off: this.x0.length, c0: circle.c, r0: circle.r };\n\t\tthis.x0.push(circle.c.x, circle.c.y, circle.r);\n\t\tthis.slots.set(key, slot);\n\t\treturn slot;\n\t}\n\n\t/** A point: an anchor, a handle, or a circle's centre. Returns its x index (y is next). */\n\tpoint(ref: VectorGeomRef): number | null {\n\t\tif (ref.circle) return this.circleSlot(ref.nodeId)?.off ?? null;\n\t\tif (ref.segment) return null;\n\t\tconst at = this.anchorOf(ref);\n\t\tif (!at) return null;\n\t\treturn this.pointSlot(at.loc, at.subpath, at.index, ref.handle ?? 'anchor')?.off ?? null;\n\t}\n\n\t/** A segment's end points (anchor and next anchor) as x indices. */\n\tline(ref: VectorGeomRef): [number, number] | null {\n\t\tif (!ref.segment) return null;\n\t\tconst at = this.anchorOf(ref);\n\t\tif (!at) return null;\n\t\tconst sp = at.loc.node.subpaths[at.subpath];\n\t\tconst next = at.index + 1 < sp.anchors.length ? at.index + 1 : sp.closed ? 0 : -1;\n\t\tif (next < 0 || next === at.index) return null;\n\t\tconst a = this.pointSlot(at.loc, at.subpath, at.index, 'anchor');\n\t\tconst b = this.pointSlot(at.loc, at.subpath, next, 'anchor');\n\t\treturn a && b ? [a.off, b.off] : null;\n\t}\n\n\t/** A circle's centre x index (y, then r follow). */\n\tcircle(ref: VectorGeomRef): number | null {\n\t\treturn ref.circle ? (this.circleSlot(ref.nodeId)?.off ?? null) : null;\n\t}\n\n\t/**\n\t * The two cubic segments meeting at an anchor, as x indices of their control points (incoming:\n\t * previous anchor, its out handle, this anchor's in handle, this anchor; outgoing: this anchor,\n\t * its out handle, the next anchor's in handle, the next anchor). Null at an open end.\n\t */\n\tjoint(ref: VectorGeomRef): { inc: [number, number, number, number]; out: [number, number, number, number] } | null {\n\t\tconst at = this.anchorOf(ref);\n\t\tif (!at || ref.segment || ref.handle) return null;\n\t\tconst sp = at.loc.node.subpaths[at.subpath];\n\t\tconst n = sp.anchors.length;\n\t\tconst prev = at.index > 0 ? at.index - 1 : sp.closed ? n - 1 : -1;\n\t\tconst next = at.index + 1 < n ? at.index + 1 : sp.closed ? 0 : -1;\n\t\tif (prev < 0 || next < 0 || n < 2) return null;\n\t\tconst s = (i: number, p: Part) => this.pointSlot(at.loc, at.subpath, i, p)!.off;\n\t\treturn { inc: [s(prev, 'anchor'), s(prev, 'out'), s(at.index, 'in'), s(at.index, 'anchor')], out: [s(at.index, 'anchor'), s(at.index, 'out'), s(next, 'in'), s(next, 'anchor')] };\n\t}\n\n\t/** Parameter indices a Fix of `ref` locks. */\n\tlockIndices(ref: VectorGeomRef): number[] {\n\t\tconst c = this.circle(ref);\n\t\tif (c !== null) return [c, c + 1, c + 2];\n\t\tconst l = this.line(ref);\n\t\tif (l) return [l[0], l[0] + 1, l[1], l[1] + 1];\n\t\tconst p = this.point(ref);\n\t\treturn p === null ? [] : [p, p + 1];\n\t}\n\n\t/** The document with every slot's parameters from `x` written back into the paths. */\n\twriteBack(doc: VectorDocument, x: readonly number[]): VectorDocument {\n\t\tconst byNode = new Map<string, Slot[]>();\n\t\tfor (const s of this.slots.values()) byNode.set(s.nodeId, [...(byNode.get(s.nodeId) ?? []), s]);\n\t\tconst changed = new Map<string, VectorPathNode>();\n\t\tfor (const [nodeId, slots] of byNode) {\n\t\t\tconst loc = this.located.get(nodeId)!;\n\t\t\tconst inv = invertMatrix(loc.world);\n\t\t\tconst toLocal = (p: Vec2) => applyMatrix(inv, p);\n\t\t\tlet subpaths = loc.node.subpaths;\n\t\t\tconst circle = slots.find((s): s is CircleSlot => s.kind === 'circle');\n\t\t\tif (circle) {\n\t\t\t\tconst c = { x: x[circle.off], y: x[circle.off + 1] };\n\t\t\t\tconst k = x[circle.off + 2] / circle.r0;\n\t\t\t\tconst map = (p: Vec2) => toLocal({ x: c.x + (applyMatrix(loc.world, p).x - circle.c0.x) * k, y: c.y + (applyMatrix(loc.world, p).y - circle.c0.y) * k });\n\t\t\t\tsubpaths = subpaths.map((sp) => ({ ...sp, anchors: sp.anchors.map((a) => ({ ...a, ...map(a), ...(a.in ? { in: map(a.in) } : {}), ...(a.out ? { out: map(a.out) } : {}) })) }));\n\t\t\t}\n\t\t\tconst points = slots.filter((s): s is PointSlot => s.kind === 'point');\n\t\t\tif (points.length) {\n\t\t\t\tsubpaths = subpaths.map((sp, si) => {\n\t\t\t\t\tif (!points.some((p) => p.subpath === si)) return sp;\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...sp,\n\t\t\t\t\t\tanchors: sp.anchors.map((a, ai) => {\n\t\t\t\t\t\t\tconst mine = points.filter((p) => p.subpath === si && p.index === ai);\n\t\t\t\t\t\t\tif (!mine.length) return a;\n\t\t\t\t\t\t\tconst next: VectorAnchor = { ...a };\n\t\t\t\t\t\t\tconst anchorSlot = mine.find((p) => p.part === 'anchor');\n\t\t\t\t\t\t\tif (anchorSlot) {\n\t\t\t\t\t\t\t\tconst q = toLocal({ x: x[anchorSlot.off], y: x[anchorSlot.off + 1] });\n\t\t\t\t\t\t\t\tconst dx = q.x - a.x;\n\t\t\t\t\t\t\t\tconst dy = q.y - a.y;\n\t\t\t\t\t\t\t\tnext.x = q.x;\n\t\t\t\t\t\t\t\tnext.y = q.y;\n\t\t\t\t\t\t\t\t// Handles that are not parameters move with their anchor.\n\t\t\t\t\t\t\t\tif (a.in && !mine.some((p) => p.part === 'in')) next.in = { x: a.in.x + dx, y: a.in.y + dy };\n\t\t\t\t\t\t\t\tif (a.out && !mine.some((p) => p.part === 'out')) next.out = { x: a.out.x + dx, y: a.out.y + dy };\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tfor (const part of ['in', 'out'] as const) {\n\t\t\t\t\t\t\t\tconst h = mine.find((p) => p.part === part);\n\t\t\t\t\t\t\t\tif (!h) continue;\n\t\t\t\t\t\t\t\tconst q = toLocal({ x: x[h.off], y: x[h.off + 1] });\n\t\t\t\t\t\t\t\tnext[part] = Math.hypot(q.x - next.x, q.y - next.y) < 1e-9 ? null : q;\n\t\t\t\t\t\t\t\tif (!next[part]) delete next[part];\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn next;\n\t\t\t\t\t\t})\n\t\t\t\t\t};\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (subpaths !== loc.node.subpaths) changed.set(nodeId, { ...loc.node, subpaths });\n\t\t}\n\t\tif (!changed.size) return doc;\n\t\tconst map = (list: VectorNode[]): VectorNode[] =>\n\t\t\tlist.map((n) => {\n\t\t\t\tconst c = changed.get(n.id);\n\t\t\t\tif (c) return c;\n\t\t\t\tif (n.type === 'group') {\n\t\t\t\t\tconst children = map(n.children);\n\t\t\t\t\treturn children.some((ch, i) => ch !== n.children[i]) ? { ...n, children } : n;\n\t\t\t\t}\n\t\t\t\treturn n;\n\t\t\t});\n\t\treturn { ...doc, nodes: map(doc.nodes) };\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Residuals\n// ---------------------------------------------------------------------------\n\ninterface Built {\n\tid: string;\n\tkind: VectorConstraintKind;\n\trefs: VectorGeomRef[];\n\trows: number;\n\t/** Fix: parameters locked instead of rows. */\n\tlocks: number[];\n\teval(x: readonly number[], out: number[]): void;\n\t/** The current value of a dimensional constraint (degrees for angles). */\n\tmeasure?(x: readonly number[]): number;\n}\n\nconst pt = (x: readonly number[], i: number): Vec2 => ({ x: x[i], y: x[i + 1] });\n\n/** The point of a cubic's curvature: (2/3)·cross(P1 − P0, P2 − P1) / |P1 − P0|³ at its start. */\nfunction startCurvature(p0: Vec2, p1: Vec2, p2: Vec2): number {\n\tconst d = sub(p1, p0);\n\tconst l = len(d);\n\treturn l > 1e-9 ? ((2 / 3) * cross(d, sub(p2, p1))) / (l * l * l) : 0;\n}\n\n/**\n * Interprets a constraint against the model (allocating its parameters), deciding orientations from\n * the starting geometry. Throws a `VectorConstraintError` saying what references it needs.\n */\nfunction interpret(c: VectorConstraint, m: Model, target: number): Built {\n\tconst refs = c.refs;\n\tconst n = refs.length;\n\tconst need = (what: string): never => {\n\t\tthrow new VectorConstraintError(`${c.kind} needs ${what}.`, [c.id]);\n\t};\n\tconst x0 = m.x0;\n\tconst P = (i: number) => (i < n ? m.point(refs[i]) : null);\n\tconst L = (i: number) => (i < n ? m.line(refs[i]) : null);\n\tconst C = (i: number) => (i < n ? m.circle(refs[i]) : null);\n\tconst base = { id: c.id, kind: c.kind, refs, locks: [] as number[] };\n\tconst rowsOf = (rows: number, f: (x: readonly number[], out: number[]) => void, measure?: (x: readonly number[]) => number): Built => ({ ...base, rows, eval: f, ...(measure ? { measure } : {}) });\n\tconst lineDir = (x: readonly number[], l: [number, number]) => sub(pt(x, l[1]), pt(x, l[0]));\n\tswitch (c.kind) {\n\t\tcase 'fix': {\n\t\t\tif (!n) need('one or more references');\n\t\t\tconst locks = refs.flatMap((r) => m.lockIndices(r));\n\t\t\tif (locks.length < refs.length * 2) need('anchors, segments or circles that exist');\n\t\t\treturn { ...base, rows: 0, locks, eval: () => {} };\n\t\t}\n\t\tcase 'coincident': {\n\t\t\tif (n !== 2) need('two references');\n\t\t\tconst a = P(0);\n\t\t\tconst b = P(1);\n\t\t\tif (a !== null && b !== null) return rowsOf(2, (x, out) => out.push(x[a] - x[b], x[a + 1] - x[b + 1]));\n\t\t\tfor (const [pi, li] of [\n\t\t\t\t[0, 1],\n\t\t\t\t[1, 0]\n\t\t\t]) {\n\t\t\t\tconst p = P(pi);\n\t\t\t\tconst l = L(li);\n\t\t\t\tif (p !== null && l) return rowsOf(1, (x, out) => out.push(cross(unit(lineDir(x, l)), sub(pt(x, p), pt(x, l[0])))));\n\t\t\t\tconst ci = C(li);\n\t\t\t\tif (p !== null && ci !== null && !refs[pi].circle) return rowsOf(1, (x, out) => out.push(len(sub(pt(x, p), pt(x, ci))) - x[ci + 2]));\n\t\t\t}\n\t\t\treturn need('two points, a point and a segment, or a point and a circle');\n\t\t}\n\t\tcase 'collinear': {\n\t\t\tconst a = L(0);\n\t\t\tconst b = L(1);\n\t\t\tif (n !== 2 || !a || !b) need('two segments');\n\t\t\treturn rowsOf(2, (x, out) => {\n\t\t\t\tconst d = unit(lineDir(x, a!));\n\t\t\t\tout.push(cross(d, sub(pt(x, b![0]), pt(x, a![0]))), cross(d, sub(pt(x, b![1]), pt(x, a![0]))));\n\t\t\t});\n\t\t}\n\t\tcase 'concentric': {\n\t\t\tif (n !== 2) need('two circles, or a circle and a point');\n\t\t\tconst a = C(0) ?? P(0);\n\t\t\tconst b = C(1) ?? P(1);\n\t\t\tif (a === null || b === null || (C(0) === null && C(1) === null)) need('two circles, or a circle and a point');\n\t\t\treturn rowsOf(2, (x, out) => out.push(x[a!] - x[b!], x[a! + 1] - x[b! + 1]));\n\t\t}\n\t\tcase 'parallel':\n\t\tcase 'perpendicular': {\n\t\t\tconst a = L(0);\n\t\t\tconst b = L(1);\n\t\t\tif (n !== 2 || !a || !b) need('two segments');\n\t\t\t// In length units (the second segment's), so the tolerance means the same as for points.\n\t\t\treturn rowsOf(1, (x, out) => {\n\t\t\t\tconst u = unit(lineDir(x, a!));\n\t\t\t\tconst v = lineDir(x, b!);\n\t\t\t\tout.push(c.kind === 'parallel' ? cross(u, v) : dot(u, v));\n\t\t\t});\n\t\t}\n\t\tcase 'horizontal':\n\t\tcase 'vertical': {\n\t\t\tconst axis = c.kind === 'horizontal' ? 1 : 0;\n\t\t\tconst l = n === 1 ? L(0) : null;\n\t\t\tif (l) return rowsOf(1, (x, out) => out.push(x[l[1] + axis] - x[l[0] + axis]));\n\t\t\tconst a = P(0);\n\t\t\tconst b = P(1);\n\t\t\tif (n === 2 && a !== null && b !== null) return rowsOf(1, (x, out) => out.push(x[b + axis] - x[a + axis]));\n\t\t\treturn need('a segment or two points');\n\t\t}\n\t\tcase 'tangent': {\n\t\t\tif (n === 1) {\n\t\t\t\tconst j = m.joint(refs[0]);\n\t\t\t\tif (!j) need('an anchor with a segment on each side, a segment and a circle, or two circles');\n\t\t\t\t// The handles point away from each other through the anchor (in the out handle's length).\n\t\t\t\treturn rowsOf(1, (x, out) => out.push(cross(unit(sub(pt(x, j!.inc[3]), pt(x, j!.inc[2]))), sub(pt(x, j!.out[1]), pt(x, j!.out[0])))));\n\t\t\t}\n\t\t\tif (n !== 2) need('a segment and a circle, two circles, or one anchor');\n\t\t\tfor (const [li, ci] of [\n\t\t\t\t[0, 1],\n\t\t\t\t[1, 0]\n\t\t\t]) {\n\t\t\t\tconst l = L(li);\n\t\t\t\tconst cc = C(ci);\n\t\t\t\tif (l && cc !== null) {\n\t\t\t\t\tconst sign = Math.sign(cross(unit(lineDir(x0, l)), sub(pt(x0, cc), pt(x0, l[0])))) || 1;\n\t\t\t\t\treturn rowsOf(1, (x, out) => out.push(sign * cross(unit(lineDir(x, l)), sub(pt(x, cc), pt(x, l[0]))) - x[cc + 2]));\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst a = C(0);\n\t\t\tconst b = C(1);\n\t\t\tif (a !== null && b !== null) {\n\t\t\t\tconst d0 = len(sub(pt(x0, a), pt(x0, b)));\n\t\t\t\tconst inside = d0 < Math.max(x0[a + 2], x0[b + 2]);\n\t\t\t\treturn rowsOf(1, (x, out) => out.push(len(sub(pt(x, a), pt(x, b))) - (inside ? Math.abs(x[a + 2] - x[b + 2]) : x[a + 2] + x[b + 2])));\n\t\t\t}\n\t\t\treturn need('a segment and a circle, two circles, or one anchor');\n\t\t}\n\t\tcase 'smooth': {\n\t\t\tconst j = n === 1 ? m.joint(refs[0]) : null;\n\t\t\tif (!j) need('one anchor with a segment on each side');\n\t\t\tconst scale = Math.max(1, len(sub(pt(x0, j!.inc[0]), pt(x0, j!.out[3]))));\n\t\t\treturn rowsOf(2, (x, out) => {\n\t\t\t\tconst [p0, p1, p2, p3] = j!.inc.map((i) => pt(x, i));\n\t\t\t\tconst [q0, q1, q2] = j!.out.map((i) => pt(x, i));\n\t\t\t\tout.push(cross(unit(sub(p3, p2)), sub(q1, q0)));\n\t\t\t\t// Curvature where the incoming cubic ends (its reverse's start) and where the outgoing starts.\n\t\t\t\tconst kin = -startCurvature(p3, p2, p1);\n\t\t\t\tconst kout = startCurvature(q0, q1, q2);\n\t\t\t\tout.push((kin - kout) * scale);\n\t\t\t\tvoid p0;\n\t\t\t});\n\t\t}\n\t\tcase 'symmetric': {\n\t\t\tconst a = P(0);\n\t\t\tconst b = P(1);\n\t\t\tconst l = L(2);\n\t\t\tif (n !== 3 || a === null || b === null || !l) need('two points and a segment (the axis)');\n\t\t\treturn rowsOf(2, (x, out) => {\n\t\t\t\tconst d = unit(lineDir(x, l!));\n\t\t\t\tconst p = pt(x, a!);\n\t\t\t\tconst q = pt(x, b!);\n\t\t\t\tconst mid = { x: (p.x + q.x) / 2, y: (p.y + q.y) / 2 };\n\t\t\t\tout.push(cross(d, sub(mid, pt(x, l![0]))), dot(d, sub(q, p)));\n\t\t\t});\n\t\t}\n\t\tcase 'equal': {\n\t\t\tif (n !== 2) need('two segments or two circles');\n\t\t\tconst la = L(0);\n\t\t\tconst lb = L(1);\n\t\t\tif (la && lb) return rowsOf(1, (x, out) => out.push(len(lineDir(x, la)) - len(lineDir(x, lb))));\n\t\t\tconst ca = C(0);\n\t\t\tconst cb = C(1);\n\t\t\tif (ca !== null && cb !== null) return rowsOf(1, (x, out) => out.push(x[ca + 2] - x[cb + 2]));\n\t\t\treturn need('two segments or two circles');\n\t\t}\n\t\tcase 'distance': {\n\t\t\tconst axis = c.axis ?? 'aligned';\n\t\t\tlet a: number | null = null;\n\t\t\tlet b: number | null = null;\n\t\t\tif (n === 1) {\n\t\t\t\tconst l = L(0);\n\t\t\t\tif (l) [a, b] = l;\n\t\t\t} else if (n === 2) {\n\t\t\t\ta = P(0);\n\t\t\t\tb = P(1);\n\t\t\t\tconst l = a === null ? L(0) : L(1);\n\t\t\t\tconst p = a === null ? P(1) : P(0);\n\t\t\t\tif ((a === null || b === null) && l && p !== null && axis === 'aligned') {\n\t\t\t\t\tconst sign = Math.sign(cross(unit(lineDir(x0, l)), sub(pt(x0, p), pt(x0, l[0])))) || 1;\n\t\t\t\t\tconst f = (x: readonly number[]) => sign * cross(unit(lineDir(x, l)), sub(pt(x, p), pt(x, l[0])));\n\t\t\t\t\treturn rowsOf(1, (x, out) => out.push(f(x) - target), f);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (a === null || b === null) need('a segment, two points, or a point and a segment');\n\t\t\tif (axis === 'aligned') {\n\t\t\t\tconst f = (x: readonly number[]) => len(sub(pt(x, b!), pt(x, a!)));\n\t\t\t\treturn rowsOf(1, (x, out) => out.push(f(x) - target), f);\n\t\t\t}\n\t\t\tconst k = axis === 'horizontal' ? 0 : 1;\n\t\t\tconst sign = Math.sign(x0[b! + k] - x0[a! + k]) || 1;\n\t\t\tconst f = (x: readonly number[]) => sign * (x[b! + k] - x[a! + k]);\n\t\t\treturn rowsOf(1, (x, out) => out.push(f(x) - target), f);\n\t\t}\n\t\tcase 'angle': {\n\t\t\tconst a = L(0);\n\t\t\tconst b = L(1);\n\t\t\tif (n !== 2 || !a || !b) need('two segments');\n\t\t\tconst ang = (x: readonly number[]) => {\n\t\t\t\tconst u = lineDir(x, a!);\n\t\t\t\tconst v = lineDir(x, b!);\n\t\t\t\treturn Math.atan2(cross(u, v), dot(u, v));\n\t\t\t};\n\t\t\tconst sign = Math.sign(ang(x0)) || 1;\n\t\t\tconst rad = (target * Math.PI) / 180;\n\t\t\t// Radians times the segments' size: an arc length, like the other rows.\n\t\t\tconst size = Math.max(1, (len(lineDir(x0, a!)) + len(lineDir(x0, b!))) / 2);\n\t\t\treturn rowsOf(\n\t\t\t\t1,\n\t\t\t\t(x, out) => out.push(wrap(ang(x) - sign * rad) * size),\n\t\t\t\t(x) => (Math.abs(ang(x)) * 180) / Math.PI\n\t\t\t);\n\t\t}\n\t\tcase 'radius':\n\t\tcase 'diameter': {\n\t\t\tconst cc = n === 1 ? C(0) : null;\n\t\t\tif (cc === null) need('one circle (a round closed path)');\n\t\t\tconst k = c.kind === 'radius' ? 1 : 2;\n\t\t\tif (!(target > 0)) throw new VectorConstraintError(`${c.kind} must be positive, got ${target}.`, [c.id]);\n\t\t\tconst f = (x: readonly number[]) => k * x[cc! + 2];\n\t\t\treturn rowsOf(1, (x, out) => out.push(f(x) - target), f);\n\t\t}\n\t}\n\treturn need('a known kind');\n}\n\n// ---------------------------------------------------------------------------\n// Parameters and values\n// ---------------------------------------------------------------------------\n\n/** The expression environment: the document's parameters and named dimensional constraints. */\nexport function vectorConstraintEnv(doc: VectorDocument): VectorExprEnv {\n\tconst env = new VectorExprEnv();\n\tfor (const p of doc.parameters ?? []) env.set(p.name, p.value);\n\tfor (const c of doc.constraints ?? []) if (c.name && c.value !== undefined && VECTOR_DIMENSIONAL_KINDS.has(c.kind)) env.set(c.name, c.value);\n\treturn env;\n}\n\n/** The value of a dimensional constraint (degrees for angles). */\nexport function vectorConstraintValue(env: VectorExprEnv, c: VectorConstraint): number {\n\tif (c.value === undefined || c.value === '') throw new VectorConstraintError(`${c.kind} needs a value.`, [c.id]);\n\ttry {\n\t\treturn c.name && env.has(c.name) ? env.value(c.name) : env.eval(c.value);\n\t} catch (e) {\n\t\tthrow new VectorConstraintError(`${c.name ?? c.kind}: ${e instanceof Error ? e.message : String(e)}`, [c.id]);\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Components\n// ---------------------------------------------------------------------------\n\nclass Comp {\n\tconstructor(\n\t\treadonly model: Model,\n\t\treadonly built: Built[],\n\t\treadonly weights: number[],\n\t\treadonly tol: number\n\t) {}\n\n\tfree(skip: number | null = null): number[] {\n\t\tconst locked = new Set<number>();\n\t\tthis.built.forEach((b, i) => {\n\t\t\tif (i !== skip) for (const j of b.locks) locked.add(j);\n\t\t});\n\t\treturn this.model.x0.map((_, i) => i).filter((i) => !locked.has(i));\n\t}\n\n\trows(skip: number | null = null): number {\n\t\treturn this.built.reduce((s, b, i) => s + (i === skip ? 0 : b.rows), 0);\n\t}\n\n\tevalFn(skip: number | null = null): ResidualFn {\n\t\treturn (x, out) => {\n\t\t\tthis.built.forEach((b, i) => {\n\t\t\t\tif (i !== skip) b.eval(x, out);\n\t\t\t});\n\t\t};\n\t}\n\n\trun(skip: number | null = null) {\n\t\tconst free = this.free(skip);\n\t\tconst rows = this.rows(skip);\n\t\tif (rows > MAX_ROWS || rows * rows * free.length > 4e8) throw new VectorConstraintError('The constraint system is too large to solve.');\n\t\treturn solveLm(this.evalFn(skip), this.model.x0, free, this.weights, this.tol, MAX_ITER);\n\t}\n\n\t/** Constraint ids in conflict: each one whose removal makes the rest solvable. */\n\tconflicts(failed: readonly number[]): string[] {\n\t\tconst ids: string[] = [];\n\t\tif (this.built.length <= MAX_CONFLICT_PROBES) {\n\t\t\tthis.built.forEach((b, i) => {\n\t\t\t\ttry {\n\t\t\t\t\tif (this.run(i).converged) ids.push(b.id);\n\t\t\t\t} catch {\n\t\t\t\t\t/* too large */\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t\tif (!ids.length) {\n\t\t\tfor (const b of this.built) {\n\t\t\t\tconst out: number[] = [];\n\t\t\t\tb.eval(failed, out);\n\t\t\t\tif (maxAbs(out) > this.tol * 10) ids.push(b.id);\n\t\t\t}\n\t\t}\n\t\treturn ids;\n\t}\n}\n\nconst scaleTol = (x: readonly number[]) => 1e-9 * x.reduce((m, v) => Math.max(m, Math.abs(v)), 1);\n\n/** A key per thing a reference touches (for grouping constraints into components). */\nfunction refKeys(r: VectorGeomRef): string[] {\n\treturn r.circle ? [`${r.nodeId}#circle`] : [`${r.nodeId}#${r.anchor ?? ''}`];\n}\n\nfunction components(cons: readonly VectorConstraint[]): VectorConstraint[][] {\n\tconst parent = cons.map((_, i) => i);\n\tconst find = (i: number): number => {\n\t\twhile (parent[i] !== i) {\n\t\t\tparent[i] = parent[parent[i]];\n\t\t\ti = parent[i];\n\t\t}\n\t\treturn i;\n\t};\n\tconst owner = new Map<string, number>();\n\tcons.forEach((c, i) => {\n\t\t// A joint (tangent / smooth at an anchor) and a node's circle reach the whole path.\n\t\tconst keys = c.refs.flatMap((r) => (c.kind === 'tangent' || c.kind === 'smooth' || r.circle ? [`${r.nodeId}`] : refKeys(r).concat(r.segment ? [`${r.nodeId}`] : [])));\n\t\tfor (const k of [...keys, ...c.refs.map((r) => r.nodeId)]) {\n\t\t\tconst j = owner.get(k);\n\t\t\tif (j === undefined) owner.set(k, i);\n\t\t\telse parent[find(i)] = find(j);\n\t\t}\n\t});\n\tconst groups = new Map<number, VectorConstraint[]>();\n\tcons.forEach((c, i) => {\n\t\tconst r = find(i);\n\t\tgroups.set(r, [...(groups.get(r) ?? []), c]);\n\t});\n\treturn [...groups.values()];\n}\n\ninterface BuildResult {\n\tcomps: Comp[];\n\tskipped: { id: string; message: string }[];\n}\n\n/**\n * Builds the component systems. `previous` is the last solved document: parameters that differ\n * from it were just edited and get a heavy weight, so the solve moves them least.\n */\nfunction build(doc: VectorDocument, cons: readonly VectorConstraint[], previous: VectorDocument | null): BuildResult {\n\tconst located = locateNodes(doc);\n\tconst before = previous ? locateNodes(previous) : null;\n\tconst env = vectorConstraintEnv(doc);\n\tconst skipped: BuildResult['skipped'] = [];\n\tconst comps: Comp[] = [];\n\tfor (const group of components(cons)) {\n\t\tconst model = new Model(located);\n\t\tconst built: Built[] = [];\n\t\tfor (const c of group) {\n\t\t\ttry {\n\t\t\t\tconst target = VECTOR_DIMENSIONAL_KINDS.has(c.kind) ? vectorConstraintValue(env, c) : 0;\n\t\t\t\tbuilt.push(interpret(c, model, target));\n\t\t\t} catch (e) {\n\t\t\t\tskipped.push({ id: c.id, message: e instanceof Error ? e.message : String(e) });\n\t\t\t}\n\t\t}\n\t\tif (!built.length) continue;\n\t\tconst weights = model.x0.map(() => 1);\n\t\tif (before) {\n\t\t\t// The same slots measured in the previous document: what moved since was just edited.\n\t\t\tconst prev = new Model(before);\n\t\t\tfor (const [key, slot] of model.slots) {\n\t\t\t\tlet off: number | null = null;\n\t\t\t\tif (slot.kind === 'circle') off = prev.circleSlot(slot.nodeId)?.off ?? null;\n\t\t\t\telse {\n\t\t\t\t\tconst loc = before.get(slot.nodeId);\n\t\t\t\t\tif (loc && loc.node.subpaths[slot.subpath]?.anchors[slot.index]) off = prev.pointSlot(loc, slot.subpath, slot.index, slot.part)?.off ?? null;\n\t\t\t\t}\n\t\t\t\tconst k = slot.kind === 'circle' ? 3 : 2;\n\t\t\t\tfor (let i = 0; i < k; i++) {\n\t\t\t\t\tconst j = slot.off + i;\n\t\t\t\t\tif (off === null || Math.abs(prev.x0[off + i] - model.x0[j]) > 1e-9) weights[j] = 1e6;\n\t\t\t\t}\n\t\t\t\tvoid key;\n\t\t\t}\n\t\t}\n\t\tcomps.push(new Comp(model, built, weights, scaleTol(model.x0)));\n\t}\n\treturn { comps, skipped };\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/** What a solve did. */\nexport interface VectorSolveResult {\n\t/** The document with the solved geometry (the same object when nothing moved). */\n\tdocument: VectorDocument;\n\t/** Every component converged (every constraint holds). */\n\tconverged: boolean;\n\t/** Constraints in conflict where a component did not converge. */\n\tconflicts: string[];\n\t/** Constraints that could not be interpreted (and were ignored), with the reason. */\n\tskipped: { id: string; message: string }[];\n}\n\n/**\n * Solves the document's constraints and writes the geometry back. `previous` (the last solved\n * document) marks what was just edited, which moves least. A component that cannot be solved is\n * left with its best compromise and reported in `conflicts`.\n */\nexport function solveVectorConstraints(doc: VectorDocument, previous: VectorDocument | null = null): VectorSolveResult {\n\tconst cons = (doc.constraints ?? []).slice(0, VECTOR_MAX_CONSTRAINTS);\n\tif (!cons.length) return { document: doc, converged: true, conflicts: [], skipped: [] };\n\tconst { comps, skipped } = build(doc, cons, previous);\n\tlet out = doc;\n\tlet converged = true;\n\tconst conflicts: string[] = [];\n\tfor (const comp of comps) {\n\t\tlet o;\n\t\ttry {\n\t\t\to = comp.run();\n\t\t} catch {\n\t\t\tconverged = false;\n\t\t\tconflicts.push(...comp.built.map((b) => b.id));\n\t\t\tcontinue;\n\t\t}\n\t\tif (!o.converged) {\n\t\t\tconverged = false;\n\t\t\tconflicts.push(...comp.conflicts(o.x));\n\t\t}\n\t\t// Nothing moved: keep the document object.\n\t\tif (o.x.some((v, i) => Math.abs(v - comp.model.x0[i]) > 1e-12)) out = comp.model.writeBack(out, o.x);\n\t}\n\treturn { document: out, converged, conflicts, skipped };\n}\n\n/** The largest residual of each constraint at the current geometry (null when it cannot be interpreted). */\nexport function vectorConstraintResiduals(doc: VectorDocument): Map<string, number | null> {\n\tconst out = new Map<string, number | null>();\n\tconst env = vectorConstraintEnv(doc);\n\tconst located = locateNodes(doc);\n\tfor (const c of doc.constraints ?? []) {\n\t\ttry {\n\t\t\tconst model = new Model(located);\n\t\t\tconst target = VECTOR_DIMENSIONAL_KINDS.has(c.kind) ? vectorConstraintValue(env, c) : 0;\n\t\t\tconst b = interpret(c, model, target);\n\t\t\tconst r: number[] = [];\n\t\t\tb.eval(model.x0, r);\n\t\t\tout.set(c.id, maxAbs(r));\n\t\t} catch {\n\t\t\tout.set(c.id, null);\n\t\t}\n\t}\n\treturn out;\n}\n\n/** The current value of a dimensional constraint's references (degrees for angles). */\nexport function measureVectorConstraint(doc: VectorDocument, c: Pick<VectorConstraint, 'kind' | 'refs' | 'axis'>): number {\n\tconst model = new Model(locateNodes(doc));\n\tconst b = interpret({ id: '', ...c, value: '1' }, model, 1);\n\tif (!b.measure) throw new VectorConstraintError(`${c.kind} has no value to measure.`);\n\treturn b.measure(model.x0);\n}\n\n/** Checks a constraint's references against the document (throws with what is needed). */\nexport function validateVectorConstraint(doc: VectorDocument, c: VectorConstraint): void {\n\tfor (const r of c.refs) {\n\t\tconst loc = locateNodes(doc).get(r.nodeId);\n\t\tif (!loc) throw new VectorConstraintError(`There is no path \"${r.nodeId}\" (constraints refer to paths, not groups or text).`, [c.id]);\n\t\tif (r.anchor && !findVectorAnchorById(loc.node, r.anchor)) throw new VectorConstraintError(`Path \"${r.nodeId}\" has no anchor \"${r.anchor}\".`, [c.id]);\n\t\tif (r.circle && !vectorPathCircle(loc.node, loc.world)) throw new VectorConstraintError(`Path \"${r.nodeId}\" is not round enough to be a circle.`, [c.id]);\n\t}\n\tinterpret(c, new Model(locateNodes(doc)), VECTOR_DIMENSIONAL_KINDS.has(c.kind) ? 1 : 0);\n}\n\n/** Next free name for a dimensional constraint (`d1`, `ang2`, `rad1`, `dia1`). */\nexport function nextVectorConstraintName(doc: VectorDocument, kind: VectorConstraintKind): string {\n\tconst prefix = kind === 'angle' ? 'ang' : kind === 'radius' ? 'rad' : kind === 'diameter' ? 'dia' : 'd';\n\tconst used = new Set([...(doc.parameters ?? []).map((p) => p.name), ...(doc.constraints ?? []).map((c) => c.name ?? '')]);\n\tfor (let i = 1; ; i++) if (!used.has(`${prefix}${i}`)) return `${prefix}${i}`;\n}\n\n/**\n * Adds a constraint to the document: names and measures a dimensional one without a value,\n * refuses one that conflicts with the others or duplicates them (over-constrains), solves and\n * returns the new document. Throws a `VectorConstraintError` naming the constraints involved.\n */\nexport function addVectorConstraint(doc: VectorDocument, input: VectorConstraint): VectorDocument {\n\tif ((doc.constraints?.length ?? 0) >= VECTOR_MAX_CONSTRAINTS) throw new VectorConstraintError('Too many constraints.');\n\tlet c: VectorConstraint = { ...input, refs: input.refs.map((r) => ({ ...r })) };\n\tvalidateVectorConstraint(doc, c);\n\tif (VECTOR_DIMENSIONAL_KINDS.has(c.kind)) {\n\t\tconst name = c.name ?? nextVectorConstraintName(doc, c.kind);\n\t\tif (!isValidVectorParameterName(name)) throw new VectorConstraintError(`\"${name}\" is not a valid name: a letter or _ then letters, digits or _.`);\n\t\tif ([...(doc.parameters ?? []), ...(doc.constraints ?? [])].some((p) => p.name === name)) throw new VectorConstraintError(`The name \"${name}\" is already used.`);\n\t\tc = { ...c, name, value: c.value?.trim() ? c.value.trim() : formatVectorValue(measureVectorConstraint(doc, c)) };\n\t} else {\n\t\tdelete c.name;\n\t\tdelete c.value;\n\t}\n\tconst trial: VectorDocument = { ...doc, constraints: [...(doc.constraints ?? []), c] };\n\tconst { comps, skipped } = build(trial, trial.constraints!, null);\n\tconst own = skipped.find((s) => s.id === c.id);\n\tif (own) throw new VectorConstraintError(own.message, [c.id]);\n\tconst comp = comps.find((k) => k.built.some((b) => b.id === c.id));\n\tif (!comp) throw new VectorConstraintError('The constraint could not be interpreted.', [c.id]);\n\tconst o = comp.run();\n\tif (!o.converged) {\n\t\tconst ids = comp.conflicts(o.x).filter((i) => i !== c.id);\n\t\tthrow new VectorConstraintError(`The constraint conflicts with ${ids.length ? ids.map((i) => `\"${i}\"`).join(', ') : 'the existing constraints'}.`, [c.id, ...ids]);\n\t}\n\tconst pos = comp.built.findIndex((b) => b.id === c.id);\n\tconst nb = comp.built[pos];\n\tif (nb.rows > 0) {\n\t\t// The new rows must raise the rank, or they only repeat what is already there.\n\t\tconst free = comp.free();\n\t\tconst others = comp.built.filter((b) => b.id !== c.id);\n\t\tconst ordered = [...others, nb];\n\t\tconst f: ResidualFn = (x, out) => ordered.forEach((b) => b.eval(x, out));\n\t\tconst rowsAll = ordered.reduce((s, b) => s + b.rows, 0);\n\t\tconst rowsOld = rowsAll - nb.rows;\n\t\tconst jac = jacobian(f, o.x, free, rowsAll);\n\t\tif (rankQr(jac, rowsAll, free.length) < rankQr(jac.subarray(0, rowsOld * free.length), rowsOld, free.length) + nb.rows) {\n\t\t\tconst mine = new Set(nb.refs.map((r) => r.nodeId));\n\t\t\tconst ids = others.filter((b) => b.refs.some((r) => mine.has(r.nodeId))).map((b) => b.id);\n\t\t\tthrow new VectorConstraintError(`The constraint would over-constrain the geometry (redundant with ${ids.length ? ids.map((i) => `\"${i}\"`).join(', ') : 'existing constraints'}).`, [c.id, ...ids]);\n\t\t}\n\t} else if (nb.locks.length) {\n\t\tconst before = comp.free(pos);\n\t\tconst after = new Set(comp.free());\n\t\tconst locked = before.map((j, k) => [j, k] as const).filter(([j]) => !after.has(j));\n\t\tconst rowsOld = comp.rows(pos);\n\t\tconst nf = before.length;\n\t\tconst jac = Array.from(jacobian(comp.evalFn(pos), o.x, before, rowsOld));\n\t\tconst rOld = rankQr(Float64Array.from(jac), rowsOld, nf);\n\t\tfor (const [, k] of locked) for (let col = 0; col < nf; col++) jac.push(col === k ? 1 : 0);\n\t\tif (!locked.length || rankQr(Float64Array.from(jac), rowsOld + locked.length, nf) < rOld + locked.length) {\n\t\t\tconst mine = new Set(nb.refs.map((r) => r.nodeId));\n\t\t\tconst ids = comp.built.filter((b) => b.id !== c.id && b.refs.some((r) => mine.has(r.nodeId))).map((b) => b.id);\n\t\t\tthrow new VectorConstraintError(`The Fix would over-constrain the geometry (redundant with ${ids.map((i) => `\"${i}\"`).join(', ') || 'existing constraints'}).`, [c.id, ...ids]);\n\t\t}\n\t}\n\treturn comp.model.writeBack(trial, o.x);\n}\n\n/** Constraint state of the drawing or a part of it. */\nexport type VectorDofStatus = 'unconstrained' | 'under' | 'fully' | 'over' | 'unknown';\n\nexport interface VectorDofReport {\n\t/** Remaining degrees of freedom (null when too large to analyse). */\n\tdof: number | null;\n\tstatus: VectorDofStatus;\n\t/** Equations that only repeat others. */\n\tredundant: number;\n\tconstraints: number;\n\t/** Per constrained path: its parameters and remaining freedom. */\n\tnodes: { nodeId: string; params: number; dof: number | null; status: VectorDofStatus }[];\n\tmessage: string;\n}\n\n/** Degrees of freedom the constraints leave (QR rank of the Jacobian per component; per path by null space). */\nexport function vectorConstraintDof(doc: VectorDocument): VectorDofReport {\n\tconst cons = doc.constraints ?? [];\n\tif (!cons.length) return { dof: 0, status: 'unconstrained', redundant: 0, constraints: 0, nodes: [], message: 'No constraints' };\n\tconst { comps } = build(doc, cons, null);\n\tlet total: number | null = 0;\n\tlet redundant = 0;\n\tlet count = 0;\n\tconst nodes = new Map<string, { params: number; dof: number | null; over: boolean }>();\n\tfor (const comp of comps) {\n\t\tconst free = comp.free();\n\t\tconst rows = comp.rows();\n\t\tcount += comp.built.length;\n\t\tconst nf = free.length;\n\t\tif (rows > MAX_ROWS) {\n\t\t\ttotal = null;\n\t\t\tcontinue;\n\t\t}\n\t\tconst jac = jacobian(comp.evalFn(), comp.model.x0, free, rows);\n\t\tconst { rank, basis } = nullSpace(jac, rows, nf);\n\t\tconst red = Math.max(0, rows - rank);\n\t\tredundant += red;\n\t\tif (total !== null) total += nf - Math.min(rank, nf);\n\t\tfor (const slot of comp.model.slots.values()) {\n\t\t\tconst k = slot.kind === 'circle' ? 3 : 2;\n\t\t\tconst cols: number[] = [];\n\t\t\tfree.forEach((j, c) => {\n\t\t\t\tif (j >= slot.off && j < slot.off + k) cols.push(c);\n\t\t\t});\n\t\t\tconst e = nodes.get(slot.nodeId) ?? { params: 0, dof: 0, over: false };\n\t\t\te.params += k;\n\t\t\tif (e.dof !== null) e.dof += cols.length && basis.length ? qr(basis.map((v) => cols.map((c) => v[c])), 0, 1e-6).rank : 0;\n\t\t\te.over ||= red > 0;\n\t\t\tnodes.set(slot.nodeId, e);\n\t\t}\n\t}\n\tconst status: VectorDofStatus = redundant > 0 ? 'over' : total === null ? 'unknown' : total === 0 ? 'fully' : 'under';\n\tconst plural = (n: number, one: string, many: string) => `${n} ${n === 1 ? one : many}`;\n\tconst message =\n\t\tstatus === 'over' ? `Over-constrained: ${plural(redundant, 'redundant equation', 'redundant equations')}` : status === 'fully' ? 'Fully constrained' : total === null ? 'Too large to analyse' : `${plural(total, 'degree of freedom', 'degrees of freedom')} remaining`;\n\treturn {\n\t\tdof: total,\n\t\tstatus,\n\t\tredundant,\n\t\tconstraints: count,\n\t\tnodes: [...nodes].map(([nodeId, e]) => ({ nodeId, params: e.params, dof: e.dof, status: e.over ? 'over' : e.dof === 0 ? 'fully' : e.dof === null ? 'unknown' : 'under' })),\n\t\tmessage\n\t};\n}\n\n/** Constraints whose references are gone (deleted paths or anchors) or no longer fit, removed. */\nexport function purgeVectorConstraints(doc: VectorDocument): { document: VectorDocument; removed: string[] } {\n\tconst cons = doc.constraints;\n\tif (!cons?.length) return { document: doc, removed: [] };\n\tconst located = locateNodes(doc);\n\tconst removed: string[] = [];\n\tconst kept = cons.filter((c) => {\n\t\tconst ok = c.refs.every((r) => {\n\t\t\tconst loc = located.get(r.nodeId);\n\t\t\tif (!loc) return false;\n\t\t\tif (!r.anchor) return !r.circle || !!vectorPathCircle(loc.node, loc.world);\n\t\t\tconst at = findVectorAnchorById(loc.node, r.anchor);\n\t\t\tif (!at) return false;\n\t\t\t// A segment needs the anchor after it.\n\t\t\tconst sp = loc.node.subpaths[at.subpath];\n\t\t\treturn !r.segment || (sp.anchors.length > 1 && (at.index + 1 < sp.anchors.length || sp.closed));\n\t\t});\n\t\tif (!ok) removed.push(c.id);\n\t\treturn ok;\n\t});\n\treturn removed.length ? { document: { ...doc, constraints: kept }, removed } : { document: doc, removed };\n}\n\nexport { VectorExprError };\n","// Portions ported from cadcraft/crates/constraints/src/auto.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * Auto-constrain: constraints a sketch already nearly satisfies, inferred within tolerances and\n * added one by one (refused ones skipped), most important first: coincident points of different\n * paths, concentric and tangent circles, a segment tangent to a circle, horizontal and vertical\n * segments, collinear, perpendicular (connected) and parallel segment pairs, equal lengths and radii.\n */\n\nimport { applyMatrix, multiplyMatrices, IDENTITY_MATRIX } from '../../geom/affine.js';\nimport type { AffineMatrix } from '../../geom/types.js';\nimport { addVectorConstraint, vectorPathCircle, VectorConstraintError } from './solve.js';\nimport type { Vec2, VectorConstraint, VectorConstraintKind, VectorDocument, VectorGeomRef, VectorNode, VectorPathNode } from '../types.js';\n\nexport interface VectorInferOptions {\n\t/** Document units (default 0.5). */\n\tdistanceTolerance?: number;\n\t/** Degrees (default 1). */\n\tangleTolerance?: number;\n\t/** Kinds to infer (default all of them). */\n\tkinds?: readonly VectorConstraintKind[];\n}\n\n/** Most candidates returned. */\nconst MAX_CANDIDATES = 2000;\n\n/** The path with every anchor given a stable id (`a1`, `a2`, …; existing ids kept). */\nexport function withVectorAnchorIds(node: VectorPathNode): VectorPathNode {\n\tif (node.subpaths.every((sp) => sp.anchors.every((a) => a.id))) return node;\n\tlet n = 0;\n\tfor (const sp of node.subpaths) for (const a of sp.anchors) if (a.id && /^a\\d+$/.test(a.id)) n = Math.max(n, Number(a.id.slice(1)));\n\tconst used = new Set(node.subpaths.flatMap((sp) => sp.anchors.map((a) => a.id).filter(Boolean)));\n\tconst next = () => {\n\t\tlet id = `a${++n}`;\n\t\twhile (used.has(id)) id = `a${++n}`;\n\t\tused.add(id);\n\t\treturn id;\n\t};\n\treturn { ...node, subpaths: node.subpaths.map((sp) => ({ ...sp, anchors: sp.anchors.map((a) => (a.id ? a : { ...a, id: next() })) })) };\n}\n\n/** The document with every anchor of the paths `ids` (all paths when absent) given an id. */\nexport function withVectorDocumentAnchorIds(doc: VectorDocument, ids?: ReadonlySet<string>): VectorDocument {\n\tlet changed = false;\n\tconst map = (list: VectorNode[]): VectorNode[] =>\n\t\tlist.map((n) => {\n\t\t\tif (n.type === 'path' && (!ids || ids.has(n.id))) {\n\t\t\t\tconst next = withVectorAnchorIds(n);\n\t\t\t\tif (next !== n) changed = true;\n\t\t\t\treturn next;\n\t\t\t}\n\t\t\tif (n.type === 'group') {\n\t\t\t\tconst children = map(n.children);\n\t\t\t\treturn children.some((c, i) => c !== n.children[i]) ? { ...n, children } : n;\n\t\t\t}\n\t\t\treturn n;\n\t\t});\n\tconst nodes = map(doc.nodes);\n\treturn changed ? { ...doc, nodes } : doc;\n}\n\ninterface Geo {\n\tpoints: { ref: VectorGeomRef; p: Vec2 }[];\n\tlines: { ref: VectorGeomRef; a: Vec2; b: Vec2 }[];\n\tcircles: { ref: VectorGeomRef; c: Vec2; r: number }[];\n}\n\nfunction collect(doc: VectorDocument, ids: ReadonlySet<string> | null): Geo {\n\tconst g: Geo = { points: [], lines: [], circles: [] };\n\tconst visit = (list: readonly VectorNode[], parent: AffineMatrix, inside: boolean) => {\n\t\tfor (const n of list) {\n\t\t\tconst m = n.transform ? multiplyMatrices(n.transform as AffineMatrix, parent) : parent;\n\t\t\tconst picked = inside || !ids || ids.has(n.id);\n\t\t\tif (n.type === 'group') visit(n.children, m, picked);\n\t\t\tif (n.type !== 'path' || !picked) continue;\n\t\t\tconst circle = vectorPathCircle(n, m);\n\t\t\tif (circle && n.subpaths.length === 1) {\n\t\t\t\tg.circles.push({ ref: { nodeId: n.id, circle: true }, ...circle });\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfor (const sp of n.subpaths) {\n\t\t\t\tconst pts = sp.anchors.map((a) => applyMatrix(m, a));\n\t\t\t\tsp.anchors.forEach((a, i) => g.points.push({ ref: { nodeId: n.id, anchor: a.id! }, p: pts[i] }));\n\t\t\t\tconst segs = sp.closed ? sp.anchors.length : sp.anchors.length - 1;\n\t\t\t\tfor (let i = 0; i < segs; i++) {\n\t\t\t\t\tconst a = sp.anchors[i];\n\t\t\t\t\tconst b = sp.anchors[(i + 1) % sp.anchors.length];\n\t\t\t\t\t// Straight segments only (no handles off their anchors).\n\t\t\t\t\tconst straight = (!a.out || (a.out.x === a.x && a.out.y === a.y)) && (!b.in || (b.in.x === b.x && b.in.y === b.y));\n\t\t\t\t\tif (straight && sp.anchors.length > 1) g.lines.push({ ref: { nodeId: n.id, anchor: a.id!, segment: true }, a: pts[i], b: pts[(i + 1) % pts.length] });\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\tvisit(doc.nodes, IDENTITY_MATRIX, false);\n\tg.points.splice(1000);\n\tg.lines.splice(1000);\n\tg.circles.splice(1000);\n\treturn g;\n}\n\nconst dist = (a: Vec2, b: Vec2) => Math.hypot(a.x - b.x, a.y - b.y);\n/** Acute angle between two directions, radians in [0, π/2]. */\nconst angleBetween = (u: Vec2, v: Vec2) => {\n\tconst a = Math.abs(Math.atan2(u.x * v.y - u.y * v.x, u.x * v.x + u.y * v.y));\n\treturn a > Math.PI / 2 ? Math.PI - a : a;\n};\nconst lineDist = (a: Vec2, b: Vec2, p: Vec2) => {\n\tconst l = dist(a, b);\n\treturn l > 1e-12 ? Math.abs((b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x)) / l : dist(a, p);\n};\n\n/**\n * Candidate constraints the paths `ids` (default all) nearly satisfy, most important first. Their\n * anchors need ids (`withVectorDocumentAnchorIds`); candidates have ids `auto-<n>`.\n */\nexport function inferVectorConstraints(doc: VectorDocument, ids: readonly string[] | null = null, options: VectorInferOptions = {}): VectorConstraint[] {\n\tconst tol = Math.max(0, options.distanceTolerance ?? 0.5);\n\tconst atol = (Math.max(0, Math.min(45, options.angleTolerance ?? 1)) * Math.PI) / 180;\n\tconst allow = (k: VectorConstraintKind) => !options.kinds || options.kinds.includes(k);\n\tconst g = collect(doc, ids ? new Set(ids) : null);\n\tconst out: VectorConstraint[] = [];\n\tconst push = (kind: VectorConstraintKind, refs: VectorGeomRef[]) => {\n\t\tif (out.length < MAX_CANDIDATES && allow(kind)) out.push({ id: `auto-${out.length + 1}`, kind, refs });\n\t};\n\tfor (let i = 0; i < g.points.length; i++)\n\t\tfor (let j = i + 1; j < g.points.length; j++) {\n\t\t\tconst a = g.points[i];\n\t\t\tconst b = g.points[j];\n\t\t\tif (a.ref.nodeId !== b.ref.nodeId && dist(a.p, b.p) <= tol) push('coincident', [a.ref, b.ref]);\n\t\t}\n\tfor (let i = 0; i < g.circles.length; i++)\n\t\tfor (let j = i + 1; j < g.circles.length; j++) {\n\t\t\tconst a = g.circles[i];\n\t\t\tconst b = g.circles[j];\n\t\t\tconst d = dist(a.c, b.c);\n\t\t\tif (d <= tol) push('concentric', [a.ref, b.ref]);\n\t\t\telse if (Math.abs(d - (a.r + b.r)) <= tol || Math.abs(d - Math.abs(a.r - b.r)) <= tol) push('tangent', [a.ref, b.ref]);\n\t\t}\n\tfor (const l of g.lines)\n\t\tfor (const c of g.circles) {\n\t\t\tif (l.ref.nodeId === c.ref.nodeId) continue;\n\t\t\tconst d = { x: l.b.x - l.a.x, y: l.b.y - l.a.y };\n\t\t\tconst ll = d.x * d.x + d.y * d.y;\n\t\t\tconst t = ll > 0 ? ((c.c.x - l.a.x) * d.x + (c.c.y - l.a.y) * d.y) / ll : -1;\n\t\t\tif (Math.abs(lineDist(l.a, l.b, c.c) - c.r) <= tol && t >= -1e-9 && t <= 1 + 1e-9 && Math.sqrt(ll) > tol) push('tangent', [l.ref, c.ref]);\n\t\t}\n\tfor (const l of g.lines) {\n\t\tconst d = { x: l.b.x - l.a.x, y: l.b.y - l.a.y };\n\t\tif (Math.hypot(d.x, d.y) <= tol) continue;\n\t\tif (angleBetween(d, { x: 1, y: 0 }) <= atol) push('horizontal', [l.ref]);\n\t\telse if (angleBetween(d, { x: 0, y: 1 }) <= atol) push('vertical', [l.ref]);\n\t}\n\tconst connected = (a: Geo['lines'][number], b: Geo['lines'][number]) => [b.a, b.b].some((p) => dist(p, a.a) <= tol || dist(p, a.b) <= tol);\n\tconst parallel: VectorGeomRef[][] = [];\n\tconst equal: VectorGeomRef[][] = [];\n\tfor (let i = 0; i < g.lines.length; i++)\n\t\tfor (let j = i + 1; j < g.lines.length; j++) {\n\t\t\tconst a = g.lines[i];\n\t\t\tconst b = g.lines[j];\n\t\t\tconst d1 = { x: a.b.x - a.a.x, y: a.b.y - a.a.y };\n\t\t\tconst d2 = { x: b.b.x - b.a.x, y: b.b.y - b.a.y };\n\t\t\tconst l1 = Math.hypot(d1.x, d1.y);\n\t\t\tconst l2 = Math.hypot(d2.x, d2.y);\n\t\t\tif (l1 <= tol || l2 <= tol) continue;\n\t\t\tconst ang = angleBetween(d1, d2);\n\t\t\tif (ang <= atol) {\n\t\t\t\tif (lineDist(a.a, a.b, b.a) <= tol && lineDist(a.a, a.b, b.b) <= tol) push('collinear', [a.ref, b.ref]);\n\t\t\t\telse parallel.push([a.ref, b.ref]);\n\t\t\t} else if (Math.abs(Math.PI / 2 - ang) <= atol && connected(a, b)) push('perpendicular', [a.ref, b.ref]);\n\t\t\tif (Math.abs(l1 - l2) <= tol) equal.push([a.ref, b.ref]);\n\t\t}\n\tfor (const p of parallel) push('parallel', p);\n\tfor (const p of equal) push('equal', p);\n\tfor (let i = 0; i < g.circles.length; i++)\n\t\tfor (let j = i + 1; j < g.circles.length; j++) if (Math.abs(g.circles[i].r - g.circles[j].r) <= tol) push('equal', [g.circles[i].ref, g.circles[j].ref]);\n\treturn out;\n}\n\n/**\n * Auto-constrain: anchors of the paths given ids, the inferred constraints added one by one with\n * `newId` ids (each solved; refused ones, redundant or conflicting, skipped). Returns the document\n * and the constraints added.\n */\nexport function autoConstrainVector(doc: VectorDocument, ids: readonly string[] | null, newId: () => string, options: VectorInferOptions = {}): { document: VectorDocument; added: VectorConstraint[] } {\n\tlet d = withVectorDocumentAnchorIds(doc, ids ? new Set(ids) : undefined);\n\tconst added: VectorConstraint[] = [];\n\tfor (const candidate of inferVectorConstraints(d, ids, options)) {\n\t\tconst c = { ...candidate, id: newId() };\n\t\ttry {\n\t\t\td = addVectorConstraint(d, c);\n\t\t\tadded.push(d.constraints![d.constraints!.length - 1]);\n\t\t} catch (e) {\n\t\t\tif (!(e instanceof VectorConstraintError)) throw e;\n\t\t}\n\t}\n\treturn { document: d, added };\n}\n","/**\n * Free Distort, perspective and skew: the selection box's corners or sides dragged, and nodes sent\n * through a map of document space while each keeps its own (rigid) transform.\n *\n * Ported from ArtCraft (MIT OR Apache-2.0, © ArtCraft Team and contributors; see\n * THIRD_PARTY_NOTICES.md): vectorcraft `crates/tools/src/xform/free.rs` (`distort_quad`: a corner\n * moved freely, or with its partner mirrored along the dominant axis for perspective, and a side's\n * two corners moved together; `shear_for_side`: a side handle shears about the opposite side, or\n * the centre with Alt).\n */\nimport { mapVectorSubpaths, type VectorPointMap } from './pathMap.js';\nimport { mapVectorPaint } from './gradient.js';\nimport { applyMatrix, invertMatrix, multiplyMatrices, nodeMatrix, type AffineMatrix } from './geometry.js';\nimport { vectorEffectInputBounds, vectorEnvelopeMap, vectorEnvelopeMesh } from './appearance.js';\nimport { mapVectorMeshPoints } from './warp.js';\nimport { matrixAbout, skewMatrix } from '../geom/affine.js';\nimport type { Vec2, VectorBounds, VectorEffect, VectorGroupNode, VectorNode, VectorPathNode, VectorTransformHandle } from './types.js';\n\n/** Default tolerance (document units) of Free Distort's adaptive path mapping. */\nexport const VECTOR_DISTORT_TOLERANCE = 0.05;\n\n/** How dragging a selection box handle distorts it: each corner freely, or in symmetric perspective. */\nexport type VectorDistortMode = 'distort' | 'perspective';\n\nconst CORNER_INDEX: Partial<Record<VectorTransformHandle, number>> = { nw: 0, ne: 1, se: 2, sw: 3 };\nconst SIDE_CORNERS: Partial<Record<VectorTransformHandle, [number, number]>> = { n: [0, 1], e: [1, 2], s: [2, 3], w: [3, 0] };\n\n/** A box's corners clockwise from the top left. */\nexport const vectorBoxCorners = (b: VectorBounds): Vec2[] => [\n\t{ x: b.x, y: b.y },\n\t{ x: b.x + b.width, y: b.y },\n\t{ x: b.x + b.width, y: b.y + b.height },\n\t{ x: b.x, y: b.y + b.height }\n];\n\n/**\n * The quad (clockwise from the top left) box `b` becomes when handle `handle` is dragged by `d`: a\n * corner moves by `d` (`distort`), or along the drag's dominant axis with the corner beside it on\n * that side moving the opposite way (`perspective`); a side moves both its corners.\n */\nexport function vectorDistortQuad(b: VectorBounds, handle: VectorTransformHandle, d: Vec2, mode: VectorDistortMode = 'distort'): Vec2[] {\n\tconst q = vectorBoxCorners(b);\n\tconst i = CORNER_INDEX[handle];\n\tif (i !== undefined) {\n\t\tif (mode === 'perspective') {\n\t\t\tif (Math.abs(d.x) >= Math.abs(d.y)) {\n\t\t\t\tconst partner = [1, 0, 3, 2][i];\n\t\t\t\tq[i].x += d.x;\n\t\t\t\tq[partner].x -= d.x;\n\t\t\t} else {\n\t\t\t\tconst partner = [3, 2, 1, 0][i];\n\t\t\t\tq[i].y += d.y;\n\t\t\t\tq[partner].y -= d.y;\n\t\t\t}\n\t\t} else {\n\t\t\tq[i].x += d.x;\n\t\t\tq[i].y += d.y;\n\t\t}\n\t\treturn q;\n\t}\n\tconst side = SIDE_CORNERS[handle];\n\tif (side) for (const k of side) q[k] = { x: q[k].x + d.x, y: q[k].y + d.y };\n\treturn q;\n}\n\nconst about = (o: Vec2, m: AffineMatrix): AffineMatrix => matrixAbout(m, o);\n\n/**\n * The skew for dragging side `handle` of box `b` by `d`: along that side, about the opposite side\n * (or the centre, `fromCenter`). Corners give the identity.\n */\nexport function vectorShearForSide(b: VectorBounds, handle: VectorTransformHandle, d: Vec2, fromCenter = false): AffineMatrix {\n\tconst center = { x: b.x + b.width / 2, y: b.y + b.height / 2 };\n\tif (handle === 'n' || handle === 's') {\n\t\tconst o = fromCenter ? center : { x: center.x, y: handle === 'n' ? b.y + b.height : b.y };\n\t\tconst lever = (handle === 'n' ? b.y : b.y + b.height) - o.y;\n\t\treturn about(o, [1, 0, Math.abs(lever) > 1e-9 ? d.x / lever : 0, 1, 0, 0]);\n\t}\n\tif (handle === 'e' || handle === 'w') {\n\t\tconst o = fromCenter ? center : { x: handle === 'w' ? b.x + b.width : b.x, y: center.y };\n\t\tconst lever = (handle === 'w' ? b.x : b.x + b.width) - o.x;\n\t\treturn about(o, [1, Math.abs(lever) > 1e-9 ? d.y / lever : 0, 0, 1, 0, 0]);\n\t}\n\treturn [1, 0, 0, 1, 0, 0];\n}\n\n/**\n * A skew by `ax` degrees horizontally (vertical lines lean right for positive angles: x' = x + tan(ax)·y)\n * and `ay` degrees vertically (y' = y + tan(ay)·x), about `origin`.\n */\nexport function vectorSkewMatrix(ax: number, ay: number, origin: Vec2 = { x: 0, y: 0 }): AffineMatrix {\n\treturn skewMatrix(ax, ay, origin);\n}\n\n/** Effects after their node's geometry went through a non-affine map: envelope meshes follow it (a preset becomes the mesh it stood for). */\nfunction mapEffects(node: VectorNode, f: VectorPointMap): VectorEffect[] | undefined {\n\tif (!node.effects?.length) return undefined;\n\treturn node.effects.map((e) => {\n\t\tif (e.type !== 'envelope') return e;\n\t\tif (e.params.style === 'mesh') return e.params.mesh ? { ...e, params: { ...e.params, mesh: mapVectorMeshPoints(e.params.mesh, f) } } : e;\n\t\tconst bounds = vectorEffectInputBounds(node, e.id);\n\t\tif (!bounds || !vectorEnvelopeMap(e.params, bounds)) return e;\n\t\treturn { ...e, params: { ...e.params, style: 'mesh', mesh: mapVectorMeshPoints(vectorEnvelopeMesh(e.params, bounds), f) } };\n\t});\n}\n\n/**\n * A node sent through `f`, a map of the space `parentToDoc` maps its parent to (the document):\n * every anchor and handle is mapped in the node's own space, adaptively so curves stay within\n * `tolerance`, and the node keeps its rigid `transform`. Gradients in the node's space have their\n * handles mapped (bounding-box ones follow the new bounds), envelope meshes and a blend's spine\n * are mapped too; stroke widths, live corner radii and effect parameters are kept.\n */\nexport function distortVectorNode<T extends VectorNode>(node: T, parentToDoc: AffineMatrix, f: VectorPointMap, tolerance = VECTOR_DISTORT_TOLERANCE): T {\n\tconst toDoc = multiplyMatrices(nodeMatrix(node), parentToDoc);\n\tconst inv = invertMatrix(toDoc);\n\tconst local: VectorPointMap = (p) => applyMatrix(inv, f(applyMatrix(toDoc, p)));\n\tconst effects = mapEffects(node, local);\n\tif (node.type === 'group') {\n\t\tconst group = node as VectorGroupNode;\n\t\tconst children = group.children.map((c) => distortVectorNode(c, toDoc, f, tolerance));\n\t\tconst spine = group.blend?.spine;\n\t\treturn {\n\t\t\t...group,\n\t\t\tchildren,\n\t\t\t...(effects ? { effects } : {}),\n\t\t\t...(spine ? { blend: { ...group.blend, spine: mapVectorSubpaths([spine], local, tolerance)[0] } } : {})\n\t\t} as T;\n\t}\n\tconst path = node as VectorPathNode;\n\treturn {\n\t\t...path,\n\t\t...(effects ? { effects } : {}),\n\t\t...(path.fill !== undefined ? { fill: mapVectorPaint(path.fill, local) } : {}),\n\t\t...(path.stroke !== undefined ? { stroke: mapVectorPaint(path.stroke, local) } : {}),\n\t\tsubpaths: mapVectorSubpaths(path.subpaths, local, tolerance)\n\t} as T;\n}\n","/**\n * Smart guides and object snapping for the vector editor. Targets — anchors, bounding-box\n * edges and centres, path segments and the artboard — are gathered once per gesture in\n * document space (rotated nodes included) and kept sorted by x and y, so each pointer move\n * finds the few near the pointer by bisection. A point snaps, in this order of priority: kept\n * to its Shift angle and slid into line with a target, onto an anchor or centre, onto an\n * intersection of two paths, onto the perpendicular foot or tangent point from where it\n * leaves, onto a path, onto a construction angle, then into line with targets on x and y. A\n * moved box lines its edges and centre up with targets, or spaces itself evenly in its row\n * and column; a resized box lands its moving edges on targets, or matches another object's\n * width or height. Every snap reports labelled guide lines in document units.\n *\n * Portions ported from vectorcraft-main/crates/tools/src/guides.rs (`Targets`, `within`,\n * `for_move`, `for_drawing`, `for_handle`, `for_anchor_drag`, `push_segments`, `snap_from`,\n * `slide`, `lined_up`, `on_segment`, `crossing`, `align_rects`, `snap_rects`, `even_spacing`,\n * `gap_marks`, `snap_scale`, `Leave`), figcraft-main/crates/engine/src/snap.rs (`spacings`,\n * `equal_spacing`, `spacing_guides`: equal-spacing cases and gap labels),\n * designcraft-main/crates/tools/src/snap.rs (`pass_dimensions`: matching other objects'\n * sizes) and cadcraft-main/crates/engine/src/snap/curves.rs (`Param::solve`: perpendicular and\n * tangent points by sign-change sampling and bisection) (MIT OR Apache-2.0, © ArtCraft Team\n * and contributors). See THIRD_PARTY_NOTICES.md.\n */\nimport { VectorRTree, VECTOR_SPATIAL_MIN } from './spatial.js';\nimport type { Point } from '../canvas/types.js';\nimport { expandVectorAppearance, vectorAppearanceNodes } from './appearance.js';\nimport { isVectorMaskActive } from './mask.js';\nimport { segmentDerivative, segmentNearest, segmentPoint, type BezierSegment } from '../canvas/pathops/bezier.js';\nimport { segmentIntersections } from '../canvas/pathops/boolean.js';\nimport {\n\tapplyMatrix,\n\tIDENTITY_MATRIX,\n\tmatrixRotation,\n\tmultiplyMatrices,\n\tnodeMatrix,\n\trotateMatrix,\n\tsubpathSegment,\n\tsubpathSegmentCount,\n\tvectorNodeBounds,\n\ttype AffineMatrix\n} from './geometry.js';\nimport type { VectorAnchorRef, VectorBounds, VectorDocument, VectorNode, VectorSubpath, VectorTransformHandle, Vec2 } from './types.js';\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * What a guide line shows: `align` lines run from a target through the snapped point or box,\n * `construction` lines from where a drawn point leaves, `spacing` marks equal gaps and `size`\n * the side whose length matches another object's.\n */\nexport type VectorGuideKind = 'align' | 'construction' | 'spacing' | 'size';\n\n/** A guide line in document units. */\nexport interface VectorGuideLine {\n\tkind: VectorGuideKind;\n\tx1: number;\n\ty1: number;\n\tx2: number;\n\ty2: number;\n}\n\n/** What a point or box snapped to. */\nexport type VectorSnapKind = 'anchor' | 'centre' | 'intersect' | 'perpendicular' | 'tangent' | 'path' | 'construction' | 'align' | 'spacing' | 'size' | 'tracking';\n\n/** A guide label in document units: the snap kind's name, or a distance for gaps and sizes. */\nexport interface VectorGuideLabel {\n\tkind: VectorSnapKind;\n\tx: number;\n\ty: number;\n\ttext: string;\n}\n\nexport interface VectorSnapGuides {\n\tlines: VectorGuideLine[];\n\tlabels: VectorGuideLabel[];\n}\n\nexport interface VectorPointSnap extends VectorSnapGuides {\n\tpoint: Vec2;\n\t/** The rule that placed the point, or null when nothing was in reach. */\n\tkind: VectorSnapKind | null;\n\t/** Whether x / y were set by a target (for falling back to the grid on the other axis). */\n\tx: boolean;\n\ty: boolean;\n}\n\nexport interface VectorRectSnap extends VectorSnapGuides {\n\t/** Correction to add to the move. */\n\tdx: number;\n\tdy: number;\n\tx: boolean;\n\ty: boolean;\n}\n\nexport interface VectorScaleSnap extends VectorSnapGuides {\n\tsx: number;\n\tsy: number;\n\tsnapped: boolean;\n}\n\n/**\n * The point a drawn point leaves from (the anchor before it, a shape's start, a handle's\n * anchor) and how it keeps to it.\n */\nexport interface VectorSnapLeave {\n\tat: Vec2;\n\t/**\n\t * Shift held: keep to 45° steps round `at` (`angle`), to a diagonal (`square`, a box's\n\t * corner) or to the nearer axis (`axis`, a dragged point).\n\t */\n\tconstrain?: 'angle' | 'square' | 'axis' | null;\n\t/** Construction guide angles through `at`, degrees counter-clockwise on screen. */\n\tangles?: readonly number[];\n\t/** Snap to the perpendicular foot and tangent points from `at` on paths (default true). */\n\tcurves?: boolean;\n}\n\n/**\n * A tracking path (CAD object snap tracking): a ray from `origin` along the unit `dir`. Acquired\n * points send `alignment` paths, an acquired end of an open path its `extension`, and the point a\n * drawn point leaves from its `polar` angles. Points snap where two paths cross, where a path\n * crosses a path of the drawing, or onto the nearest path.\n */\nexport interface VectorTrackingPath {\n\torigin: Vec2;\n\tdir: Vec2;\n\tkind: 'alignment' | 'extension' | 'polar';\n}\n\n/** A point acquired for tracking, and the direction of its path's extension when it ends an open path. */\nexport interface VectorTrackingPoint {\n\tx: number;\n\ty: number;\n\textension?: Vec2 | null;\n}\n\n/** The most points kept acquired (the oldest dropped first), as AutoCAD keeps. */\nexport const VECTOR_MAX_TRACKING_POINTS = 7;\n\n/** A unit direction for `deg` degrees counter-clockwise on the y-down page (exact on the axes). */\nexport function vectorAngleDirection(deg: number): Vec2 {\n\tconst r = (deg * Math.PI) / 180;\n\tconst exact = (n: number) => (Math.abs(n) < 1e-12 ? 0 : n);\n\treturn { x: exact(Math.cos(r)), y: exact(-Math.sin(r)) };\n}\n\n/**\n * Polar angles (degrees, counter-clockwise on the page, in [0, 360)): every multiple of\n * `increment` round the circle plus the `additional` angles, without repeats, at most 360. After\n * cadcraft's `polar_angles` (POLARANG / POLARADDANG).\n */\nexport function vectorPolarAngles(increment: number, additional: readonly number[] = []): number[] {\n\tconst out: number[] = [];\n\tconst push = (a: number) => {\n\t\tconst n = ((a % 360) + 360) % 360;\n\t\tif (out.length < 360 && !out.some((b) => Math.abs(((n - b + 540) % 360) - 180) < 1e-9)) out.push(n);\n\t};\n\tif (Number.isFinite(increment) && increment > 1e-6) {\n\t\tconst n = Math.min(360, Math.max(1, Math.ceil(360 / increment - 1e-9)));\n\t\tfor (let k = 0; k < n; k++) push(k * increment);\n\t} else push(0);\n\tfor (const a of additional) if (Number.isFinite(a)) push(a);\n\treturn out;\n}\n\n/** The increment (degrees) that `angles` are the multiples of, as lines (so 0, 45, 90, 135 is 45), or null when they are not one increment's multiples. */\nexport function vectorPolarIncrementOf(angles: readonly number[]): number | null {\n\tconst lines = [...new Set(angles.filter(Number.isFinite).map((a) => Math.round((((a % 180) + 180) % 180) * 1e6) / 1e6))].sort((x, y) => x - y);\n\tif (!lines.length || lines[0] !== 0) return null;\n\tconst inc = lines.length > 1 ? lines[1] : 180;\n\tif (Math.abs(180 / inc - Math.round(180 / inc)) > 1e-6) return null;\n\tconst n = Math.round(180 / inc);\n\treturn lines.length === n && lines.every((v, k) => Math.abs(v - k * inc) < 1e-6) ? inc : null;\n}\n\n/** The paths acquired points send: a ray along each of `angles` (degrees) from each, plus its extension. After cadcraft's `Tracker::paths`. */\nexport function vectorTrackingPaths(points: readonly VectorTrackingPoint[], angles: readonly number[] = [0, 90, 180, 270]): VectorTrackingPath[] {\n\tconst out: VectorTrackingPath[] = [];\n\tfor (const p of points) {\n\t\tif (!Number.isFinite(p.x) || !Number.isFinite(p.y)) continue;\n\t\tconst origin = { x: p.x, y: p.y };\n\t\tfor (const a of angles) out.push({ origin, dir: vectorAngleDirection(a), kind: 'alignment' });\n\t\tconst e = p.extension;\n\t\tconst len = e ? Math.hypot(e.x, e.y) : 0;\n\t\tif (e && len > 1e-12) out.push({ origin, dir: { x: e.x / len, y: e.y / len }, kind: 'extension' });\n\t}\n\treturn out;\n}\n\n/** The construction guide angles new editors start with. */\nexport const VECTOR_CONSTRUCTION_ANGLES: readonly number[] = [0, 45, 90, 135];\n\n/** Where `constrain` puts `p` (unchanged without one). */\nexport function constrainToLeave(leave: VectorSnapLeave | null | undefined, p: Vec2): Vec2 {\n\tif (!leave?.constrain) return p;\n\tconst dx = p.x - leave.at.x;\n\tconst dy = p.y - leave.at.y;\n\tswitch (leave.constrain) {\n\t\tcase 'axis':\n\t\t\treturn Math.abs(dx) > Math.abs(dy) ? { x: p.x, y: leave.at.y } : { x: leave.at.x, y: p.y };\n\t\tcase 'square': {\n\t\t\tconst s = Math.max(Math.abs(dx), Math.abs(dy));\n\t\t\treturn { x: leave.at.x + Math.sign(dx || 1) * s, y: leave.at.y + Math.sign(dy || 1) * s };\n\t\t}\n\t\tdefault: {\n\t\t\tconst len = Math.hypot(dx, dy);\n\t\t\tconst a = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);\n\t\t\treturn { x: leave.at.x + Math.cos(a) * len, y: leave.at.y + Math.sin(a) * len };\n\t\t}\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Targets\n// ---------------------------------------------------------------------------\n\ntype TargetKind = 'anchor' | 'centre' | 'edge' | 'artboard';\n\ninterface PointTarget {\n\tp: Vec2;\n\tkind: 'anchor' | 'centre';\n}\n\n/** A line to line up with: where it runs on its axis and the target point it comes from. */\ninterface LineTarget {\n\tv: number;\n\tfrom: Vec2;\n\tkind: TargetKind;\n}\n\ninterface SegmentTarget {\n\tseg: Point[];\n\tminX: number;\n\tminY: number;\n\tmaxX: number;\n\tmaxY: number;\n}\n\n/** How many anchors, and how many segments, the targets gather at most. */\nconst BUDGET = 20_000;\n/** The first near segments paired up when looking for intersections. */\nconst CROSSING_PAIRS = 32;\n/** Parameter samples per segment for perpendicular / tangent roots, and bisection steps. */\nconst CURVE_SAMPLES = 48;\nconst BISECT = 50;\nconst EQUAL = 1e-6;\n\n/** First index of `sorted` whose key is not below `v` (`strict`: above `v`). */\nfunction bound<T>(sorted: readonly T[], key: (e: T) => number, v: number, strict: boolean): number {\n\tlet lo = 0;\n\tlet hi = sorted.length;\n\twhile (lo < hi) {\n\t\tconst mid = (lo + hi) >>> 1;\n\t\tconst k = key(sorted[mid]);\n\t\tif (strict ? k <= v : k < v) lo = mid + 1;\n\t\telse hi = mid;\n\t}\n\treturn lo;\n}\n\n/** The entries of `sorted` (ascending by `key`) whose key is within `tol` of `v`, by bisection. */\nfunction within<T>(sorted: readonly T[], key: (e: T) => number, v: number, tol: number): readonly T[] {\n\treturn sorted.slice(bound(sorted, key, v - tol, false), bound(sorted, key, v + tol, true));\n}\n\nconst linesNear = (lines: readonly LineTarget[], v: number, tol: number) => within(lines, (l) => l.v, v, tol);\n\nconst distance = (a: Vec2, b: Vec2) => Math.hypot(a.x - b.x, a.y - b.y);\nconst toPoint = (p: Vec2): Point => [p.x, p.y];\nconst toVec = (p: Point): Vec2 => ({ x: p[0], y: p[1] });\n\n/** What `collect` gathers from each path besides its anchors and bounds. */\ntype Gather = 'bounds' | 'segments' | 'drawing';\n\nconst LABELS: Record<VectorSnapKind, string> = {\n\tanchor: 'anchor',\n\tcentre: 'centre',\n\tintersect: 'intersect',\n\tperpendicular: 'perpendicular',\n\ttangent: 'tangent',\n\tpath: 'path',\n\tconstruction: 'angle',\n\talign: 'align',\n\tspacing: 'spacing',\n\tsize: 'size',\n\ttracking: 'tracking'\n};\n\nconst fmtLength = (n: number) => String(Math.round(n * 100) / 100);\n\n/** A node's own (possibly rotated) frame: its size there and its centre in document space. */\nfunction nodeFrame(node: VectorNode, parent: AffineMatrix, aabb?: VectorBounds | null): { width: number; height: number; centre: Vec2 } | null {\n\tconst angle = matrixRotation(multiplyMatrices(nodeMatrix(node), parent));\n\tconst b = angle ? vectorNodeBounds(node, multiplyMatrices(parent, rotateMatrix(-angle))) : aabb !== undefined ? aabb : vectorNodeBounds(node, parent);\n\tif (!b) return null;\n\tconst c = { x: b.x + b.width / 2, y: b.y + b.height / 2 };\n\treturn { width: b.width, height: b.height, centre: angle ? applyMatrix(rotateMatrix(angle), c) : c };\n}\n\n/** True when `id` is `node` or inside it. */\nfunction contains(node: VectorNode, ids: ReadonlySet<string>): boolean {\n\tif (ids.has(node.id)) return true;\n\treturn node.type === 'group' && node.children.some((c) => contains(c, ids));\n}\n\n/**\n * Snap targets in document space, gathered once per gesture. Build them with `forMove`,\n * `forResize`, `forDrawing`, `forAnchorDrag` or `forHandle`, then call `snapFrom`,\n * `snapRect` or `snapScale` on every pointer move. Tolerances are in document units (screen\n * pixels divided by the zoom).\n */\nexport class VectorSnapTargets {\n\tprivate readonly points: PointTarget[] = [];\n\tprivate readonly xs: LineTarget[] = [];\n\tprivate readonly ys: LineTarget[] = [];\n\tprivate readonly segments: SegmentTarget[] = [];\n\t/** Bounds of the objects a moved box spaces itself evenly with. */\n\tprivate readonly boxes: VectorBounds[] = [];\n\t/** Side lengths of the other objects (in their own frames), which a resize may match. */\n\tprivate readonly sizes: number[] = [];\n\tprivate segmentTree: VectorRTree | null = null;\n\n\tprivate constructor() {}\n\n\t/** How many targets of each sort were gathered. */\n\tget counts(): { points: number; lines: number; segments: number; boxes: number } {\n\t\treturn { points: this.points.length, lines: this.xs.length + this.ys.length, segments: this.segments.length, boxes: this.boxes.length };\n\t}\n\n\t/** Targets for moving nodes `ids`: the other art's anchors, bounds and centres, the artboard, and boxes to space evenly with. */\n\tstatic forMove(doc: VectorDocument, ids: readonly string[]): VectorSnapTargets {\n\t\treturn VectorSnapTargets.collect(doc, new Set(ids), 'bounds');\n\t}\n\n\t/** Targets for resizing nodes `ids`: as for a move, plus the other objects' sizes. */\n\tstatic forResize(doc: VectorDocument, ids: readonly string[]): VectorSnapTargets {\n\t\treturn VectorSnapTargets.collect(doc, new Set(ids), 'bounds');\n\t}\n\n\t/**\n\t * Targets for a drawn point (pen, shape tools): the art but `exclude`, its anchors (as\n\t * points and in line), centres, edges and segments. The anchors and segments of `exclude`\n\t * (the path being drawn, whose bounds grow with it) stay targets.\n\t */\n\tstatic forDrawing(doc: VectorDocument, exclude: readonly string[] = []): VectorSnapTargets {\n\t\tconst skip = new Set(exclude);\n\t\tconst t = VectorSnapTargets.collect(doc, skip, 'drawing', false);\n\t\tt.pushPaths(doc, skip, () => false, true);\n\t\treturn t.index();\n\t}\n\n\t/**\n\t * Targets for dragging anchors: the other art with its segments, and the anchors and\n\t * segments that stay put on the paths being reshaped (their bounds move along, so they are\n\t * no targets). The moving anchors are left out.\n\t */\n\tstatic forAnchorDrag(doc: VectorDocument, anchors: readonly VectorAnchorRef[]): VectorSnapTargets {\n\t\tconst moving = new Map<string, Set<string>>();\n\t\tfor (const r of anchors) {\n\t\t\tconst set = moving.get(r.nodeId) ?? new Set<string>();\n\t\t\tset.add(`${r.subpath}:${r.index}`);\n\t\t\tmoving.set(r.nodeId, set);\n\t\t}\n\t\tconst t = VectorSnapTargets.collect(doc, new Set(moving.keys()), 'segments', false);\n\t\tt.pushPaths(doc, new Set(moving.keys()), (id, s, i) => !!moving.get(id)?.has(`${s}:${i}`), true);\n\t\treturn t.index();\n\t}\n\n\t/**\n\t * Targets for dragging a handle of path `nodeId`: the other art and that path's anchors\n\t * (its bounds and the segments through the handle move with it, so they would chase it).\n\t */\n\tstatic forHandle(doc: VectorDocument, nodeId: string): VectorSnapTargets {\n\t\tconst skip = new Set([nodeId]);\n\t\tconst t = VectorSnapTargets.collect(doc, skip, 'bounds', false);\n\t\tt.pushPaths(doc, skip, () => false, false);\n\t\treturn t.index();\n\t}\n\n\t/** The anchors of the paths `ids` as targets (on them and in line), and their segments when `segments`. */\n\tprivate pushPaths(doc: VectorDocument, ids: ReadonlySet<string>, moving: (id: string, subpath: number, index: number) => boolean, segments: boolean) {\n\t\tlet anchors = BUDGET;\n\t\tlet segmentBudget = segments ? BUDGET : 0;\n\t\tconst visit = (list: readonly VectorNode[], parent: AffineMatrix) => {\n\t\t\tfor (const n of list) {\n\t\t\t\tconst m = multiplyMatrices(nodeMatrix(n), parent);\n\t\t\t\tif (n.type === 'group') visit(n.children, m);\n\t\t\t\telse if (n.type === 'path' && ids.has(n.id) && n.visible !== false) {\n\t\t\t\t\tn.subpaths.forEach((sp, s) => {\n\t\t\t\t\t\tsp.anchors.forEach((a, i) => {\n\t\t\t\t\t\t\tif (anchors <= 0 || moving(n.id, s, i)) return;\n\t\t\t\t\t\t\tthis.pushAnchor(applyMatrix(m, a));\n\t\t\t\t\t\t\tanchors--;\n\t\t\t\t\t\t});\n\t\t\t\t\t\tsegmentBudget = this.pushSegments(sp, m, (i) => moving(n.id, s, i), segmentBudget);\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\tvisit(doc.nodes, IDENTITY_MATRIX);\n\t}\n\n\tprivate pushAnchor(p: Vec2) {\n\t\tthis.points.push({ p, kind: 'anchor' });\n\t\tthis.xs.push({ v: p.x, from: p, kind: 'anchor' });\n\t\tthis.ys.push({ v: p.y, from: p, kind: 'anchor' });\n\t}\n\n\t/** The segments of `sp` (mapped by `m`) that stay put while the anchors `moving` accepts move. */\n\tprivate pushSegments(sp: VectorSubpath, m: AffineMatrix, moving: (index: number) => boolean, budget: number): number {\n\t\tconst n = sp.anchors.length;\n\t\tconst count = subpathSegmentCount(sp);\n\t\tfor (let i = 0; i < count && budget > 0; i++) {\n\t\t\tif (moving(i) || moving((i + 1) % n)) continue;\n\t\t\tconst seg = subpathSegment(sp, i).map((q) => toPoint(applyMatrix(m, toVec(q))));\n\t\t\tlet minX = Infinity;\n\t\t\tlet minY = Infinity;\n\t\t\tlet maxX = -Infinity;\n\t\t\tlet maxY = -Infinity;\n\t\t\tfor (const [x, y] of seg) {\n\t\t\t\tminX = Math.min(minX, x);\n\t\t\t\tminY = Math.min(minY, y);\n\t\t\t\tmaxX = Math.max(maxX, x);\n\t\t\t\tmaxY = Math.max(maxY, y);\n\t\t\t}\n\t\t\tthis.segments.push({ seg, minX, minY, maxX, maxY });\n\t\t\tbudget--;\n\t\t}\n\t\treturn budget;\n\t}\n\n\tprivate pushRect(r: VectorBounds, kind: TargetKind, centre: Vec2 | null) {\n\t\tconst corners = [\n\t\t\t{ x: r.x, y: r.y },\n\t\t\t{ x: r.x + r.width, y: r.y },\n\t\t\t{ x: r.x + r.width, y: r.y + r.height },\n\t\t\t{ x: r.x, y: r.y + r.height }\n\t\t];\n\t\tfor (const p of corners) {\n\t\t\tthis.xs.push({ v: p.x, from: p, kind });\n\t\t\tthis.ys.push({ v: p.y, from: p, kind });\n\t\t}\n\t\tif (centre) {\n\t\t\tthis.points.push({ p: centre, kind: 'centre' });\n\t\t\tthis.xs.push({ v: centre.x, from: centre, kind: 'centre' });\n\t\t\tthis.ys.push({ v: centre.y, from: centre, kind: 'centre' });\n\t\t}\n\t}\n\n\t/** Sorts the targets for `within` (stable: of targets alike, the first gathered wins). */\n\tprivate index(): this {\n\t\t// Many segments: an R-tree over their control boxes (a scan is quicker for a few).\n\t\tthis.segmentTree = this.segments.length >= VECTOR_SPATIAL_MIN ? new VectorRTree(this.segments) : null;\n\t\tthis.points.sort((a, b) => a.p.x - b.p.x);\n\t\tthis.xs.sort((a, b) => a.v - b.v);\n\t\tthis.ys.sort((a, b) => a.v - b.v);\n\t\treturn this;\n\t}\n\n\t/**\n\t * Gathers targets from the artboard and every visible node but `exclude` (and what is inside\n\t * those): each leaf's anchors (also as lines for `drawing`), its segments (`segments`,\n\t * `drawing`), its bounds' corners as lines and its centre; and the top-level objects' boxes and\n\t * sizes (inside a group holding an excluded node, that group's other members).\n\t */\n\tprivate static collect(doc: VectorDocument, exclude: ReadonlySet<string>, gather: Gather, sort = true): VectorSnapTargets {\n\t\tconst self = new VectorSnapTargets();\n\t\tconst artboard = { x: 0, y: 0, width: doc.width, height: doc.height };\n\t\tself.pushRect(artboard, 'artboard', { x: doc.width / 2, y: doc.height / 2 });\n\t\tlet anchors = BUDGET;\n\t\tlet segments = BUDGET;\n\t\tconst visit = (list: readonly VectorNode[], parent: AffineMatrix) => {\n\t\t\tfor (const stored of list) {\n\t\t\t\tif (exclude.has(stored.id) || stored.visible === false) continue;\n\t\t\t\t// A mask group snaps to its masked result: its contents' shapes, clipped.\n\t\t\t\tif (isVectorMaskActive(stored) && !contains(stored, exclude)) {\n\t\t\t\t\tvisit(vectorAppearanceNodes(stored), multiplyMatrices(nodeMatrix(stored), parent));\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\t// Other art snaps where it draws (path effects and live booleans applied).\n\t\t\t\tconst n = contains(stored, exclude) ? stored : expandVectorAppearance(stored);\n\t\t\t\tconst m = multiplyMatrices(nodeMatrix(n), parent);\n\t\t\t\tif (n.type === 'group') {\n\t\t\t\t\tvisit(n.children, m);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (anchors <= 0 || n.type !== 'path') continue;\n\t\t\t\tfor (const sp of n.subpaths) {\n\t\t\t\t\tfor (const a of sp.anchors) {\n\t\t\t\t\t\tif (anchors-- <= 0) break;\n\t\t\t\t\t\tconst p = applyMatrix(m, a);\n\t\t\t\t\t\tif (gather === 'drawing') self.pushAnchor(p);\n\t\t\t\t\t\telse self.points.push({ p, kind: 'anchor' });\n\t\t\t\t\t}\n\t\t\t\t\tif (gather !== 'bounds') segments = self.pushSegments(sp, m, () => false, segments);\n\t\t\t\t}\n\t\t\t\tconst b = vectorNodeBounds(n, parent);\n\t\t\t\tconst f = nodeFrame(n, parent, b);\n\t\t\t\tif (b) self.pushRect(b, 'edge', f?.centre ?? null);\n\t\t\t}\n\t\t};\n\t\tvisit(doc.nodes, IDENTITY_MATRIX);\n\t\t// Boxes to space evenly with and sizes to match: whole objects, opening groups that hold an excluded node.\n\t\tconst objects = (list: readonly VectorNode[], parent: AffineMatrix) => {\n\t\t\tfor (const n of list) {\n\t\t\t\tif (exclude.has(n.id) || n.visible === false || self.boxes.length >= BUDGET) continue;\n\t\t\t\tif (n.type === 'group' && contains(n, exclude)) {\n\t\t\t\t\tobjects(n.children, multiplyMatrices(nodeMatrix(n), parent));\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst b = vectorNodeBounds(n, parent);\n\t\t\t\tif (b) self.boxes.push(b);\n\t\t\t\tconst f = nodeFrame(n, parent, b);\n\t\t\t\tif (f) self.sizes.push(f.width, f.height);\n\t\t\t}\n\t\t};\n\t\tobjects(doc.nodes, IDENTITY_MATRIX);\n\t\treturn sort ? self.index() : self;\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Points\n\t// -------------------------------------------------------------------------\n\n\t/** Snaps a single point (see `snapFrom`). */\n\tsnapPoint(p: Vec2, tol: number): VectorPointSnap {\n\t\treturn this.snapFrom(p, null, tol);\n\t}\n\n\t/**\n\t * Snaps a point `p`, drawn or dragged from `leave` when given. With `leave.constrain` it keeps\n\t * to its angle round `leave.at`, sliding along that way into line with the nearest target.\n\t * Otherwise it lands, in order, on an anchor or centre, an intersection of two paths, the\n\t * perpendicular foot or a tangent point from `leave.at`, a path, a construction guide\n\t * through `leave.at` (where a target's line crosses it if one does nearby), or else lines up\n\t * with targets on x and y.\n\t */\n\tsnapFrom(p: Vec2, leave: VectorSnapLeave | null | undefined, tol: number, tracking: readonly VectorTrackingPath[] = []): VectorPointSnap {\n\t\tconst none = (q: Vec2): VectorPointSnap => ({ point: q, kind: null, x: false, y: false, lines: [], labels: [] });\n\t\tconst at = (q: Vec2, kind: VectorSnapKind, lines: VectorGuideLine[] = []): VectorPointSnap => ({\n\t\t\tpoint: q,\n\t\t\tkind,\n\t\t\tx: true,\n\t\t\ty: true,\n\t\t\tlines,\n\t\t\tlabels: [this.label(q, kind)]\n\t\t});\n\t\tif (leave?.constrain) {\n\t\t\tconst q = constrainToLeave(leave, p);\n\t\t\tconst hit = this.slide(leave.at, { x: q.x - leave.at.x, y: q.y - leave.at.y }, q, tol, true);\n\t\t\treturn hit ? { point: hit.point, x: true, y: true, ...this.linedUp(hit.point, hit.from) } : none(q);\n\t\t}\n\t\tconst point = this.nearestPoint(p, tol);\n\t\tif (point) return at(point.p, point.kind);\n\t\tconst near = this.segmentsNear(p, tol);\n\t\tconst crossing = this.crossing(near, p, tol);\n\t\tif (crossing) return at(crossing, 'intersect');\n\t\t// Tracking: the paths near `p` (polar ones from where the point leaves among them).\n\t\tconst paths = trackingNear([...tracking, ...polarPaths(leave)], p, tol);\n\t\tif (paths.length) {\n\t\t\tconst cross = pathCrossing(paths, p, tol);\n\t\t\tif (cross) return { ...at(cross.point, 'tracking', cross.paths.map((t) => line('construction', t.origin, cross.point))), labels: [this.label(cross.point, 'tracking', 'tracking')] };\n\t\t\tconst hit = pathOnSegment(paths, near, p, tol);\n\t\t\tif (hit) return at(hit.point, 'intersect', [line('construction', hit.path.origin, hit.point)]);\n\t\t}\n\t\tif (leave && leave.curves !== false) {\n\t\t\tconst foot = this.curvePoint(near, leave.at, p, tol);\n\t\t\tif (foot) return at(foot.point, foot.kind, [line('construction', leave.at, foot.point)]);\n\t\t}\n\t\tconst on = this.onSegment(near);\n\t\tif (on) return at(on, 'path');\n\t\t// Along the nearest acquired point's path (polar ones are the construction guides below).\n\t\tconst slide = paths.filter((t) => t.path.kind !== 'polar').sort((a, b) => a.d - b.d)[0];\n\t\tif (slide) return { ...at(slide.foot, 'tracking', [line('construction', slide.path.origin, slide.foot)]), labels: [this.label(slide.foot, 'tracking', slide.path.kind === 'extension' ? 'extension' : 'tracking')] };\n\t\tif (leave?.angles?.length) {\n\t\t\tconst c = construction(leave, p, tol);\n\t\t\tif (c) {\n\t\t\t\tconst hit = this.slide(leave.at, c.dir, c.point, tol, false);\n\t\t\t\tif (hit) {\n\t\t\t\t\tconst lined = this.linedUp(hit.point, hit.from);\n\t\t\t\t\treturn { point: hit.point, x: true, y: true, kind: lined.kind, lines: [line('construction', leave.at, hit.point), ...lined.lines], labels: lined.labels };\n\t\t\t\t}\n\t\t\t\treturn { point: c.point, kind: 'construction', x: true, y: true, lines: [line('construction', leave.at, c.point)], labels: [] };\n\t\t\t}\n\t\t}\n\t\tconst nearest = (lines: readonly LineTarget[], v: number) => {\n\t\t\tlet best: LineTarget | null = null;\n\t\t\tfor (const l of linesNear(lines, v, tol)) if (!best || Math.abs(l.v - v) < Math.abs(best.v - v)) best = l;\n\t\t\treturn best;\n\t\t};\n\t\tconst bx = nearest(this.xs, p.x);\n\t\tconst by = nearest(this.ys, p.y);\n\t\tif (!bx && !by) return none(p);\n\t\tconst out = { x: bx ? bx.v : p.x, y: by ? by.v : p.y };\n\t\tconst lines: VectorGuideLine[] = [];\n\t\tif (bx) lines.push(line('align', bx.from, { x: out.x, y: out.y }));\n\t\tif (by) lines.push(line('align', by.from, { x: out.x, y: out.y }));\n\t\treturn { point: out, kind: 'align', x: !!bx, y: !!by, lines, labels: [this.label(out, 'align')] };\n\t}\n\n\tprivate label(p: Vec2, kind: VectorSnapKind, text = LABELS[kind]): VectorGuideLabel {\n\t\treturn { kind, x: p.x, y: p.y, text };\n\t}\n\n\t/** The anchor or centre nearest `p`, within `tol`. */\n\tprivate nearestPoint(p: Vec2, tol: number): PointTarget | null {\n\t\tlet best: PointTarget | null = null;\n\t\tlet bestD = Infinity;\n\t\tfor (const t of within(this.points, (e) => e.p.x, p.x, tol)) {\n\t\t\tconst d = distance(t.p, p);\n\t\t\tif (d <= tol && d < bestD) {\n\t\t\t\tbest = t;\n\t\t\t\tbestD = d;\n\t\t\t}\n\t\t}\n\t\treturn best;\n\t}\n\n\t/**\n\t * Slides `q`, on the line through `at` along `dir` (beyond `at` only for a `ray`), to where\n\t * the nearest target line crosses it, at most twice `tol` away: where it lands and the\n\t * target point that line comes from.\n\t */\n\tprivate slide(at: Vec2, dir: Vec2, q: Vec2, tol: number, ray: boolean): { point: Vec2; from: Vec2 } | null {\n\t\tconst len2 = dir.x * dir.x + dir.y * dir.y;\n\t\tif (!Number.isFinite(len2) || len2 <= 1e-18) return null;\n\t\tconst reach = 2 * tol;\n\t\tlet best: { moved: number; point: Vec2; from: Vec2 } | null = null;\n\t\tfor (const vertical of [true, false]) {\n\t\t\tconst d = vertical ? dir.x : dir.y;\n\t\t\tif (Math.abs(d) <= 1e-9) continue;\n\t\t\tconst a = vertical ? at.x : at.y;\n\t\t\tfor (const l of linesNear(vertical ? this.xs : this.ys, vertical ? q.x : q.y, reach)) {\n\t\t\t\tconst s = (l.v - a) / d;\n\t\t\t\t// Exactly on the line, whatever the rounding along the way.\n\t\t\t\tconst hit = vertical ? { x: l.v, y: at.y + dir.y * s } : { x: at.x + dir.x * s, y: l.v };\n\t\t\t\tconst moved = distance(hit, q);\n\t\t\t\tif (moved <= reach && (!ray || s > 0) && (!best || moved < best.moved)) best = { moved, point: hit, from: l.from };\n\t\t\t}\n\t\t}\n\t\treturn best && { point: best.point, from: best.from };\n\t}\n\n\t/** The guides of a point `q` slid into line with the target at `from`: the line and \"align\", or the target's own label when `q` sits on it. */\n\tprivate linedUp(q: Vec2, from: Vec2): { kind: VectorSnapKind } & VectorSnapGuides {\n\t\tconst on = this.nearestPoint(q, 1e-9);\n\t\tif (on) return { kind: on.kind, lines: [], labels: [this.label(on.p, on.kind)] };\n\t\treturn { kind: 'align', lines: [line('align', from, q)], labels: [this.label(q, 'align')] };\n\t}\n\n\t/** Target segments whose control box reaches `p` and which pass within `tol` of it, nearest first. */\n\tprivate segmentsNear(p: Vec2, tol: number): { seg: Point[]; t: number; distance: number }[] {\n\t\tconst out: { seg: Point[]; t: number; distance: number }[] = [];\n\t\tconst pt = toPoint(p);\n\t\tconst candidates = this.segmentTree ? this.segmentTree.search({ minX: p.x - tol, minY: p.y - tol, maxX: p.x + tol, maxY: p.y + tol }).map((i) => this.segments[i]) : this.segments;\n\t\tfor (const s of candidates) {\n\t\t\tif (p.x < s.minX - tol || p.x > s.maxX + tol || p.y < s.minY - tol || p.y > s.maxY + tol) continue;\n\t\t\tconst hit = segmentNearest(s.seg, pt);\n\t\t\tif (hit.distance <= tol) out.push({ seg: s.seg, t: hit.t, distance: hit.distance });\n\t\t}\n\t\treturn out.sort((a, b) => a.distance - b.distance);\n\t}\n\n\tprivate onSegment(near: readonly { seg: Point[]; t: number }[]): Vec2 | null {\n\t\treturn near.length ? toVec(segmentPoint(near[0].seg, near[0].t)) : null;\n\t}\n\n\t/** Where two of the target segments near `p` cross, the crossing nearest `p` within `tol`. */\n\tprivate crossing(near: readonly { seg: Point[] }[], p: Vec2, tol: number): Vec2 | null {\n\t\tconst list = near.slice(0, CROSSING_PAIRS);\n\t\tlet best: Vec2 | null = null;\n\t\tlet bestD = Infinity;\n\t\tfor (let i = 0; i < list.length; i++) {\n\t\t\tfor (let j = i + 1; j < list.length; j++) {\n\t\t\t\tfor (const hit of segmentIntersections(list[i].seg as BezierSegment, list[j].seg as BezierSegment)) {\n\t\t\t\t\tconst q = toVec(hit.point);\n\t\t\t\t\tconst d = distance(q, p);\n\t\t\t\t\tif (d <= tol && d < bestD) {\n\t\t\t\t\t\tbest = q;\n\t\t\t\t\t\tbestD = d;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn best;\n\t}\n\n\t/**\n\t * The foot of a perpendicular, or a tangent point, from `base` on a target segment near `p`\n\t * (within `tol` of it): roots of `(B(t) − base) · B′(t)` and `(B(t) − base) × B′(t)`\n\t * bracketed by sign changes of samples and narrowed by bisection.\n\t */\n\tprivate curvePoint(near: readonly { seg: Point[] }[], base: Vec2, p: Vec2, tol: number): { point: Vec2; kind: 'perpendicular' | 'tangent' } | null {\n\t\tlet best: { point: Vec2; kind: 'perpendicular' | 'tangent'; d: number } | null = null;\n\t\tfor (const { seg } of near) {\n\t\t\tfor (const kind of ['perpendicular', 'tangent'] as const) {\n\t\t\t\tif (kind === 'tangent' && seg.length === 2) continue;\n\t\t\t\tfor (const q of solveOnSegment(seg, base, kind === 'perpendicular' ? dotCondition : crossCondition)) {\n\t\t\t\t\tconst d = distance(q, p);\n\t\t\t\t\tif (d <= tol && (!best || d < best.d)) best = { point: q, kind, d };\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn best && { point: best.point, kind: best.kind };\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Boxes\n\t// -------------------------------------------------------------------------\n\n\t/**\n\t * Snaps a moving box `r` (the moved selection's bounds): its edge or centre nearest a target\n\t * line on each axis (`axes` limits which may move). With `spacing`, an axis that lines up\n\t * with nothing spaces the box evenly in its row or column. Returns the correction and guides.\n\t */\n\tsnapRect(r: VectorBounds, tol: number, options: { spacing?: boolean; axes?: { x?: boolean; y?: boolean } } = {}): VectorRectSnap {\n\t\tconst axes = { x: options.axes?.x !== false, y: options.axes?.y !== false };\n\t\tconst nearest = (lines: readonly LineTarget[], values: number[]) => {\n\t\t\tlet best: { d: number; from: Vec2 } | null = null;\n\t\t\tfor (const v of values) for (const l of linesNear(lines, v, tol)) if (!best || Math.abs(l.v - v) < Math.abs(best.d)) best = { d: l.v - v, from: l.from };\n\t\t\treturn best;\n\t\t};\n\t\tconst bx = axes.x ? nearest(this.xs, [r.x, r.x + r.width / 2, r.x + r.width]) : null;\n\t\tconst by = axes.y ? nearest(this.ys, [r.y, r.y + r.height / 2, r.y + r.height]) : null;\n\t\tconst out: VectorRectSnap = { dx: bx?.d ?? 0, dy: by?.d ?? 0, x: !!bx, y: !!by, lines: [], labels: [] };\n\t\tconst done = { ...r, x: r.x + out.dx, y: r.y + out.dy };\n\t\tif (bx) {\n\t\t\tconst x = bx.from.x;\n\t\t\tout.lines.push({ kind: 'align', x1: x, y1: Math.min(bx.from.y, done.y), x2: x, y2: Math.max(bx.from.y, done.y + done.height) });\n\t\t}\n\t\tif (by) {\n\t\t\tconst y = by.from.y;\n\t\t\tout.lines.push({ kind: 'align', x1: Math.min(by.from.x, done.x), y1: y, x2: Math.max(by.from.x, done.x + done.width), y2: y });\n\t\t}\n\t\tif (options.spacing && this.boxes.length) {\n\t\t\tfor (const horizontal of [true, false]) {\n\t\t\t\tif (horizontal ? out.x || !axes.x : out.y || !axes.y) continue;\n\t\t\t\tconst moved = { ...r, x: r.x + out.dx, y: r.y + out.dy };\n\t\t\t\tconst even = this.evenSpacing(moved, horizontal, tol);\n\t\t\t\tif (!even) continue;\n\t\t\t\tif (horizontal) {\n\t\t\t\t\tout.dx += even.shift;\n\t\t\t\t\tout.x = true;\n\t\t\t\t} else {\n\t\t\t\t\tout.dy += even.shift;\n\t\t\t\t\tout.y = true;\n\t\t\t\t}\n\t\t\t\tfor (const gap of even.gaps) this.gapMarks(gap, horizontal, tol, out);\n\t\t\t}\n\t\t}\n\t\tif (bx || by) out.labels.unshift(this.label({ x: r.x + out.dx + r.width / 2, y: r.y + out.dy + r.height / 2 }, 'align'));\n\t\treturn out;\n\t}\n\n\t/**\n\t * Equal spacing along x (`horizontal`) or y for the moved box `r`, among the objects of its\n\t * row (column): the shift within `tol` that centres it between its neighbours, or leaves it\n\t * as far from the neighbour before or after it as two other objects of the row are apart;\n\t * and the gaps that are equal then, as (start, end, where across).\n\t */\n\tprivate evenSpacing(r: VectorBounds, horizontal: boolean, tol: number): { shift: number; gaps: [number, number, number][] } | null {\n\t\tconst span = (b: VectorBounds, x: boolean): [number, number] => (x ? [b.x, b.x + b.width] : [b.y, b.y + b.height]);\n\t\tconst [r0, r1] = span(r, horizontal);\n\t\tconst len = r1 - r0;\n\t\tconst [c0, c1] = span(r, !horizontal);\n\t\tconst row = this.boxes\n\t\t\t.map((b) => ({ b, a0: span(b, horizontal)[0], a1: span(b, horizontal)[1] }))\n\t\t\t.filter((e) => span(e.b, !horizontal)[0] < c1 && span(e.b, !horizontal)[1] > c0 && (e.a1 <= r0 + tol || e.a0 >= r1 - tol))\n\t\t\t.sort((a, b) => a.a0 - b.a0);\n\t\tif (!row.length) return null;\n\t\t// Across the middle of what two boxes share (or of the space between them).\n\t\tconst mid = (a: VectorBounds, b: VectorBounds) => (Math.max(span(a, !horizontal)[0], span(b, !horizontal)[0]) + Math.min(span(a, !horizontal)[1], span(b, !horizontal)[1])) / 2;\n\t\tconst pairs: { g: number; gap: [number, number, number] }[] = [];\n\t\tfor (const p of row) {\n\t\t\tconst next = bound(row, (e) => e.a0, p.a1, false);\n\t\t\tconst q = row[next];\n\t\t\tif (!q) continue;\n\t\t\tconst straddles = p.a1 <= r0 + tol && q.a0 >= r1 - tol;\n\t\t\tif (q.a0 > p.a1 && !straddles) pairs.push({ g: q.a0 - p.a1, gap: [p.a1, q.a0, mid(p.b, q.b)] });\n\t\t}\n\t\tlet before: (typeof row)[number] | null = null;\n\t\tlet after: (typeof row)[number] | null = null;\n\t\tfor (const e of row) {\n\t\t\tif (e.a1 <= r0 + tol && (!before || e.a1 > before.a1)) before = e;\n\t\t\tif (e.a0 >= r1 - tol && (!after || e.a0 < after.a0)) after = e;\n\t\t}\n\t\tconst starts: { start: number; repeats: number | null }[] = [];\n\t\tif (before && after && after.a0 - before.a1 > len) starts.push({ start: (before.a1 + after.a0 - len) / 2, repeats: null });\n\t\tfor (const { g } of pairs) {\n\t\t\tif (before) starts.push({ start: before.a1 + g, repeats: g });\n\t\t\tif (after) starts.push({ start: after.a0 - g - len, repeats: g });\n\t\t}\n\t\tlet pick: { start: number; repeats: number | null } | null = null;\n\t\tfor (const s of starts) if (Math.abs(s.start - r0) <= tol && (!pick || Math.abs(s.start - r0) < Math.abs(pick.start - r0))) pick = s;\n\t\tif (!pick) return null;\n\t\tconst { start, repeats } = pick;\n\t\tconst at = horizontal ? { ...r, x: start } : { ...r, y: start };\n\t\tconst end = start + len;\n\t\tconst gaps: [number, number, number][] = [];\n\t\tif (before && (repeats === null || Math.abs(start - before.a1 - repeats) <= EQUAL)) gaps.push([before.a1, start, mid(before.b, at)]);\n\t\tif (after && (repeats === null || Math.abs(after.a0 - end - repeats) <= EQUAL)) gaps.push([end, after.a0, mid(after.b, at)]);\n\t\tif (repeats !== null) for (const p of pairs) if (Math.abs(p.g - repeats) <= EQUAL) gaps.push(p.gap);\n\t\treturn { shift: start - r0, gaps };\n\t}\n\n\t/** A gap mark: a line across the gap with a tick at each end, and its length as a label. */\n\tprivate gapMarks([u0, u1, c]: [number, number, number], horizontal: boolean, tick: number, out: VectorSnapGuides) {\n\t\tconst pt = (u: number, v: number): Vec2 => (horizontal ? { x: u, y: v } : { x: v, y: u });\n\t\tout.lines.push(line('spacing', pt(u0, c), pt(u1, c)), line('spacing', pt(u0, c - tick), pt(u0, c + tick)), line('spacing', pt(u1, c - tick), pt(u1, c + tick)));\n\t\tout.labels.push(this.label(pt((u0 + u1) / 2, c), 'spacing', fmtLength(u1 - u0)));\n\t}\n\n\t/** The other objects' side length nearest `length`, within `tol`, or null. */\n\tmatchSize(length: number, tol: number): number | null {\n\t\tlet best: number | null = null;\n\t\tfor (const s of this.sizes) if (Math.abs(s - length) <= tol && (best === null || Math.abs(s - length) < Math.abs(best - length))) best = s;\n\t\treturn best;\n\t}\n\n\t/**\n\t * Snaps a selection-box resize. `sx / sy` are the scale `scaleFor` gave for dragging\n\t * `handle` of `frame` (in the box's own, possibly rotated frame), `mods.shift` keeps the\n\t * proportions and `mods.alt` scales about the centre. The handle lands on an anchor or\n\t * centre within `tol`, else the corner or the side it moves lines up with the nearest\n\t * target, whatever the box's angle; a handle with one way to go (a side, or a proportional\n\t * corner) slides along it until one of them is on a target. An axis that snaps to nothing\n\t * may match another object's width or height instead.\n\t */\n\tsnapScale(\n\t\tframe: VectorBounds & { angle: number },\n\t\thandle: Exclude<VectorTransformHandle, 'rotate'>,\n\t\tsx: number,\n\t\tsy: number,\n\t\tmods: { shift?: boolean; alt?: boolean },\n\t\ttol: number\n\t): VectorScaleSnap {\n\t\tconst toDoc = frame.angle ? rotateMatrix(frame.angle) : IDENTITY_MATRIX;\n\t\tconst toLocal = frame.angle ? rotateMatrix(-frame.angle) : IDENTITY_MATRIX;\n\t\tconst corner = handle.length === 2;\n\t\tconst proportional = !!mods.shift;\n\t\tconst centre = { x: frame.x + frame.width / 2, y: frame.y + frame.height / 2 };\n\t\tconst origin = mods.alt ? centre : handlePos(opposite(handle), frame);\n\t\tconst hp = handlePos(handle, frame);\n\t\tconst d0 = { x: hp.x - origin.x, y: hp.y - origin.y };\n\t\tconst valid = (n: number, s: number) => Number.isFinite(n) && n * s > 0;\n\t\tconst nearest = (v: number, lines: readonly LineTarget[]) => {\n\t\t\tlet best: LineTarget | null = null;\n\t\t\tfor (const l of linesNear(lines, v, tol)) if (!best || Math.abs(l.v - v) < Math.abs(best.v - v)) best = l;\n\t\t\treturn best;\n\t\t};\n\t\ttype Snap = { hx: LineTarget | null; hy: LineTarget | null; on: PointTarget | null };\n\t\tlet nx = sx;\n\t\tlet ny = sy;\n\t\tlet snap: Snap = { hx: null, hy: null, on: null };\n\t\tif (corner && !proportional) {\n\t\t\t// The corner goes anywhere: onto an anchor or centre, else it takes a target's x, its y, or both.\n\t\t\tconst p = applyMatrix(toDoc, { x: origin.x + d0.x * sx, y: origin.y + d0.y * sy });\n\t\t\tconst hx = nearest(p.x, this.xs);\n\t\t\tconst hy = nearest(p.y, this.ys);\n\t\t\tconst on = this.nearestPoint(p, tol);\n\t\t\tconst tries: { tx: number | null; ty: number | null; s: Snap }[] = [];\n\t\t\tif (on) tries.push({ tx: on.p.x, ty: on.p.y, s: { hx: null, hy: null, on } });\n\t\t\ttries.push({ tx: hx?.v ?? null, ty: hy?.v ?? null, s: { hx, hy, on: null } });\n\t\t\ttries.push({ tx: hx?.v ?? null, ty: null, s: { hx, hy: null, on: null } });\n\t\t\ttries.push({ tx: null, ty: hy?.v ?? null, s: { hx: null, hy, on: null } });\n\t\t\tfor (const { tx, ty, s } of tries) {\n\t\t\t\tif (tx === null && ty === null) continue;\n\t\t\t\tconst q = applyMatrix(toLocal, { x: tx ?? p.x, y: ty ?? p.y });\n\t\t\t\tlet cx = (q.x - origin.x) / d0.x;\n\t\t\t\tlet cy = (q.y - origin.y) / d0.y;\n\t\t\t\t// Square to the page, the axis without a target stays exactly where it was.\n\t\t\t\tif (!frame.angle) {\n\t\t\t\t\tif (tx === null) cx = sx;\n\t\t\t\t\tif (ty === null) cy = sy;\n\t\t\t\t}\n\t\t\t\tif (valid(cx, sx) && valid(cy, sy)) {\n\t\t\t\t\tnx = cx;\n\t\t\t\t\tny = cy;\n\t\t\t\t\tsnap = s;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// One parameter `s`: the scale is `c + s·e` on each axis.\n\t\t\tconst movesX = handle === 'e' || handle === 'w';\n\t\t\tlet e: Vec2;\n\t\t\tlet c: Vec2;\n\t\t\tlet s0: number;\n\t\t\tif (corner) [e, c, s0] = [{ x: Math.sign(sx), y: Math.sign(sy) }, { x: 0, y: 0 }, Math.abs(sx)];\n\t\t\telse if (movesX) [e, c, s0] = proportional ? [{ x: 1, y: Math.sign(sx) }, { x: 0, y: 0 }, sx] : [{ x: 1, y: 0 }, { x: 0, y: 1 }, sx];\n\t\t\telse [e, c, s0] = proportional ? [{ x: Math.sign(sy), y: 1 }, { x: 0, y: 0 }, sy] : [{ x: 0, y: 1 }, { x: 1, y: 0 }, sy];\n\t\t\t// The points that may land on a target: the handle, and the two ends of its side.\n\t\t\tconst i = RING.indexOf(handle);\n\t\t\tconst ends = corner ? [] : [RING[(i + 1) % 8], RING[(i + 7) % 8]];\n\t\t\t// How far the handle travels per unit of `s`: a snap may not drag it far from the pointer.\n\t\t\tconst travel = Math.hypot(e.x * d0.x, e.y * d0.y);\n\t\t\tlet best: { s: number; inLine: boolean; moved: number; snap: Snap } | null = null;\n\t\t\tconst consider = (s: number, inLine: boolean, found: Snap) => {\n\t\t\t\tconst moved = Math.abs(s - s0) * travel;\n\t\t\t\tif (!valid(s, s0) || moved > 2 * tol) return;\n\t\t\t\tif (!best || (inLine === best.inLine ? moved < best.moved : !inLine)) best = { s, inLine, moved, snap: found };\n\t\t\t};\n\t\t\tfor (const h of [handle, ...ends]) {\n\t\t\t\tconst hv = handlePos(h, frame);\n\t\t\t\tconst v = { x: hv.x - origin.x, y: hv.y - origin.y };\n\t\t\t\tconst base = applyMatrix(toDoc, { x: origin.x + c.x * v.x, y: origin.y + c.y * v.y });\n\t\t\t\tconst dir = applyMatrix(toDoc, { x: e.x * v.x, y: e.y * v.y });\n\t\t\t\tconst p = { x: base.x + dir.x * s0, y: base.y + dir.y * s0 };\n\t\t\t\tconst l2 = dir.x * dir.x + dir.y * dir.y;\n\t\t\t\tconst on = l2 > 1e-18 ? this.nearestPoint(p, tol) : null;\n\t\t\t\t// An anchor or centre beside its way: it stops at the nearest point of the way.\n\t\t\t\tif (on) consider(((on.p.x - base.x) * dir.x + (on.p.y - base.y) * dir.y) / l2, false, { hx: null, hy: null, on });\n\t\t\t\tif (Math.abs(dir.x) > 1e-9) {\n\t\t\t\t\tconst hit = nearest(p.x, this.xs);\n\t\t\t\t\tif (hit) consider((hit.v - base.x) / dir.x, true, { hx: hit, hy: null, on: null });\n\t\t\t\t}\n\t\t\t\tif (Math.abs(dir.y) > 1e-9) {\n\t\t\t\t\tconst hit = nearest(p.y, this.ys);\n\t\t\t\t\tif (hit) consider((hit.v - base.y) / dir.y, true, { hx: null, hy: hit, on: null });\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst found = best as { s: number; snap: Snap } | null;\n\t\t\tif (found) {\n\t\t\t\tnx = c.x + found.s * e.x;\n\t\t\t\tny = c.y + found.s * e.y;\n\t\t\t\tsnap = found.snap;\n\t\t\t}\n\t\t}\n\t\tconst out: VectorScaleSnap = { sx: nx, sy: ny, snapped: false, lines: [], labels: [] };\n\t\tconst handleAt = () => applyMatrix(toDoc, { x: origin.x + d0.x * out.sx, y: origin.y + d0.y * out.sy });\n\t\tif (snap.on) {\n\t\t\tout.snapped = true;\n\t\t\tout.labels.push(this.label(snap.on.p, snap.on.kind));\n\t\t\treturn out;\n\t\t}\n\t\t// Sides that line up with nothing may match another object's width or height.\n\t\tconst movesX = handle !== 'n' && handle !== 's';\n\t\tconst movesY = handle !== 'e' && handle !== 'w';\n\t\tconst sized: ('x' | 'y')[] = [];\n\t\tif (!proportional) {\n\t\t\tif (movesX && !snap.hx && !(frame.angle && snap.hy) && frame.width > 1e-9) {\n\t\t\t\tconst len = this.matchSize(Math.abs(out.sx) * frame.width, tol);\n\t\t\t\tif (len !== null && len > 0) {\n\t\t\t\t\tout.sx = Math.sign(out.sx || 1) * (len / frame.width);\n\t\t\t\t\tsized.push('x');\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (movesY && !snap.hy && !(frame.angle && snap.hx) && frame.height > 1e-9) {\n\t\t\t\tconst len = this.matchSize(Math.abs(out.sy) * frame.height, tol);\n\t\t\t\tif (len !== null && len > 0) {\n\t\t\t\t\tout.sy = Math.sign(out.sy || 1) * (len / frame.height);\n\t\t\t\t\tsized.push('y');\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// The resized box in its frame, and its corners on the page.\n\t\tconst x0 = origin.x + (frame.x - origin.x) * out.sx;\n\t\tconst x1 = origin.x + (frame.x + frame.width - origin.x) * out.sx;\n\t\tconst y0 = origin.y + (frame.y - origin.y) * out.sy;\n\t\tconst y1 = origin.y + (frame.y + frame.height - origin.y) * out.sy;\n\t\tconst box = { x0: Math.min(x0, x1), x1: Math.max(x0, x1), y0: Math.min(y0, y1), y1: Math.max(y0, y1) };\n\t\tconst page = [\n\t\t\t{ x: box.x0, y: box.y0 },\n\t\t\t{ x: box.x1, y: box.y0 },\n\t\t\t{ x: box.x1, y: box.y1 },\n\t\t\t{ x: box.x0, y: box.y1 }\n\t\t].map((q) => applyMatrix(toDoc, q));\n\t\tconst pminY = Math.min(...page.map((q) => q.y));\n\t\tconst pmaxY = Math.max(...page.map((q) => q.y));\n\t\tconst pminX = Math.min(...page.map((q) => q.x));\n\t\tconst pmaxX = Math.max(...page.map((q) => q.x));\n\t\tif (snap.hx) out.lines.push({ kind: 'align', x1: snap.hx.v, y1: Math.min(snap.hx.from.y, pminY), x2: snap.hx.v, y2: Math.max(snap.hx.from.y, pmaxY) });\n\t\tif (snap.hy) out.lines.push({ kind: 'align', x1: Math.min(snap.hy.from.x, pminX), y1: snap.hy.v, x2: Math.max(snap.hy.from.x, pmaxX), y2: snap.hy.v });\n\t\tif (snap.hx || snap.hy) {\n\t\t\tout.snapped = true;\n\t\t\tout.labels.push(this.label(handleAt(), 'align'));\n\t\t}\n\t\tfor (const axis of sized) {\n\t\t\tout.snapped = true;\n\t\t\t// Along the matched side, just outside the box.\n\t\t\tconst off = tol * 2;\n\t\t\tconst [a, b] = axis === 'x' ? [{ x: box.x0, y: box.y1 + off }, { x: box.x1, y: box.y1 + off }] : [{ x: box.x1 + off, y: box.y0 }, { x: box.x1 + off, y: box.y1 }];\n\t\t\tconst pa = applyMatrix(toDoc, a);\n\t\t\tconst pb = applyMatrix(toDoc, b);\n\t\t\tout.lines.push(line('size', pa, pb));\n\t\t\tout.labels.push(this.label({ x: (pa.x + pb.x) / 2, y: (pa.y + pb.y) / 2 }, 'size', `${axis === 'x' ? 'W' : 'H'} ${fmtLength(axis === 'x' ? box.x1 - box.x0 : box.y1 - box.y0)}`));\n\t\t}\n\t\treturn out;\n\t}\n}\n\n/** The eight resize handles clockwise from the top left (each side sits between its two corners). */\nconst RING: readonly Exclude<VectorTransformHandle, 'rotate'>[] = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'];\n\nfunction opposite(h: Exclude<VectorTransformHandle, 'rotate'>): Exclude<VectorTransformHandle, 'rotate'> {\n\treturn RING[(RING.indexOf(h) + 4) % 8];\n}\n\nfunction handlePos(h: Exclude<VectorTransformHandle, 'rotate'>, r: VectorBounds): Vec2 {\n\tconst x = h.includes('w') ? r.x : h.includes('e') ? r.x + r.width : r.x + r.width / 2;\n\tconst y = h.startsWith('n') ? r.y : h.startsWith('s') ? r.y + r.height : r.y + r.height / 2;\n\treturn { x, y };\n}\n\nfunction line(kind: VectorGuideKind, a: Vec2, b: Vec2): VectorGuideLine {\n\treturn { kind, x1: a.x, y1: a.y, x2: b.x, y2: b.y };\n}\n\n/** The polar paths from where a point leaves: both ways along each construction angle. */\nfunction polarPaths(leave: VectorSnapLeave | null | undefined): VectorTrackingPath[] {\n\tif (!leave?.angles?.length || leave.constrain) return [];\n\tconst out: VectorTrackingPath[] = [];\n\tfor (const a of leave.angles) {\n\t\tconst dir = vectorAngleDirection(a);\n\t\tout.push({ origin: leave.at, dir, kind: 'polar' }, { origin: leave.at, dir: { x: -dir.x, y: -dir.y }, kind: 'polar' });\n\t}\n\treturn out;\n}\n\n/** The tracking paths within `tol` of `p` (and `p` farther than that from where they start), with their feet. */\nfunction trackingNear(paths: readonly VectorTrackingPath[], p: Vec2, tol: number): { path: VectorTrackingPath; foot: Vec2; d: number }[] {\n\tconst out: { path: VectorTrackingPath; foot: Vec2; d: number }[] = [];\n\tfor (const path of paths) {\n\t\tconst t = (p.x - path.origin.x) * path.dir.x + (p.y - path.origin.y) * path.dir.y;\n\t\tif (!(t > tol)) continue;\n\t\tconst foot = { x: path.origin.x + path.dir.x * t, y: path.origin.y + path.dir.y * t };\n\t\tconst d = distance(foot, p);\n\t\tif (d <= tol) out.push({ path, foot, d });\n\t\tif (out.length >= 64) break;\n\t}\n\treturn out;\n}\n\n/** Where two tracking paths from different points cross nearest `p`, within `tol`. After cadcraft's `Tracker::find` (step 1). */\nfunction pathCrossing(near: readonly { path: VectorTrackingPath }[], p: Vec2, tol: number): { point: Vec2; paths: VectorTrackingPath[] } | null {\n\tlet best: { point: Vec2; paths: VectorTrackingPath[]; d: number } | null = null;\n\tfor (let i = 0; i < near.length; i++) {\n\t\tfor (let j = i + 1; j < near.length; j++) {\n\t\t\tconst a = near[i].path;\n\t\t\tconst b = near[j].path;\n\t\t\tif (distance(a.origin, b.origin) <= 1e-9) continue;\n\t\t\tconst den = a.dir.x * b.dir.y - a.dir.y * b.dir.x;\n\t\t\tif (Math.abs(den) < 1e-12) continue;\n\t\t\tconst wx = b.origin.x - a.origin.x;\n\t\t\tconst wy = b.origin.y - a.origin.y;\n\t\t\tconst s = (wx * b.dir.y - wy * b.dir.x) / den;\n\t\t\tconst u = (wx * a.dir.y - wy * a.dir.x) / den;\n\t\t\tif (s <= 0 || u <= 0) continue;\n\t\t\tconst q = { x: a.origin.x + a.dir.x * s, y: a.origin.y + a.dir.y * s };\n\t\t\t// Exactly on an axis-aligned path.\n\t\t\tif (a.dir.x === 0) q.x = a.origin.x;\n\t\t\tif (a.dir.y === 0) q.y = a.origin.y;\n\t\t\tif (b.dir.x === 0) q.x = b.origin.x;\n\t\t\tif (b.dir.y === 0) q.y = b.origin.y;\n\t\t\tconst d = distance(q, p);\n\t\t\tif (d <= tol && (!best || d < best.d)) best = { point: q, paths: [a, b], d };\n\t\t}\n\t}\n\treturn best && { point: best.point, paths: best.paths };\n}\n\n/** Where a tracking path crosses a target segment near `p`, within `tol`. After cadcraft's `Tracker::find` (step 2). */\nfunction pathOnSegment(paths: readonly { path: VectorTrackingPath; foot: Vec2 }[], near: readonly { seg: Point[] }[], p: Vec2, tol: number): { point: Vec2; path: VectorTrackingPath } | null {\n\tlet best: { point: Vec2; path: VectorTrackingPath; d: number } | null = null;\n\tfor (const { path, foot } of paths) {\n\t\t// The stretch of the path round the foot that can hold a crossing within `tol` of `p`.\n\t\tconst reach = 3 * tol;\n\t\tconst a: Point = [foot.x - path.dir.x * reach, foot.y - path.dir.y * reach];\n\t\tconst b: Point = [foot.x + path.dir.x * reach, foot.y + path.dir.y * reach];\n\t\tfor (const { seg } of near.slice(0, CROSSING_PAIRS)) {\n\t\t\tfor (const hit of segmentIntersections([a, b] as BezierSegment, seg as BezierSegment)) {\n\t\t\t\tconst q = toVec(hit.point);\n\t\t\t\tconst t = (q.x - path.origin.x) * path.dir.x + (q.y - path.origin.y) * path.dir.y;\n\t\t\t\tconst d = distance(q, p);\n\t\t\t\tif (t > tol && d <= tol && (!best || d < best.d)) best = { point: q, path, d };\n\t\t\t}\n\t\t}\n\t}\n\treturn best && { point: best.point, path: best.path };\n}\n\n/** The construction guide through `leave.at` nearest `p`, within `tol` (and `p` farther than that from `at`): its direction and where `p` lands. */\nfunction construction(leave: VectorSnapLeave, p: Vec2, tol: number): { dir: Vec2; point: Vec2 } | null {\n\tconst v = { x: p.x - leave.at.x, y: p.y - leave.at.y };\n\tif (Math.hypot(v.x, v.y) <= tol) return null;\n\tlet best: { dir: Vec2; point: Vec2; d: number } | null = null;\n\tfor (const a of leave.angles ?? []) {\n\t\t// Counter-clockwise as seen on the y-down page.\n\t\tconst r = (a * Math.PI) / 180;\n\t\t// Exactly along an axis for multiples of 90°.\n\t\tconst exact = (n: number) => (Math.abs(n) < 1e-12 ? 0 : n);\n\t\tconst dir = { x: exact(Math.cos(r)), y: exact(-Math.sin(r)) };\n\t\tconst k = v.x * dir.x + v.y * dir.y;\n\t\tconst point = { x: leave.at.x + dir.x * k, y: leave.at.y + dir.y * k };\n\t\tconst d = distance(point, p);\n\t\tif (d <= tol && (!best || d < best.d)) best = { dir, point, d };\n\t}\n\treturn best && { dir: best.dir, point: best.point };\n}\n\nconst dotCondition = (r: Point, d: Point) => r[0] * d[0] + r[1] * d[1];\nconst crossCondition = (r: Point, d: Point) => r[0] * d[1] - r[1] * d[0];\n\n/**\n * The points of a segment where `cond(B(t) − base, B′(t))` is zero: sign changes of samples,\n * narrowed by bisection, kept when the normalised condition is (nearly) zero there.\n */\nfunction solveOnSegment(seg: readonly Point[], base: Vec2, cond: (r: Point, d: Point) => number): Vec2[] {\n\tconst b: Point = [base.x, base.y];\n\tconst g = (t: number) => {\n\t\tconst p = segmentPoint(seg, t);\n\t\treturn cond([p[0] - b[0], p[1] - b[1]], segmentDerivative(seg, t));\n\t};\n\tconst vals: number[] = [];\n\tlet scale = 0;\n\tfor (let i = 0; i <= CURVE_SAMPLES; i++) {\n\t\tconst t = i / CURVE_SAMPLES;\n\t\tconst p = segmentPoint(seg, t);\n\t\tconst d = segmentDerivative(seg, t);\n\t\tconst r: Point = [p[0] - b[0], p[1] - b[1]];\n\t\tvals.push(cond(r, d));\n\t\tscale = Math.max(scale, Math.hypot(r[0], r[1]) * Math.hypot(d[0], d[1]));\n\t}\n\t// Every point qualifies (a line seen from a point on it): no particular solution.\n\tif (!(scale > 0) || vals.every((v) => Math.abs(v) <= scale * 1e-12)) return [];\n\tconst out: Vec2[] = [];\n\tfor (let i = 0; i < CURVE_SAMPLES; i++) {\n\t\tconst ga = vals[i];\n\t\tconst gb = vals[i + 1];\n\t\tif (!(Number.isFinite(ga) && Number.isFinite(gb)) || (ga > 0 === gb > 0 && ga !== 0)) continue;\n\t\tlet lo = i / CURVE_SAMPLES;\n\t\tlet hi = (i + 1) / CURVE_SAMPLES;\n\t\tlet flo = ga;\n\t\tfor (let k = 0; k < BISECT; k++) {\n\t\t\tconst m = (lo + hi) / 2;\n\t\t\tconst fm = g(m);\n\t\t\tif (fm === 0) {\n\t\t\t\tlo = hi = m;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (fm > 0 === flo > 0) {\n\t\t\t\tlo = m;\n\t\t\t\tflo = fm;\n\t\t\t} else hi = m;\n\t\t}\n\t\tconst t = (lo + hi) / 2;\n\t\tconst p = segmentPoint(seg, t);\n\t\tconst dv = segmentDerivative(seg, t);\n\t\tconst r = Math.hypot(p[0] - b[0], p[1] - b[1]);\n\t\tconst dl = Math.hypot(dv[0], dv[1]);\n\t\tif (!(r > 1e-9 && dl > 0) || Math.abs(cond([p[0] - b[0], p[1] - b[1]], dv)) > 1e-6 * r * dl) continue;\n\t\tif (!out.some((q) => Math.hypot(q.x - p[0], q.y - p[1]) <= 1e-9 * (1 + r))) out.push({ x: p[0], y: p[1] });\n\t}\n\treturn out;\n}\n","/**\n * Path editing on subpaths in a node's own space: anchor deletion that keeps the shape,\n * segment bending, Curvature-tool anchor moves, cutting at anchors (Scissors), Join, Average,\n * Remove Redundant Points and Smooth.\n *\n * Portions ported from (MIT OR Apache-2.0, © ArtCraft Team and contributors; see\n * THIRD_PARTY_NOTICES.md):\n * - vectorcraft-main/crates/pathops/src/edit.rs (`remove_anchor`, `golden_min`, `smooth`,\n * `remove_redundant_points`, `merge_pair`, `average`, `join`)\n * - vectorcraft-main/crates/geom/src/path.rs (`SubPath::cut_at`)\n * - vectorcraft-main/crates/tools/src/draw2/curvature.rs (`curvature_move`)\n * - figcraft-main/crates/engine/src/vectoredit/ops.rs (`bend`)\n */\nimport type { Point } from '../canvas/types.js';\nimport { fitCubicWithTangents } from '../canvas/curveFit.js';\nimport { segmentPoint } from '../canvas/pathops/bezier.js';\nimport { cloneAnchor, inferAnchorType, insertAnchor, moveAnchorHandle, subpathSegment, subpathSegmentCount } from './geometry.js';\nimport type { VectorAnchor, VectorSubpath, Vec2 } from './types.js';\n\nconst EPS = 1e-9;\n\n// ---------------------------------------------------------------------------\n// Small helpers\n// ---------------------------------------------------------------------------\n\nconst unit = (x: number, y: number): Point | null => {\n\tconst l = Math.hypot(x, y);\n\treturn l > 1e-12 && Number.isFinite(l) ? [x / l, y / l] : null;\n};\n\n/** Segment `i` as four control points (a line has its controls at the thirds of its chord). */\nfunction cubicOf(sp: VectorSubpath, i: number): Point[] {\n\tconst s = subpathSegment(sp, i);\n\tif (s.length === 4) return s;\n\tconst [a, b] = s;\n\treturn [a, [a[0] + (b[0] - a[0]) / 3, a[1] + (b[1] - a[1]) / 3], [a[0] + (2 * (b[0] - a[0])) / 3, a[1] + (2 * (b[1] - a[1])) / 3], b];\n}\n\nconst isLine = (sp: VectorSubpath, i: number) => subpathSegment(sp, i).length === 2;\n\n/** Direction the curve leaves `c[0]`. */\nfunction startTangent(c: readonly Point[]): Point {\n\tfor (let k = 1; k < 4; k++) {\n\t\tconst u = unit(c[k][0] - c[0][0], c[k][1] - c[0][1]);\n\t\tif (u) return u;\n\t}\n\treturn [1, 0];\n}\n\n/** Direction the curve arrives at `c[3]` (forward along the curve). */\nfunction endTangent(c: readonly Point[]): Point {\n\tfor (let k = 2; k >= 0; k--) {\n\t\tconst u = unit(c[3][0] - c[k][0], c[3][1] - c[k][1]);\n\t\tif (u) return u;\n\t}\n\treturn [1, 0];\n}\n\n/** `n + 1` points of a cubic at even parameters (`withStart` false skips the first). */\nfunction sample(c: readonly Point[], n: number, out: Point[], withStart: boolean) {\n\tfor (let i = withStart ? 0 : 1; i <= n; i++) out.push(segmentPoint(c, i / n));\n}\n\nconst onAnchor = (a: VectorAnchor, h: Vec2 | null | undefined) => !h || (Math.abs(h.x - a.x) < EPS && Math.abs(h.y - a.y) < EPS);\n\n/** Drops handles that sit on their anchor and sets the type the handles show (a symmetric pair that is no longer equal becomes smooth). */\nfunction tidy(a: VectorAnchor, keepType = true): VectorAnchor {\n\tconst next = cloneAnchor(a);\n\tif (onAnchor(next, next.in)) delete next.in;\n\tif (onAnchor(next, next.out)) delete next.out;\n\tconst shown = inferAnchorType(next);\n\tconst type = keepType && a.type ? (a.type === 'symmetric' && shown !== 'symmetric' ? (shown === 'corner' ? 'corner' : 'smooth') : a.type === 'smooth' && shown === 'corner' ? 'corner' : a.type) : shown;\n\tif (type === 'corner') delete next.type;\n\telse next.type = type;\n\treturn next;\n}\n\n/** Index of the anchor before / after `i` (round the start of a closed subpath), or -1. */\nfunction neighbours(sp: VectorSubpath, i: number): [number, number] {\n\tconst n = sp.anchors.length;\n\tconst wrap = sp.closed && n > 1;\n\treturn [i > 0 ? i - 1 : wrap ? n - 1 : -1, i < n - 1 ? i + 1 : wrap ? 0 : -1];\n}\n\n// ---------------------------------------------------------------------------\n// Deleting anchors without changing the shape\n// ---------------------------------------------------------------------------\n\n/**\n * The value of `f` with the least error for an argument in [a, b], by a golden-section search\n * (a fixed number of steps). An error that is not a number never wins.\n */\nfunction goldenMin<T extends { error: number }>(f: (x: number) => T, a: number, b: number): T {\n\tconst R = 0.618033988749895;\n\tlet x1 = b - R * (b - a);\n\tlet x2 = a + R * (b - a);\n\tlet f1 = f(x1);\n\tlet f2 = f(x2);\n\tfor (let i = 0; i < 40; i++) {\n\t\tif (f1.error < f2.error || Number.isNaN(f2.error)) {\n\t\t\tb = x2;\n\t\t\tx2 = x1;\n\t\t\tf2 = f1;\n\t\t\tx1 = b - R * (b - a);\n\t\t\tf1 = f(x1);\n\t\t} else {\n\t\t\ta = x1;\n\t\t\tx1 = x2;\n\t\t\tf1 = f2;\n\t\t\tx2 = a + R * (b - a);\n\t\t\tf2 = f(x2);\n\t\t}\n\t}\n\treturn f1.error <= f2.error ? f1 : f2;\n}\n\n/**\n * One cubic through `pts`: `N + 1` samples of a left curve then `N` of a right one. The new\n * curve passes their joint at some parameter k: the left samples sit at k·i/N, the right ones\n * at k + (1 − k)·i/N. Searches k for the closest fit.\n */\nfunction fitAcross(pts: Point[], N: number, t0: Point, t1: Point) {\n\tconst fit = (k: number) => {\n\t\tconst u: number[] = [];\n\t\tfor (let i = 0; i <= N; i++) u.push((k * i) / N);\n\t\tfor (let i = 1; i <= N; i++) u.push(k + ((1 - k) * i) / N);\n\t\treturn fitCubicWithTangents(pts, u, t0, t1);\n\t};\n\treturn goldenMin(fit, 0, 1);\n}\n\n/**\n * Removes anchor `index` without opening the path: its neighbours keep their handle directions\n * and their facing handles are refitted so one cubic follows the two segments that met there\n * as closely as possible (least maximum error over where the new curve passes the removed\n * point). Two straight segments become one; removing an end of an open subpath drops its\n * segment. Returns the subpath unchanged (a copy) when `index` is out of range.\n */\nexport function removeAnchorKeepingShape(sp: VectorSubpath, index: number): VectorSubpath {\n\tconst anchors = sp.anchors.map(cloneAnchor);\n\tconst n = anchors.length;\n\tif (index < 0 || index >= n) return { anchors, closed: sp.closed };\n\tif ((sp.closed || (index > 0 && index < n - 1)) && n >= 3) {\n\t\tconst pi = (index + n - 1) % n;\n\t\tconst ni = (index + 1) % n;\n\t\tif (!(isLine(sp, pi) && isLine(sp, index))) {\n\t\t\tconst left = cubicOf(sp, pi);\n\t\t\tconst right = cubicOf(sp, index);\n\t\t\tconst N = 16;\n\t\t\tconst pts: Point[] = [];\n\t\t\tsample(left, N, pts, true);\n\t\t\tsample(right, N, pts, false);\n\t\t\tconst t0 = startTangent(left);\n\t\t\tconst t1 = endTangent(right);\n\t\t\tconst { curve } = fitAcross(pts, N, t0, t1);\n\t\t\tif ([curve.p1, curve.p2].every((p) => Number.isFinite(p[0]) && Number.isFinite(p[1]))) {\n\t\t\t\tanchors[pi] = tidy({ ...anchors[pi], out: { x: curve.p1[0], y: curve.p1[1] } });\n\t\t\t\tanchors[ni] = tidy({ ...anchors[ni], in: { x: curve.p2[0], y: curve.p2[1] } });\n\t\t\t}\n\t\t}\n\t}\n\tanchors.splice(index, 1);\n\tlet closed = sp.closed;\n\tif (!closed && anchors.length) {\n\t\t// A new end has nothing beyond it.\n\t\tanchors[0] = tidy({ ...anchors[0], in: null });\n\t\tconst last = anchors.length - 1;\n\t\tanchors[last] = tidy({ ...anchors[last], out: null });\n\t}\n\tif (anchors.length < 3) closed = closed && anchors.length === 2 && anchors.some((a) => a.in || a.out);\n\treturn { anchors, closed };\n}\n\n/** Removes several anchors of a subpath, keeping the shape (see `removeAnchorKeepingShape`). */\nexport function removeAnchorsKeepingShape(sp: VectorSubpath, indices: readonly number[]): VectorSubpath {\n\tconst order = [...new Set(indices)].filter((i) => i >= 0 && i < sp.anchors.length).sort((a, b) => b - a);\n\treturn order.reduce((acc, i) => removeAnchorKeepingShape(acc, i), { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed });\n}\n\n// ---------------------------------------------------------------------------\n// Bending a segment\n// ---------------------------------------------------------------------------\n\n/**\n * Bends segment `segment` so the curve passes through `p` at parameter `t` (clamped to\n * 0.05–0.95): its handles go to the thirds of the chord plus one shared offset, so\n * `B(t) = L(t) + 3t(1 − t)·offset` exactly. The end anchors keep their types (a smooth or\n * symmetric end turns its other handle to stay in line).\n */\nexport function bendSegment(sp: VectorSubpath, segment: number, t: number, p: Vec2): VectorSubpath {\n\tconst anchors = sp.anchors.map(cloneAnchor);\n\tconst n = anchors.length;\n\tif (segment < 0 || segment >= subpathSegmentCount(sp) || !Number.isFinite(t) || !Number.isFinite(p.x) || !Number.isFinite(p.y)) return { anchors, closed: sp.closed };\n\tconst tc = Math.min(0.95, Math.max(0.05, t));\n\tconst ia = segment;\n\tconst ib = (segment + 1) % n;\n\tconst a = anchors[ia];\n\tconst b = anchors[ib];\n\tconst k = 3 * tc * (1 - tc);\n\tconst ox = (p.x - (a.x + (b.x - a.x) * tc)) / k;\n\tconst oy = (p.y - (a.y + (b.y - a.y) * tc)) / k;\n\tconst c1 = { x: a.x + (b.x - a.x) / 3 + ox, y: a.y + (b.y - a.y) / 3 + oy };\n\tconst c2 = { x: b.x + (a.x - b.x) / 3 + ox, y: b.y + (a.y - b.y) / 3 + oy };\n\tanchors[ia] = moveAnchorHandle(a, 'out', c1);\n\tanchors[ib] = moveAnchorHandle(b, 'in', c2);\n\treturn { anchors, closed: sp.closed };\n}\n\n/** The point at parameter `t` of segment `segment` (for tests and previews). */\nexport function subpathPointAt(sp: VectorSubpath, segment: number, t: number): Vec2 {\n\tconst [x, y] = segmentPoint(subpathSegment(sp, segment), t);\n\treturn { x, y };\n}\n\n// ---------------------------------------------------------------------------\n// Curvature-tool anchor move\n// ---------------------------------------------------------------------------\n\n/**\n * Moves anchor `index` to `q` the way the Curvature tool drags a point: only the two segments\n * at it change. A smooth or symmetric anchor turns its handles along the line through its\n * neighbours; the neighbours' handles on those segments, and a corner's own handles, keep\n * their directions while their lengths scale with their segment's chord, so the segments\n * beyond don't move and a corner without handles keeps none.\n */\nexport function curvatureMoveAnchor(sp: VectorSubpath, index: number, q: Vec2): VectorSubpath {\n\tconst anchors = sp.anchors.map(cloneAnchor);\n\tconst a = sp.anchors[index];\n\tif (!a) return { anchors, closed: sp.closed };\n\tconst own: [Vec2 | null, Vec2 | null] = [\n\t\ta.in && !onAnchor(a, a.in) ? { x: a.in.x - a.x, y: a.in.y - a.y } : null,\n\t\ta.out && !onAnchor(a, a.out) ? { x: a.out.x - a.x, y: a.out.y - a.y } : null\n\t];\n\tconst ends: [Vec2 | null, Vec2 | null] = [null, null];\n\tneighbours(sp, index).forEach((j, side) => {\n\t\tif (j < 0 || j === index) return;\n\t\tconst b = anchors[j];\n\t\tconst old = Math.hypot(a.x - b.x, a.y - b.y);\n\t\tconst s = old > 1e-9 ? Math.hypot(q.x - b.x, q.y - b.y) / old : 1;\n\t\tconst key = side === 0 ? 'out' : 'in';\n\t\tconst h = b[key];\n\t\tif (h) b[key] = { x: b.x + (h.x - b.x) * s, y: b.y + (h.y - b.y) * s };\n\t\tconst o = own[side];\n\t\tif (o) own[side] = { x: o.x * s, y: o.y * s };\n\t\tends[side] = { x: b.x, y: b.y };\n\t});\n\tconst moved: VectorAnchor = { x: q.x, y: q.y };\n\tif (own[0]) moved.in = { x: q.x + own[0].x, y: q.y + own[0].y };\n\tif (own[1]) moved.out = { x: q.x + own[1].x, y: q.y + own[1].y };\n\tif (a.type) moved.type = a.type;\n\tif (a.corner) moved.corner = { ...a.corner };\n\tif (a.id) moved.id = a.id;\n\tconst [prev, next] = ends;\n\tif (a.type && a.type !== 'corner' && prev && next) {\n\t\tconst dx = next.x - prev.x;\n\t\tconst dy = next.y - prev.y;\n\t\tconst l = Math.hypot(dx, dy);\n\t\tif (l > 1e-9) {\n\t\t\t// A handle it lacks comes out a third of the way to that side's neighbour.\n\t\t\tconst len = (h: Vec2 | null, to: Vec2) => (h ? Math.hypot(h.x, h.y) : Math.hypot(q.x - to.x, q.y - to.y) / 3);\n\t\t\tlet li = len(own[0], prev);\n\t\t\tlet lo = len(own[1], next);\n\t\t\tif (a.type === 'symmetric') li = lo = (li + lo) / 2;\n\t\t\tmoved.in = { x: q.x - (dx * li) / l, y: q.y - (dy * li) / l };\n\t\t\tmoved.out = { x: q.x + (dx * lo) / l, y: q.y + (dy * lo) / l };\n\t\t}\n\t}\n\tanchors[index] = moved;\n\treturn { anchors, closed: sp.closed };\n}\n\n// ---------------------------------------------------------------------------\n// Scissors\n// ---------------------------------------------------------------------------\n\n/**\n * Cuts a subpath at the anchors `at`: each becomes the end of one open piece and the start of\n * the next, its handle off each piece dropped (the shape doesn't change). A closed subpath\n * opens at its first cut, so one cut leaves one piece whose ends coincide. The ends of an open\n * subpath and indices out of range don't cut; with no cut left the subpath comes back as it is.\n */\nexport function cutSubpathAt(sp: VectorSubpath, at: readonly number[]): VectorSubpath[] {\n\tconst n = sp.anchors.length;\n\tconst cuts = [...new Set(at)].filter((i) => Number.isInteger(i) && i >= 0 && i < n && (sp.closed || (i > 0 && i < n - 1))).sort((a, b) => a - b);\n\tif (!cuts.length || n < 2) return [{ anchors: sp.anchors.map(cloneAnchor), closed: sp.closed }];\n\tconst first = cuts[0];\n\t// A closed subpath's anchors from the first cut round to it again; an open one's as they are.\n\tconst run = sp.closed ? Array.from({ length: n + 1 }, (_, i) => sp.anchors[(first + i) % n]) : sp.anchors;\n\tconst cutSet = new Set(sp.closed ? [...cuts.map((c) => (c - first + n) % n), n] : cuts);\n\tconst pieces: VectorSubpath[] = [];\n\tlet cur: VectorAnchor[] = [];\n\trun.forEach((a, i) => {\n\t\tif (!cutSet.has(i)) {\n\t\t\tcur.push(cloneAnchor(a));\n\t\t\treturn;\n\t\t}\n\t\tif (cur.length) {\n\t\t\tcur.push(tidy({ ...a, out: null }, false));\n\t\t\tpieces.push({ anchors: cur, closed: false });\n\t\t\tcur = [];\n\t\t}\n\t\tif (i < n) cur.push(tidy({ ...a, in: null }, false));\n\t});\n\tif (cur.length > 1) pieces.push({ anchors: cur, closed: false });\n\treturn pieces;\n}\n\n/**\n * Cuts a subpath at parameter `t` of segment `segment` (Scissors on a segment): an anchor is\n * inserted there without changing the shape, then cut. Within `endTolerance` of the segment's\n * ends (in parameter) the existing anchor is cut instead.\n */\nexport function splitSubpathAt(sp: VectorSubpath, segment: number, t: number, endTolerance = 1e-6): VectorSubpath[] {\n\tconst n = sp.anchors.length;\n\tif (segment < 0 || segment >= subpathSegmentCount(sp)) return [{ anchors: sp.anchors.map(cloneAnchor), closed: sp.closed }];\n\tif (t <= endTolerance) return cutSubpathAt(sp, [segment]);\n\tif (t >= 1 - endTolerance) return cutSubpathAt(sp, [(segment + 1) % n]);\n\tconst { subpath, index } = insertAnchor(sp, segment, t);\n\treturn cutSubpathAt(subpath, [index]);\n}\n\n// ---------------------------------------------------------------------------\n// Join\n// ---------------------------------------------------------------------------\n\nconst endOf = (sp: VectorSubpath, last: boolean): VectorAnchor => sp.anchors[last ? sp.anchors.length - 1 : 0];\nconst dist = (a: Vec2, b: Vec2) => Math.hypot(a.x - b.x, a.y - b.y);\n\nfunction reverse(sp: VectorSubpath): VectorSubpath {\n\treturn {\n\t\tclosed: sp.closed,\n\t\tanchors: sp.anchors\n\t\t\t.slice()\n\t\t\t.reverse()\n\t\t\t.map((a) => {\n\t\t\t\tconst next: VectorAnchor = { x: a.x, y: a.y };\n\t\t\t\tif (a.out) next.in = { ...a.out };\n\t\t\t\tif (a.in) next.out = { ...a.in };\n\t\t\t\tif (a.type) next.type = a.type;\n\t\t\t\tif (a.corner) next.corner = { ...a.corner };\n\t\t\t\tif (a.id) next.id = a.id;\n\t\t\t\treturn next;\n\t\t\t})\n\t};\n}\n\n/**\n * Join: open subpaths are joined nearest-endpoints-first into one open subpath (endpoints\n * within `tolerance` merge into one anchor, otherwise a straight segment connects them). A\n * single open subpath is closed instead (its ends merged when they meet). Closed subpaths and\n * lone anchors pass through unchanged, before the joined one.\n */\nexport function joinSubpaths(subpaths: readonly VectorSubpath[], tolerance: number): VectorSubpath[] {\n\tconst closed: VectorSubpath[] = [];\n\tlet open: VectorSubpath[] = [];\n\tfor (const sp of subpaths) {\n\t\tconst copy = { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed };\n\t\tif (sp.closed || sp.anchors.length < 2) {\n\t\t\tif (sp.anchors.length) closed.push(copy);\n\t\t} else open.push(copy);\n\t}\n\tif (open.length === 1) {\n\t\tconst sp = open[0];\n\t\tconst first = sp.anchors[0];\n\t\tconst last = sp.anchors[sp.anchors.length - 1];\n\t\tif (sp.anchors.length > 2 && dist(first, last) <= tolerance) {\n\t\t\tsp.anchors.pop();\n\t\t\tsp.anchors[0] = tidy({ ...first, in: last.in ?? null }, false);\n\t\t} else {\n\t\t\tsp.anchors[0] = tidy({ ...first, in: null }, false);\n\t\t\tsp.anchors[sp.anchors.length - 1] = tidy({ ...sp.anchors[sp.anchors.length - 1], out: null }, false);\n\t\t}\n\t\tsp.closed = true;\n\t\treturn [...closed, sp];\n\t}\n\twhile (open.length > 1) {\n\t\t// The closest pair of endpoints on different subpaths.\n\t\tlet best = { d: Infinity, i: 0, j: 1, ei: true, ej: false };\n\t\tfor (let i = 0; i < open.length; i++) {\n\t\t\tfor (let j = i + 1; j < open.length; j++) {\n\t\t\t\tfor (const ei of [false, true]) {\n\t\t\t\t\tfor (const ej of [false, true]) {\n\t\t\t\t\t\tconst d = dist(endOf(open[i], ei), endOf(open[j], ej));\n\t\t\t\t\t\tif (d < best.d) best = { d, i, j, ei, ej };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tlet a = open[best.i];\n\t\tlet b = open[best.j];\n\t\topen = open.filter((_, k) => k !== best.j);\n\t\t// `a` ends at the meeting point, `b` starts there.\n\t\tif (!best.ei) a = reverse(a);\n\t\tif (best.ej) b = reverse(b);\n\t\tconst anchors = a.anchors;\n\t\tif (best.d <= tolerance) {\n\t\t\tconst [head, ...rest] = b.anchors;\n\t\t\tconst l = anchors[anchors.length - 1];\n\t\t\tanchors[anchors.length - 1] = tidy({ ...l, out: head.out ?? null }, false);\n\t\t\tanchors.push(...rest);\n\t\t} else {\n\t\t\tconst l = anchors[anchors.length - 1];\n\t\t\tanchors[anchors.length - 1] = tidy({ ...l, out: null }, false);\n\t\t\tanchors.push(tidy({ ...b.anchors[0], in: null }, false), ...b.anchors.slice(1));\n\t\t}\n\t\topen[best.i > best.j ? best.i - 1 : best.i] = { anchors, closed: false };\n\t}\n\treturn [...closed, ...open];\n}\n\n// ---------------------------------------------------------------------------\n// Smooth and Remove Redundant Points\n// ---------------------------------------------------------------------------\n\n/**\n * Smooth: pulls handles towards Catmull-Rom-like tangents (a third of the way to each\n * neighbour along the line through both). `amount` 0–1 (0 = unchanged, 1 = fully smooth).\n * Only the anchors in `only` change when it is given; endpoints of open subpaths keep their\n * handles. Subpaths of fewer than three anchors are unchanged.\n */\nexport function smoothSubpath(sp: VectorSubpath, amount = 1, only?: ReadonlySet<number>): VectorSubpath {\n\tconst t = Math.min(1, Math.max(0, amount));\n\tconst orig = sp.anchors;\n\tconst n = orig.length;\n\tconst anchors = orig.map(cloneAnchor);\n\tif (n < 3 || t === 0) return { anchors, closed: sp.closed };\n\tfor (let i = 0; i < n; i++) {\n\t\tif (!sp.closed && (i === 0 || i === n - 1)) continue;\n\t\tif (only && !only.has(i)) continue;\n\t\tconst prev = orig[(i + n - 1) % n];\n\t\tconst next = orig[(i + 1) % n];\n\t\tconst p = orig[i];\n\t\tconst dir = unit(next.x - prev.x, next.y - prev.y);\n\t\tif (!dir) continue;\n\t\tconst li = dist(p, prev) / 3;\n\t\tconst lo = dist(p, next) / 3;\n\t\tconst hin = p.in ?? p;\n\t\tconst hout = p.out ?? p;\n\t\tconst target = (h: Vec2, x: number, y: number) => ({ x: h.x + (x - h.x) * t, y: h.y + (y - h.y) * t });\n\t\tanchors[i] = tidy(\n\t\t\t{\n\t\t\t\tx: p.x,\n\t\t\t\ty: p.y,\n\t\t\t\tin: target(hin, p.x - dir[0] * li, p.y - dir[1] * li),\n\t\t\t\tout: target(hout, p.x + dir[0] * lo, p.y + dir[1] * lo)\n\t\t\t},\n\t\t\tfalse\n\t\t);\n\t}\n\treturn { anchors, closed: sp.closed };\n}\n\ninterface Seg {\n\tc: Point[];\n\tline: boolean;\n}\n\nfunction segsOf(sp: VectorSubpath): Seg[] {\n\tconst out: Seg[] = [];\n\tfor (let i = 0; i < subpathSegmentCount(sp); i++) {\n\t\tconst s = subpathSegment(sp, i);\n\t\tout.push(s.length === 2 ? { c: s, line: true } : { c: s, line: false });\n\t}\n\treturn out;\n}\n\n/** A subpath from consecutive segments (anchor types inferred; original types kept where they still hold). */\nfunction segsToSubpath(segs: readonly Seg[], closed: boolean, types: Map<string, VectorAnchor['type']>): VectorSubpath | null {\n\tif (!segs.length) return null;\n\tconst anchors: VectorAnchor[] = [];\n\tconst at = (p: Point): VectorAnchor => ({ x: p[0], y: p[1] });\n\tanchors.push(at(segs[0].c[0]));\n\tfor (const s of segs) {\n\t\tconst end = s.c[s.c.length - 1];\n\t\tif (!s.line) anchors[anchors.length - 1].out = { x: s.c[1][0], y: s.c[1][1] };\n\t\tconst next = at(end);\n\t\tif (!s.line) next.in = { x: s.c[2][0], y: s.c[2][1] };\n\t\tanchors.push(next);\n\t}\n\tif (closed) {\n\t\tconst last = anchors.pop()!;\n\t\tif (last.in) anchors[0].in = last.in;\n\t}\n\tconst typed = anchors.map((a) => {\n\t\tconst type = types.get(`${a.x},${a.y}`);\n\t\treturn tidy(type ? { ...a, type } : a, !!type);\n\t});\n\treturn { anchors: typed, closed };\n}\n\nconst isZeroSeg = (s: Seg) => s.c.every((p) => Math.abs(p[0] - s.c[0][0]) < 1e-9 && Math.abs(p[1] - s.c[0][1]) < 1e-9);\n\n/** One segment replacing `a` then `b` within `tol`, or null. */\nfunction mergePair(a: Seg, b: Seg, tol: number): Seg | null {\n\tconst a0 = a.c[0];\n\tconst a3 = a.c[a.c.length - 1];\n\tconst b3 = b.c[b.c.length - 1];\n\tif (a.line && b.line) {\n\t\tconst cx = b3[0] - a0[0];\n\t\tconst cy = b3[1] - a0[1];\n\t\tconst len = Math.hypot(cx, cy);\n\t\tif (len < 1e-12) return null;\n\t\tconst mx = a3[0] - a0[0];\n\t\tconst my = a3[1] - a0[1];\n\t\tconst along = (mx * cx + my * cy) / len;\n\t\treturn Math.abs((mx * cy - my * cx) / len) <= tol && along >= 0 && along <= len ? { c: [a0, b3], line: true } : null;\n\t}\n\tconst ca = a.line ? [a0, a0, a3, a3] : a.c;\n\tconst cb = b.line ? [a3, a3, b3, b3] : b.c;\n\tconst ta = endTangent(ca);\n\tconst tb = startTangent(cb);\n\t// Only merge across (near-)smooth joints.\n\tif (ta[0] * tb[0] + ta[1] * tb[1] < 0.99) return null;\n\tconst pts: Point[] = [];\n\tsample(ca, 16, pts, true);\n\tsample(cb, 16, pts, false);\n\tconst { curve, error } = fitAcross(pts, 16, startTangent(ca), endTangent(cb));\n\treturn error <= tol ? { c: [curve.p0, curve.p1, curve.p2, curve.p3], line: false } : null;\n}\n\n/**\n * Remove Redundant Points: drops anchors that can go without changing the shape by more than\n * `tolerance` (collinear corners, duplicate points, and smooth joints that one cubic can\n * replace).\n */\nexport function removeRedundantPoints(sp: VectorSubpath, tolerance: number): VectorSubpath {\n\tconst tol = Math.max(tolerance, 1e-9);\n\tconst types = new Map(sp.anchors.filter((a) => a.type).map((a) => [`${a.x},${a.y}`, a.type] as const));\n\tconst segs = segsOf(sp).filter((s) => !isZeroSeg(s));\n\tif (!segs.length) return { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed };\n\tconst minSegs = sp.closed ? 2 : 1;\n\tlet changed = true;\n\twhile (changed && segs.length > minSegs) {\n\t\tchanged = false;\n\t\tconst n = segs.length;\n\t\tconst joints = sp.closed ? n : n - 1;\n\t\tfor (let j = 0; j < joints; j++) {\n\t\t\tconst [i0, i1] = sp.closed ? [(j + n - 1) % n, j] : [j, j + 1];\n\t\t\tconst m = mergePair(segs[i0], segs[i1], tol);\n\t\t\tif (!m) continue;\n\t\t\tsegs[i0] = m;\n\t\t\tsegs.splice(i1, 1);\n\t\t\t// The merged segment now starts the loop.\n\t\t\tif (sp.closed && i1 === 0) segs.unshift(segs.pop()!);\n\t\t\tchanged = true;\n\t\t\tbreak;\n\t\t}\n\t}\n\treturn segsToSubpath(segs, sp.closed, types) ?? { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed };\n}\n","/**\n * Pencil helpers: lazy-mouse smoothing (a pulled string between the pointer and the pen) and\n * ink-to-shape recognition (a hand-drawn stroke that is a line, rectangle, ellipse or polygon).\n *\n * Portions ported from (MIT OR Apache-2.0, © ArtCraft Team and contributors; see\n * THIRD_PARTY_NOTICES.md):\n * - photocraft-main/crates/paint/src/dynamics.rs (`Smoother`: pulled-string mode, `string`,\n * `finish` with catch-up on stroke end)\n * - wordcraft-main/crates/engine/src/cmd/ink_shape.rs (`recognise`, `ellipse_rms`,\n * `snap_square`, `corners`, `turning`, `rdp`, `upright`)\n */\nimport type { Point } from '../canvas/types.js';\n\n// ---------------------------------------------------------------------------\n// Lazy mouse\n// ---------------------------------------------------------------------------\n\n/**\n * Pulled-string smoothing: the pen stays put until the pointer is more than `length` away,\n * then follows it at that distance, so small wobbles never reach the stroke. Feed pointer\n * positions to `push`; it returns the pen positions to draw through (possibly none).\n */\nexport class LazyMouse {\n\tprivate pos: Point | null = null;\n\tprivate last: Point | null = null;\n\n\t/** `length` is the string length in the units of the points (0 turns smoothing off). */\n\tconstructor(readonly length: number) {}\n\n\tpush(p: Point): Point[] {\n\t\tthis.last = p;\n\t\tif (!(this.length > 0)) return [p];\n\t\tconst pos = this.pos;\n\t\tif (!pos) {\n\t\t\tthis.pos = p;\n\t\t\treturn [p];\n\t\t}\n\t\tconst dx = p[0] - pos[0];\n\t\tconst dy = p[1] - pos[1];\n\t\tconst d = Math.hypot(dx, dy);\n\t\tif (d <= this.length) return [];\n\t\tconst k = (d - this.length) / d;\n\t\tconst next: Point = [pos[0] + dx * k, pos[1] + dy * k];\n\t\tthis.pos = next;\n\t\treturn [next];\n\t}\n\n\t/** The string from the pen to the pointer while they are apart, else null. */\n\tstring(): [Point, Point] | null {\n\t\tconst { pos, last } = this;\n\t\tif (!(this.length > 0) || !pos || !last || (pos[0] === last[0] && pos[1] === last[1])) return null;\n\t\treturn [pos, last];\n\t}\n\n\t/** End of stroke: the pen catches up with the pointer (the point to add, if it lagged). */\n\tfinish(): Point[] {\n\t\tconst { pos, last } = this;\n\t\tif (!(this.length > 0) || !pos || !last || (pos[0] === last[0] && pos[1] === last[1])) return [];\n\t\tthis.pos = last;\n\t\treturn [last];\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Ink to shape\n// ---------------------------------------------------------------------------\n\n/** A shape recognised in a stroke, in the stroke's coordinates. */\nexport type VectorInkShape =\n\t| { kind: 'line'; a: Point; b: Point }\n\t| { kind: 'rect'; x: number; y: number; width: number; height: number }\n\t| { kind: 'ellipse'; x: number; y: number; width: number; height: number }\n\t| { kind: 'polygon'; points: Point[] };\n\nexport interface RecognizeInkOptions {\n\t/** Strokes whose longer side is smaller than this stay ink (default 6). */\n\tminSize?: number;\n}\n\n/** Most corners a recognised polygon may have. */\nexport const VECTOR_INK_MAX_CORNERS = 8;\nconst MAX_POINTS = 100_000;\n\n/** \"line\", \"square\", \"rectangle\", \"circle\", \"ellipse\", \"triangle\" or \"polygon\". */\nexport function vectorInkShapeName(shape: VectorInkShape): string {\n\tswitch (shape.kind) {\n\t\tcase 'line':\n\t\t\treturn 'line';\n\t\tcase 'rect':\n\t\t\treturn shape.width === shape.height ? 'square' : 'rectangle';\n\t\tcase 'ellipse':\n\t\t\treturn shape.width === shape.height ? 'circle' : 'ellipse';\n\t\tcase 'polygon':\n\t\t\treturn shape.points.length === 3 ? 'triangle' : 'polygon';\n\t}\n}\n\nconst dist = (a: Point, b: Point) => Math.hypot(b[0] - a[0], b[1] - a[1]);\n\nfunction bbox(pts: readonly Point[]): [number, number, number, number] {\n\tif (!pts.length) return [0, 0, 0, 0];\n\tlet [x0, y0] = pts[0];\n\tlet [x1, y1] = pts[0];\n\tfor (const [x, y] of pts) {\n\t\tx0 = Math.min(x0, x);\n\t\ty0 = Math.min(y0, y);\n\t\tx1 = Math.max(x1, x);\n\t\ty1 = Math.max(y1, y);\n\t}\n\treturn [x0, y0, x1 - x0, y1 - y0];\n}\n\nfunction segDistance(a: Point, b: Point, p: Point): number {\n\tconst dx = b[0] - a[0];\n\tconst dy = b[1] - a[1];\n\tconst len2 = dx * dx + dy * dy;\n\tconst t = len2 > 0 ? Math.min(1, Math.max(0, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / len2)) : 0;\n\treturn dist([a[0] + t * dx, a[1] + t * dy], p);\n}\n\nfunction polygonDistance(c: readonly Point[], p: Point): number {\n\tlet best = Infinity;\n\tfor (let i = 0; i < c.length; i++) best = Math.min(best, segDistance(c[i], c[(i + 1) % c.length], p));\n\treturn best;\n}\n\n/** RMS of each point's normalised radius minus one on the ellipse inscribed in the box (0 on it). */\nfunction ellipseRms(p: readonly Point[], x0: number, y0: number, w: number, h: number): number {\n\tconst rx = w / 2;\n\tconst ry = h / 2;\n\tif (rx <= 0 || ry <= 0 || !p.length) return Infinity;\n\tconst cx = x0 + rx;\n\tconst cy = y0 + ry;\n\tlet sum = 0;\n\tfor (const [x, y] of p) sum += (Math.hypot((x - cx) / rx, (y - cy) / ry) - 1) ** 2;\n\treturn Math.sqrt(sum / p.length);\n}\n\n/** A box drawn nearly square made square (same centre). */\nfunction snapSquare(x: number, y: number, w: number, h: number): [number, number, number, number] {\n\tif (Math.abs(w - h) <= 0.12 * Math.max(w, h)) {\n\t\tconst s = (w + h) / 2;\n\t\treturn [x + (w - s) / 2, y + (h - s) / 2, s, s];\n\t}\n\treturn [x, y, w, h];\n}\n\n/** How far the path turns at `b` coming from `a` towards `c`, in degrees (0 = straight on). */\nfunction turning(a: Point, b: Point, c: Point): number {\n\tconst u = [b[0] - a[0], b[1] - a[1]];\n\tconst v = [c[0] - b[0], c[1] - b[1]];\n\tconst lu = Math.hypot(u[0], u[1]);\n\tconst lv = Math.hypot(v[0], v[1]);\n\tif (lu <= 0 || lv <= 0) return 0;\n\treturn (Math.acos(Math.min(1, Math.max(-1, (u[0] * v[0] + u[1] * v[1]) / (lu * lv)))) * 180) / Math.PI;\n}\n\n/** Ramer–Douglas–Peucker reduction of an open polyline (iterative), keeping both ends. */\nfunction rdp(p: readonly Point[], eps: number): Point[] {\n\tconst n = p.length;\n\tif (n < 3) return p.slice();\n\tconst keep = new Array<boolean>(n).fill(false);\n\tkeep[0] = keep[n - 1] = true;\n\tconst stack: [number, number][] = [[0, n - 1]];\n\twhile (stack.length) {\n\t\tconst [a, b] = stack.pop()!;\n\t\tlet far = a;\n\t\tlet farD = 0;\n\t\tfor (let i = a + 1; i < b; i++) {\n\t\t\tconst d = segDistance(p[a], p[b], p[i]);\n\t\t\tif (d > farD) {\n\t\t\t\tfarD = d;\n\t\t\t\tfar = i;\n\t\t\t}\n\t\t}\n\t\tif (farD > eps) {\n\t\t\tkeep[far] = true;\n\t\t\tstack.push([a, far], [far, b]);\n\t\t}\n\t}\n\treturn p.filter((_, i) => keep[i]);\n}\n\n/** Corners of the closed path through `p`: RDP split at the point farthest from the start, then flat and crowded corners merged away. */\nfunction corners(p: readonly Point[], diag: number): Point[] {\n\tconst eps = 0.05 * diag;\n\tconst start = p[0];\n\tlet far = 0;\n\tlet farD = 0;\n\tp.forEach((q, i) => {\n\t\tconst d = dist(start, q);\n\t\tif (d > farD) {\n\t\t\tfarD = d;\n\t\t\tfar = i;\n\t\t}\n\t});\n\tconst a = p.slice(0, far);\n\tconst b = p.slice(far);\n\tconst ring = rdp([...a, ...b.slice(0, 1)], eps);\n\tconst back = rdp([...b, start], eps);\n\t// The halves share the far point and the start.\n\tring.pop();\n\tback.pop();\n\tring.push(...back);\n\twhile (ring.length > 3) {\n\t\tconst n = ring.length;\n\t\tconst turn = (i: number) => turning(ring[(i + n - 1) % n], ring[i], ring[(i + 1) % n]);\n\t\tlet flat = 0;\n\t\tlet flatT = Infinity;\n\t\tfor (let i = 0; i < n; i++) {\n\t\t\tconst t = turn(i);\n\t\t\tif (t < flatT) {\n\t\t\t\tflatT = t;\n\t\t\t\tflat = i;\n\t\t\t}\n\t\t}\n\t\tlet short = -1;\n\t\tfor (let j = 0; j < n; j++) {\n\t\t\tif (dist(ring[j], ring[(j + 1) % n]) < 0.1 * diag) {\n\t\t\t\tshort = j;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (flatT < 30) ring.splice(flat, 1);\n\t\telse if (short >= 0) {\n\t\t\t// Of the two crowded corners keep the sharper.\n\t\t\tconst k = turn(short) < turn((short + 1) % n) ? short : (short + 1) % n;\n\t\t\tring.splice(k, 1);\n\t\t} else break;\n\t}\n\treturn ring;\n}\n\n/** A quadrilateral whose sides run (nearly) across and down, alternately. */\nfunction upright(c: readonly Point[]): boolean {\n\tconst n = c.length;\n\tconst dirs = c.map((a, i) => {\n\t\tconst b = c[(i + 1) % n];\n\t\tconst deg = ((((Math.atan2(b[1] - a[1], b[0] - a[0]) * 180) / Math.PI) % 180) + 180) % 180;\n\t\tif (deg <= 15 || deg >= 165) return 'across';\n\t\tif (deg >= 75 && deg <= 105) return 'down';\n\t\treturn null;\n\t});\n\treturn dirs.every((d) => d !== null) && dirs.every((d, i) => d !== dirs[(i + 1) % n]);\n}\n\n/**\n * The shape a hand-drawn stroke draws, if it is one: an open stroke is a line when it stays\n * close to the chord between its ends; a closed one is an ellipse when its points sit on the\n * ellipse inscribed in their bounds, else a polygon of 3–8 corners when that polygon stays\n * close to every point (an upright four-cornered one is a rectangle). Nearly square boxes\n * become squares and circles. Anything else (scribbles, curves, letters) is null.\n */\nexport function recognizeInkShape(points: readonly Point[], options: RecognizeInkOptions = {}): VectorInkShape | null {\n\tconst minSize = options.minSize ?? 6;\n\tconst p: Point[] = [];\n\tfor (const q of points.slice(0, MAX_POINTS)) {\n\t\tif (!Number.isFinite(q[0]) || !Number.isFinite(q[1]) || Math.abs(q[0]) >= 1e9 || Math.abs(q[1]) >= 1e9) continue;\n\t\tconst last = p[p.length - 1];\n\t\tif (!last || last[0] !== q[0] || last[1] !== q[1]) p.push([q[0], q[1]]);\n\t}\n\tif (p.length < 2) return null;\n\tconst first = p[0];\n\tconst last = p[p.length - 1];\n\tconst [x0, y0, w, h] = bbox(p);\n\tconst size = Math.max(w, h);\n\tconst diag = Math.hypot(w, h);\n\tif (size < minSize) return null;\n\tlet length = 0;\n\tfor (let i = 1; i < p.length; i++) length += dist(p[i - 1], p[i]);\n\tconst gap = dist(first, last);\n\t// A line: the stroke barely leaves the chord between its ends and doesn't double back.\n\tif (gap > 0.85 * length && gap >= minSize) {\n\t\tconst off = Math.max(...p.map((q) => segDistance(first, last, q)));\n\t\tif (off <= Math.max(0.06 * gap, (1.5 * minSize) / 6)) return { kind: 'line', a: first, b: last };\n\t}\n\t// Other shapes are closed: the stroke ends near where it began and goes around.\n\tif (gap > 0.2 * size || length < 2 * size) return null;\n\tconst rms = ellipseRms(p, x0, y0, w, h);\n\tconst roundish = w > 0.2 * size && h > 0.2 * size;\n\tconst ellipse = (): VectorInkShape => {\n\t\tconst [x, y, sw, sh] = snapSquare(x0, y0, w, h);\n\t\treturn { kind: 'ellipse', x, y, width: sw, height: sh };\n\t};\n\tif (roundish && rms < 0.06) return ellipse();\n\tconst c = corners(p, diag);\n\tconst fits = c.length >= 3 && p.every((q) => polygonDistance(c, q) <= 0.08 * diag);\n\tif (fits && c.length === 4 && upright(c)) {\n\t\tconst [bx, by, bw, bh] = bbox(c);\n\t\tconst [x, y, sw, sh] = snapSquare(bx, by, bw, bh);\n\t\treturn { kind: 'rect', x, y, width: sw, height: sh };\n\t}\n\tif (fits && c.length <= VECTOR_INK_MAX_CORNERS) return { kind: 'polygon', points: c };\n\t// A wobbly round stroke is still an ellipse.\n\treturn roundish && rms < 0.1 ? ellipse() : null;\n}\n","/**\n * Pathfinder operands, the Shape Builder and fill regions over a vector document.\n *\n * Everything works in document space: each path is mapped through its own transform and its\n * parents', with live corners cut (`subpathsToBezier`), so rotated nodes and nodes inside\n * rotated groups take part as drawn. The controller maps results back into the space of the node\n * that receives them.\n *\n * - **Pathfinder operands**: a filled path is its fill area (every subpath closed); an unfilled\n * path with a stroke is the area its stroke paints (alignment, caps, joins and arrowheads\n * included) and results from it are filled with the stroke's paint; for Divide an unfilled open\n * path is a cutting line instead.\n * - **Shape Builder**: the planar map of the selected paths (`PlanarMap`). Closed subpaths bound\n * faces whatever their paint (a stroke-only shape's inside is a face), filled open subpaths that\n * enclose an area are closed by an invisible edge, and unfilled open paths cut the faces they\n * cross; their pieces between junctions can be erased. Merging faces unites them along the\n * map's own edges and takes the area away from every shape covering them. Erasing (Alt) also\n * takes the edges of the other outlines, cut where paths meet: a closed subpath opens there,\n * an open one splits, and an edge bounding a face deleted in the same gesture goes with it.\n * - **Fill regions** (paint bucket): the planar map of every visible outline; the region around a\n * point is traced from a ray (`PlanarMap.regionAt`).\n *\n * The Shape Builder follows vectorcraft-main/crates/tools/src/builder.rs (`BuilderMap`, `hit`,\n * `sides`, `line_between`, `erased_edges`, `Hit::Edge`, `shape_builder_shapes`, `sample_polyline`)\n * and crates/engine/src/cmd/buildcmds.rs (`sb_merge`), with figcraft-main/crates/engine/src/shapebuilder.rs\n * (strokes as their outlined area). MIT OR Apache-2.0, © ArtCraft Team and contributors. See\n * THIRD_PARTY_NOTICES.md.\n */\nimport type { Point } from '../canvas/types.js';\nimport {\n\tbezierPathBounds,\n\tcontourSignedArea,\n\tcloseContour,\n\tdistanceToBezierPath,\n\tpathToContours,\n\tsegmentPoint,\n\tsegmentTangent,\n\tcontoursToPath,\n\ttype BezierPath,\n\ttype BezierSegment\n} from '../canvas/pathops/bezier.js';\nimport { PlanarMap, cutOutPiece, type PlanarChain } from '../canvas/pathops/planar.js';\nimport type { PathfinderOperation, PathfinderShape } from '../canvas/pathops/pathfinder.js';\nimport { hasPaint, multiplyMatrices, nodeMatrix, subpathsToBezier, vectorPathWorldBezier, vectorStrokeOutline, IDENTITY_MATRIX, type AffineMatrix } from './geometry.js';\nimport { vectorMaskedGeometry } from './mask.js';\nimport { expandVectorAppearance } from './appearance.js';\nimport type { Vec2, VectorDocument, VectorNode, VectorPaint, VectorPathNode, VectorSubpath } from './types.js';\n\n/** A path node with the matrix of its parent space (parent → document). */\nexport interface VectorPathEntry {\n\tnode: VectorPathNode;\n\tparent: AffineMatrix;\n}\n\n/**\n * The path nodes inside `roots` (or every node when `roots` is null), bottom to top, with their\n * parent matrices. Hidden nodes are skipped, and locked ones unless `locked` is true. Live nodes\n * (path effects, live booleans) take part as drawn (`expandVectorAppearance`: their entries are\n * the expanded paths, whose ids may not be in the document) unless `expand` is false.\n */\nexport function vectorPathEntries(doc: VectorDocument, roots: readonly string[] | null, options: { locked?: boolean; expand?: boolean } = {}): VectorPathEntry[] {\n\tconst out: VectorPathEntry[] = [];\n\tconst visit = (stored: VectorNode, parent: AffineMatrix, inside: boolean) => {\n\t\tif (stored.visible === false || (stored.locked && !options.locked)) return;\n\t\t// As drawn: effects and live booleans evaluated, mask groups as their masked shapes.\n\t\tconst node = options.expand === false ? stored : expandVectorAppearance(vectorMaskedGeometry(stored));\n\t\tconst picked = inside || roots === null || roots.includes(node.id);\n\t\tif (node.type === 'group') {\n\t\t\tconst m = multiplyMatrices(nodeMatrix(node), parent);\n\t\t\tfor (const c of node.children) visit(c, m, picked);\n\t\t} else if (picked && node.type === 'path') out.push({ node, parent });\n\t};\n\tfor (const n of doc.nodes) visit(n, IDENTITY_MATRIX, false);\n\treturn out;\n}\n\nconst usable = (sp: VectorSubpath) => sp.anchors.length > 1;\n\n/** The entry's outline in document space with the given subpaths. */\nfunction worldOf(entry: VectorPathEntry, subpaths: VectorSubpath[]): BezierPath {\n\treturn vectorPathWorldBezier({ ...entry.node, subpaths }, entry.parent);\n}\n\n/** Whether a subpath, closed, encloses an area (rather than retracing itself). */\nexport function subpathEnclosesArea(sp: VectorSubpath): boolean {\n\tconst contour = pathToContours(subpathsToBezier([{ ...sp, closed: true }]))[0];\n\tif (!contour) return false;\n\tconst segs = closeContour(contour, true);\n\tconst b = bezierPathBounds(contoursToPath([{ segments: segs, closed: true }]));\n\tif (!b) return false;\n\tconst w = b.maxX - b.minX;\n\tconst h = b.maxY - b.minY;\n\treturn Math.abs(contourSignedArea(segs)) > 1e-9 * (w * w + h * h);\n}\n\n/** How an operand takes part in a Pathfinder operation: its fill, its stroke's area, a cutting line or its bare outline. */\nexport type VectorPathfinderPaint = 'fill' | 'stroke' | 'line' | 'outline';\n\nconst paintKey = (paint: VectorPaint | undefined) => (typeof paint === 'string' ? paint.trim().toLowerCase() : JSON.stringify(paint ?? null));\n\n/** The Pathfinder shape of a path (document space), and what it stands for. */\nexport function vectorPathfinderShape(entry: VectorPathEntry, op: PathfinderOperation): PathfinderShape & { paint: VectorPathfinderPaint } {\n\tconst { node } = entry;\n\tconst subpaths = node.subpaths.filter(usable);\n\tconst closed = subpaths.map((s) => ({ ...s, closed: true }));\n\tconst fillRule = node.fillRule ?? 'nonzero';\n\tif (hasPaint(node.fill)) return { path: worldOf(entry, closed), fillRule, key: `fill:${paintKey(node.fill)}`, paint: 'fill' };\n\tconst open = subpaths.length > 0 && subpaths.every((s) => !s.closed);\n\tif (op === 'divide' && open) return { path: worldOf(entry, subpaths), line: true, paint: 'line' };\n\tif (hasPaint(node.stroke) && (node.strokeWidth ?? 1) > 0) {\n\t\t// Node transforms are rigid, so the stroke laid out in document space keeps its width.\n\t\tconst path = vectorStrokeOutline({ ...node, subpaths }, entry.parent);\n\t\treturn { path, fillRule: 'nonzero', key: `fill:${paintKey(node.stroke)}`, paint: 'stroke' };\n\t}\n\treturn { path: worldOf(entry, closed), fillRule, key: 'none', paint: 'outline' };\n}\n\nconst STROKE_KEYS = [\n\t'stroke',\n\t'strokeWidth',\n\t'strokeLinecap',\n\t'strokeLinejoin',\n\t'strokeDasharray',\n\t'strokeAlign',\n\t'startArrow',\n\t'endArrow',\n\t'arrowAlign',\n\t'startArrowScale',\n\t'endArrowScale',\n\t'strokeWidthProfile'\n] as const;\n\n/**\n * Style of a Pathfinder or Shape Builder result taken from `node`: the node's own style for a\n * filled area, the stroke's paint as a fill for a stroke's area, or (for Outline edges) a stroke\n * in the paint of the area the edge bounds. Never carries the node's id, transform or geometry.\n */\nexport function vectorResultStyle(node: VectorPathNode, paint: VectorPathfinderPaint, edge = false): Omit<VectorPathNode, 'id' | 'subpaths'> {\n\tconst { id: _id, subpaths: _s, transform: _t, effects: _e, booleanOperation: _b, ...rest } = node;\n\tconst base: Omit<VectorPathNode, 'id' | 'subpaths'> = { ...rest, fillRule: 'nonzero' };\n\tif (edge) {\n\t\tfor (const k of STROKE_KEYS) delete (base as Record<string, unknown>)[k];\n\t\tconst stroke = paint === 'stroke' || !hasPaint(node.fill) ? node.stroke : node.fill;\n\t\treturn { ...base, fill: null, stroke: stroke ?? '#000000', strokeWidth: node.strokeWidth ?? 1 };\n\t}\n\tif (paint === 'stroke') {\n\t\tfor (const k of STROKE_KEYS) delete (base as Record<string, unknown>)[k];\n\t\treturn { ...base, fill: node.stroke ?? null, stroke: null };\n\t}\n\treturn base;\n}\n\n// ---------------------------------------------------------------------------\n// Shape Builder\n// ---------------------------------------------------------------------------\n\n/**\n * A run of a path between the points where other paths meet it (or it ends): a piece of an\n * unfilled open path, or an edge of another path's outline.\n */\nexport interface VectorBuilderLinePiece {\n\tpath: BezierPath;\n\tsegments: BezierSegment[];\n\t/** A loop with no junction on it (the whole subpath). */\n\tclosed: boolean;\n\t/** Index of the entry it belongs to (the front-most where outlines coincide). */\n\tsource: number;\n\tbounds: { minX: number; minY: number; maxX: number; maxY: number };\n}\n\n/** The Shape Builder's planar map of a set of paths. */\nexport interface VectorShapeBuilderMap {\n\tentries: VectorPathEntry[];\n\tmap: PlanarMap;\n\t/** Entries with no closed subpath (cutting lines; their pieces can be erased). */\n\tlines: boolean[];\n\tpieces: VectorBuilderLinePiece[];\n\t/** The edges of the other entries' outlines, cut where paths meet (built on first use: erasing). */\n\treadonly edges: VectorBuilderLinePiece[];\n\t/** How far an edge may lie from the outline it came from (document units). */\n\ttolerance: number;\n}\n\n/** What a Shape Builder gesture touches at a point. */\nexport type VectorBuilderHit = { face: number } | { line: number } | { edge: number };\n\n/** The faces, line pieces and edges a gesture touched, in the order touched. */\nexport interface VectorBuilderTouched {\n\tfaces: number[];\n\tlines: number[];\n\tedges: number[];\n}\n\n/**\n * The Shape Builder map of `entries` (bottom to top). Filled open subpaths that enclose an area\n * are closed. Null when nothing can be mapped.\n */\nexport function vectorShapeBuilderMap(entries: readonly VectorPathEntry[]): VectorShapeBuilderMap | null {\n\tconst lines: boolean[] = [];\n\tconst inputs = entries.map((entry) => {\n\t\tconst filled = hasPaint(entry.node.fill);\n\t\tconst subpaths = entry.node.subpaths.filter(usable).map((sp) => (!sp.closed && filled && subpathEnclosesArea(sp) ? { ...sp, closed: true } : sp));\n\t\tlines.push(!subpaths.some((s) => s.closed));\n\t\treturn { path: worldOf(entry, subpaths), fillRule: entry.node.fillRule ?? 'nonzero' };\n\t});\n\tconst map = PlanarMap.build(inputs);\n\tif (!map) return null;\n\tlet chains: PlanarChain[] | null = null;\n\tconst runs = (line: boolean) =>\n\t\t(chains ??= map.chains())\n\t\t\t.filter((c) => lines[c.source] === line && !(line && c.closed))\n\t\t\t.map((c) => {\n\t\t\t\tconst path = contoursToPath([{ segments: c.segments, closed: c.closed }]);\n\t\t\t\treturn { path, segments: c.segments, closed: c.closed, source: c.source, bounds: bezierPathBounds(path)! };\n\t\t\t});\n\tconst pieces = lines.some(Boolean) ? runs(true) : [];\n\tlet edges: VectorBuilderLinePiece[] | null = null;\n\tlet size = 0;\n\tfor (const input of inputs) {\n\t\tconst b = bezierPathBounds(input.path);\n\t\tif (b) size = Math.max(size, b.maxX - b.minX, b.maxY - b.minY);\n\t}\n\treturn {\n\t\tentries: [...entries],\n\t\tmap,\n\t\tlines,\n\t\tpieces,\n\t\tget edges() {\n\t\t\treturn (edges ??= runs(false));\n\t\t},\n\t\ttolerance: 0.2 * Math.max(size / 2000, 0.25)\n\t};\n}\n\n/** The run of `pieces` nearest `p` within `tol`, with its distance. */\nfunction pieceNear(pieces: readonly VectorBuilderLinePiece[], p: Vec2, tol: number): { index: number; d: number } | null {\n\tconst point: Point = [p.x, p.y];\n\tlet best: { index: number; d: number } | null = null;\n\tpieces.forEach((piece, index) => {\n\t\tconst b = piece.bounds;\n\t\tif (p.x < b.minX - tol || p.x > b.maxX + tol || p.y < b.minY - tol || p.y > b.maxY + tol) return;\n\t\tconst d = distanceToBezierPath(piece.path, point);\n\t\tif (d <= tol && (!best || d < best.d)) best = { index, d };\n\t});\n\treturn best;\n}\n\n/**\n * The face under `p`, or with `lineTolerance` (erasing) first the nearer of the line piece and\n * the outline edge that close.\n */\nexport function vectorShapeBuilderHit(m: VectorShapeBuilderMap, p: Vec2, lineTolerance: number | null = null): VectorBuilderHit | null {\n\tif (lineTolerance !== null && lineTolerance > 0) {\n\t\tconst line = pieceNear(m.pieces, p, lineTolerance);\n\t\tconst edge = pieceNear(m.edges, p, lineTolerance);\n\t\tif (edge && (!line || edge.d < line.d)) return { edge: edge.index };\n\t\tif (line) return { line: line.index };\n\t}\n\tconst face = m.map.faceAt([p.x, p.y]);\n\treturn face >= 0 ? { face } : null;\n}\n\n/** Points along a polyline no more than `step` apart (the pointer's path through faces). */\nexport function samplePolyline(points: readonly Vec2[], step: number): Vec2[] {\n\tconst out: Vec2[] = [];\n\tconst s = Math.max(step, 1e-3);\n\tpoints.forEach((p, i) => {\n\t\tif (i > 0) {\n\t\t\tconst a = points[i - 1];\n\t\t\tconst n = Math.min(4000, Math.ceil(Math.hypot(p.x - a.x, p.y - a.y) / s));\n\t\t\tfor (let k = 1; k < n; k++) out.push({ x: a.x + ((p.x - a.x) * k) / n, y: a.y + ((p.y - a.y) * k) / n });\n\t\t}\n\t\tout.push(p);\n\t});\n\treturn out;\n}\n\n/** Adds what a point hits to `touched` (each face or piece once). */\nexport function addBuilderHit(touched: VectorBuilderTouched, hit: VectorBuilderHit | null): boolean {\n\tif (!hit) return false;\n\tconst [list, i] = 'face' in hit ? [touched.faces, hit.face] : 'line' in hit ? [touched.lines, hit.line] : [touched.edges, hit.edge];\n\tif (list.includes(i)) return false;\n\tlist.push(i);\n\treturn true;\n}\n\n/** The faces just either side of the middle of a line piece (-1 outside every face). */\nfunction sides(m: VectorShapeBuilderMap, piece: VectorBuilderLinePiece): [number, number] {\n\tconst seg = piece.segments[Math.floor(piece.segments.length / 2)];\n\tconst mid = segmentPoint(seg, 0.5);\n\tconst t = segmentTangent(seg, 0.5);\n\tconst chord = Math.hypot(seg[seg.length - 1][0] - seg[0][0], seg[seg.length - 1][1] - seg[0][1]);\n\tconst off = Math.max(1e-6, Math.min(0.05, 0.05 * chord));\n\treturn [m.map.faceAt([mid[0] - t[1] * off, mid[1] + t[0] * off]), m.map.faceAt([mid[0] + t[1] * off, mid[1] - t[0] * off])];\n}\n\n/**\n * The edges a gesture deletes: those it touched that bound none of the faces it touched (an edge\n * of a deleted or merged face goes with the face, so an erase drag that crosses an edge into a\n * face deletes the face).\n */\nexport function vectorBuilderErasedEdges(m: VectorShapeBuilderMap, touched: VectorBuilderTouched): number[] {\n\tif (!touched.edges.length) return [];\n\tconst faces = new Set(touched.faces);\n\treturn touched.edges.filter((k) => m.edges[k] && !sides(m, m.edges[k]).some((f) => f >= 0 && faces.has(f)));\n}\n\n/**\n * What becomes of one entry: unchanged (absent), removed (`null`), or new outlines (document\n * space). `fill`: what is left of its area, filled non-zero (subpaths an erased edge opened stay\n * open); `outline`: its own outline with erased edges cut out, its fill rule kept; `pieces`: the\n * pieces of a cutting line that stay, each its own path.\n */\nexport type VectorBuilderReplacement = { kind: 'fill'; path: BezierPath } | { kind: 'outline'; path: BezierPath } | { kind: 'pieces'; paths: BezierPath[] } | null;\n\nexport interface VectorShapeBuilderPlan {\n\treplace: Map<number, VectorBuilderReplacement>;\n\t/** The merged shape (document space), styled like entry `style`; `currentFill` when only outlines enclosed it. */\n\tmerged: { path: BezierPath; style: number; currentFill: boolean } | null;\n}\n\n/**\n * What a gesture does: merging (or with `erase`, deleting) the touched faces takes them away\n * from every shape covering them; line pieces touched (erasing) or lying between touched faces\n * go; erased edges (`vectorBuilderErasedEdges`) are cut out of every outline they lie on;\n * merging adds the union of the faces as a new shape styled like the top-most shape over the\n * first face touched (or, for a face only outlines enclose, the top-most shape with the current\n * fill).\n */\nexport function planShapeBuilder(m: VectorShapeBuilderMap, touched: VectorBuilderTouched, erase: boolean): VectorShapeBuilderPlan {\n\tconst replace = new Map<number, VectorBuilderReplacement>();\n\tconst picked = new Set(touched.faces);\n\tconst faces = m.map.faces;\n\tconst sourcesOf = faces.map((_, i) => m.map.faceSources(i));\n\tconst cuts = erase ? vectorBuilderErasedEdges(m, touched).map((k) => m.edges[k]) : [];\n\tm.entries.forEach((entry, i) => {\n\t\tif (m.lines[i]) {\n\t\t\tconst own = m.pieces.flatMap((piece, k) => (piece.source === i ? [k] : []));\n\t\t\tconst keep = own.filter((k) => !touched.lines.includes(k) && !sides(m, m.pieces[k]).every((f) => f >= 0 && picked.has(f)));\n\t\t\tif (keep.length === own.length) return;\n\t\t\treplace.set(i, keep.length ? { kind: 'pieces', paths: keep.map((k) => m.pieces[k].path) } : null);\n\t\t\treturn;\n\t\t}\n\t\tconst inUnion = touched.faces.some((f) => sourcesOf[f].includes(i));\n\t\tif (!inUnion && !cuts.length) return;\n\t\tlet rest: BezierPath;\n\t\tif (inUnion) {\n\t\t\tconst kept = faces.flatMap((_, f) => (sourcesOf[f].includes(i) && !picked.has(f) ? [f] : []));\n\t\t\trest = kept.length ? m.map.merge(kept) : [];\n\t\t} else rest = worldOf(entry, entry.node.subpaths.filter(usable));\n\t\t// Each erased edge comes off every outline it lies on (shared edges from each shape).\n\t\tconst fillCloses = hasPaint(entry.node.fill);\n\t\tlet cut = false;\n\t\tfor (const piece of cuts) {\n\t\t\tconst r = rest.length ? cutOutPiece(rest, piece, fillCloses, m.tolerance) : null;\n\t\t\tif (r) {\n\t\t\t\trest = r;\n\t\t\t\tcut = true;\n\t\t\t}\n\t\t}\n\t\tif (!inUnion && !cut) return;\n\t\treplace.set(i, rest.length ? { kind: inUnion ? 'fill' : 'outline', path: rest } : null);\n\t});\n\tlet merged: VectorShapeBuilderPlan['merged'] = null;\n\tif (!erase && touched.faces.length) {\n\t\tconst path = m.map.merge(touched.faces);\n\t\tconst covered = touched.faces.map((f) => sourcesOf[f]).find((s) => s.length);\n\t\tconst lastShape = m.lines.lastIndexOf(false);\n\t\tconst style = covered ? covered[covered.length - 1] : lastShape >= 0 ? lastShape : m.entries.length - 1;\n\t\tif (path.length) merged = { path, style, currentFill: !covered };\n\t}\n\treturn { replace, merged };\n}\n\n// ---------------------------------------------------------------------------\n// Fill regions\n// ---------------------------------------------------------------------------\n\n/**\n * The planar map of every visible path's outline as drawn (locked ones included), for the paint\n * bucket. `gap` closes gaps up to that distance between loose ends.\n */\nexport function vectorFillMap(doc: VectorDocument, gap = 0): PlanarMap | null {\n\tconst entries = vectorPathEntries(doc, null, { locked: true });\n\treturn PlanarMap.build(\n\t\tentries.map((e) => ({ path: worldOf(e, e.node.subpaths.filter(usable)), fills: false })),\n\t\t{ gap, maxSegments: 20000 }\n\t);\n}\n","import { HistoryManager } from '../history/HistoryManager.js';\nimport type { Command } from '../history/types.js';\nimport { fitCubics } from '../canvas/curveFit.js';\nimport { pathBoolean } from '../canvas/pathops/boolean.js';\nimport { pathfinder as runPathfinder } from '../canvas/pathops/pathfinder.js';\nimport type { PlanarMap } from '../canvas/pathops/planar.js';\nimport { bezierPathBounds, bezierPathToSvg, transformBezierPath, type BezierPath } from '../canvas/pathops/bezier.js';\nimport type { Point } from '../canvas/types.js';\nimport {\n\tbezierToSubpaths,\n\tboundsFromPoints,\n\tboundsIntersect,\n\tcloneVectorDocument,\n\tcloneVectorNode,\n\tcollectPathNodes,\n\tcollectPaintedNodes,\n\tconvertAnchor,\n\tellipseSubpath,\n\tfindVectorNode,\n\thitTestVectorNode,\n\thitTestVectorNodes,\n\tinsertAnchor,\n\tlineSubpath,\n\tlocateVectorNode,\n\tmoveAnchorHandle,\n\tnearestSegment,\n\thasPaint,\n\tnodeMatrix,\n\tinvertMatrix,\n\tapplyMatrix,\n\tapplyMatrixLinear,\n\tmultiplyMatrices,\n\tmatrixRotation,\n\tvectorWorldMatrix,\n\tvectorParentMatrix,\n\tvectorPathWorldBezier,\n\tIDENTITY_MATRIX,\n\tpolygonSubpath,\n\trectSubpath,\n\tremoveAnchors,\n\treverseSubpath,\n\trotateMatrix,\n\tscaleMatrix,\n\tstarSubpath,\n\tsubpathsToBezier,\n\ttransformVectorNode,\n\ttranslateAnchor,\n\ttranslateMatrix,\n\tunionBounds,\n\tvectorNodeBounds,\n\twalkVectorNodes,\n\ttype AffineMatrix\n} from './geometry.js';\nimport {\n\tcornerBisectorPoint,\n\tcornerRadiusChange,\n\tfitCornerRadius,\n\tnextVectorCornerKind,\n\tsetAnchorCorner,\n\tsetSubpathCorners,\n\tvectorCorners,\n\ttype VectorCornerInfo\n} from './corners.js';\nimport { strokeWidthProfileOf, vectorStrokeOutlinePath, vectorStrokeParts } from './stroke.js';\nimport { nearestStrokePosition, strokePositionPoint } from './varstroke.js';\nimport {\n\tgenerateWidthProfile,\n\tinsertWidthPoint,\n\tpressureWidthProfile,\n\tremoveWidthPoints,\n\treverseWidthProfile,\n\tsanitizeWidthProfile,\n\tsetWidthPoint,\n\twidthProfileAt,\n\twidthProfileMaxScale,\n\twidthProfilePreset,\n\ttype VectorTaperOptions,\n\ttype VectorWaveOptions,\n\ttype VectorWidthPreset,\n\tVECTOR_WIDTH_PROFILE_MAX_POINTS,\n\tVECTOR_WIDTH_SAME_T\n} from './width.js';\nimport { parseVectorSvg, vectorDocumentToSvg, vectorPathLocalBounds, type VectorSvgExportOptions } from './svg.js';\nimport {\n\tapplyVectorGradientPatch,\n\tdefaultVectorGradient,\n\tinsertVectorGradientStop,\n\tisVectorGradient,\n\tsanitizeVectorGradient,\n\tmoveVectorGradientHandle,\n\tmoveVectorGradientStop,\n\tremoveVectorGradientStop,\n\tsetVectorGradientVector,\n\tsortedVectorGradientStops,\n\tvectorGradientMidpointFromPosition,\n\ttype VectorGradientHandle,\n\ttype VectorGradientPatch\n} from './gradient.js';\nimport {\n\tgetVectorGradientAnnotator,\n\thitVectorGradientAnnotator,\n\tvectorGradientBarPosition,\n\tVECTOR_GRADIENT_OFF_BAR,\n\tVECTOR_GRADIENT_STOP_GAP,\n\ttype VectorGradientAnnotator,\n\ttype VectorGradientPart,\n\ttype VectorGradientTarget\n} from './gradientTool.js';\nimport {\n\texpandVectorBlend,\n\tisVectorBlendGroup,\n\trefreshVectorBlends,\n\treleaseVectorBlend,\n\tvectorBlendKeyTargets\n} from './blendGroup.js';\nimport {\n\tcreateVectorEffect,\n\texpandVectorAppearance,\n\thasOwnVectorAppearance,\n\thasVectorAppearance,\n\tisVectorLiveBoolean,\n\tnormalizeVectorEffectParams,\n\tvectorAppearanceOutline,\n\tvectorEnteredGroups,\n\tvectorEnvelopeFrame,\n\tvectorNodeEnvelope,\n\tvectorInstanceDrawn,\n\ttype VectorEnvelopeParams,\n\tVECTOR_EFFECT_LABELS\n} from './appearance.js';\nimport { isIdentityMatrix } from '../geom/affine.js';\nimport { vectorNodesNear } from './spatial.js';\nimport { isVectorMaskActive, isVectorMaskGroup, isVectorMaskType, vectorMaskedGeometry } from './mask.js';\nimport {\n\tisVectorConnector,\n\trerouteVectorConnectors,\n\trouteVectorConnector,\n\tvectorConnectionSites,\n\tvectorConnectorTargetAt,\n\ttype VectorConnectionSite\n} from './connectors.js';\nimport { VECTOR_CONNECTOR_ROUTES, VECTOR_CONNECTOR_SIDES, type VectorArrowhead, type VectorConnector, type VectorConnectorEnd, type VectorConnectorRoute, type VectorConnectorSide } from './types.js';\nimport { vectorBoundsInside, isVectorFrame, vectorFrameDocument, type VectorFrameGroup } from './frames.js';\nimport { VECTOR_HORIZONTAL_RESIZING, VECTOR_VERTICAL_RESIZING, type VectorFrame, type VectorHorizontalResizing, type VectorResizing, type VectorVerticalResizing } from './types.js';\nimport type { TextShaper } from '../text/types.js';\nimport { resolveVectorSymbols, stripVectorSymbolArt, vectorSymbolUses } from './symbols.js';\nimport {\n\taddVectorConstraint,\n\tfindVectorAnchorById,\n\tpurgeVectorConstraints,\n\tsolveVectorConstraints,\n\tvectorConstraintDof,\n\tvectorConstraintEnv,\n\tVectorConstraintError,\n\tVECTOR_DIMENSIONAL_KINDS,\n\ttype VectorDofReport\n} from './constraints/solve.js';\nimport { autoConstrainVector, withVectorAnchorIds, type VectorInferOptions } from './constraints/auto.js';\nimport { checkVectorExpression, isValidVectorParameterName } from './constraints/expr.js';\nimport {\n\tDEFAULT_VECTOR_FONT,\n\tgetVectorTextShaper,\n\tlayoutVectorText,\n\tpathTextFrameFromBezier,\n\tpatchVectorCharacterStyle,\n\tpatchVectorParagraphStyle,\n\treplaceVectorText,\n\tsanitizeVectorTextFrame,\n\tsetVectorTextShaper,\n\tstyleVectorText,\n\tvectorTextCaret,\n\tvectorTextHasOutlines,\n\tvectorTextIndexAt,\n\tvectorTextName,\n\tvectorTextPaths,\n\tvectorTextPaintSource,\n\tvectorTextRange,\n\tVectorTextError,\n\ttype VectorTextLayout\n} from './text.js';\nimport { applyVectorHomography, vectorHomography, vectorHomographyKeepsQuad } from './perspective.js';\nimport { distortVectorNode, vectorBoxCorners, vectorDistortQuad, vectorShearForSide, vectorSkewMatrix, VECTOR_DISTORT_TOLERANCE, type VectorDistortMode } from './distort.js';\nimport {\n\tdragVectorMeshPoint,\n\tpullVectorMesh,\n\tresizeVectorMesh,\n\tvectorMeshEditPoints,\n\tvectorMeshFromOutline,\n\tvectorMeshOutline,\n\tvectorMeshParamAt\n} from './warp.js';\nimport { constrainToLeave, VECTOR_CONSTRUCTION_ANGLES, VECTOR_MAX_TRACKING_POINTS, VectorSnapTargets, vectorAngleDirection, vectorTrackingPaths, type VectorSnapGuides, type VectorSnapLeave, type VectorTrackingPoint } from './snap.js';\nimport {\n\tbendSegment as bendSubpathSegment,\n\tcurvatureMoveAnchor,\n\tcutSubpathAt,\n\tjoinSubpaths,\n\tremoveAnchorsKeepingShape,\n\tremoveRedundantPoints as removeRedundantSubpathPoints,\n\tsmoothSubpath,\n\tsplitSubpathAt\n} from './pathEdit.js';\nimport { eraseFromFill, eraseFromOutline, eraserRegion, knifePath, polylineBounds, simplifyPolyline } from './pathCut.js';\nimport { LazyMouse, recognizeInkShape, vectorInkShapeName, type VectorInkShape } from './ink.js';\nimport {\n\taddBuilderHit,\n\tplanShapeBuilder,\n\tsamplePolyline,\n\tvectorFillMap,\n\tvectorPathEntries,\n\tvectorPathfinderShape,\n\tvectorResultStyle,\n\tvectorBuilderErasedEdges,\n\tvectorShapeBuilderHit,\n\tvectorShapeBuilderMap,\n\ttype VectorBuilderTouched,\n\ttype VectorShapeBuilderMap\n} from './builder.js';\nimport { VECTOR_BLEND_MODES, type VectorBlendMode, type VectorFilter, type VectorFilterType, type VectorGradient, type VectorPaintLayer, type VectorStrokeLayer, type VectorWarpMesh } from './types.js';\nimport {\n\tisVectorBlendMode,\n\tnewVectorStrokeLayer,\n\tnormalizeVectorPaintStacks,\n\tVECTOR_STROKE_SETTING_KEYS,\n\tsanitizeVectorFills,\n\tsanitizeVectorStrokeSettings,\n\tsanitizeVectorStrokes,\n\tvectorFills,\n\tvectorPaintLayerNodes,\n\tvectorStrokes,\n\tvectorTargetPaint,\n\twithVectorFills,\n\twithVectorStrokes,\n\twithVectorTargetPaint\n} from './paintLayers.js';\nimport { createVectorFilter, isVectorFilterType, normalizeVectorFilterParams, VECTOR_FILTER_LABELS } from './filters.js';\nimport type {\n\tVec2,\n\tVectorEffect,\n\tVectorEffectType,\n\tVectorAlign,\n\tVectorAnchor,\n\tVectorAnchorRef,\n\tVectorAnchorType,\n\tVectorArrange,\n\tVectorAverageAxis,\n\tVectorBlend,\n\tVectorBlendOptions,\n\tVectorBooleanOperation,\n\tVectorBounds,\n\tVectorCornerKind,\n\tVectorDefaults,\n\tVectorDistribute,\n\tVectorDocument,\n\tVectorGroupNode,\n\tVectorHandleRef,\n\tVectorMask,\n\tVectorMaskType,\n\tVectorModifiers,\n\tVectorNode,\n\tVectorPathfinderOperation,\n\tVectorPathNode,\n\tVectorSegmentRef,\n\tVectorStyle,\n\tVectorSubpath,\n\tVectorTool,\n\tVectorTransformHandle,\n\tVectorViewport,\n\tVectorWidthPoint,\n\tVectorCharacterStyle,\n\tVectorParagraphStyle,\n\tVectorTextAlign,\n\tVectorTextFrame,\n\tVectorTextNode,\n\tVectorMatrix,\n\tVectorSymbol,\n\tVectorInstanceNode,\n\tVectorInstanceOverrides,\n\tVectorPaint,\n\tVectorConstraint,\n\tVectorConstraintKind,\n\tVectorGeomRef,\n\tVectorParameter\n} from './types.js';\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport interface VectorSnapOptions {\n\t/** Grid size in document units (0 = off, default 0). Points and boxes that snap to no object snap to it. */\n\tgrid: number;\n\t/**\n\t * Snap to other objects (default true): anchors, paths and their intersections, bounding-box\n\t * edges and centres, the artboard, construction angles, equal spacing and matching sizes.\n\t */\n\tobjects: boolean;\n\t/** Show smart guide lines and labels while snapping (default true). */\n\tguides: boolean;\n\t/** How near a target pulls, in screen pixels (default 5). */\n\tthreshold: number;\n\t/** Construction guide angles through the point a drawn point leaves from, in degrees (default 0, 45, 90, 135). Polar tracking: see `vectorPolarAngles`. */\n\tangles: readonly number[];\n\t/**\n\t * Object snap tracking (default true): resting the pointer on an anchor, centre or crossing\n\t * while drawing acquires it (again releases it); acquired points send alignment paths, an open\n\t * path's end its extension, and points snap where paths cross, cross the drawing or onto them.\n\t */\n\ttracking?: boolean;\n\t/** Acquired points send paths along every polar angle (`angles`) instead of only horizontal and vertical (default false). */\n\ttrackPolar?: boolean;\n\t/** The pen measures polar angles from its last segment's direction instead of the page's x axis (default false). */\n\tpolarRelative?: boolean;\n}\n\n/** Snap options new editors start with. Hold Cmd / Ctrl (`mod`) during a drag to bypass snapping. */\nexport const DEFAULT_VECTOR_SNAP: Readonly<VectorSnapOptions> = { grid: 0, objects: true, guides: true, threshold: 5, angles: VECTOR_CONSTRUCTION_ANGLES, tracking: true, trackPolar: false, polarRelative: false };\n\n/** How path-editing gestures and the pencil, knife and eraser behave. */\nexport interface VectorPathEditOptions {\n\t/**\n\t * Deleting anchors refits the neighbouring handles so the shape stays (default true). Off,\n\t * the neighbours are joined with their handles as they are.\n\t */\n\tkeepShapeOnDelete: boolean;\n\t/**\n\t * Dragging one anchor with the direct tool bends its two segments like the Curvature tool,\n\t * keeping the segments beyond still (default false). Alt held during the drag flips it.\n\t */\n\tcurvatureMoves: boolean;\n\t/** Pencil lazy-mouse smoothing, 0–1: the pulled string is up to 100 screen pixels long (default 0, off). */\n\tpencilSmoothing: number;\n\t/** Pencil strokes that look like a line, rectangle, ellipse or polygon become that shape (default false). */\n\trecognizeShapes: boolean;\n\t/** Eraser diameter in document units (default 10). */\n\teraserSize: number;\n\t/**\n\t * Record pointer pressure on pencil strokes as a width profile: `pen` (default) for pen input\n\t * only, `all` for any pointer that reports pressure, `off` never.\n\t */\n\tpencilPressure: 'pen' | 'all' | 'off';\n\t/**\n\t * Paint bucket: gaps up to this distance (document units) between loose ends of outlines are\n\t * closed when finding the region to fill (default 0, off).\n\t */\n\tfillGap: number;\n}\n\n/** The gap the toolbars' \"Close gaps\" toggle sets (document units). */\nexport const VECTOR_FILL_GAP = 6;\n\n/** Path-editing options new editors start with. */\nexport const DEFAULT_VECTOR_PATH_EDIT: Readonly<VectorPathEditOptions> = {\n\tkeepShapeOnDelete: true,\n\tcurvatureMoves: false,\n\tpencilSmoothing: 0,\n\trecognizeShapes: false,\n\teraserSize: 10,\n\tpencilPressure: 'pen',\n\tfillGap: 0\n};\n\n/** Longest pencil lazy-mouse string (at smoothing 1), in screen pixels. */\nexport const VECTOR_PENCIL_STRING_PX = 100;\n\nexport interface VectorEditorOptions {\n\tdocument?: VectorDocument;\n\t/** History to record into, e.g. one shared with the rest of the app (default: a new one). */\n\thistory?: HistoryManager;\n\t/** Starting tool (default `select`). */\n\ttool?: VectorTool;\n\t/** Style and settings for new shapes and paths. */\n\tdefaults?: VectorDefaults;\n\tviewport?: Partial<VectorViewport>;\n\tsnap?: Partial<VectorSnapOptions>;\n\tediting?: Partial<VectorPathEditOptions>;\n\t/** Pick distance for anchors, handles and outlines in screen pixels (default 6). */\n\thitRadius?: number;\n\t/** Id factory for new nodes. */\n\tcreateId?: () => string;\n\t/** Called after the document changes (edits, undo, redo, load). */\n\tonChange?: (document: VectorDocument) => void;\n\t/** Start in Free Distort mode (see `setFreeDistort`). */\n\tfreeDistort?: boolean;\n\t/**\n\t * The text shaper text nodes lay out with (see `setVectorTextShaper`; it is shared by every\n\t * editor on the page). Default: the browser's fonts, or estimated metrics outside a browser.\n\t */\n\ttextShaper?: TextShaper | null;\n\t/** Font and alignment new text gets (default 24 px sans-serif, left). */\n\ttextDefaults?: Partial<VectorTextDefaults>;\n}\n\n/** Font and alignment of new text. */\nexport interface VectorTextDefaults extends VectorCharacterStyle {\n\talign?: VectorTextAlign;\n}\n\n/** Text to add (`addText`): point text at (`x`, `y`), area text with `width` and `height`, or text on `path`. */\nexport interface VectorTextInput extends VectorStyle {\n\ttext: string;\n\t/** Point text: the first baseline's start; area text: the box's top left. */\n\tx?: number;\n\ty?: number;\n\t/** Area text: the box (both needed). */\n\twidth?: number;\n\theight?: number;\n\t/** Text on a path: SVG path data or a subpath (document units); `x`, `y`, `width` and `height` are then ignored. */\n\tpath?: string | VectorSubpath;\n\t/** Text on a path: margins from the path's start and end, side, orientation and baseline. */\n\tpathOptions?: Omit<Extract<VectorTextFrame, { kind: 'path' }>, 'kind' | 'path'>;\n\tfont?: Partial<VectorCharacterStyle>;\n\tparagraph?: VectorParagraphStyle;\n\tname?: string;\n}\n\n/** A constraint reference from loose input: an anchor by `subpath` / `index` (or its `id` as `anchor`), its handle, its segment, or a path as a circle. */\nexport interface VectorConstraintRefInput {\n\tnodeId: string;\n\tsubpath?: number;\n\tindex?: number;\n\tanchor?: string;\n\thandle?: 'in' | 'out';\n\tsegment?: boolean;\n\tcircle?: boolean;\n}\n\n/** A constraint to add (`addConstraint`). */\nexport interface VectorConstraintInput {\n\tkind: VectorConstraintKind;\n\trefs: VectorConstraintRefInput[];\n\tvalue?: string | number;\n\tname?: string;\n\taxis?: 'aligned' | 'horizontal' | 'vertical';\n}\n\n/** The text being edited on the canvas: the node and the selection in UTF-16 code units (`anchor` where it started, `focus` the caret). */\nexport interface VectorTextEditState {\n\tnodeId: string;\n\tanchor: number;\n\tfocus: number;\n}\n\n/** A pointer event as the controller needs it. Positions are in screen pixels relative to the canvas. */\nexport interface VectorPointerInput extends VectorModifiers {\n\t/** 0 = primary, 1 = middle (pans), 2 = secondary. Default 0. */\n\tbutton?: number;\n\t/** `PointerEvent.pressure` (0–1), recorded on pencil strokes. */\n\tpressure?: number;\n\t/** `PointerEvent.pointerType` (`mouse`, `pen`, `touch`). */\n\tpointerType?: string;\n}\n\n/** One width point of a path's stroke profile; `subpath` is the subpath it is shown on. */\nexport interface VectorWidthPointRef {\n\tnodeId: string;\n\tindex: number;\n\tsubpath: number;\n}\n\n/** A spot on a stroke (width tool): where a drag would add a width point. Positions in document units. */\nexport interface VectorStrokeSpot {\n\tnodeId: string;\n\tsubpath: number;\n\t/** Position along the subpath by length, 0–1. */\n\tt: number;\n\tpoint: Vec2;\n\t/** Ends of the stroke's width across the path there. */\n\tleft: Vec2;\n\tright: Vec2;\n}\n\n/**\n * The width point being edited numerically (double-click with the width tool): `index` is the\n * point's, or null for a new point at `t` (a double-click on a stroke away from its points).\n * `left` / `right` are side widths in document units (how far the stroke reaches on each side),\n * `point` is where it sits on the path, in document space.\n */\nexport interface VectorWidthPointEdit {\n\tnodeId: string;\n\tsubpath: number;\n\tindex: number | null;\n\tt: number;\n\tleft: number;\n\tright: number;\n\tpoint: Vec2;\n}\n\n/** A key press as the controller needs it. */\nexport interface VectorKeyInput extends VectorModifiers {\n\t/** `KeyboardEvent.key`. */\n\tkey: string;\n}\n\n/** What the pointer is over. */\nexport interface VectorHover {\n\tnodeId: string | null;\n\tanchor?: VectorAnchorRef;\n\thandle?: VectorHandleRef;\n\t/** A segment under the pointer (direct tool and scissors); `point` is in document space. */\n\tsegment?: { nodeId: string; subpath: number; segment: number; t: number; point: Vec2 };\n\ttransformHandle?: VectorTransformHandle;\n\t/** A live corner widget (direct tool): the corner's anchor. */\n\tcorner?: VectorAnchorRef;\n\t/** A width point (width tool): its centre or the end of its left / right side. */\n\twidthPoint?: VectorWidthPointRef & { part: 'center' | 'left' | 'right' };\n\t/** A spot on a stroke away from its width points (width tool). */\n\tstrokeSpot?: VectorStrokeSpot;\n\t/** A part of the gradient annotator (gradient tool). */\n\tgradient?: VectorGradientPart;\n\t/** A control point of the selected envelope's mesh (`index`), or a spot inside it (`index` null): Envelope tool. */\n\tenvelope?: { nodeId: string; index: number | null };\n}\n\n/** The drag in progress, for overlays and status text. */\nexport type VectorInteraction =\n\t| { type: 'marquee'; bounds: VectorBounds }\n\t| { type: 'move'; dx: number; dy: number }\n\t| { type: 'scale'; handle: VectorTransformHandle; sx: number; sy: number }\n\t| { type: 'rotate'; angle: number }\n\t| { type: 'anchors'; dx: number; dy: number }\n\t| { type: 'handle'; ref: VectorHandleRef }\n\t| { type: 'bend'; ref: VectorSegmentRef }\n\t| { type: 'corner'; ref: VectorAnchorRef; radius: number }\n\t| { type: 'draw'; tool: VectorTool; nodeId: string; bounds: VectorBounds; string?: [Vec2, Vec2] }\n\t| { type: 'stroke'; tool: 'knife' | 'eraser'; points: Vec2[]; width: number }\n\t| { type: 'builder'; mode: 'merge' | 'erase'; points: Vec2[] }\n\t| { type: 'width'; ref: VectorWidthPointRef; mode: 'width' | 'move' | 'copy'; width: number }\n\t| { type: 'pen'; nodeId: string }\n\t/** A gradient tool drag: drawing a new vector, or a handle, stop, midpoint or the bar (`removing`: a stop dragged off the bar). */\n\t| { type: 'gradient'; part: 'create' | VectorGradientPart['kind']; handle?: VectorGradientHandle; removing?: boolean }\n\t/** Free Distort: the selection box's corners as dragged so far (document space, clockwise from the top left). */\n\t| { type: 'distort'; mode: VectorDistortMode | 'skew'; corners: Vec2[] }\n\t/** Envelope tool: a mesh point (`index`) or the surface (`index` null) being dragged. */\n\t| { type: 'envelope'; nodeId: string; index: number | null }\n\t/** The Type tool dragging out an area text box (document space). */\n\t| { type: 'textBox'; bounds: VectorBounds }\n\t/** Selecting text by dragging over it. */\n\t| { type: 'textSelect'; nodeId: string }\n\t| { type: 'pan' };\n\n/** A snapshot of the editor. The same object is returned until something changes. */\nexport interface VectorEditorState {\n\tdocument: VectorDocument;\n\ttool: VectorTool;\n\t/** Selected node ids (top-level nodes or group members picked with the direct tool). */\n\tselection: readonly string[];\n\t/** Selected anchors (direct tool). */\n\tselectedAnchors: readonly VectorAnchorRef[];\n\t/** The selected width points (width tool, all on one path); Delete removes them. */\n\tselectedWidthPoints: readonly VectorWidthPointRef[];\n\t/** The width point open for numeric editing (double-click with the width tool), if any. */\n\twidthPointEdit: VectorWidthPointEdit | null;\n\t/** The path whose anchors are being edited, if any. */\n\teditingId: string | null;\n\t/** Axis-aligned bounds of the selected nodes in document units. */\n\tselectionBounds: VectorBounds | null;\n\t/** The selection box, rotated with the selection when its nodes share a rotation. */\n\tselectionFrame: VectorSelectionFrame | null;\n\tviewport: VectorViewport;\n\tdefaults: Readonly<Required<Pick<VectorDefaults, 'polygonSides' | 'starPoints' | 'starInnerRatio' | 'cornerRadius'>> & Pick<VectorDefaults, 'cornerKind'> & VectorStyle>;\n\tsnap: VectorSnapOptions;\n\tediting: VectorPathEditOptions;\n\t/** Smart guides of the snap in progress, in document units (null when there are none). */\n\tguides: VectorSnapGuides | null;\n\thitRadius: number;\n\thover: VectorHover | null;\n\t/** A CSS cursor for the canvas. */\n\tcursor: string;\n\tinteraction: VectorInteraction | null;\n\t/** The path the pen tool is adding to. */\n\tpen: { nodeId: string; subpath: number } | null;\n\t/** Last pointer position in document units (for the pen preview), or null when outside. */\n\tpointer: Vec2 | null;\n\t/** Shape Builder and paint bucket highlight: faces, line pieces and the gesture's trail. */\n\tbuilder: VectorBuilderPreview | null;\n\t/** The Blend tool's first click: the shape (and anchor of its first subpath) the next click blends from. */\n\tblendStart: VectorBlendStart | null;\n\t/** Options new blends get. */\n\tblendDefaults: VectorBlendOptions;\n\t/** The paint the gradient tool and gradient editor work on, and the selected stop (index into the sorted stops). */\n\t/** The paint the gradient tool and editor work on: `target`, and `layer` of its stack (null: the top layer, through the shorthand). */\n\tgradientEdit: { target: VectorGradientTarget; stop: number | null; layer: number | null };\n\t/**\n\t * Free Distort mode of the selection box: dragging a corner distorts in perspective (each corner\n\t * on its own; Shift+Alt+Mod: symmetric perspective), a side moves its two corners, and Mod-dragging\n\t * a side skews along it (Alt: about the centre).\n\t */\n\tfreeDistort: boolean;\n\t/** The text being edited on the canvas, or null. Typing goes to `textInput`, keys to `keyDown`. */\n\ttextEdit: VectorTextEditState | null;\n\t/** Font and alignment new text gets. */\n\ttextDefaults: VectorTextDefaults;\n\t/**\n\t * Isolation mode: `document` is then the art of symbol `symbolId`, in the symbol's own space.\n\t * `host` is the whole document with that art (draw it dimmed behind, through `hostTransform`,\n\t * which lines the instance it was entered from up with the art).\n\t */\n\tsymbolEdit: { symbolId: string; name: string; instanceId: string | null; host: VectorDocument; hostTransform: VectorMatrix | null } | null;\n\t/** Constraints the last solve could not satisfy (`conflicts`) or interpret (`skipped`), or null when all hold. */\n\tconstraintIssues: { conflicts: string[]; skipped: { id: string; message: string }[] } | null;\n\t/** Points acquired for object snap tracking (document units), oldest first. */\n\ttrackingPoints: readonly VectorTrackingPoint[];\n\t/** A distance or coordinate being typed while the pen draws (Enter places the point), or null. */\n\ttypedEntry: string | null;\n\tcanUndo: boolean;\n\tcanRedo: boolean;\n\tundoLabel: string;\n\tredoLabel: string;\n\t/** Bumped on every change. */\n\trevision: number;\n}\n\nexport type VectorActionResult = { ok: true } | { ok: false; message: string };\n\n/** What Make Clipping Mask / Make Opacity Mask return: the new group's id, or why not. */\nexport type VectorMaskResult = { ok: true; id: string } | { ok: false; message: string };\n\n/** A connector as `getConnectors` lists it. */\nexport interface VectorConnectorListing {\n\tid: string;\n\tname?: string;\n\troute: VectorConnectorRoute;\n\tradius?: number;\n\t/** Each end: the glued object, its side setting and the side in use, and where the end is (document units). */\n\tstart: { nodeId?: string; side?: VectorConnectorSide; resolvedSide?: string | null; x?: number; y?: number };\n\tend: { nodeId?: string; side?: VectorConnectorSide; resolvedSide?: string | null; x?: number; y?: number };\n}\n\n/** A frame as `getFrames` lists it. */\nexport interface VectorFrameListing {\n\tid: string;\n\tname: string;\n\t/** The group holding it (null for an artboard: a top-level frame). */\n\tparentId: string | null;\n\tartboard: boolean;\n\t/** Its rectangle in its own space, fill and clip. */\n\tframe: VectorFrame;\n\t/** Its rectangle in document space (axis-aligned). */\n\tbounds: VectorBounds | null;\n\t/** Its children, bottom to top, with their resizing constraints. */\n\tchildren: { id: string; type: VectorNode['type']; name?: string; horizontal: VectorHorizontalResizing; vertical: VectorVerticalResizing }[];\n}\n\n/** The shape the Blend tool blends from (its first click). */\nexport interface VectorBlendStart {\n\tnodeId: string;\n\t/** The clicked anchor of its first subpath, or null for the shape as a whole. */\n\tanchor: number | null;\n}\n\n/** Blend settings and options of `VectorEditorController.blend`. */\nexport interface VectorBlendInput extends VectorBlendOptions {\n\t/** Nodes to blend (default: the selection), in stacking order unless `keepOrder`. */\n\tids?: readonly string[];\n\t/** Blend in the order given instead of stacking order. */\n\tkeepOrder?: boolean;\n\t/** Start anchor of each key's first subpath, by key id. */\n\tstarts?: Record<string, number>;\n\tname?: string;\n}\n\nconst BLEND_OPTION_KEYS = ['spacing', 'steps', 'distance', 'orientToSpine', 'linear'] as const;\n\n/** The blend options in `input`, valid ones only. */\nfunction blendOptionsOf(input: Partial<VectorBlendOptions>): VectorBlendOptions {\n\tconst out: VectorBlendOptions = {};\n\tif (input.spacing === 'steps' || input.spacing === 'distance' || input.spacing === 'smoothColor') out.spacing = input.spacing;\n\tif (typeof input.steps === 'number' && Number.isFinite(input.steps)) out.steps = Math.min(1000, Math.max(1, Math.round(input.steps)));\n\tif (typeof input.distance === 'number' && Number.isFinite(input.distance) && input.distance > 0) out.distance = input.distance;\n\tif (typeof input.orientToSpine === 'boolean') out.orientToSpine = input.orientToSpine;\n\tif (typeof input.linear === 'boolean') out.linear = input.linear;\n\treturn out;\n}\n\n/**\n * What the Shape Builder or paint bucket highlights, in document units: the faces (filled paths\n * as SVG path data) under the pointer or touched by the drag, the line pieces an erase would\n * delete, and the drag's trail. `mode` is `merge`, `erase` (Alt) or `fill` (paint bucket).\n */\nexport interface VectorBuilderPreview {\n\tmode: 'merge' | 'erase' | 'fill';\n\tfaces: BezierPath[];\n\tlines: BezierPath[];\n\ttrail: Vec2[] | null;\n}\n\n/** Result of a Shape Builder gesture. */\nexport interface VectorShapeBuilderResult {\n\tok: boolean;\n\tmessage?: string;\n\t/** Id of the merged shape (none when erasing). */\n\tmerged: string | null;\n\tfaces: number;\n\tlines: number;\n\t/** Outline edges erased (cut out of the paths they lie on). */\n\tedges: number;\n}\n\n/** A face of the Shape Builder map (document units). */\nexport interface VectorShapeBuilderFace {\n\tindex: number;\n\t/** SVG path data. */\n\td: string;\n\tarea: number;\n\t/** Ids of the paths whose fill covers it, bottom to top (empty for an area only outlines enclose). */\n\tsources: string[];\n\t/** A point inside it. */\n\tpoint: Vec2 | null;\n}\n\nexport interface AddVectorNodeOptions {\n\t/** Group to add into (default: the root). */\n\tparentId?: string;\n\t/** Position in the parent's list (default: on top). */\n\tindex?: number;\n\t/** Select the new node (default true). */\n\tselect?: boolean;\n\t/** Undo step label. */\n\tlabel?: string;\n}\n\n/** A shape description for `addShape` (and the agent command of the same name). */\nexport interface VectorShapeInput extends VectorStyle {\n\tkind: 'rect' | 'ellipse' | 'polygon' | 'star' | 'line';\n\tx: number;\n\ty: number;\n\twidth: number;\n\theight: number;\n\t/** Live corner radius of a rectangle, polygon or star (default: the editor default). */\n\tcornerRadius?: number;\n\tcornerKind?: VectorCornerKind;\n\tsides?: number;\n\tpoints?: number;\n\tinnerRatio?: number;\n\tname?: string;\n}\n\n/** Every field of `VectorStyle`, in the order style panels and commands list them. */\nexport const VECTOR_STYLE_KEYS = [\n\t'fill',\n\t'stroke',\n\t'strokeWidth',\n\t'strokeLinecap',\n\t'strokeLinejoin',\n\t'strokeDasharray',\n\t'fillRule',\n\t'strokeAlign',\n\t'startArrow',\n\t'endArrow',\n\t'arrowAlign',\n\t'startArrowScale',\n\t'endArrowScale',\n\t'strokeWidthProfile',\n\t'fills',\n\t'strokes'\n] as const satisfies readonly (keyof VectorStyle)[];\n\n/** Clipboard payload type written by `copy()`. */\nexport const VECTOR_CLIPBOARD_TYPE = 'pixerate/vector';\n\nconst DEFAULT_DEFAULTS = {\n\tfill: '#d4d4d8',\n\tstroke: '#18181b',\n\tstrokeWidth: 2,\n\tpolygonSides: 6,\n\tstarPoints: 5,\n\tstarInnerRatio: 0.5,\n\tcornerRadius: 0\n};\n\nconst MIN_ZOOM = 0.05;\nconst MAX_ZOOM = 64;\n/** Screen distance of the rotation handle above the selection box. */\nexport const VECTOR_ROTATE_HANDLE_OFFSET = 24;\n\n// ---------------------------------------------------------------------------\n// Helpers shared with the render module\n// ---------------------------------------------------------------------------\n\nconst sameRef = (a: VectorAnchorRef, b: VectorAnchorRef) => a.nodeId === b.nodeId && a.subpath === b.subpath && a.index === b.index;\n\n/** Ids of the path nodes whose anchors are shown (the edited path plus selected paths). */\nexport function getEditablePathIds(state: Pick<VectorEditorState, 'document' | 'selection' | 'editingId' | 'tool' | 'pen'>): string[] {\n\tif (state.tool !== 'direct' && state.tool !== 'pen') return [];\n\tconst ids = new Set<string>();\n\tif (state.pen) ids.add(state.pen.nodeId);\n\tif (state.tool === 'direct') {\n\t\tif (state.editingId) ids.add(state.editingId);\n\t\tfor (const id of state.selection) {\n\t\t\tconst node = findVectorNode(state.document.nodes, id);\n\t\t\tif (node && !node.locked) for (const p of collectPathNodes(node)) if (!p.locked) ids.add(p.id);\n\t\t}\n\t}\n\treturn [...ids].filter((id) => findVectorNode(state.document.nodes, id)?.type === 'path');\n}\n\n/** Every anchor drawn on the canvas. */\nexport function getVisibleAnchors(state: Pick<VectorEditorState, 'document' | 'selection' | 'editingId' | 'tool' | 'pen'>): VectorAnchorRef[] {\n\tconst out: VectorAnchorRef[] = [];\n\tfor (const id of getEditablePathIds(state)) {\n\t\tconst node = findVectorNode(state.document.nodes, id) as VectorPathNode;\n\t\tnode.subpaths.forEach((sp, s) => sp.anchors.forEach((_, i) => out.push({ nodeId: id, subpath: s, index: i })));\n\t}\n\treturn out;\n}\n\n/**\n * Handles drawn on the canvas: those of selected anchors and of the segments next to them\n * (direct tool), or of the last anchor of the pen path.\n */\nexport function getVisibleHandles(\n\tstate: Pick<VectorEditorState, 'document' | 'selection' | 'editingId' | 'tool' | 'pen' | 'selectedAnchors'>\n): VectorHandleRef[] {\n\tconst out: VectorHandleRef[] = [];\n\tconst seen = new Set<string>();\n\tconst push = (node: VectorPathNode, s: number, i: number, handle: 'in' | 'out') => {\n\t\tconst sp = node.subpaths[s];\n\t\tconst a = sp?.anchors[i];\n\t\tif (!a || !a[handle]) return;\n\t\tconst key = `${node.id}:${s}:${i}:${handle}`;\n\t\tif (seen.has(key)) return;\n\t\tseen.add(key);\n\t\tout.push({ nodeId: node.id, subpath: s, index: i, handle });\n\t};\n\tif (state.tool === 'pen' && state.pen) {\n\t\tconst node = findVectorNode(state.document.nodes, state.pen.nodeId);\n\t\tconst sp = node?.type === 'path' ? node.subpaths[state.pen.subpath] : undefined;\n\t\tif (node?.type === 'path' && sp) {\n\t\t\tconst last = sp.anchors.length - 1;\n\t\t\tpush(node, state.pen.subpath, last, 'in');\n\t\t\tpush(node, state.pen.subpath, last, 'out');\n\t\t}\n\t\treturn out;\n\t}\n\tif (state.tool !== 'direct') return out;\n\tconst editable = new Set(getEditablePathIds(state));\n\tfor (const ref of state.selectedAnchors) {\n\t\tif (!editable.has(ref.nodeId)) continue;\n\t\tconst node = findVectorNode(state.document.nodes, ref.nodeId) as VectorPathNode;\n\t\tconst sp = node.subpaths[ref.subpath];\n\t\tif (!sp) continue;\n\t\tconst n = sp.anchors.length;\n\t\tpush(node, ref.subpath, ref.index, 'in');\n\t\tpush(node, ref.subpath, ref.index, 'out');\n\t\tconst prev = ref.index > 0 ? ref.index - 1 : sp.closed ? n - 1 : -1;\n\t\tconst next = ref.index < n - 1 ? ref.index + 1 : sp.closed ? 0 : -1;\n\t\tif (prev >= 0) push(node, ref.subpath, prev, 'out');\n\t\tif (next >= 0) push(node, ref.subpath, next, 'in');\n\t}\n\treturn out;\n}\n\n/** Live corner widgets sit at least this far inside their corner (screen px along each side of a right angle). */\nexport const VECTOR_CORNER_WIDGET_INSET = 10;\n\n/** A live corner widget of the direct tool: drag it to round the corners, Alt-click to change their kind. */\nexport interface VectorCornerWidget {\n\tref: VectorAnchorRef;\n\t/** Document position of the widget. */\n\tx: number;\n\ty: number;\n\t/** The radius stored on the anchor and the radius drawn (no larger than fits). */\n\tradius: number;\n\tdrawnRadius: number;\n\tkind: VectorCornerKind;\n\t/** The corner in its node's own space. */\n\tcorner: VectorCornerInfo;\n}\n\n/**\n * The live corner widgets the direct tool shows: on each editable path, at its selected corner\n * anchors (or at every corner when none of its anchors is selected). Corners whose sides are\n * shorter than three insets on screen hide theirs. Dragging one sets the radius of all of them.\n * Ported from ArtCraft vectorcraft `crates/tools/src/corners.rs` (MIT OR Apache-2.0, see\n * THIRD_PARTY_NOTICES.md), as is the `corner` drag below.\n */\nexport function getVectorCornerWidgets(\n\tstate: Pick<VectorEditorState, 'document' | 'selection' | 'editingId' | 'tool' | 'pen' | 'selectedAnchors' | 'viewport'>\n): VectorCornerWidget[] {\n\tif (state.tool !== 'direct' || state.pen) return [];\n\tconst zoom = state.viewport.zoom;\n\tconst out: VectorCornerWidget[] = [];\n\tfor (const id of getEditablePathIds(state)) {\n\t\tconst node = findVectorNode(state.document.nodes, id) as VectorPathNode;\n\t\tconst picked = state.selectedAnchors.filter((r) => r.nodeId === id);\n\t\tconst m = vectorWorldMatrix(state.document.nodes, id) ?? IDENTITY_MATRIX;\n\t\tfor (const c of vectorCorners(node.subpaths)) {\n\t\t\tif (picked.length && !picked.some((r) => r.subpath === c.subpath && r.index === c.index)) continue;\n\t\t\tconst side = Math.min(c.lu, c.lv);\n\t\t\tif (side * zoom < 3 * VECTOR_CORNER_WIDGET_INSET) continue;\n\t\t\tconst a = node.subpaths[c.subpath].anchors[c.index];\n\t\t\tconst radius = a.corner?.radius ?? 0;\n\t\t\tconst drawn = fitCornerRadius(c, radius);\n\t\t\t// On the bisector at the centre of the arc as drawn, at least the inset in and at most half the shorter side.\n\t\t\tconst k = Math.min(Math.max(drawn / c.sin, VECTOR_CORNER_WIDGET_INSET / (zoom * Math.sqrt(1 + c.cos))), side / 2);\n\t\t\tconst p = applyMatrix(m, cornerBisectorPoint(c, k));\n\t\t\tout.push({ ref: { nodeId: id, subpath: c.subpath, index: c.index }, x: p.x, y: p.y, radius, drawnRadius: drawn, kind: a.corner?.kind ?? 'round', corner: c });\n\t\t}\n\t}\n\treturn out;\n}\n\n/**\n * The line a path's stroke follows (node space; shortened under `tip` arrowheads) and the\n * distance a side scale of 1 reaches from it (half the drawn width: twice the stroke width for\n * inside / outside strokes). Null for paths without a visible stroke.\n */\nexport function vectorStrokeWidthGeometry(node: VectorPathNode): { line: BezierPath; unit: number } | null {\n\tif (!hasPaint(node.stroke) || !((node.strokeWidth ?? 1) > 0)) return null;\n\tconst parts = vectorStrokeParts(subpathsToBezier(node.subpaths), node);\n\treturn parts.line.length ? { line: parts.line, unit: parts.drawWidth / 2 } : null;\n}\n\n/** A width point of the width tool, in document units. */\nexport interface VectorWidthHandle {\n\tref: VectorWidthPointRef;\n\t/** The point on the path. */\n\tx: number;\n\ty: number;\n\t/** Ends of its left and right sides. */\n\tleft: Vec2;\n\tright: Vec2;\n\tt: number;\n\t/** Side scales (multiples of half the stroke width). */\n\tleftScale: number;\n\trightScale: number;\n\tselected: boolean;\n}\n\n/**\n * Moves the points at `indices` of `profile` by `dt` along the path (kept in 0–1) and re-sorts\n * it (stably, so steps keep their order); `renumber` maps each old index to its new one.\n */\nfunction shiftWidthPoints(profile: readonly VectorWidthPoint[], indices: readonly number[], dt: number): { points: VectorWidthPoint[]; renumber: Map<number, number> } {\n\tconst moving = new Set(indices);\n\tconst tagged = profile.map((p, i) => ({ p: moving.has(i) ? { ...p, t: Math.min(Math.max(p.t + dt, 0), 1) } : { ...p }, i }));\n\ttagged.sort((a, b) => a.p.t - b.p.t || a.i - b.i);\n\treturn { points: tagged.map((x) => x.p), renumber: new Map(tagged.map((x, k) => [x.i, k])) };\n}\n\n/**\n * The width points the width tool shows: every profile point on every subpath of the stroke\n * under the pointer, the selected paths and the stroke being edited. Ported from ArtCraft\n * vectorcraft `crates/tools/src/distort/width.rs` (MIT OR Apache-2.0, see\n * THIRD_PARTY_NOTICES.md), as are the `width` drag and hover below.\n */\nexport function getVectorWidthHandles(\n\tstate: Pick<VectorEditorState, 'document' | 'selection' | 'tool' | 'hover' | 'interaction' | 'selectedWidthPoints'>\n): VectorWidthHandle[] {\n\tif (state.tool !== 'width') return [];\n\tconst ids = new Set<string>();\n\tfor (const id of state.selection) {\n\t\tconst n = findVectorNode(state.document.nodes, id);\n\t\tif (n) for (const p of collectPathNodes(n)) ids.add(p.id);\n\t}\n\tif (state.hover?.nodeId) ids.add(state.hover.nodeId);\n\tif (state.interaction?.type === 'width') ids.add(state.interaction.ref.nodeId);\n\tfor (const r of state.selectedWidthPoints) ids.add(r.nodeId);\n\tconst out: VectorWidthHandle[] = [];\n\tfor (const id of ids) {\n\t\tconst node = findVectorNode(state.document.nodes, id);\n\t\tif (node?.type !== 'path' || node.visible === false || !node.strokeWidthProfile?.length) continue;\n\t\tconst g = vectorStrokeWidthGeometry(node);\n\t\tif (!g) continue;\n\t\tconst profile = sanitizeWidthProfile(node.strokeWidthProfile);\n\t\tconst m = vectorWorldMatrix(state.document.nodes, id) ?? IDENTITY_MATRIX;\n\t\tconst contours = node.subpaths.length;\n\t\tfor (let sp = 0; sp < contours; sp++) {\n\t\t\tprofile.forEach((w, index) => {\n\t\t\t\tconst at = strokePositionPoint(g.line, sp, w.t);\n\t\t\t\tif (!at) return;\n\t\t\t\tconst n = { x: at.tangent[1], y: -at.tangent[0] };\n\t\t\t\tconst c = { x: at.point[0], y: at.point[1] };\n\t\t\t\tconst p = applyMatrix(m, c);\n\t\t\t\tconst l = applyMatrix(m, { x: c.x + n.x * w.left * g.unit, y: c.y + n.y * w.left * g.unit });\n\t\t\t\tconst r = applyMatrix(m, { x: c.x - n.x * w.right * g.unit, y: c.y - n.y * w.right * g.unit });\n\t\t\t\tout.push({\n\t\t\t\t\tref: { nodeId: id, index, subpath: sp },\n\t\t\t\t\tx: p.x,\n\t\t\t\t\ty: p.y,\n\t\t\t\t\tleft: l,\n\t\t\t\t\tright: r,\n\t\t\t\t\tt: w.t,\n\t\t\t\t\tleftScale: w.left,\n\t\t\t\t\trightScale: w.right,\n\t\t\t\t\tselected: state.selectedWidthPoints.some((r) => r.nodeId === id && r.index === index)\n\t\t\t\t});\n\t\t\t});\n\t\t}\n\t}\n\treturn out;\n}\n\n/** Looks up an anchor (positions in its node's own space). */\nexport function getAnchor(doc: VectorDocument, ref: VectorAnchorRef): VectorAnchor | undefined {\n\tconst node = findVectorNode(doc.nodes, ref.nodeId);\n\treturn node?.type === 'path' ? node.subpaths[ref.subpath]?.anchors[ref.index] : undefined;\n}\n\n/** An anchor with its position and handles in document space (its node's transforms applied). */\nexport function getAnchorWorld(doc: VectorDocument, ref: VectorAnchorRef): { x: number; y: number; in?: Vec2; out?: Vec2; type?: VectorAnchorType } | undefined {\n\tconst a = getAnchor(doc, ref);\n\tif (!a) return undefined;\n\tconst m = vectorWorldMatrix(doc.nodes, ref.nodeId) ?? IDENTITY_MATRIX;\n\tconst p = applyMatrix(m, a);\n\treturn { x: p.x, y: p.y, ...(a.in ? { in: applyMatrix(m, a.in) } : {}), ...(a.out ? { out: applyMatrix(m, a.out) } : {}), ...(a.type ? { type: a.type } : {}) };\n}\n\n/**\n * The selection box: `angle` is the shared rotation of the selected nodes (0 when they\n * differ), `x / y / width / height` the bounds in the box's own frame (document space\n * rotated by −angle). A frame point `q` is at `rotateMatrix(angle)` applied to `q`.\n */\nexport interface VectorSelectionFrame {\n\tx: number;\n\ty: number;\n\twidth: number;\n\theight: number;\n\tangle: number;\n}\n\n/** The selection frame of nodes `ids` (see `VectorSelectionFrame`), or null when they have no points. */\nexport function getVectorSelectionFrame(doc: VectorDocument, ids: readonly string[]): VectorSelectionFrame | null {\n\tconst nodes = ids.map((id) => ({ node: findVectorNode(doc.nodes, id), parent: vectorParentMatrix(doc.nodes, id) ?? IDENTITY_MATRIX })).filter((n) => n.node);\n\tif (!nodes.length) return null;\n\tconst angles = nodes.map((n) => matrixRotation(multiplyMatrices(nodeMatrix(n.node!), n.parent)));\n\tconst same = angles.every((a) => Math.abs(normalizeAngle(a - angles[0])) < 1e-6);\n\tconst angle = same ? angles[0] : 0;\n\tconst unrotate = rotateMatrix(-angle);\n\tconst b = unionBounds(nodes.map((n) => vectorNodeBounds(n.node!, multiplyMatrices(n.parent, unrotate))));\n\treturn b ? { ...b, angle } : null;\n}\n\nconst normalizeAngle = (a: number) => ((((a + 180) % 360) + 360) % 360) - 180;\n\n/** Screen positions of the selection box handles for a box given in screen pixels. */\nexport function getTransformHandlePositions(box: VectorBounds): { handle: VectorTransformHandle; x: number; y: number }[] {\n\tconst { x, y, width: w, height: h } = box;\n\treturn [\n\t\t{ handle: 'nw', x, y },\n\t\t{ handle: 'n', x: x + w / 2, y },\n\t\t{ handle: 'ne', x: x + w, y },\n\t\t{ handle: 'e', x: x + w, y: y + h / 2 },\n\t\t{ handle: 'se', x: x + w, y: y + h },\n\t\t{ handle: 's', x: x + w / 2, y: y + h },\n\t\t{ handle: 'sw', x, y: y + h },\n\t\t{ handle: 'w', x, y: y + h / 2 },\n\t\t{ handle: 'rotate', x: x + w / 2, y: y - VECTOR_ROTATE_HANDLE_OFFSET }\n\t];\n}\n\n/**\n * Screen geometry of a selection box. `box` and each handle's `x / y` are the unrotated box\n * in screen pixels; draw them inside a group with `transform` (a `rotate(angle cx cy)`),\n * or use `screen` for the true on-screen positions (hit-testing).\n */\nexport interface VectorSelectionBoxGeometry {\n\tbox: VectorBounds;\n\tangle: number;\n\ttransform: string;\n\thandles: { handle: VectorTransformHandle; x: number; y: number; screen: Vec2 }[];\n\t/** The four corners on screen, clockwise from the top left. */\n\tcorners: Vec2[];\n}\n\nexport function getVectorSelectionBoxGeometry(frame: VectorSelectionFrame, viewport: VectorViewport): VectorSelectionBoxGeometry {\n\tconst z = viewport.zoom;\n\tconst box = { x: frame.x * z + viewport.x, y: frame.y * z + viewport.y, width: frame.width * z, height: frame.height * z };\n\tconst rot = rotateMatrix(frame.angle, { x: viewport.x, y: viewport.y });\n\tconst toScreen = (p: Vec2): Vec2 => (frame.angle ? applyMatrix(rot, p) : { x: p.x, y: p.y });\n\tconst handles = getTransformHandlePositions(box).map((h) => ({ ...h, screen: toScreen(h) }));\n\tconst corners = [\n\t\t{ x: box.x, y: box.y },\n\t\t{ x: box.x + box.width, y: box.y },\n\t\t{ x: box.x + box.width, y: box.y + box.height },\n\t\t{ x: box.x, y: box.y + box.height }\n\t].map(toScreen);\n\tconst r = (n: number) => Math.round(n * 1000) / 1000;\n\treturn { box, angle: frame.angle, transform: frame.angle ? `rotate(${r(frame.angle)} ${r(viewport.x)} ${r(viewport.y)})` : '', handles, corners };\n}\n\n/**\n * The node the Envelope tool edits: the one selected node with an enabled envelope, with the\n * envelope's mesh and box (node space) and the node → document matrix. Null otherwise.\n */\nexport function getVectorEnvelopeTarget(\n\tdoc: VectorDocument,\n\tselection: readonly string[]\n): { node: VectorNode; frame: { effectId: string; bounds: VectorBounds; mesh: VectorWarpMesh }; world: AffineMatrix } | null {\n\tif (selection.length !== 1) return null;\n\tconst node = findVectorNode(doc.nodes, selection[0]);\n\tif (!node || !vectorNodeEnvelope(node)) return null;\n\tconst frame = vectorEnvelopeFrame(node);\n\treturn frame ? { node, frame, world: vectorWorldMatrix(doc.nodes, node.id) ?? IDENTITY_MATRIX } : null;\n}\n\nconst RESIZE_CURSOR_ORDER = ['ew-resize', 'nwse-resize', 'ns-resize', 'nesw-resize'];\nconst HANDLE_ANGLES: Partial<Record<VectorTransformHandle, number>> = { e: 0, se: 45, s: 90, sw: 135, w: 180, nw: 225, n: 270, ne: 315 };\n\n/** The resize cursor for a handle of a box rotated by `angle` degrees. */\nexport function vectorResizeCursor(handle: VectorTransformHandle, angle = 0): string {\n\tif (handle === 'rotate') return 'grab';\n\tconst a = (((HANDLE_ANGLES[handle]! + angle) % 180) + 180) % 180;\n\treturn RESIZE_CURSOR_ORDER[Math.round(a / 45) % 4];\n}\n\n\nconst TOOL_LABELS: Record<VectorTool, string> = {\n\tselect: 'Select',\n\tdirect: 'Direct Select',\n\tpen: 'Pen',\n\tpencil: 'Pencil',\n\trect: 'Rectangle',\n\tellipse: 'Ellipse',\n\tpolygon: 'Polygon',\n\tstar: 'Star',\n\tline: 'Line',\n\tscissors: 'Scissors',\n\tknife: 'Knife',\n\teraser: 'Eraser',\n\twidth: 'Width',\n\tshapeBuilder: 'Shape Builder',\n\tpaintBucket: 'Paint Bucket',\n\tblend: 'Blend',\n\tgradient: 'Gradient',\n\tenvelope: 'Envelope Mesh',\n\ttype: 'Type',\n\ttypePath: 'Type on a Path',\n\tframe: 'Frame',\n\tconnector: 'Connector'\n};\n\nconst TOOL_KEYS: Record<string, VectorTool> = {\n\tv: 'select',\n\ta: 'direct',\n\tp: 'pen',\n\tn: 'pencil',\n\tr: 'rect',\n\to: 'ellipse',\n\ty: 'polygon',\n\ts: 'star',\n\tl: 'line',\n\tc: 'scissors',\n\tk: 'knife',\n\te: 'eraser',\n\tw: 'width',\n\tm: 'shapeBuilder',\n\tb: 'paintBucket',\n\tj: 'blend',\n\tg: 'gradient',\n\tu: 'envelope',\n\tt: 'type',\n\tq: 'typePath',\n\tf: 'frame',\n\tx: 'connector'\n};\n\nexport function vectorToolLabel(tool: VectorTool): string {\n\treturn TOOL_LABELS[tool];\n}\n\n/** Keyboard shortcut of each tool (single letters, no modifiers). */\nexport const VECTOR_TOOL_SHORTCUTS: Readonly<Record<VectorTool, string>> = {\n\tselect: 'V',\n\tdirect: 'A',\n\tpen: 'P',\n\tpencil: 'N',\n\trect: 'R',\n\tellipse: 'O',\n\tpolygon: 'Y',\n\tstar: 'S',\n\tline: 'L',\n\tscissors: 'C',\n\tknife: 'K',\n\teraser: 'E',\n\twidth: 'W',\n\tshapeBuilder: 'M',\n\tpaintBucket: 'B',\n\tblend: 'J',\n\tgradient: 'G',\n\tenvelope: 'U',\n\ttype: 'T',\n\ttypePath: 'Q',\n\tframe: 'F',\n\tconnector: 'X'\n};\n\n/** Short glyphs for tool buttons (pair them with `vectorToolLabel` for the accessible name). */\nexport const VECTOR_TOOL_GLYPHS: Readonly<Record<VectorTool, string>> = {\n\tselect: '↖',\n\tdirect: '⌖',\n\tpen: '✒',\n\tpencil: '✎',\n\trect: '▭',\n\tellipse: '◯',\n\tpolygon: '⬡',\n\tstar: '☆',\n\tline: '╱',\n\tscissors: '✂',\n\tknife: '⟋',\n\teraser: '⌫',\n\twidth: '⇹',\n\tshapeBuilder: '⊕',\n\tpaintBucket: '◍',\n\tblend: '⧓',\n\tgradient: '◐',\n\tenvelope: '⌗',\n\ttype: 'T',\n\ttypePath: '𝒯',\n\tframe: '⬚',\n\tconnector: '⤳'\n};\n\n/** Tools that drag out a shape (they snap while hovering). */\nconst SHAPE_TOOLS: ReadonlySet<VectorTool> = new Set(['rect', 'ellipse', 'polygon', 'star', 'line', 'frame']);\n\nconst BOOLEAN_OPS: Record<VectorBooleanOperation, 'union' | 'difference' | 'intersection' | 'xor'> = {\n\tunion: 'union',\n\tsubtract: 'difference',\n\tintersect: 'intersection',\n\texclude: 'xor'\n};\n\nconst BOOLEAN_LABELS: Record<VectorBooleanOperation, string> = { union: 'Union', subtract: 'Subtract', intersect: 'Intersect', exclude: 'Exclude' };\n\n/** Names of the Pathfinder operations (undo steps, groups and buttons). */\nexport const VECTOR_PATHFINDER_LABELS: Readonly<Record<VectorPathfinderOperation, string>> = {\n\tdivide: 'Divide',\n\ttrim: 'Trim',\n\tmerge: 'Merge',\n\tcrop: 'Crop',\n\toutline: 'Outline',\n\tminusBack: 'Minus Back'\n};\n\n/** Replaces nodes by id anywhere in the tree (structural sharing for untouched branches). */\n/**\n * Every path with its fill and stroke stacks reconciled (see `normalizeVectorPaintStacks`): the\n * same array, and the same node objects, where nothing changes.\n */\nfunction normalizeVectorTree(nodes: readonly VectorNode[], prefer: 'stack' | 'shorthand'): VectorNode[] {\n\tlet changed = false;\n\tconst out = nodes.map((n) => {\n\t\tlet next: VectorNode = n.type === 'path' || n.type === 'text' ? normalizeVectorPaintStacks(n, prefer) : n;\n\t\tif (next.type === 'group') {\n\t\t\tconst children = normalizeVectorTree(next.children, prefer);\n\t\t\tif (children !== next.children) next = { ...next, children };\n\t\t}\n\t\tif (next !== n) changed = true;\n\t\treturn next;\n\t});\n\treturn changed ? out : (nodes as VectorNode[]);\n}\n\n/** A document as loaded: a copy, stacks preferred over stale shorthands. */\nfunction loadedDocument(doc: VectorDocument): VectorDocument {\n\tconst copy = cloneVectorDocument(stripVectorSymbolArt(doc));\n\tconst nodes = normalizeVectorTree(copy.nodes, 'stack');\n\tconst symbols = copy.symbols?.map((s) => ({ ...s, nodes: normalizeVectorTree(s.nodes, 'stack') }));\n\treturn resolveVectorSymbols({ ...copy, nodes, ...(symbols ? { symbols } : {}) });\n}\n\nfunction mapTree(nodes: readonly VectorNode[], fn: (node: VectorNode) => VectorNode | null): VectorNode[] {\n\tlet changed = false;\n\tconst out: VectorNode[] = [];\n\tfor (const node of nodes) {\n\t\tlet next = fn(node);\n\t\tif (next && next.type === 'group') {\n\t\t\tconst children = mapTree(next.children, fn);\n\t\t\tif (children !== next.children) next = { ...next, children };\n\t\t}\n\t\tif (next !== node) changed = true;\n\t\tif (next) out.push(next);\n\t}\n\treturn changed ? out : (nodes as VectorNode[]);\n}\n\n/** Replaces node `id` anywhere in the tree with `replacements` (none removes it). */\nfunction spliceTree(nodes: readonly VectorNode[], id: string, replacements: readonly VectorNode[]): VectorNode[] {\n\tlet changed = false;\n\tconst out: VectorNode[] = [];\n\tfor (const node of nodes) {\n\t\tif (node.id === id) {\n\t\t\tchanged = true;\n\t\t\tout.push(...replacements);\n\t\t\tcontinue;\n\t\t}\n\t\tif (node.type === 'group') {\n\t\t\tconst children = spliceTree(node.children, id, replacements);\n\t\t\tif (children !== node.children) {\n\t\t\t\tchanged = true;\n\t\t\t\tout.push({ ...node, children });\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tout.push(node);\n\t}\n\treturn changed ? out : (nodes as VectorNode[]);\n}\n\nconst toPoint = (p: Vec2): Point => [p.x, p.y];\n\n/** A node's place and state (transform, visibility, lock, operand operation), for a node taking its place. */\nconst placementOf = (n: VectorNode) => ({\n\t...(n.transform ? { transform: n.transform } : {}),\n\t...(n.visible === false ? { visible: false } : {}),\n\t...(n.locked ? { locked: true } : {}),\n\t...(n.booleanOperation ? { booleanOperation: n.booleanOperation } : {})\n});\n\nconst isRoot = (m: AffineMatrix) => m === IDENTITY_MATRIX || (m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0);\n\nlet idCounter = 0;\nconst defaultCreateId = () => `vn-${Date.now().toString(36)}${(++idCounter).toString(36)}`;\n\n// ---------------------------------------------------------------------------\n// Controller\n// ---------------------------------------------------------------------------\n\ninterface Snapshot {\n\tdoc: VectorDocument;\n\tselection: string[];\n\tanchors: VectorAnchorRef[];\n\teditingId: string | null;\n\tpen: { nodeId: string; subpath: number } | null;\n\t/** The symbol being edited in isolation when the snapshot was taken (`doc` is then its art). */\n\tsymbolEdit: SymbolEditContext | null;\n}\n\n/** Isolation mode: `doc` holds a symbol's art; `host` is the document it belongs to. */\ninterface SymbolEditContext {\n\tsymbolId: string;\n\t/** The instance it was entered from (its placement lines the art up with the document), if any. */\n\tinstanceId: string | null;\n\thost: VectorDocument;\n\t/** Where the instance put the symbol's origin, and its scale (√|det|): the view follows them in and out. */\n\torigin: Vec2 | null;\n\tscale: number;\n}\n\n/** Snap targets of a drag, gathered on its first move that needs them. */\ntype DragTargets = { targets?: VectorSnapTargets };\n\ntype Drag =\n\t| ({ kind: 'move'; start: Vec2; origin: VectorBounds | null; before: Snapshot; base: VectorDocument; moved: boolean; duplicated: boolean; clickId: string | null } & DragTargets)\n\t| ({ kind: 'scale'; handle: VectorTransformHandle; start: Vec2; frame: VectorSelectionFrame; before: Snapshot; base: VectorDocument; op?: string } & DragTargets)\n\t| {\n\t\t\tkind: 'envelope';\n\t\t\tnodeId: string;\n\t\t\teffectId: string;\n\t\t\t/** The mesh at the start (node space) and the document it belongs to. */\n\t\t\tbase: VectorWarpMesh;\n\t\t\tbaseDoc: VectorDocument;\n\t\t\tbefore: Snapshot;\n\t\t\tindex: number | null;\n\t\t\ts: number;\n\t\t\tt: number;\n\t\t\tstart: Vec2;\n\t\t\tmoved: boolean;\n\t }\n\t| { kind: 'rotate'; start: Vec2; center: Vec2; before: Snapshot; base: VectorDocument }\n\t| { kind: 'marquee'; start: Vec2; base: string[]; baseAnchors: VectorAnchorRef[]; anchorsMode: boolean }\n\t| ({ kind: 'anchors'; start: Vec2; before: Snapshot; base: VectorDocument; moved: boolean; clickRef: VectorAnchorRef | null; lead: VectorAnchorRef | null } & DragTargets)\n\t| ({ kind: 'handle'; ref: VectorHandleRef; before: Snapshot; base: VectorDocument } & DragTargets)\n\t| { kind: 'bend'; ref: VectorSegmentRef; t: number; start: Vec2; before: Snapshot; base: VectorDocument; moved: boolean }\n\t| { kind: 'corner'; widget: VectorCornerWidget; refs: VectorAnchorRef[]; start: Vec2; before: Snapshot; base: VectorDocument; moved: boolean; alt: boolean; shift: boolean }\n\t| { kind: 'stroke'; tool: 'knife' | 'eraser'; points: Vec2[]; before: Snapshot }\n\t| { kind: 'builder'; erase: boolean; points: Vec2[]; touched: VectorBuilderTouched; map: VectorShapeBuilderMap }\n\t| { kind: 'pull'; ref: VectorAnchorRef; before: Snapshot; base: VectorDocument; label: string; outOnly: boolean; closing: boolean; altBreaks: boolean }\n\t| ({ kind: 'shape'; tool: VectorTool; start: Vec2; nodeId: string; before: Snapshot } & DragTargets)\n\t| { kind: 'connector'; nodeId: string; start: Vec2; before: Snapshot }\n\t| { kind: 'textBox'; start: Vec2; moved: boolean }\n\t| { kind: 'textSelect'; nodeId: string }\n\t| { kind: 'pencil'; nodeId: string; points: Point[]; before: Snapshot; lazy: LazyMouse; pressures: number[] | null; pressure: number }\n\t| {\n\t\t\tkind: 'width';\n\t\t\tref: VectorWidthPointRef;\n\t\t\t/** `copy`: Alt-drag a centre to slide a copy of the point. */\n\t\t\tmode: 'width' | 'move' | 'copy';\n\t\t\tside: 'both' | 'left' | 'right';\n\t\t\t/** The index the drag started on (the source of a copy). */\n\t\t\tsource: number;\n\t\t\t/** Other selected points of the path that move or widen with it (indices into `profile`). */\n\t\t\tgroup: number[];\n\t\t\t/** Shift-click (adding the point): a click without a move keeps the other selected points. */\n\t\t\tshift: boolean;\n\t\t\t/** Old index → new index after a group move re-sorted the profile. */\n\t\t\trenumber: Map<number, number> | null;\n\t\t\t/** The stroke line's point and left normal where the drag started (node space). */\n\t\t\torigin: Vec2;\n\t\t\tnormal: Vec2;\n\t\t\t/** Half the drawn stroke width: side scale 1 reaches this far. */\n\t\t\tunit: number;\n\t\t\tprofile: VectorWidthPoint[];\n\t\t\tbefore: Snapshot;\n\t\t\tbase: VectorDocument;\n\t\t\tstart: Vec2;\n\t\t\tmoved: boolean;\n\t\t\tcreated: boolean;\n\t }\n\t| {\n\t\t\tkind: 'gradient';\n\t\t\t/** What was grabbed: null draws a new vector over `targets`. */\n\t\t\tpart: VectorGradientPart | null;\n\t\t\tnodeId: string | null;\n\t\t\ttargets: string[];\n\t\t\tannotator: VectorGradientAnnotator | null;\n\t\t\tstart: Vec2;\n\t\t\tstartDoc: Vec2;\n\t\t\tbefore: Snapshot;\n\t\t\tbase: VectorDocument;\n\t\t\tmoved: boolean;\n\t\t\t/** The dragged stop's index now (null once dragged off the bar). */\n\t\t\tstop: number | null;\n\t }\n\t| { kind: 'pan'; start: Vec2; viewport: VectorViewport };\n\n/**\n * Headless state for a vector editor: a document of paths and groups, select / direct /\n * pen / pencil / shape tools driven by pointer and key events, undo, transforms, boolean\n * operations, alignment, grouping, SVG import / export and a viewport. Any UI forwards\n * pointer events (in canvas pixels) and renders `getState()`; `getState()` returns the same\n * object until something changes, so it works with `useSyncExternalStore` and Svelte runes.\n */\nexport class VectorEditorController {\n\treadonly history: HistoryManager;\n\tprivate doc: VectorDocument;\n\tprivate selection: string[] = [];\n\tprivate anchors: VectorAnchorRef[] = [];\n\tprivate widthPoints: VectorWidthPointRef[] = [];\n\tprivate widthEdit: VectorWidthPointEdit | null = null;\n\tprivate editingId: string | null = null;\n\tprivate pen: { nodeId: string; subpath: number } | null = null;\n\tprivate tool: VectorTool;\n\tprivate defaults: VectorEditorState['defaults'];\n\tprivate viewport: VectorViewport;\n\tprivate snap: VectorSnapOptions;\n\tprivate editing: VectorPathEditOptions;\n\tprivate hitRadius: number;\n\tprivate hover: VectorHover | null = null;\n\tprivate pointer: Vec2 | null = null;\n\tprivate drag: Drag | null = null;\n\tprivate interaction: VectorInteraction | null = null;\n\tprivate guides: VectorSnapGuides | null = null;\n\t/** Whether the last `snapDrawn` landed on an object (guides may be hidden). */\n\tprivate snappedToObject = false;\n\t/** Drawing targets for hovering the pen and shape tools, kept while the document and the excluded path stay the same. */\n\tprivate hoverTargets: { doc: VectorDocument; exclude: string; targets: VectorSnapTargets } | null = null;\n\tprivate spaceHeld = false;\n\tprivate clipboard: VectorNode[] | null = null;\n\t/** Shape Builder map of the selection, kept while the document and the selection stay the same. */\n\tprivate builderCache: { doc: VectorDocument; key: string; map: VectorShapeBuilderMap | null } | null = null;\n\t/** Paint bucket map of the visible outlines. */\n\tprivate fillCache: { doc: VectorDocument; gap: number; map: PlanarMap | null } | null = null;\n\tprivate builderPreview: VectorBuilderPreview | null = null;\n\tprivate blendStart: VectorBlendStart | null = null;\n\t/** Options new blends get (the toolbars edit them along with the selected blend's). */\n\tprivate blendDefaults: VectorBlendOptions = {};\n\tprivate gradientEdit: VectorEditorState['gradientEdit'] = { target: 'fill', stop: null, layer: null };\n\tprivate freeDistort = false;\n\t/** The text edit session: selection, the snapshot before a text node it created (until the first change), the merge key of its undo step. */\n\tprivate textEdit: (VectorTextEditState & { before: Snapshot | null; key: string; created: boolean }) | null = null;\n\tprivate textEditCount = 0;\n\tprivate symbolEdit: SymbolEditContext | null = null;\n\t/** The document as last solved: what differs from it was just edited, so the solver moves it least. */\n\tprivate lastSolved: VectorDocument | null = null;\n\tprivate constraintIssues: VectorEditorState['constraintIssues'] = null;\n\tprivate textDefaults: VectorTextDefaults = { ...DEFAULT_VECTOR_FONT };\n\t/** What the highlight shows, to skip notifying when the pointer stays over the same face. */\n\tprivate builderHoverKey: string | null = null;\n\tprivate readonly createId: () => string;\n\tprivate readonly onChange?: (document: VectorDocument) => void;\n\tprivate readonly listeners = new Set<(state: VectorEditorState) => void>();\n\tprivate readonly cleanups: Array<() => void> = [];\n\tprivate applying = false;\n\t/** Set while a commit records a change that was already applied, so `restore` still reports it. */\n\tprivate committing = false;\n\tprivate revision = 0;\n\tprivate state!: VectorEditorState;\n\n\tconstructor(options: VectorEditorOptions = {}) {\n\t\tthis.history = options.history ?? new HistoryManager();\n\t\tthis.doc = options.document ? loadedDocument(options.document) : { width: 800, height: 600, nodes: [] };\n\t\tthis.doc = this.refreshed(this.doc);\n\t\tthis.tool = options.tool ?? 'select';\n\t\tthis.defaults = { ...DEFAULT_DEFAULTS, ...(options.defaults ?? {}) } as VectorEditorState['defaults'];\n\t\tthis.viewport = { x: 0, y: 0, zoom: 1, ...(options.viewport ?? {}) };\n\t\tthis.snap = { ...DEFAULT_VECTOR_SNAP, ...(options.snap ?? {}) };\n\t\tthis.editing = { ...DEFAULT_VECTOR_PATH_EDIT, ...(options.editing ?? {}) };\n\t\tthis.hitRadius = options.hitRadius ?? 6;\n\t\tthis.createId = options.createId ?? defaultCreateId;\n\t\tthis.onChange = options.onChange;\n\t\tthis.freeDistort = !!options.freeDistort;\n\t\tif (options.textShaper !== undefined) setVectorTextShaper(options.textShaper);\n\t\tif (options.textDefaults) this.textDefaults = { ...this.textDefaults, ...options.textDefaults };\n\t\tthis.state = this.computeState();\n\t\tthis.connect();\n\t}\n\n\t/** Listens to the history (once; again after `destroy`). */\n\tprivate connect(): void {\n\t\tif (this.cleanups.length) return;\n\t\tthis.cleanups.push(\n\t\t\tthis.history.subscribe((h) => {\n\t\t\t\tif (!this.applying && this.historyDiffers(h)) this.notify();\n\t\t\t})\n\t\t);\n\t\t// Catch up on history changes made while disconnected.\n\t\tif (this.historyDiffers(this.history.getState())) this.state = this.computeState();\n\t}\n\n\tprivate historyDiffers(h: ReturnType<HistoryManager['getState']>): boolean {\n\t\tconst s = this.state;\n\t\treturn s.canUndo !== h.canUndo || s.canRedo !== h.canRedo || s.undoLabel !== h.undoName || s.redoLabel !== h.redoName;\n\t}\n\n\t/**\n\t * Stops listening to the history and drops all listeners. A later `subscribe` reconnects, so a\n\t * controller torn down by React StrictMode's double effect keeps working when it is reused.\n\t */\n\tdestroy(): void {\n\t\tif (this.dwellTimer) clearTimeout(this.dwellTimer);\n\t\tthis.dwellTimer = null;\n\t\tthis.cleanups.splice(0).forEach((fn) => fn());\n\t\tthis.listeners.clear();\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// State\n\t// -------------------------------------------------------------------------\n\n\t/** The current snapshot (stable until the next change). */\n\tgetState = (): VectorEditorState => this.state;\n\n\t/** Calls `listener` after every change. Returns an unsubscribe function. */\n\tsubscribe = (listener: (state: VectorEditorState) => void): (() => void) => {\n\t\tthis.connect();\n\t\tthis.listeners.add(listener);\n\t\treturn () => {\n\t\t\tthis.listeners.delete(listener);\n\t\t};\n\t};\n\n\tgetDocument(): VectorDocument {\n\t\treturn this.doc;\n\t}\n\n\tgetNode(id: string): VectorNode | undefined {\n\t\treturn findVectorNode(this.doc.nodes, id);\n\t}\n\n\tprivate computeState(): VectorEditorState {\n\t\tconst h = this.history.getState();\n\t\treturn {\n\t\t\tdocument: this.doc,\n\t\t\ttool: this.tool,\n\t\t\tselection: this.selection,\n\t\t\tselectedAnchors: this.anchors,\n\t\t\tselectedWidthPoints: this.widthPoints,\n\t\t\twidthPointEdit: this.widthEdit,\n\t\t\teditingId: this.editingId,\n\t\t\tselectionBounds: this.boundsOf(this.selection),\n\t\t\tselectionFrame: this.frameOf(this.selectedRoots()),\n\t\t\tviewport: this.viewport,\n\t\t\tdefaults: this.defaults,\n\t\t\tsnap: this.snap,\n\t\t\tediting: this.editing,\n\t\t\tguides: this.guides,\n\t\t\thitRadius: this.hitRadius,\n\t\t\thover: this.hover,\n\t\t\tcursor: this.cursorFor(),\n\t\t\tinteraction: this.interaction,\n\t\t\tpen: this.pen,\n\t\t\tpointer: this.pointer,\n\t\t\tbuilder: this.builderPreview,\n\t\t\tblendStart: this.blendStart,\n\t\t\tblendDefaults: this.blendDefaults,\n\t\t\tgradientEdit: this.gradientEdit,\n\t\t\tfreeDistort: this.freeDistort,\n\t\t\ttextEdit: this.textEdit ? { nodeId: this.textEdit.nodeId, anchor: this.textEdit.anchor, focus: this.textEdit.focus } : null,\n\t\t\ttextDefaults: this.textDefaults,\n\t\t\tsymbolEdit: this.symbolEditState(),\n\t\t\tconstraintIssues: this.constraintIssues,\n\t\t\ttrackingPoints: this.trackingPoints,\n\t\t\ttypedEntry: this.typed,\n\t\t\tcanUndo: h.canUndo,\n\t\t\tcanRedo: h.canRedo,\n\t\t\tundoLabel: h.undoName,\n\t\t\tredoLabel: h.redoName,\n\t\t\trevision: this.revision\n\t\t};\n\t}\n\n\t/** `doc` with its blends' steps brought up to date (the same object when they already are). */\n\tprivate refreshed(doc: VectorDocument): VectorDocument {\n\t\t// Shorthand fills and strokes written by any edit go to the top layer of their stack.\n\t\tconst nodes = refreshVectorBlends(normalizeVectorTree(doc.nodes, 'shorthand'));\n\t\tlet out = nodes === doc.nodes ? doc : { ...doc, nodes };\n\t\t// In isolation mode the edited symbol's art is the document's nodes.\n\t\tconst edit = this.symbolEdit;\n\t\tif (edit && out.symbols?.some((s) => s.id === edit.symbolId && s.nodes !== out.nodes)) {\n\t\t\tout = { ...out, symbols: out.symbols.map((s) => (s.id === edit.symbolId ? { ...s, nodes: out.nodes } : s)) };\n\t\t}\n\t\t// Instances draw their symbols' art.\n\t\tout = resolveVectorSymbols(out);\n\t\t// Connectors follow what they are glued to (after the constraints have moved it).\n\t\treturn rerouteVectorConnectors(this.solved(out));\n\t}\n\n\t/**\n\t * `doc` with its constraints solved (geometry edited since the last solve moving least) and\n\t * constraints on deleted anchors removed; the same object when nothing changes.\n\t */\n\tprivate solved(doc: VectorDocument): VectorDocument {\n\t\tif (!doc.constraints?.length) {\n\t\t\tthis.constraintIssues = null;\n\t\t\tthis.lastSolved = doc;\n\t\t\treturn doc;\n\t\t}\n\t\tif (this.lastSolved && doc.nodes === this.lastSolved.nodes && doc.constraints === this.lastSolved.constraints && doc.parameters === this.lastSolved.parameters) return doc;\n\t\tconst purged = purgeVectorConstraints(doc).document;\n\t\tconst r = solveVectorConstraints(purged, this.lastSolved);\n\t\tthis.constraintIssues = r.conflicts.length || r.skipped.length ? { conflicts: [...new Set(r.conflicts)], skipped: r.skipped } : null;\n\t\tthis.lastSolved = r.document;\n\t\treturn r.document;\n\t}\n\n\tprivate notify(docChanged = false) {\n\t\t// Blends regenerate their steps after every edit of a key (drags included).\n\t\tconst fresh = this.refreshed(this.doc);\n\t\tif (fresh !== this.doc) this.doc = fresh;\n\t\tif (this.blendStart && !this.getNode(this.blendStart.nodeId)) this.blendStart = null;\n\t\tthis.revision++;\n\t\tthis.state = this.computeState();\n\t\tif (docChanged) this.onChange?.(this.storedDocument());\n\t\tconst state = this.state;\n\t\tthis.listeners.forEach((listener) => {\n\t\t\ttry {\n\t\t\t\tlistener(state);\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('[VectorEditorController] Error in state listener:', error);\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate boundsOf(ids: readonly string[]): VectorBounds | null {\n\t\treturn unionBounds(\n\t\t\tids.map((id) => {\n\t\t\t\tconst n = this.getNode(id);\n\t\t\t\treturn n ? vectorNodeBounds(n, vectorParentMatrix(this.doc.nodes, id) ?? IDENTITY_MATRIX) : null;\n\t\t\t})\n\t\t);\n\t}\n\n\tprivate frameOf(ids: readonly string[]): VectorSelectionFrame | null {\n\t\treturn getVectorSelectionFrame(this.doc, ids);\n\t}\n\n\t/** Local → document matrix of a node in `doc` (default: the current document). */\n\tprivate worldOf(id: string, doc: VectorDocument = this.doc): AffineMatrix {\n\t\treturn vectorWorldMatrix(doc.nodes, id) ?? IDENTITY_MATRIX;\n\t}\n\n\t/** A document point in node `id`'s own space. */\n\tprivate toLocal(id: string, p: Vec2, doc: VectorDocument = this.doc): Vec2 {\n\t\treturn applyMatrix(invertMatrix(this.worldOf(id, doc)), p);\n\t}\n\n\tprivate cursorFor(): string {\n\t\tif (this.drag?.kind === 'pan' || this.spaceHeld) return this.drag ? 'grabbing' : 'grab';\n\t\tif (this.drag?.kind === 'rotate') return 'grabbing';\n\t\tif (this.drag?.kind === 'move') return 'move';\n\t\tconst h = this.hover;\n\t\tswitch (this.tool) {\n\t\t\tcase 'select':\n\t\t\t\tif (h?.transformHandle && this.freeDistort && h.transformHandle !== 'rotate') return 'crosshair';\n\t\t\t\tif (h?.transformHandle) return vectorResizeCursor(h.transformHandle, this.frameOf(this.selectedRoots())?.angle ?? 0);\n\t\t\t\treturn h?.nodeId ? 'move' : 'default';\n\t\t\tcase 'type':\n\t\t\t\treturn 'text';\n\t\t\tcase 'typePath':\n\t\t\t\treturn h?.nodeId ? 'text' : 'crosshair';\n\t\t\tcase 'envelope':\n\t\t\t\tif (this.drag?.kind === 'envelope') return 'grabbing';\n\t\t\t\tif (h?.envelope) return h.envelope.index === null ? 'grab' : 'pointer';\n\t\t\t\treturn h?.nodeId ? 'pointer' : 'default';\n\t\t\tcase 'direct':\n\t\t\t\tif (h?.anchor || h?.handle || h?.corner) return 'pointer';\n\t\t\t\tif (h?.segment) return 'copy';\n\t\t\t\treturn h?.nodeId ? 'move' : 'default';\n\t\t\tcase 'pen':\n\t\t\t\treturn h?.anchor ? 'pointer' : 'crosshair';\n\t\t\tcase 'width':\n\t\t\t\tif (this.drag?.kind === 'width') return this.drag.mode === 'width' ? 'ew-resize' : 'grabbing';\n\t\t\t\tif (h?.widthPoint) return h.widthPoint.part === 'center' ? 'grab' : 'ew-resize';\n\t\t\t\treturn h?.strokeSpot ? 'copy' : 'default';\n\t\t\tcase 'shapeBuilder':\n\t\t\t\tif (!this.builderPreview?.faces.length && !this.builderPreview?.lines.length) return 'crosshair';\n\t\t\t\treturn this.builderPreview.mode === 'erase' ? 'not-allowed' : 'cell';\n\t\t\tcase 'paintBucket':\n\t\t\t\treturn this.builderPreview ? 'cell' : 'crosshair';\n\t\t\tcase 'blend':\n\t\t\t\treturn h?.nodeId ? (this.blendStart && this.blendStart.nodeId !== h.nodeId ? 'copy' : 'pointer') : 'crosshair';\n\t\t\tcase 'gradient': {\n\t\t\t\tconst part = this.drag?.kind === 'gradient' ? this.drag.part : h?.gradient;\n\t\t\t\tif (!part) return 'crosshair';\n\t\t\t\tif (part.kind === 'bar') return this.drag ? 'grabbing' : 'copy';\n\t\t\t\tif (part.kind === 'handle' && part.handle === 'start') return this.drag ? 'grabbing' : 'move';\n\t\t\t\treturn 'pointer';\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\treturn 'crosshair';\n\t\t}\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Undoable changes\n\t// -------------------------------------------------------------------------\n\n\tprivate snapshot(): Snapshot {\n\t\treturn { doc: this.doc, selection: this.selection, anchors: this.anchors, editingId: this.editingId, pen: this.pen, symbolEdit: this.symbolEdit };\n\t}\n\n\tprivate restore(s: Snapshot) {\n\t\tconst docChanged = s.doc !== this.doc || this.committing;\n\t\t// Undo / redo can renumber width points: drop the ones that are gone and close the editor.\n\t\tif (!this.committing) this.widthEdit = null;\n\t\tthis.committing = false;\n\t\tthis.applying = true;\n\t\ttry {\n\t\t\tthis.doc = s.doc;\n\t\t\t// Undo and redo step in and out of isolation mode with the edits made there (the view follows).\n\t\t\tconst was = this.symbolEdit;\n\t\t\tthis.symbolEdit = s.symbolEdit ?? null;\n\t\t\tif (was !== this.symbolEdit) {\n\t\t\t\tif (was?.origin) this.viewport = this.viewOutOf(was);\n\t\t\t\tif (this.symbolEdit?.origin) this.viewport = this.viewInto(this.symbolEdit);\n\t\t\t}\n\t\t\tthis.selection = s.selection.filter((id) => this.getNode(id));\n\t\t\tthis.anchors = s.anchors.filter((r) => getAnchor(this.doc, r));\n\t\t\tthis.editingId = s.editingId && this.getNode(s.editingId) ? s.editingId : null;\n\t\t\tthis.pen = s.pen && (this.getNode(s.pen.nodeId) as VectorPathNode | undefined)?.subpaths?.[s.pen.subpath] ? s.pen : null;\n\t\t\tif (this.textEdit) {\n\t\t\t\tconst t = this.getNode(this.textEdit.nodeId);\n\t\t\t\tif (t?.type !== 'text') this.textEdit = null;\n\t\t\t\telse {\n\t\t\t\t\tconst n = t.text.length;\n\t\t\t\t\tthis.textEdit = { ...this.textEdit, anchor: Math.min(this.textEdit.anchor, n), focus: Math.min(this.textEdit.focus, n), before: null };\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (this.widthPoints.length) {\n\t\t\t\tconst kept = this.widthPoints.filter((r) => {\n\t\t\t\t\tconst n = this.getNode(r.nodeId);\n\t\t\t\t\treturn n?.type === 'path' && !!n.strokeWidthProfile?.[r.index];\n\t\t\t\t});\n\t\t\t\tif (kept.length !== this.widthPoints.length) this.widthPoints = kept;\n\t\t\t}\n\t\t} finally {\n\t\t\tthis.applying = false;\n\t\t}\n\t\tthis.notify(docChanged);\n\t}\n\n\t/**\n\t * Records the change from `before` to the current state as one undo step. Consecutive\n\t * changes with the same `merge` key (a slider drag) fold into one step.\n\t */\n\tprivate commit(name: string, before: Snapshot, merge?: string) {\n\t\tthis.doc = this.refreshed(this.doc);\n\t\tconst after = this.snapshot();\n\t\tif (before.doc === after.doc) {\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tconst command: Command = {\n\t\t\tname,\n\t\t\texecute: () => this.restore(after),\n\t\t\tundo: () => this.restore(before),\n\t\t\tredo: () => this.restore(after),\n\t\t\t...(merge ? { tag: merge } : {})\n\t\t};\n\t\tthis.committing = true;\n\t\tif (merge) this.history.executeMerged(command, { key: merge });\n\t\telse this.history.execute(command);\n\t\tthis.committing = false;\n\t}\n\n\t/** Runs `fn` (which updates fields) and records it as one undo step (merged by `merge` key). */\n\tprivate change(name: string, fn: () => void, merge?: string): void {\n\t\tconst before = this.snapshot();\n\t\tfn();\n\t\tthis.commit(name, before, merge);\n\t}\n\n\t/** Ends a merged edit (a slider released): the next change starts a new undo step. */\n\tendMergedEdit(): void {\n\t\tthis.history.endGesture();\n\t}\n\n\tprivate setDoc(nodes: VectorNode[]) {\n\t\tif (nodes !== this.doc.nodes) this.doc = { ...this.doc, nodes };\n\t}\n\n\tprivate replaceNode(id: string, next: VectorNode | null) {\n\t\tthis.setDoc(mapTree(this.doc.nodes, (n) => (n.id === id ? next : n)));\n\t}\n\n\tprivate freshIds<T extends VectorNode>(node: T): T {\n\t\tconst copy = cloneVectorNode(node);\n\t\tconst used = new Set<string>();\n\t\twalkVectorNodes(this.doc.nodes, (n) => used.add(n.id));\n\t\tconst next = () => {\n\t\t\tlet id = this.createId();\n\t\t\twhile (used.has(id)) id = this.createId();\n\t\t\tused.add(id);\n\t\t\treturn id;\n\t\t};\n\t\tconst assign = (n: VectorNode) => {\n\t\t\tn.id = next();\n\t\t\tif (n.type === 'group') n.children.forEach(assign);\n\t\t};\n\t\tassign(copy);\n\t\treturn copy;\n\t}\n\n\tprivate newId(): string {\n\t\tlet id = this.createId();\n\t\twhile (this.getNode(id)) id = this.createId();\n\t\treturn id;\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Viewport\n\t// -------------------------------------------------------------------------\n\n\tscreenToDocument(p: Vec2): Vec2 {\n\t\tconst v = this.viewport;\n\t\treturn { x: (p.x - v.x) / v.zoom, y: (p.y - v.y) / v.zoom };\n\t}\n\n\tdocumentToScreen(p: Vec2): Vec2 {\n\t\tconst v = this.viewport;\n\t\treturn { x: p.x * v.zoom + v.x, y: p.y * v.zoom + v.y };\n\t}\n\n\tsetViewport(viewport: Partial<VectorViewport>): void {\n\t\tconst next = { ...this.viewport, ...viewport };\n\t\tnext.zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, next.zoom));\n\t\tif (next.x === this.viewport.x && next.y === this.viewport.y && next.zoom === this.viewport.zoom) return;\n\t\tthis.viewport = next;\n\t\tthis.notify();\n\t}\n\n\t/** Zooms by `factor` keeping the document point under `screen` in place. */\n\tzoomAt(screen: Vec2, factor: number): void {\n\t\tconst zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, this.viewport.zoom * factor));\n\t\tconst doc = this.screenToDocument(screen);\n\t\tthis.setViewport({ zoom, x: screen.x - doc.x * zoom, y: screen.y - doc.y * zoom });\n\t}\n\n\tpanBy(dx: number, dy: number): void {\n\t\tthis.setViewport({ x: this.viewport.x + dx, y: this.viewport.y + dy });\n\t}\n\n\t/** Fits the artboard (or `bounds`) into a canvas of `size` screen pixels. */\n\tzoomToFit(size: { width: number; height: number }, options: { padding?: number; bounds?: VectorBounds } = {}): void {\n\t\tconst padding = options.padding ?? 40;\n\t\tconst b = options.bounds ?? { x: 0, y: 0, width: this.doc.width, height: this.doc.height };\n\t\tconst zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, Math.min((size.width - padding * 2) / (b.width || 1), (size.height - padding * 2) / (b.height || 1))));\n\t\tthis.setViewport({ zoom, x: (size.width - b.width * zoom) / 2 - b.x * zoom, y: (size.height - b.height * zoom) / 2 - b.y * zoom });\n\t}\n\n\t/** Zoom at the canvas centre-ish: positive `delta` zooms out (wheel convention). Use with `wheel`. */\n\twheel(screen: Vec2, delta: { x: number; y: number }, mods: VectorModifiers = {}): void {\n\t\tif (mods.mod) this.zoomAt(screen, Math.exp(-delta.y * 0.01));\n\t\telse this.panBy(-(mods.shift ? delta.y : delta.x), -(mods.shift ? 0 : delta.y));\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Settings\n\t// -------------------------------------------------------------------------\n\n\tsetTool(tool: VectorTool): void {\n\t\tif (tool === this.tool) return;\n\t\tif (tool !== 'type' && tool !== 'typePath') this.endTextEdit();\n\t\tthis.finishPath();\n\t\tthis.cancelDrag();\n\t\tthis.clearTracking(false);\n\t\tthis.tool = tool;\n\t\tthis.hover = null;\n\t\tthis.guides = null;\n\t\tthis.widthPoints = [];\n\t\tthis.widthEdit = null;\n\t\tthis.builderPreview = null;\n\t\tthis.builderHoverKey = null;\n\t\tthis.blendStart = null;\n\t\tif (tool !== 'direct') {\n\t\t\tthis.anchors = [];\n\t\t\tthis.editingId = null;\n\t\t}\n\t\tthis.notify();\n\t}\n\n\tsetDefaults(defaults: Partial<VectorDefaults>): void {\n\t\tthis.defaults = { ...this.defaults, ...defaults };\n\t\tthis.notify();\n\t}\n\n\tsetSnap(snap: Partial<VectorSnapOptions>): void {\n\t\tthis.snap = { ...this.snap, ...snap };\n\t\tif (!this.snap.guides || !this.snap.objects) this.guides = null;\n\t\tthis.notify();\n\t}\n\n\t/** Changes how deleting, dragging anchors, the pencil and the eraser behave. */\n\tsetEditing(editing: Partial<VectorPathEditOptions>): void {\n\t\tconst next = { ...this.editing, ...editing };\n\t\tnext.pencilSmoothing = Math.min(1, Math.max(0, Number.isFinite(next.pencilSmoothing) ? next.pencilSmoothing : 0));\n\t\tif (!(next.eraserSize > 0)) next.eraserSize = this.editing.eraserSize;\n\t\tthis.editing = next;\n\t\tthis.notify();\n\t}\n\n\t/** Turns object snapping on or off (the toolbar's snapping toggle). */\n\ttoggleSnapping(on: boolean = !this.snap.objects): void {\n\t\tthis.setSnap({ objects: on });\n\t}\n\n\tprivate gridValue(v: number): number {\n\t\tconst g = this.snap.grid;\n\t\treturn g > 0 ? Math.round(v / g) * g : v;\n\t}\n\n\tprivate snapPoint(p: Vec2): Vec2 {\n\t\treturn { x: this.gridValue(p.x), y: this.gridValue(p.y) };\n\t}\n\n\t/** The pull distance in document units. */\n\tprivate snapTolerance(): number {\n\t\treturn this.snap.threshold / this.viewport.zoom;\n\t}\n\n\tprivate setGuides(guides: VectorSnapGuides | null) {\n\t\tthis.guides = guides && this.snap.guides && (guides.lines.length || guides.labels.length) ? guides : null;\n\t}\n\n\t/**\n\t * Where a drawn or dragged point `p` goes: onto the smart guide `targets` (gathered by\n\t * `gather` when missing), else onto the grid, kept to `leave`'s constraint. Cmd / Ctrl\n\t * bypasses snapping.\n\t */\n\tprivate snapDrawn(p: Vec2, leave: VectorSnapLeave | null, input: VectorModifiers, targets: () => VectorSnapTargets): Vec2 {\n\t\tthis.snappedToObject = false;\n\t\tthis.lastSnap = null;\n\t\tif (input.mod) {\n\t\t\tthis.setGuides(null);\n\t\t\treturn constrainToLeave(leave, p);\n\t\t}\n\t\tif (this.snap.objects) {\n\t\t\tconst withAngles = leave ? { ...leave, angles: leave.angles ?? this.snap.angles } : null;\n\t\t\tconst tracking = this.snap.tracking !== false ? vectorTrackingPaths(this.trackingPoints, this.snap.trackPolar ? this.snap.angles.flatMap((a) => [a, a + 180]) : undefined) : [];\n\t\t\tconst res = targets().snapFrom(p, withAngles, this.snapTolerance(), tracking);\n\t\t\tthis.lastSnap = res.kind ? { kind: res.kind, point: res.point } : null;\n\t\t\tif (res.kind) {\n\t\t\t\tthis.snappedToObject = true;\n\t\t\t\tthis.setGuides(res);\n\t\t\t\t// An axis lined up with nothing still goes to the grid.\n\t\t\t\treturn leave?.constrain ? res.point : { x: res.x ? res.point.x : this.gridValue(res.point.x), y: res.y ? res.point.y : this.gridValue(res.point.y) };\n\t\t\t}\n\t\t}\n\t\tthis.setGuides(null);\n\t\treturn constrainToLeave(leave, this.snapPoint(p));\n\t}\n\n\t/** Drawing targets for the pen and shape tools, reused while the document stays the same. */\n\tprivate drawingTargets(exclude: string | null): VectorSnapTargets {\n\t\tconst key = exclude ?? '';\n\t\tconst cached = this.hoverTargets;\n\t\tif (cached && cached.doc === this.doc && cached.exclude === key) return cached.targets;\n\t\tconst targets = VectorSnapTargets.forDrawing(this.doc, exclude ? [exclude] : []);\n\t\tthis.hoverTargets = { doc: this.doc, exclude: key, targets };\n\t\treturn targets;\n\t}\n\n\t/** Where the pointer at `p` would place a point with the pen or a shape tool (and the guides showing why). */\n\tprivate hoverSnap(p: Vec2, input: VectorModifiers): Vec2 {\n\t\tif (this.tool === 'pen') {\n\t\t\tconst sp = this.pen ? (this.getNode(this.pen.nodeId) as VectorPathNode | undefined)?.subpaths[this.pen.subpath] : undefined;\n\t\t\tif (this.pen && sp && this.hover?.anchor) {\n\t\t\t\tthis.setGuides(null);\n\t\t\t\treturn p;\n\t\t\t}\n\t\t\tconst last = this.pen && sp ? getAnchorWorld(this.doc, { nodeId: this.pen.nodeId, subpath: this.pen.subpath, index: sp.anchors.length - 1 }) : undefined;\n\t\t\tconst leave = last ? { at: { x: last.x, y: last.y }, constrain: input.shift ? ('angle' as const) : null, ...this.penAngles() } : null;\n\t\t\tconst q = this.snapDrawn(p, leave, input, () => this.drawingTargets(this.pen?.nodeId ?? null));\n\t\t\tthis.dwell();\n\t\t\treturn q;\n\t\t}\n\t\tif (SHAPE_TOOLS.has(this.tool)) {\n\t\t\tconst q = this.snapDrawn(p, null, input, () => this.drawingTargets(null));\n\t\t\tthis.dwell();\n\t\t\treturn q;\n\t\t}\n\t\tthis.setGuides(null);\n\t\treturn p;\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Document\n\t// -------------------------------------------------------------------------\n\n\t/** Replaces the document. Undoable unless `{ history: false }` (then history is cleared). */\n\tsetDocument(doc: VectorDocument, options: { history?: boolean; label?: string } = {}): void {\n\t\tthis.cancelDrag();\n\t\tconst next = loadedDocument(doc);\n\t\tif (options.history === false) {\n\t\t\tthis.doc = next;\n\t\t\tthis.selection = [];\n\t\t\tthis.anchors = [];\n\t\t\tthis.editingId = null;\n\t\t\tthis.pen = null;\n\t\t\tthis.history.clear();\n\t\t\tthis.notify(true);\n\t\t\treturn;\n\t\t}\n\t\tthis.change(options.label ?? 'Replace Document', () => {\n\t\t\tthis.doc = next;\n\t\t\tthis.selection = [];\n\t\t\tthis.anchors = [];\n\t\t\tthis.editingId = null;\n\t\t\tthis.pen = null;\n\t\t});\n\t}\n\n\t/** Changes the artboard size or background. */\n\tsetArtboard(patch: Partial<Pick<VectorDocument, 'width' | 'height' | 'background'>>): void {\n\t\tthis.change('Change Artboard', () => {\n\t\t\tthis.doc = { ...this.doc, ...patch };\n\t\t});\n\t}\n\n\t/** Replaces the document with parsed SVG markup (one undo step). */\n\tloadSvg(svg: string, options: { history?: boolean } = {}): VectorActionResult {\n\t\ttry {\n\t\t\tconst doc = parseVectorSvg(svg, { createId: () => this.createId() });\n\t\t\tthis.setDocument(doc, { history: options.history, label: 'Import SVG' });\n\t\t\treturn { ok: true };\n\t\t} catch (error) {\n\t\t\treturn { ok: false, message: error instanceof Error ? error.message : String(error) };\n\t\t}\n\t}\n\n\t/** Adds the nodes of SVG markup on top of the document, selected. */\n\timportSvg(svg: string): VectorActionResult {\n\t\ttry {\n\t\t\tconst parsed = parseVectorSvg(svg);\n\t\t\tconst nodes = parsed.nodes.map((n) => this.freshIds(n));\n\t\t\tif (!nodes.length) return { ok: false, message: 'The SVG has no shapes the editor can read.' };\n\t\t\tthis.change('Import SVG', () => {\n\t\t\t\tthis.setDoc([...this.doc.nodes, ...nodes]);\n\t\t\t\tthis.selection = nodes.map((n) => n.id);\n\t\t\t\tthis.anchors = [];\n\t\t\t});\n\t\t\treturn { ok: true };\n\t\t} catch (error) {\n\t\t\treturn { ok: false, message: error instanceof Error ? error.message : String(error) };\n\t\t}\n\t}\n\n\ttoSvg(options?: VectorSvgExportOptions): string {\n\t\treturn vectorDocumentToSvg(this.storedDocument(), options);\n\t}\n\n\ttoJSON(): VectorDocument {\n\t\treturn cloneVectorDocument(stripVectorSymbolArt(this.storedDocument()));\n\t}\n\n\t/** Adds a node (ids are kept unless taken). Returns its id. */\n\taddNode(node: VectorNode, options: AddVectorNodeOptions = {}): string {\n\t\tconst taken = (() => {\n\t\t\tlet clash = false;\n\t\t\twalkVectorNodes([node], (n) => {\n\t\t\t\tif (this.getNode(n.id)) clash = true;\n\t\t\t});\n\t\t\treturn clash;\n\t\t})();\n\t\tconst next = taken ? this.freshIds(node) : cloneVectorNode(node);\n\t\tthis.change(options.label ?? `Add ${next.type === 'group' ? 'Group' : (next.name ?? 'Path')}`, () => {\n\t\t\tconst insert = (list: VectorNode[]) => {\n\t\t\t\tconst copy = list.slice();\n\t\t\t\tcopy.splice(options.index ?? copy.length, 0, next);\n\t\t\t\treturn copy;\n\t\t\t};\n\t\t\tif (options.parentId) {\n\t\t\t\tthis.setDoc(mapTree(this.doc.nodes, (n) => (n.id === options.parentId && n.type === 'group' ? { ...n, children: insert(n.children) } : n)));\n\t\t\t} else this.setDoc(insert(this.doc.nodes));\n\t\t\tif (options.select !== false) {\n\t\t\t\tthis.selection = [next.id];\n\t\t\t\tthis.anchors = [];\n\t\t\t}\n\t\t});\n\t\treturn next.id;\n\t}\n\n\t/** Builds a path node for a shape with the current defaults (not added). */\n\tcreateShapeNode(input: VectorShapeInput): VectorPathNode {\n\t\tconst b = { x: input.x, y: input.y, width: input.width, height: input.height };\n\t\tconst d = this.defaults;\n\t\tlet subpath: VectorSubpath;\n\t\tswitch (input.kind) {\n\t\t\tcase 'rect':\n\t\t\t\tsubpath = rectSubpath(b);\n\t\t\t\tbreak;\n\t\t\tcase 'ellipse':\n\t\t\t\tsubpath = ellipseSubpath(b);\n\t\t\t\tbreak;\n\t\t\tcase 'polygon':\n\t\t\t\tsubpath = polygonSubpath(b, input.sides ?? d.polygonSides);\n\t\t\t\tbreak;\n\t\t\tcase 'star':\n\t\t\t\tsubpath = starSubpath(b, input.points ?? d.starPoints, input.innerRatio ?? d.starInnerRatio);\n\t\t\t\tbreak;\n\t\t\tcase 'line':\n\t\t\t\tsubpath = lineSubpath({ x: b.x, y: b.y }, { x: b.x + b.width, y: b.y + b.height });\n\t\t\t\tbreak;\n\t\t}\n\t\tconst radius = input.cornerRadius ?? d.cornerRadius;\n\t\tconst kind = input.cornerKind ?? d.cornerKind;\n\t\t// Live corners: the shape keeps its sharp anchors, so the radius stays editable.\n\t\tif ((input.kind === 'rect' || input.kind === 'polygon' || input.kind === 'star') && radius > 0) subpath = setSubpathCorners(subpath, radius, kind);\n\t\tconst names = { rect: 'Rectangle', ellipse: 'Ellipse', polygon: 'Polygon', star: 'Star', line: 'Line' };\n\t\tconst style = this.styleFor(input.kind === 'line');\n\t\tconst own: VectorStyle = {};\n\t\tfor (const key of VECTOR_STYLE_KEYS) {\n\t\t\tif (input[key] !== undefined) (own as any)[key] = input[key];\n\t\t}\n\t\t// Given style edits the defaults' top layers; given stacks replace theirs.\n\t\treturn withStyle({ id: this.newId(), type: 'path', name: input.name ?? names[input.kind], ...style, subpaths: [subpath] }, own) as VectorPathNode;\n\t}\n\n\t/** Adds a shape drawn with the current defaults. Returns its id. */\n\taddShape(input: VectorShapeInput, options: AddVectorNodeOptions = {}): string {\n\t\tconst node = this.createShapeNode(input);\n\t\treturn this.addNode(node, { label: `Add ${node.name}`, ...options });\n\t}\n\n\t/** Adds a path from SVG path data. Returns its id. */\n\taddPath(d: string, style: VectorStyle & { name?: string } = {}, options: AddVectorNodeOptions = {}): string {\n\t\tconst subpaths = bezierToSubpaths(d);\n\t\tconst { name, ...own } = style;\n\t\tconst node = withStyle({ id: this.newId(), type: 'path', name: name ?? 'Path', ...this.styleFor(false), subpaths }, own) as VectorPathNode;\n\t\treturn this.addNode(node, { label: 'Add Path', ...options });\n\t}\n\n\tprivate styleFor(open: boolean): VectorStyle {\n\t\tconst d = this.defaults;\n\t\tconst out: VectorStyle = { fill: open ? null : (d.fill ?? null), stroke: d.stroke ?? null, strokeWidth: d.strokeWidth ?? 1 };\n\t\tif (d.strokeLinecap) out.strokeLinecap = d.strokeLinecap;\n\t\tif (d.strokeLinejoin) out.strokeLinejoin = d.strokeLinejoin;\n\t\tif (d.strokeDasharray) out.strokeDasharray = d.strokeDasharray;\n\t\tif (d.fillRule) out.fillRule = d.fillRule;\n\t\tif (d.strokeAlign && d.strokeAlign !== 'center') out.strokeAlign = d.strokeAlign;\n\t\tfor (const key of ['startArrow', 'endArrow', 'arrowAlign', 'startArrowScale', 'endArrowScale'] as const) if (d[key]) (out as any)[key] = d[key];\n\t\tif (d.strokeWidthProfile?.length) out.strokeWidthProfile = d.strokeWidthProfile.map((p) => ({ ...p }));\n\t\t// Default stacks: new shapes get every layer (open paths no fills).\n\t\tlet styled: VectorStyle = out;\n\t\tif (Array.isArray(d.fills)) styled = withVectorFills(styled, open ? [] : d.fills);\n\t\tif (Array.isArray(d.strokes)) styled = withVectorStrokes(styled, d.strokes);\n\t\treturn styled;\n\t}\n\n\t/** Merges `patch` into a node (paths take style and geometry; groups name / visibility / opacity). */\n\tupdateNode(id: string, patch: Partial<VectorPathNode> | Partial<VectorGroupNode>, options: { label?: string } = {}): VectorActionResult {\n\t\tconst node = this.getNode(id);\n\t\tif (!node) return { ok: false, message: `There is no node \"${id}\".` };\n\t\tconst { id: _id, type: _type, ...rest } = patch as any;\n\t\tthis.change(options.label ?? 'Edit Node', () => this.replaceNode(id, { ...node, ...rest } as VectorNode));\n\t\treturn { ok: true };\n\t}\n\n\t/** Applies style (and opacity) to the selected (or given) nodes; groups pass it to every path inside. */\n\tsetStyle(style: VectorStyle & { opacity?: number }, ids: readonly string[] = this.selection, options: { label?: string } = {}): VectorActionResult {\n\t\tconst targets = new Set<string>();\n\t\tfor (const id of ids) {\n\t\t\tconst node = this.getNode(id);\n\t\t\tif (!node) return { ok: false, message: `There is no node \"${id}\".` };\n\t\t\tfor (const p of collectPaintedNodes(node)) targets.add(p.id);\n\t\t}\n\t\tif (!targets.size) return { ok: false, message: 'Nothing is selected.' };\n\t\tthis.change(options.label ?? 'Change Style', () => {\n\t\t\tthis.setDoc(mapTree(this.doc.nodes, (n) => (targets.has(n.id) ? withStyle(n as VectorPathNode, style) : n)));\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/**\n\t * Sets the variable-width profile of the selected (or given) paths: a preset id\n\t * (`uniform` removes the profile), points, or a parametric taper / wave. Returns an error for\n\t * an unknown preset.\n\t */\n\tsetWidthProfile(\n\t\tprofile: VectorWidthPreset | readonly VectorWidthPoint[] | { taper?: VectorTaperOptions; wave?: VectorWaveOptions } | null,\n\t\toptions: { ids?: readonly string[] } = {}\n\t): VectorActionResult {\n\t\tlet points: VectorWidthPoint[];\n\t\tif (profile === null) points = [];\n\t\telse if (typeof profile === 'string') {\n\t\t\tconst preset = widthProfilePreset(profile);\n\t\t\tif (!preset) return { ok: false, message: `There is no width profile \"${profile}\".` };\n\t\t\tpoints = preset;\n\t\t} else if (Array.isArray(profile)) points = sanitizeWidthProfile(profile);\n\t\telse {\n\t\t\tconst p = profile as { taper?: VectorTaperOptions; wave?: VectorWaveOptions };\n\t\t\tpoints = p.taper || p.wave ? generateWidthProfile(p) : [];\n\t\t}\n\t\tthis.widthPoints = [];\n\t\tthis.widthEdit = null;\n\t\treturn this.setStyle({ strokeWidthProfile: points.length ? points : null }, options.ids ?? this.selection, { label: points.length ? 'Width Profile' : 'Uniform Width' });\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Gradients\n\t// -------------------------------------------------------------------------\n\n\t/** Which paint (fill or stroke) the gradient tool and editor work on. */\n\tsetGradientTarget(target: VectorGradientTarget, layer: number | null = null): void {\n\t\tif (target === this.gradientEdit.target && layer === this.gradientEdit.layer) return;\n\t\tthis.gradientEdit = { target, stop: null, layer };\n\t\tthis.notify();\n\t}\n\n\t/** Selects a stop of the gradient being edited (index into its sorted stops), or none. */\n\tselectGradientStop(index: number | null): void {\n\t\tif (index === this.gradientEdit.stop) return;\n\t\tthis.gradientEdit = { ...this.gradientEdit, stop: index };\n\t\tthis.notify();\n\t}\n\n\t/** The gradient tool's annotator for the selection (screen pixels), or null when its paint is not a gradient. */\n\tgetGradientAnnotator(): VectorGradientAnnotator | null {\n\t\treturn getVectorGradientAnnotator(this.doc, this.selection, this.viewport, this.gradientEdit.target, this.gradientEdit.layer);\n\t}\n\n\t/** Unlocked paths of `ids` (default: the selection). */\n\tprivate gradientPaths(ids: readonly string[] = this.selection): VectorPathNode[] {\n\t\tconst out = new Map<string, VectorPathNode>();\n\t\tfor (const id of ids) {\n\t\t\tconst node = this.getNode(id);\n\t\t\tif (node && !node.locked) for (const p of collectPathNodes(node)) if (!p.locked) out.set(p.id, p);\n\t\t}\n\t\treturn [...out.values()];\n\t}\n\n\t/**\n\t * Edits the gradient of the `target` paint (default: the editor's) of the selected (or given)\n\t * paths with `patch`: a colour or no paint becomes a gradient (`patch.type`, default linear,\n\t * from the colour to white). Consecutive edits with the same `merge` key are one undo step.\n\t */\n\tsetGradient(patch: VectorGradientPatch, options: { ids?: readonly string[]; target?: VectorGradientTarget; layer?: number | null; label?: string; merge?: string } = {}): VectorActionResult {\n\t\tconst target = options.target ?? this.gradientEdit.target;\n\t\tconst layer = options.layer !== undefined ? options.layer : options.target === undefined ? this.gradientEdit.layer : null;\n\t\tconst paths = this.gradientPaths(options.ids ?? this.selection);\n\t\tif (!paths.length) return { ok: false, message: 'Nothing is selected.' };\n\t\tconst next = new Map<string, VectorPathNode>();\n\t\tlet first: VectorGradient | null = null;\n\t\tfor (const path of paths) {\n\t\t\tconst paint = vectorTargetPaint(path, target, layer);\n\t\t\tconst base = isVectorGradient(paint) ? paint : defaultVectorGradient(patch.type ?? 'linear', paint);\n\t\t\tconst g = applyVectorGradientPatch(base, patch);\n\t\t\tif (!g) return { ok: false, message: 'That is not a valid gradient: it needs at least one stop with an offset and a colour.' };\n\t\t\tfirst ??= g;\n\t\t\tnext.set(path.id, withVectorTargetPaint(path, target, layer, g));\n\t\t}\n\t\tthis.change(options.label ?? 'Gradient', () => this.setDoc(mapTree(this.doc.nodes, (n) => next.get(n.id) ?? n)), options.merge);\n\t\tconst stops = (first as VectorGradient | null)?.stops.length ?? 0;\n\t\tif (this.gradientEdit.stop !== null && this.gradientEdit.stop >= stops) this.gradientEdit = { ...this.gradientEdit, stop: null };\n\t\treturn { ok: true };\n\t}\n\n\t/** Applies `fn` to every target gradient of the selection (paths whose paint is not a gradient are skipped). */\n\tprivate editGradients(label: string, fn: (stops: VectorGradient['stops']) => VectorGradient['stops'] | null, merge?: string): VectorActionResult {\n\t\tconst { target, layer } = this.gradientEdit;\n\t\tconst paths = this.gradientPaths().filter((p) => isVectorGradient(vectorTargetPaint(p, target, layer)));\n\t\tif (!paths.length) return { ok: false, message: 'The selection has no gradient.' };\n\t\tconst next = new Map<string, VectorPathNode>();\n\t\tfor (const path of paths) {\n\t\t\tconst g = vectorTargetPaint(path, target, layer) as VectorGradient;\n\t\t\tconst stops = fn(sortedVectorGradientStops(g));\n\t\t\tif (!stops) return { ok: false, message: 'A gradient keeps at least two stops.' };\n\t\t\tnext.set(path.id, withVectorTargetPaint(path, target, layer, { ...g, stops }));\n\t\t}\n\t\tthis.change(label, () => this.setDoc(mapTree(this.doc.nodes, (n) => next.get(n.id) ?? n)), merge);\n\t\treturn { ok: true };\n\t}\n\n\t/** Changes stop `index` (colour, opacity, offset or midpoint) of the selection's gradients; selects it. */\n\tsetGradientStop(index: number, patch: { color?: string; opacity?: number; offset?: number; midpoint?: number }, options: { merge?: string } = {}): VectorActionResult {\n\t\tlet moved = index;\n\t\tconst result = this.editGradients(\n\t\t\t'Gradient Stop',\n\t\t\t(stops) => {\n\t\t\t\tif (!stops[index]) return stops;\n\t\t\t\tlet next = stops.map((s, i) => {\n\t\t\t\t\tif (i !== index) return s;\n\t\t\t\t\tconst out = { ...s };\n\t\t\t\t\tif (patch.color !== undefined) out.color = patch.color;\n\t\t\t\t\tif (patch.opacity !== undefined && Number.isFinite(patch.opacity)) {\n\t\t\t\t\t\tconst o = Math.min(1, Math.max(0, patch.opacity));\n\t\t\t\t\t\tif (o < 1) out.opacity = o;\n\t\t\t\t\t\telse delete out.opacity;\n\t\t\t\t\t}\n\t\t\t\t\tif (patch.midpoint !== undefined && Number.isFinite(patch.midpoint)) {\n\t\t\t\t\t\tconst m = Math.min(0.95, Math.max(0.05, patch.midpoint));\n\t\t\t\t\t\tif (Math.abs(m - 0.5) > 1e-3) out.midpoint = m;\n\t\t\t\t\t\telse delete out.midpoint;\n\t\t\t\t\t}\n\t\t\t\t\treturn out;\n\t\t\t\t});\n\t\t\t\tif (patch.offset !== undefined && Number.isFinite(patch.offset)) ({ stops: next, index: moved } = moveVectorGradientStop(next, index, patch.offset));\n\t\t\t\treturn next;\n\t\t\t},\n\t\t\toptions.merge\n\t\t);\n\t\tif (result.ok) this.selectGradientStop(moved);\n\t\treturn result;\n\t}\n\n\t/** Adds a stop at `offset` (painting what the gradient paints there) to the selection's gradients and selects it. */\n\taddGradientStop(offset: number): VectorActionResult {\n\t\tlet index = 0;\n\t\tconst { target, layer } = this.gradientEdit;\n\t\tconst result = this.editGradients('Add Gradient Stop', (stops) => {\n\t\t\tconst path = this.gradientPaths().find((p) => isVectorGradient(vectorTargetPaint(p, target, layer)));\n\t\t\tconst r = insertVectorGradientStop({ ...(vectorTargetPaint(path!, target, layer) as VectorGradient), stops }, offset);\n\t\t\tindex = r.index;\n\t\t\treturn r.stops;\n\t\t});\n\t\tif (result.ok) this.selectGradientStop(index);\n\t\treturn result;\n\t}\n\n\t/** Removes stop `index` of the selection's gradients (never below two stops). */\n\tremoveGradientStop(index: number): VectorActionResult {\n\t\tconst result = this.editGradients('Delete Gradient Stop', (stops) => removeVectorGradientStop(stops, index));\n\t\tif (result.ok) this.selectGradientStop(null);\n\t\treturn result;\n\t}\n\n\t/** Reverses the selection's gradients (stops and midpoints). */\n\treverseGradient(): VectorActionResult {\n\t\treturn this.setGradient({ reverse: true }, { label: 'Reverse Gradient' });\n\t}\n\n\t/** `doc` with path `id`'s `target` paint replaced. */\n\tprivate withGradient(doc: VectorDocument, id: string, target: VectorGradientTarget, g: VectorGradient): VectorDocument {\n\t\tconst layer = this.gradientEdit.layer;\n\t\treturn { ...doc, nodes: mapTree(doc.nodes, (n) => (n.id === id && n.type === 'path' ? withVectorTargetPaint(n, target, layer, g) : n)) };\n\t}\n\n\tprivate downGradient(screen: Vec2, p: Vec2, input: VectorPointerInput) {\n\t\tconst before = this.snapshot();\n\t\tconst a = this.getGradientAnnotator();\n\t\tlet part = a ? hitVectorGradientAnnotator(a, screen) : null;\n\t\tif (a && part) {\n\t\t\t// Alt on a radial gradient's centre grabs its focal point (they sit together until moved).\n\t\t\tif (part.kind === 'handle' && part.handle === 'start' && input.alt && a.gradient.type === 'radial') part = { kind: 'handle', handle: 'focal' };\n\t\t\tif (part.kind === 'stop') this.gradientEdit = { ...this.gradientEdit, stop: part.index };\n\t\t\tthis.drag = { kind: 'gradient', part, nodeId: a.nodeId, targets: [a.nodeId], annotator: a, start: screen, startDoc: p, before, base: this.doc, moved: false, stop: part.kind === 'stop' ? part.index : null };\n\t\t\tthis.interaction = { type: 'gradient', part: part.kind, ...(part.kind === 'handle' ? { handle: part.handle } : {}) };\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\t// Elsewhere a drag draws a new vector over the path under the pointer (selecting it) or the selection.\n\t\tconst hit = this.hitPath(p);\n\t\tif (hit && !this.gradientPaths().some((n) => n.id === hit.id)) {\n\t\t\tthis.selection = [hit.id];\n\t\t\tthis.gradientEdit = { ...this.gradientEdit, stop: null };\n\t\t}\n\t\tconst targets = this.gradientPaths().map((n) => n.id);\n\t\tif (!targets.length) {\n\t\t\tif (this.selection.length) this.clearSelection();\n\t\t\treturn;\n\t\t}\n\t\tthis.drag = { kind: 'gradient', part: null, nodeId: null, targets, annotator: null, start: screen, startDoc: p, before, base: this.doc, moved: false, stop: null };\n\t\tthis.interaction = { type: 'gradient', part: 'create' };\n\t\tthis.notify();\n\t}\n\n\tprivate moveGradient(drag: Extract<Drag, { kind: 'gradient' }>, screen: Vec2, p: Vec2, input: VectorPointerInput) {\n\t\tif (!drag.moved && Math.hypot(screen.x - drag.start.x, screen.y - drag.start.y) < 3) return;\n\t\tdrag.moved = true;\n\t\tconst target = this.gradientEdit.target;\n\t\tconst base = drag.base;\n\t\tconst constrained = (from: Vec2, to: Vec2) => {\n\t\t\tif (!input.shift) return to;\n\t\t\tconst ang = Math.round(Math.atan2(to.y - from.y, to.x - from.x) / (Math.PI / 4)) * (Math.PI / 4);\n\t\t\tconst len = Math.hypot(to.x - from.x, to.y - from.y);\n\t\t\treturn { x: from.x + Math.cos(ang) * len, y: from.y + Math.sin(ang) * len };\n\t\t};\n\t\tif (!drag.part) {\n\t\t\tconst end = constrained(drag.startDoc, p);\n\t\t\tlet doc = base;\n\t\t\tfor (const id of drag.targets) {\n\t\t\t\tconst node = findVectorNode(base.nodes, id) as VectorPathNode | undefined;\n\t\t\t\tif (!node) continue;\n\t\t\t\tconst paint = vectorTargetPaint(node, target, this.gradientEdit.layer);\n\t\t\t\tconst g = isVectorGradient(paint) ? paint : defaultVectorGradient('linear', paint);\n\t\t\t\tdoc = this.withGradient(doc, id, target, setVectorGradientVector(g, vectorPathLocalBounds(node), this.toLocal(id, drag.startDoc, base), this.toLocal(id, end, base)));\n\t\t\t}\n\t\t\tthis.doc = doc;\n\t\t\tthis.notify(true);\n\t\t\treturn;\n\t\t}\n\t\tconst id = drag.nodeId!;\n\t\tconst node = findVectorNode(base.nodes, id) as VectorPathNode;\n\t\tconst g = vectorTargetPaint(node, target, this.gradientEdit.layer) as VectorGradient;\n\t\tconst bounds = vectorPathLocalBounds(node);\n\t\tconst local = (q: Vec2) => this.toLocal(id, q, base);\n\t\tconst part = drag.part;\n\t\tlet next: VectorGradient = g;\n\t\tif (part.kind === 'handle') {\n\t\t\tlet to = p;\n\t\t\t// Shift keeps the vector at 45° steps from the start.\n\t\t\tif (part.handle === 'end' || part.handle === 'radius') to = constrained(this.screenToDocument(drag.annotator!.start), p);\n\t\t\tnext = moveVectorGradientHandle(g, bounds, part.handle, local(to), local(drag.startDoc));\n\t\t} else if (part.kind === 'bar') {\n\t\t\tnext = moveVectorGradientHandle(g, bounds, 'start', local(p), local(drag.startDoc));\n\t\t} else {\n\t\t\tconst a = drag.annotator!;\n\t\t\tconst pos = vectorGradientBarPosition(a, screen);\n\t\t\tconst stops = sortedVectorGradientStops(g);\n\t\t\tif (part.kind === 'stop') {\n\t\t\t\tconst off = Math.abs(pos.across - VECTOR_GRADIENT_STOP_GAP) > VECTOR_GRADIENT_OFF_BAR;\n\t\t\t\tconst removed = off ? removeVectorGradientStop(stops, part.index) : null;\n\t\t\t\tif (removed) {\n\t\t\t\t\tnext = { ...g, stops: removed };\n\t\t\t\t\tdrag.stop = null;\n\t\t\t\t} else {\n\t\t\t\t\tconst r = moveVectorGradientStop(stops, part.index, Math.min(1, Math.max(0, pos.offset)));\n\t\t\t\t\tnext = { ...g, stops: r.stops };\n\t\t\t\t\tdrag.stop = r.index;\n\t\t\t\t}\n\t\t\t\tthis.interaction = { type: 'gradient', part: 'stop', ...(removed ? { removing: true } : {}) };\n\t\t\t\tthis.gradientEdit = { ...this.gradientEdit, stop: drag.stop };\n\t\t\t} else {\n\t\t\t\tconst m = vectorGradientMidpointFromPosition(stops, part.index, pos.offset);\n\t\t\t\tif (m !== null) next = { ...g, stops: stops.map((s, i) => (i !== part.index ? s : Math.abs(m - 0.5) < 1e-3 ? (({ midpoint: _m, ...rest }) => rest)(s) : { ...s, midpoint: m })) };\n\t\t\t}\n\t\t}\n\t\tthis.doc = this.withGradient(base, id, target, next);\n\t\tthis.notify(true);\n\t}\n\n\tprivate upGradient(drag: Extract<Drag, { kind: 'gradient' }>) {\n\t\tconst part = drag.part;\n\t\tif (!drag.moved) {\n\t\t\tif (part?.kind === 'bar') {\n\t\t\t\tthis.addGradientStop(part.offset);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tconst label = !part\n\t\t\t? 'Gradient'\n\t\t\t: part.kind === 'stop'\n\t\t\t\t? drag.stop === null\n\t\t\t\t\t? 'Delete Gradient Stop'\n\t\t\t\t\t: 'Move Gradient Stop'\n\t\t\t\t: part.kind === 'midpoint'\n\t\t\t\t\t? 'Gradient Midpoint'\n\t\t\t\t\t: part.kind === 'bar' || (part.kind === 'handle' && part.handle === 'start')\n\t\t\t\t\t\t? 'Move Gradient'\n\t\t\t\t\t\t: 'Gradient Handle';\n\t\treturn this.commit(label, drag.before);\n\t}\n\n\t/** Removes one width point of a path's profile. */\n\tremoveWidthPoint(ref: Pick<VectorWidthPointRef, 'nodeId' | 'index'>): VectorActionResult {\n\t\treturn this.removeWidthPoints([ref]);\n\t}\n\n\t/** Removes width points (default: the selected ones — the width tool's Delete), as one undo step. */\n\tremoveWidthPoints(refs: readonly Pick<VectorWidthPointRef, 'nodeId' | 'index'>[] = this.widthPoints): VectorActionResult {\n\t\tconst byNode = new Map<string, number[]>();\n\t\tfor (const r of refs) {\n\t\t\tconst node = this.getNode(r.nodeId);\n\t\t\tif (node?.type !== 'path' || !node.strokeWidthProfile?.[r.index]) continue;\n\t\t\tbyNode.set(r.nodeId, [...(byNode.get(r.nodeId) ?? []), r.index]);\n\t\t}\n\t\tif (!byNode.size) return { ok: false, message: 'There is no such width point.' };\n\t\tconst count = [...byNode.values()].reduce((n, l) => n + new Set(l).size, 0);\n\t\tthis.widthPoints = [];\n\t\tthis.widthEdit = null;\n\t\tthis.change(count > 1 ? 'Delete Width Points' : 'Delete Width Point', () => {\n\t\t\tfor (const [id, indices] of byNode) {\n\t\t\t\tconst node = this.getNode(id) as VectorPathNode;\n\t\t\t\tconst rest = removeWidthPoints(sanitizeWidthProfile(node.strokeWidthProfile), indices);\n\t\t\t\tthis.replaceNode(id, withStyle(node, { strokeWidthProfile: rest.length ? rest : null }));\n\t\t\t}\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** Selects width points (width tool); points on other paths than the first are dropped. */\n\tselectWidthPoints(refs: readonly VectorWidthPointRef[]): void {\n\t\tconst first = refs[0]?.nodeId;\n\t\tconst next: VectorWidthPointRef[] = [];\n\t\tfor (const r of refs) {\n\t\t\tif (r.nodeId !== first || next.some((q) => q.index === r.index)) continue;\n\t\t\tconst n = this.getNode(r.nodeId);\n\t\t\tif (n?.type === 'path' && n.strokeWidthProfile?.[r.index]) next.push({ nodeId: r.nodeId, index: r.index, subpath: r.subpath ?? 0 });\n\t\t}\n\t\tthis.widthPoints = next;\n\t\tthis.notify();\n\t}\n\n\t/**\n\t * Sets one width point numerically: `index` names an existing point of path `nodeId`, null\n\t * (or left out) adds one at `values.t`. `left` / `right` are side widths in document units\n\t * (how far the stroke reaches on each side of the path), `t` the position along each subpath\n\t * (0–1). A point moved onto another one's `t` makes a step there. Selects the point and\n\t * refreshes an open width-point editor; one undo step. Ported from ArtCraft vectorcraft's\n\t * Width Point Edit dialog (`crates/tools/src/distort/width.rs`, MIT OR Apache-2.0).\n\t */\n\tsetWidthPoint(target: { nodeId: string; index?: number | null }, values: { t?: number; left?: number; right?: number }): VectorActionResult {\n\t\tconst node = this.getNode(target.nodeId);\n\t\tif (node?.type !== 'path') return { ok: false, message: 'There is no such path.' };\n\t\tconst g = vectorStrokeWidthGeometry(node);\n\t\tif (!g || !(g.unit > 0)) return { ok: false, message: 'The path has no visible stroke.' };\n\t\tconst num = (v: unknown) => (typeof v === 'number' && Number.isFinite(v) ? v : undefined);\n\t\tconst t = num(values.t);\n\t\tconst left = num(values.left);\n\t\tconst right = num(values.right);\n\t\tconst scale = (w: number | undefined) => (w === undefined ? undefined : Math.max(0, w) / g.unit);\n\t\tconst profile = sanitizeWidthProfile(node.strokeWidthProfile);\n\t\tlet points: VectorWidthPoint[];\n\t\tlet index: number;\n\t\tconst existing = target.index ?? null;\n\t\tif (existing === null) {\n\t\t\tif (t === undefined) return { ok: false, message: 'A new width point needs a position t.' };\n\t\t\tif (profile.length >= VECTOR_WIDTH_PROFILE_MAX_POINTS) return { ok: false, message: 'The profile has too many width points.' };\n\t\t\t({ points, index } = insertWidthPoint(profile.length ? profile : null, t, { left: scale(left), right: scale(right) }, { step: true }));\n\t\t} else {\n\t\t\tconst p = profile[existing];\n\t\t\tif (!p) return { ok: false, message: 'There is no such width point.' };\n\t\t\tconst sides = { left: scale(left) ?? p.left, right: scale(right) ?? p.right };\n\t\t\tconst tt = t === undefined ? p.t : Math.min(Math.max(t, 0), 1);\n\t\t\tif (Math.abs(tt - p.t) <= VECTOR_WIDTH_SAME_T) {\n\t\t\t\tpoints = setWidthPoint(profile, existing, sides);\n\t\t\t\tindex = existing;\n\t\t\t} else {\n\t\t\t\t// Free to pass its neighbours: take it out and put it back at its new place.\n\t\t\t\tconst rest = removeWidthPoints(profile, [existing]);\n\t\t\t\tif (rest.length) ({ points, index } = insertWidthPoint(rest, tt, sides, { step: true }));\n\t\t\t\telse {\n\t\t\t\t\tpoints = [{ t: tt, ...sides }];\n\t\t\t\t\tindex = 0;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tconst subpath = this.widthEdit?.nodeId === node.id ? this.widthEdit.subpath : this.widthPoints.find((r) => r.nodeId === node.id)?.subpath ?? 0;\n\t\tconst edit = this.widthEdit;\n\t\tthis.change('Width Point', () => this.replaceNode(node.id, withStyle(node, { strokeWidthProfile: points })));\n\t\tthis.widthPoints = [{ nodeId: node.id, index, subpath }];\n\t\tif (edit?.nodeId === node.id && (edit.index === existing || existing === null)) this.widthEdit = this.widthEditFor(node.id, subpath, index, null);\n\t\tthis.notify();\n\t\treturn { ok: true };\n\t}\n\n\t/**\n\t * Opens the width-point editor (what a double-click with the width tool does) on a width\n\t * point, or on a spot of a stroke (`t` without `index`: applying it adds a point there).\n\t * Returns false when the path has no visible stroke.\n\t */\n\topenWidthPointEdit(target: { nodeId: string; subpath?: number; index?: number | null; t?: number }): boolean {\n\t\tconst edit = this.widthEditFor(target.nodeId, target.subpath ?? 0, target.index ?? null, target.t ?? null);\n\t\tif (!edit) return false;\n\t\tthis.widthEdit = edit;\n\t\tthis.widthPoints = edit.index === null ? [] : [{ nodeId: edit.nodeId, index: edit.index, subpath: edit.subpath }];\n\t\tthis.notify();\n\t\treturn true;\n\t}\n\n\t/** Closes the width-point editor without changing anything. */\n\tcloseWidthPointEdit(): void {\n\t\tif (!this.widthEdit) return;\n\t\tthis.widthEdit = null;\n\t\tthis.notify();\n\t}\n\n\t/** Applies `values` to the width point being edited (see `setWidthPoint`) and closes the editor. */\n\tcommitWidthPointEdit(values: { t?: number; left?: number; right?: number }): VectorActionResult {\n\t\tconst edit = this.widthEdit;\n\t\tif (!edit) return { ok: false, message: 'No width point is being edited.' };\n\t\tconst r = this.setWidthPoint({ nodeId: edit.nodeId, index: edit.index }, edit.index === null ? { t: edit.t, ...values } : values);\n\t\tif (r.ok) this.closeWidthPointEdit();\n\t\treturn r;\n\t}\n\n\t/** The editor's view of point `index` (or of the stroke at `t` when index is null) of `nodeId`. */\n\tprivate widthEditFor(nodeId: string, subpath: number, index: number | null, t: number | null): VectorWidthPointEdit | null {\n\t\tconst node = this.getNode(nodeId);\n\t\tif (node?.type !== 'path') return null;\n\t\tconst g = vectorStrokeWidthGeometry(node);\n\t\tif (!g) return null;\n\t\tconst profile = sanitizeWidthProfile(node.strokeWidthProfile);\n\t\tconst p = index === null ? null : profile[index];\n\t\tif (index !== null && !p) return null;\n\t\tconst tt = p ? p.t : Math.min(Math.max(t ?? 0, 0), 1);\n\t\tconst sides = p ?? widthProfileAt(profile.length ? profile : null, tt);\n\t\tconst sp = Math.min(Math.max(0, subpath), Math.max(0, node.subpaths.length - 1));\n\t\tconst at = strokePositionPoint(g.line, sp, tt);\n\t\tconst round = (v: number) => Math.round(v * 1e6) / 1e6;\n\t\tconst q = at ? applyMatrix(this.worldOf(nodeId), { x: at.point[0], y: at.point[1] }) : { x: 0, y: 0 };\n\t\tconst point = { x: round(q.x), y: round(q.y) };\n\t\treturn { nodeId, subpath: sp, index, t: round(tt), left: round(sides.left * g.unit), right: round(sides.right * g.unit), point };\n\t}\n\n\t/**\n\t * Sets the live corner radius (and optionally the kind) of anchors: `refs`, else the selected\n\t * anchors (direct tool), else every anchor of the `ids` / selected paths. Radius 0 makes the\n\t * corners sharp again. Corners shrink to fit when drawn, so any radius is accepted.\n\t */\n\tsetCornerRadius(radius: number, options: { kind?: VectorCornerKind; smoothing?: number; refs?: readonly VectorAnchorRef[]; ids?: readonly string[] } = {}): VectorActionResult {\n\t\tif (!Number.isFinite(radius) || radius < 0) return { ok: false, message: 'The radius must be a number of at least 0.' };\n\t\tif (options.smoothing !== undefined && !(options.smoothing >= 0 && options.smoothing <= 1)) return { ok: false, message: 'Corner smoothing must be from 0 to 1.' };\n\t\tconst refs = this.cornerTargets(options);\n\t\tif (!refs.length) return { ok: false, message: 'Nothing is selected.' };\n\t\tthis.change(radius > 0 ? 'Corner Radius' : 'Sharpen Corners', () => {\n\t\t\tthis.doc = this.updateAnchors(this.doc, refs, (a) => setAnchorCorner(a, radius, options.kind, options.smoothing));\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** Sets corner smoothing (squircle, 0–1) of the same anchors as `setCornerRadius`, keeping their radii. */\n\tsetCornerSmoothing(smoothing: number, options: { refs?: readonly VectorAnchorRef[]; ids?: readonly string[]; merge?: string } = {}): VectorActionResult {\n\t\tif (!(smoothing >= 0 && smoothing <= 1)) return { ok: false, message: 'Corner smoothing must be from 0 to 1.' };\n\t\tconst refs = this.cornerTargets(options).filter((r) => (getAnchor(this.doc, r)?.corner?.radius ?? 0) > 0);\n\t\tif (!refs.length) return { ok: false, message: 'Give the corners a radius first: smoothing shapes rounded corners.' };\n\t\tthis.change(\n\t\t\t'Corner Smoothing',\n\t\t\t() => {\n\t\t\t\tthis.doc = this.updateAnchors(this.doc, refs, (a) => setAnchorCorner(a, a.corner?.radius ?? 0, undefined, smoothing));\n\t\t\t},\n\t\t\toptions.merge\n\t\t);\n\t\treturn { ok: true };\n\t}\n\n\t/** Sets the live corner kind of the same anchors as `setCornerRadius`, keeping their radii. */\n\tsetCornerKind(kind: VectorCornerKind, options: { refs?: readonly VectorAnchorRef[]; ids?: readonly string[] } = {}): VectorActionResult {\n\t\tconst refs = this.cornerTargets(options);\n\t\tif (!refs.length) return { ok: false, message: 'Nothing is selected.' };\n\t\tthis.change('Corner Kind', () => {\n\t\t\tthis.doc = this.updateAnchors(this.doc, refs, (a) => setAnchorCorner(a, a.corner?.radius ?? 0, kind));\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** Anchors a corner change applies to (see `setCornerRadius`). */\n\tprivate cornerTargets(options: { refs?: readonly VectorAnchorRef[]; ids?: readonly string[] }): VectorAnchorRef[] {\n\t\tif (options.refs) return options.refs.filter((r) => getAnchor(this.doc, r));\n\t\tif (!options.ids && this.tool === 'direct' && this.anchors.length) return this.anchors.slice();\n\t\tconst out: VectorAnchorRef[] = [];\n\t\tfor (const id of options.ids ?? this.selection) {\n\t\t\tconst node = this.getNode(id);\n\t\t\tif (!node) continue;\n\t\t\tfor (const p of collectPathNodes(node)) {\n\t\t\t\tif (p.locked) continue;\n\t\t\t\tp.subpaths.forEach((sp, s) => sp.anchors.forEach((_, i) => out.push({ nodeId: p.id, subpath: s, index: i })));\n\t\t\t}\n\t\t}\n\t\treturn out;\n\t}\n\n\t/**\n\t * Outline Stroke: replaces each selected (or given) path's stroke with a filled path of the\n\t * area it paints (alignment, joins, caps, dashes and arrowheads included), placed above a copy\n\t * of the path that keeps the fill (left out when there is none). Returns the new paths' ids.\n\t */\n\toutlineStroke(options: { ids?: readonly string[] } = {}): string[] {\n\t\tconst roots = options.ids ?? this.selectedRoots();\n\t\tconst strokesOf = (p: VectorPathNode) => vectorPaintLayerNodes(p).filter((l) => l.kind === 'stroke' && l.drawn && (l.node.strokeWidth ?? 1) > 0);\n\t\tconst paths: VectorPathNode[] = [];\n\t\tfor (const id of roots) {\n\t\t\tconst node = this.getNode(id);\n\t\t\tif (node) for (const p of collectPathNodes(node)) if (!p.locked && strokesOf(p).length) paths.push(p);\n\t\t}\n\t\tif (!paths.length) return [];\n\t\tconst created: string[] = [];\n\t\tconst replace = new Map<string, VectorNode[]>();\n\t\tfor (const p of paths) {\n\t\t\tconst { effects, ...rest } = cloneVectorNode(p);\n\t\t\tconst unstroked = withVectorStrokes(rest as VectorPathNode, []);\n\t\t\tfor (const key of VECTOR_STROKE_SETTING_KEYS) delete unstroked[key];\n\t\t\tconst list: VectorNode[] = vectorFills(p).length ? [{ ...unstroked, ...(effects ? { effects } : {}) }] : [];\n\t\t\t// One filled path per stroke layer, bottom to top, painted (and blended) like it.\n\t\t\tconst layers = strokesOf(p);\n\t\t\tfor (const l of layers) {\n\t\t\t\t// In the node's own space: the new paths keep its transform.\n\t\t\t\t// As drawn: path effects applied (the outline itself carries none).\n\t\t\t\tconst outline = bezierToSubpaths(vectorStrokeOutlinePath(vectorAppearanceOutline(p), l.node));\n\t\t\t\tif (!outline.length) continue;\n\t\t\t\tconst id = this.newId();\n\t\t\t\tconst { strokes: _s, fills: _f, ...plain } = unstroked;\n\t\t\t\tconst name = layers.length > 1 ? `${p.name ?? 'Path'} Stroke ${l.index + 1}` : `${p.name ?? 'Path'} Stroke`;\n\t\t\t\tconst fill: VectorPaintLayer = { paint: l.layer.paint };\n\t\t\t\tif (l.layer.opacity !== undefined) fill.opacity = l.layer.opacity;\n\t\t\t\tif (l.layer.blendMode) fill.blendMode = l.layer.blendMode;\n\t\t\t\tlist.push(withVectorFills({ ...plain, id, name, subpaths: outline, stroke: null, fillRule: 'nonzero' } as VectorPathNode, [fill]));\n\t\t\t\tcreated.push(id);\n\t\t\t}\n\t\t\treplace.set(p.id, list);\n\t\t}\n\t\tif (!created.length) return [];\n\t\tthis.change('Outline Stroke', () => {\n\t\t\tlet nodes = this.doc.nodes;\n\t\t\tfor (const [id, list] of replace) nodes = spliceTree(nodes, id, list);\n\t\t\tthis.setDoc(nodes);\n\t\t\tthis.selection = created;\n\t\t\tthis.anchors = [];\n\t\t\tthis.editingId = null;\n\t\t});\n\t\treturn created;\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Fill and stroke stacks, blend modes, filters\n\t// -------------------------------------------------------------------------\n\n\t/** Paths of the selected (or given) nodes whose stacks can be edited (groups give every path inside; blend steps are left out). */\n\tprivate stackTargets(ids: readonly string[] | undefined): VectorPathNode[] | string {\n\t\tconst list = ids ?? this.selection;\n\t\tif (!list.length) return 'Nothing is selected.';\n\t\tconst out = new Map<string, VectorPathNode>();\n\t\tfor (const id of list) {\n\t\t\tconst node = this.getNode(id);\n\t\t\tif (!node) return `There is no node \"${id}\".`;\n\t\t\tfor (const p of collectPaintedNodes(node)) if (!p.blendStep) out.set(p.id, p as VectorPathNode);\n\t\t}\n\t\tif (!out.size) return 'The selection has no paths or text (fills and strokes belong to them).';\n\t\treturn [...out.values()];\n\t}\n\n\t/** Applies `fn` to the `kind` stack of every target path as one undo step (an error string from any path cancels it). */\n\tprivate editStacks(\n\t\tkind: 'fill' | 'stroke',\n\t\tids: readonly string[] | undefined,\n\t\tlabel: string,\n\t\tfn: (stack: VectorStrokeLayer[], path: VectorPathNode) => VectorStrokeLayer[] | string,\n\t\tmerge?: string\n\t): VectorActionResult {\n\t\tconst targets = this.stackTargets(ids);\n\t\tif (typeof targets === 'string') return { ok: false, message: targets };\n\t\tconst next = new Map<string, VectorNode>();\n\t\tfor (const path of targets) {\n\t\t\tconst stack = kind === 'fill' ? vectorFills(path) : vectorStrokes(path);\n\t\t\tconst result = fn(stack.map((l) => ({ ...l })), path);\n\t\t\tif (typeof result === 'string') return { ok: false, message: result };\n\t\t\tnext.set(path.id, kind === 'fill' ? withVectorFills(path, result) : withVectorStrokes(path, result));\n\t\t}\n\t\tthis.change(label, () => this.setDoc(mapTree(this.doc.nodes, (n) => next.get(n.id) ?? n)), merge);\n\t\treturn { ok: true };\n\t}\n\n\t/**\n\t * Reads a layer patch from untrusted input: paint, opacity, blend mode, visibility and (strokes)\n\t * the stroke settings, where `null` removes a setting. Returns an error string for invalid values.\n\t */\n\tprivate layerPatch(kind: 'fill' | 'stroke', patch: Record<string, unknown>): { set: Partial<VectorStrokeLayer>; unset: string[] } | string {\n\t\tconst set: Record<string, unknown> = {};\n\t\tconst unset: string[] = [];\n\t\tif ('paint' in patch) {\n\t\t\tconst paint = patch.paint;\n\t\t\tif (paint === null || (typeof paint === 'string' && paint.trim())) set.paint = paint === null ? null : (paint as string).trim();\n\t\t\telse {\n\t\t\t\tconst g = sanitizeVectorGradient(paint);\n\t\t\t\tif (!g) return 'paint must be a CSS colour, a gradient { type, stops: [{ offset, color }] } or null.';\n\t\t\t\tset.paint = g;\n\t\t\t}\n\t\t}\n\t\tif (patch.opacity !== undefined) {\n\t\t\tif (typeof patch.opacity !== 'number' || !Number.isFinite(patch.opacity)) return 'opacity must be a number from 0 to 1.';\n\t\t\tconst o = Math.min(1, Math.max(0, patch.opacity));\n\t\t\tif (o < 1) set.opacity = o;\n\t\t\telse unset.push('opacity');\n\t\t}\n\t\tif (patch.blendMode !== undefined) {\n\t\t\tif (!isVectorBlendMode(patch.blendMode)) return `Unknown blend mode \"${String(patch.blendMode)}\". Use one of: ${VECTOR_BLEND_MODES.join(', ')}.`;\n\t\t\tif (patch.blendMode !== 'normal') set.blendMode = patch.blendMode;\n\t\t\telse unset.push('blendMode');\n\t\t}\n\t\tif (patch.visible !== undefined) {\n\t\t\tif (typeof patch.visible !== 'boolean') return 'visible must be true or false.';\n\t\t\tif (patch.visible) unset.push('visible');\n\t\t\telse set.visible = false;\n\t\t}\n\t\tif (kind === 'stroke') {\n\t\t\tconst settings = sanitizeVectorStrokeSettings(patch);\n\t\t\tfor (const key of VECTOR_STROKE_SETTING_KEYS) {\n\t\t\t\tif (!(key in patch) || patch[key] === undefined) continue;\n\t\t\t\tif (patch[key] === null || (key === 'strokeDasharray' && Array.isArray(patch[key]) && !(patch[key] as unknown[]).length)) unset.push(key);\n\t\t\t\telse if (settings[key] === undefined) return `Invalid ${key}: ${JSON.stringify(patch[key])}.`;\n\t\t\t\telse set[key] = settings[key];\n\t\t\t}\n\t\t}\n\t\treturn { set: set as Partial<VectorStrokeLayer>, unset };\n\t}\n\n\t/**\n\t * Adds a fill or stroke layer to the selected (or given) paths (groups: every path inside), at\n\t * the top of the stack or at `index` (0 is the bottom). A new stroke takes the settings of the\n\t * stack's top stroke unless `layer` gives them. One undo step.\n\t */\n\taddPaintLayer(kind: 'fill' | 'stroke', layer: Record<string, unknown> = {}, options: { ids?: readonly string[]; index?: number } = {}): VectorActionResult {\n\t\tconst patch = this.layerPatch(kind, layer);\n\t\tif (typeof patch === 'string') return { ok: false, message: patch };\n\t\treturn this.editStacks(kind, options.ids, kind === 'fill' ? 'Add Fill' : 'Add Stroke', (stack, path) => {\n\t\t\tconst base: VectorStrokeLayer = kind === 'fill' ? { paint: '#808080' } : newVectorStrokeLayer(path, {});\n\t\t\tconst added = { ...base, ...patch.set } as VectorStrokeLayer;\n\t\t\tfor (const key of patch.unset) delete (added as unknown as Record<string, unknown>)[key];\n\t\t\tconst at = options.index === undefined ? stack.length : Math.min(stack.length, Math.max(0, Math.round(options.index)));\n\t\t\treturn [...stack.slice(0, at), added, ...stack.slice(at)];\n\t\t});\n\t}\n\n\t/**\n\t * Changes layer `index` (0 is the bottom) of the fill or stroke stack of the selected (or given)\n\t * paths: paint, opacity, blend mode, visibility and, for strokes, any stroke setting (`null`\n\t * removes it). Fails, changing nothing, when a path has no layer `index`. With `merge` (true, or\n\t * a key), consecutive edits of the layer fold into one undo step (a slider drag).\n\t */\n\tupdatePaintLayer(kind: 'fill' | 'stroke', index: number, patch: Record<string, unknown>, options: { ids?: readonly string[]; merge?: boolean | string } = {}): VectorActionResult {\n\t\tconst parsed = this.layerPatch(kind, patch);\n\t\tif (typeof parsed === 'string') return { ok: false, message: parsed };\n\t\tconst merge = options.merge === true ? `paint-layer:${kind}:${index}` : options.merge || undefined;\n\t\treturn this.editStacks(\n\t\t\tkind,\n\t\t\toptions.ids,\n\t\t\tkind === 'fill' ? 'Edit Fill' : 'Edit Stroke',\n\t\t\t(stack, path) => {\n\t\t\t\tif (!stack[index]) return `Path \"${path.id}\" has no ${kind} ${index} (it has ${stack.length}; 0 is the bottom).`;\n\t\t\t\tconst layer = { ...stack[index], ...parsed.set } as VectorStrokeLayer;\n\t\t\t\tfor (const key of parsed.unset) delete (layer as unknown as Record<string, unknown>)[key];\n\t\t\t\treturn stack.map((l, i) => (i === index ? layer : l));\n\t\t\t},\n\t\t\tmerge\n\t\t);\n\t}\n\n\t/** Removes layer `index` of the fill or stroke stack of the selected (or given) paths. */\n\tremovePaintLayer(kind: 'fill' | 'stroke', index: number, options: { ids?: readonly string[] } = {}): VectorActionResult {\n\t\treturn this.editStacks(kind, options.ids, kind === 'fill' ? 'Remove Fill' : 'Remove Stroke', (stack, path) =>\n\t\t\tstack[index] ? stack.filter((_, i) => i !== index) : `Path \"${path.id}\" has no ${kind} ${index} (it has ${stack.length}; 0 is the bottom).`\n\t\t);\n\t}\n\n\t/** Moves layer `from` of the fill or stroke stack to position `to` (0 is the bottom) on the selected (or given) paths. */\n\tmovePaintLayer(kind: 'fill' | 'stroke', from: number, to: number, options: { ids?: readonly string[] } = {}): VectorActionResult {\n\t\treturn this.editStacks(kind, options.ids, kind === 'fill' ? 'Reorder Fills' : 'Reorder Strokes', (stack, path) => {\n\t\t\tif (!stack[from]) return `Path \"${path.id}\" has no ${kind} ${from} (it has ${stack.length}; 0 is the bottom).`;\n\t\t\tconst out = stack.slice();\n\t\t\tconst [layer] = out.splice(from, 1);\n\t\t\tout.splice(Math.min(out.length, Math.max(0, Math.round(to))), 0, layer);\n\t\t\treturn out;\n\t\t});\n\t}\n\n\t/** Replaces the whole fill or stroke stack of the selected (or given) paths (malformed layers are dropped). */\n\tsetPaintLayers(kind: 'fill' | 'stroke', layers: readonly unknown[], options: { ids?: readonly string[] } = {}): VectorActionResult {\n\t\tconst clean = kind === 'fill' ? sanitizeVectorFills(layers) : sanitizeVectorStrokes(layers);\n\t\treturn this.editStacks(kind, options.ids, kind === 'fill' ? 'Set Fills' : 'Set Strokes', () => clean as VectorStrokeLayer[]);\n\t}\n\n\t/** Sets the blend mode of the selected (or given) nodes (paths or groups); `normal` removes it. */\n\tsetBlendMode(mode: VectorBlendMode, options: { ids?: readonly string[] } = {}): VectorActionResult {\n\t\tif (!isVectorBlendMode(mode)) return { ok: false, message: `Unknown blend mode \"${String(mode)}\". Use one of: ${VECTOR_BLEND_MODES.join(', ')}.` };\n\t\tconst ids = options.ids ?? this.selectedRoots();\n\t\tif (!ids.length) return { ok: false, message: 'Nothing is selected.' };\n\t\tfor (const id of ids) if (!this.getNode(id)) return { ok: false, message: `There is no node \"${id}\".` };\n\t\tconst set = new Set(ids);\n\t\tthis.change('Blend Mode', () =>\n\t\t\tthis.setDoc(\n\t\t\t\tmapTree(this.doc.nodes, (n) => {\n\t\t\t\t\tif (!set.has(n.id) || (n.blendMode ?? 'normal') === mode) return n;\n\t\t\t\t\tconst { blendMode: _b, ...rest } = n;\n\t\t\t\t\treturn (mode === 'normal' ? rest : { ...rest, blendMode: mode }) as VectorNode;\n\t\t\t\t})\n\t\t\t)\n\t\t);\n\t\treturn { ok: true };\n\t}\n\n\t/**\n\t * Adds a filter of `type` to the selected (or given) nodes, at the end of each stack (or at\n\t * `index`), with default parameters merged with `params`. One undo step; returns the new filter\n\t * ids (one per node, in order), empty for an unknown type or nothing selected.\n\t */\n\taddFilter(type: VectorFilterType, options: { ids?: readonly string[]; params?: Record<string, unknown>; index?: number } = {}): string[] {\n\t\tif (!isVectorFilterType(type)) return [];\n\t\tconst targets = this.effectTargets(options.ids);\n\t\tif (!targets.length) return [];\n\t\tconst created: string[] = [];\n\t\tconst next = new Map<string, VectorNode>();\n\t\tfor (const node of targets) {\n\t\t\tconst list = node.filters ?? [];\n\t\t\tlet k = list.length + 1;\n\t\t\twhile (list.some((f) => f.id === `filter${k}`)) k++;\n\t\t\tconst filter = createVectorFilter(type, `filter${k}`, options.params)!;\n\t\t\tconst at = options.index === undefined ? list.length : Math.min(list.length, Math.max(0, Math.round(options.index)));\n\t\t\tnext.set(node.id, { ...node, filters: [...list.slice(0, at), filter, ...list.slice(at)] } as VectorNode);\n\t\t\tcreated.push(filter.id);\n\t\t}\n\t\tthis.change(`Add ${VECTOR_FILTER_LABELS[type]}`, () => this.setDoc(mapTree(this.doc.nodes, (n) => next.get(n.id) ?? n)));\n\t\treturn created;\n\t}\n\n\tprivate editFilters(nodeId: string, label: string, fn: (filters: VectorFilter[]) => VectorFilter[] | string, merge?: string): VectorActionResult {\n\t\tconst node = this.getNode(nodeId);\n\t\tif (!node) return { ok: false, message: `There is no node \"${nodeId}\".` };\n\t\tconst result = fn(node.filters ?? []);\n\t\tif (typeof result === 'string') return { ok: false, message: result };\n\t\tthis.change(label, () => this.replaceNode(nodeId, (result.length ? { ...node, filters: result } : (({ filters: _f, ...rest }) => rest)(node)) as VectorNode), merge);\n\t\treturn { ok: true };\n\t}\n\n\t/**\n\t * Turns a filter on or off and changes its parameters (invalid values are ignored). With\n\t * `merge`, consecutive updates of the same filter fold into one undo step.\n\t */\n\tupdateFilter(nodeId: string, filterId: string, patch: { enabled?: boolean; params?: Record<string, unknown> }, options: { merge?: boolean | string } = {}): VectorActionResult {\n\t\tconst merge = options.merge === true ? `filter:${nodeId}:${filterId}` : options.merge || undefined;\n\t\tconst current = this.getNode(nodeId)?.filters?.find((f) => f.id === filterId);\n\t\treturn this.editFilters(\n\t\t\tnodeId,\n\t\t\t`Edit ${current ? VECTOR_FILTER_LABELS[current.type] : 'Filter'}`,\n\t\t\t(list) => {\n\t\t\t\tconst i = list.findIndex((f) => f.id === filterId);\n\t\t\t\tif (i < 0) return `Node \"${nodeId}\" has no filter \"${filterId}\".`;\n\t\t\t\tconst f = list[i];\n\t\t\t\tconst params = patch.params ? normalizeVectorFilterParams(f.type, patch.params, f.params as never) : f.params;\n\t\t\t\tconst next = { ...f, params, ...(patch.enabled !== undefined ? { enabled: patch.enabled } : {}) } as VectorFilter;\n\t\t\t\tif (next.enabled !== false) delete (next as { enabled?: boolean }).enabled;\n\t\t\t\treturn list.map((x, j) => (j === i ? next : x));\n\t\t\t},\n\t\t\tmerge\n\t\t);\n\t}\n\n\t/** Removes a filter from a node's stack. */\n\tremoveFilter(nodeId: string, filterId: string): VectorActionResult {\n\t\treturn this.editFilters(nodeId, 'Remove Filter', (list) => (list.some((f) => f.id === filterId) ? list.filter((f) => f.id !== filterId) : `Node \"${nodeId}\" has no filter \"${filterId}\".`));\n\t}\n\n\t/** Moves a filter to position `index` of its node's stack. */\n\tmoveFilter(nodeId: string, filterId: string, index: number): VectorActionResult {\n\t\treturn this.editFilters(nodeId, 'Reorder Filters', (list) => {\n\t\t\tconst i = list.findIndex((f) => f.id === filterId);\n\t\t\tif (i < 0) return `Node \"${nodeId}\" has no filter \"${filterId}\".`;\n\t\t\tconst out = list.slice();\n\t\t\tconst [f] = out.splice(i, 1);\n\t\t\tout.splice(Math.min(out.length, Math.max(0, Math.round(index))), 0, f);\n\t\t\treturn out;\n\t\t});\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Path effects\n\t// -------------------------------------------------------------------------\n\n\t/** Selected (or given) nodes that can carry effects: the roots of the selection. */\n\tprivate effectTargets(ids?: readonly string[]): VectorNode[] {\n\t\treturn (ids ?? this.selectedRoots()).map((id) => this.getNode(id)).filter((n): n is VectorNode => !!n && !n.blendStep);\n\t}\n\n\t/**\n\t * Adds an effect of `type` to the selected (or given) nodes, at the bottom of each stack (or\n\t * at `index`), with default parameters merged with `params`. A repeater's copies start one\n\t * shape width apart. One undo step; returns the new effect ids (one per node, in order).\n\t */\n\taddEffect(type: VectorEffectType, options: { ids?: readonly string[]; params?: Record<string, unknown>; index?: number } = {}): string[] {\n\t\tconst targets = this.effectTargets(options.ids);\n\t\tif (!targets.length || !createVectorEffect(type, '')) return [];\n\t\tconst created: string[] = [];\n\t\tconst next = new Map<string, VectorNode>();\n\t\tfor (const node of targets) {\n\t\t\tconst list = node.effects ?? [];\n\t\t\tlet k = list.length + 1;\n\t\t\twhile (list.some((e) => e.id === `fx${k}`)) k++;\n\t\t\tconst params: Record<string, unknown> = { ...(options.params ?? {}) };\n\t\t\tif (type === 'repeater' && params.position === undefined) {\n\t\t\t\tconst b = bezierPathBounds(vectorAppearanceOutline(node));\n\t\t\t\tif (b && b.maxX - b.minX > 0) params.position = { x: Math.round((b.maxX - b.minX) * 1.25 * 100) / 100, y: 0 };\n\t\t\t}\n\t\t\tconst effect = createVectorEffect(type, `fx${k}`, params)!;\n\t\t\tconst at = options.index === undefined ? list.length : Math.min(list.length, Math.max(0, Math.round(options.index)));\n\t\t\tnext.set(node.id, { ...node, effects: [...list.slice(0, at), effect, ...list.slice(at)] } as VectorNode);\n\t\t\tcreated.push(effect.id);\n\t\t}\n\t\tthis.change(`Add ${VECTOR_EFFECT_LABELS[type]}`, () => this.setDoc(mapTree(this.doc.nodes, (n) => next.get(n.id) ?? n)));\n\t\treturn created;\n\t}\n\n\tprivate editEffects(nodeId: string, label: string, fn: (effects: VectorEffect[]) => VectorEffect[] | string, merge?: string): VectorActionResult {\n\t\tconst node = this.getNode(nodeId);\n\t\tif (!node) return { ok: false, message: `There is no node \"${nodeId}\".` };\n\t\tconst result = fn(node.effects ?? []);\n\t\tif (typeof result === 'string') return { ok: false, message: result };\n\t\tthis.change(label, () => this.replaceNode(nodeId, (result.length ? { ...node, effects: result } : (({ effects: _e, ...rest }) => rest)(node)) as VectorNode), merge);\n\t\treturn { ok: true };\n\t}\n\n\t/**\n\t * Turns an effect on or off and changes its parameters (unknown or invalid values are\n\t * ignored). With `merge` (true, or a key of your own), consecutive updates of the same effect\n\t * fold into one undo step, as a slider drag should; call `endMergedEdit` when it ends.\n\t */\n\tupdateEffect(nodeId: string, effectId: string, patch: { enabled?: boolean; params?: Record<string, unknown> }, options: { merge?: boolean | string } = {}): VectorActionResult {\n\t\tconst merge = options.merge === true ? `effect:${nodeId}:${effectId}` : options.merge || undefined;\n\t\tconst node = this.getNode(nodeId);\n\t\tconst current = node?.effects?.find((e) => e.id === effectId);\n\t\tconst label = current ? VECTOR_EFFECT_LABELS[current.type] ?? 'Effect' : 'Effect';\n\t\treturn this.editEffects(\n\t\t\tnodeId,\n\t\t\t`Edit ${label}`,\n\t\t\t(list) => {\n\t\t\t\tconst i = list.findIndex((e) => e.id === effectId);\n\t\t\t\tif (i < 0) return `Node \"${nodeId}\" has no effect \"${effectId}\".`;\n\t\t\t\tconst e = list[i];\n\t\t\t\tconst raw = patch.params && e.type === 'envelope' && node ? this.envelopeParams(node, e, patch.params) : patch.params;\n\t\t\t\tconst params = raw ? normalizeVectorEffectParams(e.type, raw, e.params as never) : e.params;\n\t\t\t\tconst next = { ...e, params, ...(patch.enabled !== undefined ? { enabled: patch.enabled } : {}) } as VectorEffect;\n\t\t\t\tif (next.enabled !== false) delete (next as { enabled?: boolean }).enabled;\n\t\t\t\treturn list.map((x, j) => (j === i ? next : x));\n\t\t\t},\n\t\t\tmerge\n\t\t);\n\t}\n\n\t/** Removes an effect from a node's stack. */\n\tremoveEffect(nodeId: string, effectId: string): VectorActionResult {\n\t\treturn this.editEffects(nodeId, 'Remove Effect', (list) => (list.some((e) => e.id === effectId) ? list.filter((e) => e.id !== effectId) : `Node \"${nodeId}\" has no effect \"${effectId}\".`));\n\t}\n\n\t/** Moves an effect to position `index` of its stack (0 is applied first). */\n\tmoveEffect(nodeId: string, effectId: string, index: number): VectorActionResult {\n\t\treturn this.editEffects(nodeId, 'Reorder Effects', (list) => {\n\t\t\tconst i = list.findIndex((e) => e.id === effectId);\n\t\t\tif (i < 0) return `Node \"${nodeId}\" has no effect \"${effectId}\".`;\n\t\t\tconst to = Math.min(list.length - 1, Math.max(0, Math.round(index)));\n\t\t\tconst out = list.slice();\n\t\t\tconst [e] = out.splice(i, 1);\n\t\t\tout.splice(to, 0, e);\n\t\t\treturn out;\n\t\t});\n\t}\n\n\t/**\n\t * Expand Appearance: bakes the path effects and live booleans of the selected (or given)\n\t * nodes, and of everything inside them, into plain paths. A node keeps its id (a group when\n\t * it draws as several shapes); the shapes it made get new ids. Returns the ids of the expanded\n\t * nodes (empty when none was live).\n\t */\n\texpandAppearance(options: { ids?: readonly string[] } = {}): string[] {\n\t\tconst roots = (options.ids ?? this.selectedRoots()).map((id) => this.getNode(id)).filter((n): n is VectorNode => !!n && hasVectorAppearance(n));\n\t\tif (!roots.length) return [];\n\t\tconst fresh = (n: VectorNode): VectorNode => {\n\t\t\tconst id = n.id.includes('~fx') ? this.newId() : n.id;\n\t\t\treturn n.type === 'group' ? { ...n, id, children: n.children.map(fresh) } : { ...n, id };\n\t\t};\n\t\tconst bake = (n: VectorNode): VectorNode => {\n\t\t\tif (!hasVectorAppearance(n)) return n;\n\t\t\tif (!hasOwnVectorAppearance(n)) return { ...(n as VectorGroupNode), children: (n as VectorGroupNode).children.map(bake) };\n\t\t\tlet e = expandVectorAppearance(n);\n\t\t\t// A live group that draws as one shape becomes that path, in the group's place.\n\t\t\tif (e.type === 'group' && e.children.length === 1 && e.children[0].type === 'path') {\n\t\t\t\tconst { id: _id, opacity, name, ...child } = e.children[0];\n\t\t\t\tconst k = (e.opacity ?? 1) * (opacity ?? 1);\n\t\t\t\te = { ...child, id: e.id, name: e.name ?? name, ...placementOf(e), ...(k < 1 ? { opacity: k } : {}) } as VectorPathNode;\n\t\t\t}\n\t\t\treturn fresh(e);\n\t\t};\n\t\tconst next = new Map(roots.map((n) => [n.id, bake(n)]));\n\t\tthis.change('Expand Appearance', () => {\n\t\t\tthis.setDoc(mapTree(this.doc.nodes, (n) => next.get(n.id) ?? n));\n\t\t\tthis.anchors = [];\n\t\t\tthis.editingId = null;\n\t\t});\n\t\treturn roots.map((n) => n.id);\n\t}\n\n\trename(id: string, name: string): VectorActionResult {\n\t\treturn this.updateNode(id, { name }, { label: 'Rename' });\n\t}\n\n\tsetVisible(id: string, visible: boolean): VectorActionResult {\n\t\treturn this.updateNode(id, { visible }, { label: visible ? 'Show' : 'Hide' });\n\t}\n\n\tsetLocked(id: string, locked: boolean): VectorActionResult {\n\t\tconst result = this.updateNode(id, { locked }, { label: locked ? 'Lock' : 'Unlock' });\n\t\tif (locked && result.ok && this.selection.includes(id)) {\n\t\t\tthis.selection = this.selection.filter((s) => s !== id);\n\t\t\tthis.notify();\n\t\t}\n\t\treturn result;\n\t}\n\n\t/** Removes nodes by id. */\n\tremoveNodes(ids: readonly string[], options: { label?: string } = {}): VectorActionResult {\n\t\tconst drop = new Set(ids.filter((id) => this.getNode(id)));\n\t\tif (!drop.size) return { ok: false, message: 'Nothing to delete.' };\n\t\tthis.change(options.label ?? 'Delete', () => {\n\t\t\t// Deleting a mask releases its group (the next child must not become the mask).\n\t\t\tthis.setDoc(mapTree(this.doc.nodes, (n) => (drop.has(n.id) ? null : isVectorMaskGroup(n) && drop.has(n.children[0].id) ? { ...n, mask: null } : n)));\n\t\t\tthis.selection = this.selection.filter((id) => !drop.has(id) && this.getNode(id));\n\t\t\tthis.anchors = this.anchors.filter((r) => getAnchor(this.doc, r));\n\t\t\tif (this.editingId && !this.getNode(this.editingId)) this.editingId = null;\n\t\t\tif (this.pen && !this.getNode(this.pen.nodeId)) this.pen = null;\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Selection\n\t// -------------------------------------------------------------------------\n\n\tselect(ids: readonly string[], options: { add?: boolean; toggle?: boolean } = {}): void {\n\t\tconst valid = ids.filter((id) => this.getNode(id));\n\t\tlet next: string[];\n\t\tif (options.toggle) {\n\t\t\tconst set = new Set(this.selection);\n\t\t\tfor (const id of valid) set.has(id) ? set.delete(id) : set.add(id);\n\t\t\tnext = [...set];\n\t\t} else next = options.add ? [...new Set([...this.selection, ...valid])] : valid;\n\t\tthis.selection = next;\n\t\tif (!options.add && !options.toggle) this.anchors = this.anchors.filter((r) => next.includes(r.nodeId));\n\t\tthis.notify();\n\t}\n\n\tselectAll(): void {\n\t\tif (this.tool === 'direct' && this.editingId) {\n\t\t\tthis.anchors = getVisibleAnchors(this.view());\n\t\t} else {\n\t\t\tthis.selection = this.doc.nodes.filter((n) => !n.locked && n.visible !== false).map((n) => n.id);\n\t\t}\n\t\tthis.notify();\n\t}\n\n\tclearSelection(): void {\n\t\tif (!this.selection.length && !this.anchors.length && !this.editingId) return;\n\t\tthis.selection = [];\n\t\tthis.anchors = [];\n\t\tthis.editingId = null;\n\t\tthis.notify();\n\t}\n\n\tselectAnchors(refs: readonly VectorAnchorRef[], options: { add?: boolean } = {}): void {\n\t\tconst valid = refs.filter((r) => getAnchor(this.doc, r));\n\t\tthis.anchors = options.add ? [...this.anchors, ...valid.filter((r) => !this.anchors.some((a) => sameRef(a, r)))] : valid;\n\t\tif (valid.length && !this.editingId) this.editingId = valid[0].nodeId;\n\t\tthis.notify();\n\t}\n\n\t/** Top-level ids of the selection (members of selected groups are covered by the group). */\n\tprivate selectedRoots(): string[] {\n\t\treturn this.selection.filter((id) => {\n\t\t\tlet covered = false;\n\t\t\tconst loc = locateVectorNode(this.doc.nodes, id);\n\t\t\tlet parent = loc?.parent ?? null;\n\t\t\twhile (parent) {\n\t\t\t\tif (this.selection.includes(parent.id)) covered = true;\n\t\t\t\tparent = locateVectorNode(this.doc.nodes, parent.id)?.parent ?? null;\n\t\t\t}\n\t\t\treturn !covered;\n\t\t});\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Transforms\n\t// -------------------------------------------------------------------------\n\n\t/** `m`, given in a frame rotated by `angle`, as a document-space matrix. */\n\tprivate inFrame(angle: number, m: AffineMatrix): AffineMatrix {\n\t\tif (!angle) return m;\n\t\treturn multiplyMatrices(multiplyMatrices(rotateMatrix(-angle), m), rotateMatrix(angle));\n\t}\n\n\t/** The selection frame's corners in document space, clockwise from the top left. */\n\tprivate frameCorners(frame: VectorSelectionFrame): Vec2[] {\n\t\tconst rot = rotateMatrix(frame.angle);\n\t\treturn vectorBoxCorners(frame).map((c) => (frame.angle ? applyMatrix(rot, c) : c));\n\t}\n\n\t/** `doc` with nodes `ids` sent through `f` (a map of document space), each in its own space (see `distortVectorNode`). */\n\tprivate distorted(doc: VectorDocument, ids: ReadonlySet<string>, f: (p: Vec2) => Vec2, tolerance = VECTOR_DISTORT_TOLERANCE): VectorDocument {\n\t\tconst visit = (list: readonly VectorNode[], parent: AffineMatrix): VectorNode[] => {\n\t\t\tlet changed = false;\n\t\t\tconst out = list.map((n) => {\n\t\t\t\tlet next = n;\n\t\t\t\tif (ids.has(n.id) && !n.locked) next = distortVectorNode(n, parent, f, tolerance);\n\t\t\t\telse if (n.type === 'group') {\n\t\t\t\t\tconst children = visit(n.children, multiplyMatrices(nodeMatrix(n), parent));\n\t\t\t\t\tif (children !== n.children) next = { ...n, children };\n\t\t\t\t}\n\t\t\t\tif (next !== n) changed = true;\n\t\t\t\treturn next;\n\t\t\t});\n\t\t\treturn changed ? out : (list as VectorNode[]);\n\t\t};\n\t\tconst nodes = visit(doc.nodes, IDENTITY_MATRIX);\n\t\treturn nodes === doc.nodes ? doc : { ...doc, nodes };\n\t}\n\n\t/**\n\t * Free Distort mode of the selection box (`state.freeDistort`): dragging a corner distorts the\n\t * selection in perspective, each corner on its own (Shift+Alt+Mod: symmetric perspective, the\n\t * corner beside it moving the other way); dragging a side moves its two corners; Mod-dragging a\n\t * side skews along it (Alt: about the centre). Each drag is one undo step.\n\t */\n\tsetFreeDistort(on: boolean): void {\n\t\tif (this.freeDistort === on) return;\n\t\tthis.freeDistort = on;\n\t\tthis.notify();\n\t}\n\n\t/**\n\t * Distorts the selected (or given) nodes so the corners of their selection box (clockwise from\n\t * the top left, in the box's own rotation) land on `corners` (document space): a projective map,\n\t * so straight lines stay straight. Curves are followed within `tolerance` (document units).\n\t * One undo step.\n\t */\n\tdistort(corners: readonly Vec2[], options: { ids?: readonly string[]; tolerance?: number } = {}): VectorActionResult {\n\t\tconst ids = options.ids ?? this.selectedRoots();\n\t\tconst frame = this.frameOf(ids);\n\t\tif (!frame) return { ok: false, message: 'Nothing is selected.' };\n\t\tif (!(frame.width > 0 && frame.height > 0)) return { ok: false, message: 'The selection has no area to distort.' };\n\t\tconst from = this.frameCorners(frame);\n\t\tconst h = corners.length === 4 ? vectorHomography(from, corners) : null;\n\t\tif (!h || !vectorHomographyKeepsQuad(h, from)) return { ok: false, message: 'The four corners must make a convex quadrilateral, clockwise from the top left.' };\n\t\tthis.change('Distort', () => {\n\t\t\tthis.doc = this.distorted(this.doc, new Set(ids), (p) => applyVectorHomography(h, p) ?? p, options.tolerance);\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/**\n\t * Skews the selected (or given) nodes along the selection box's own axes by `ax` degrees\n\t * horizontally (x' = x + tan(ax)·y: vertical lines lean right for positive angles) and `ay`\n\t * vertically, about `origin` (default: the box centre, document space). One undo step.\n\t */\n\tskew(ax: number, ay = 0, options: { ids?: readonly string[]; origin?: Vec2 } = {}): VectorActionResult {\n\t\tconst frame = this.frameOf(options.ids ?? this.selectedRoots());\n\t\tif (!frame) return { ok: false, message: 'Nothing is selected.' };\n\t\tif (![ax, ay].every((v) => Number.isFinite(v) && Math.abs(v) < 89.9)) return { ok: false, message: 'Skew angles must be between −89.9° and 89.9°.' };\n\t\tconst origin = applyMatrix(rotateMatrix(-frame.angle), options.origin ?? this.frameCenter(frame));\n\t\treturn this.transform(this.inFrame(frame.angle, vectorSkewMatrix(ax, ay, origin)), { ids: options.ids, label: 'Skew' });\n\t}\n\n\t/** A selection box handle dragged in Free Distort mode. */\n\tprivate moveDistort(drag: Extract<Drag, { kind: 'scale' }>, p: Vec2, input: VectorPointerInput) {\n\t\tconst angle = drag.frame.angle;\n\t\tconst unrotate = (v: Vec2) => (angle ? applyMatrix(rotateMatrix(-angle), v) : v);\n\t\tconst q = unrotate(p);\n\t\tconst q0 = unrotate(drag.start);\n\t\tconst d = { x: q.x - q0.x, y: q.y - q0.y };\n\t\tconst ids = new Set(this.selectedRoots());\n\t\tconst from = this.frameCorners(drag.frame);\n\t\tconst corner = drag.handle.length === 2;\n\t\tthis.setGuides(null);\n\t\tif (!corner && input.mod) {\n\t\t\tconst m = this.inFrame(angle, vectorShearForSide(drag.frame, drag.handle, d, !!input.alt));\n\t\t\tdrag.op = 'Skew';\n\t\t\tthis.doc = this.transformed(drag.base, ids, m);\n\t\t\tthis.interaction = { type: 'distort', mode: 'skew', corners: from.map((c) => applyMatrix(m, c)) };\n\t\t\tthis.notify(true);\n\t\t\treturn;\n\t\t}\n\t\tconst mode: VectorDistortMode = corner && input.mod && input.alt && input.shift ? 'perspective' : 'distort';\n\t\tconst rot = rotateMatrix(angle);\n\t\tconst quad = vectorDistortQuad(drag.frame, drag.handle, d, mode).map((c) => (angle ? applyMatrix(rot, c) : c));\n\t\tconst h = vectorHomography(from, quad);\n\t\t// A quad that folds over keeps the last good shape.\n\t\tif (!h || !vectorHomographyKeepsQuad(h, from)) return;\n\t\tdrag.op = mode === 'perspective' ? 'Perspective' : 'Distort';\n\t\tthis.doc = this.distorted(drag.base, ids, (pt) => applyVectorHomography(h, pt) ?? pt);\n\t\tthis.interaction = { type: 'distort', mode, corners: quad };\n\t\tthis.notify(true);\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Envelopes\n\t// -------------------------------------------------------------------------\n\n\t/** Selected (or given) roots that can carry an envelope. */\n\tprivate envelopeTargets(ids?: readonly string[]): VectorNode[] {\n\t\treturn this.effectTargets(ids).filter((n) => !n.locked);\n\t}\n\n\t/** Envelope parameters for `node` after `patch`: a preset switched to a mesh becomes the mesh it stood for. */\n\tprivate envelopeParams(node: VectorNode, current: Extract<VectorEffect, { type: 'envelope' }> | null, patch: Record<string, unknown>): Record<string, unknown> {\n\t\tif (patch.style !== 'mesh' || patch.mesh !== undefined || !current || current.params.style === 'mesh') return patch;\n\t\tconst frame = vectorEnvelopeFrame(node, current.id);\n\t\tif (!frame) return patch;\n\t\tconst rows = typeof patch.rows === 'number' ? patch.rows : current.params.rows;\n\t\tconst columns = typeof patch.columns === 'number' ? patch.columns : current.params.columns;\n\t\treturn { ...patch, mesh: resizeVectorMesh(frame.mesh, columns, rows) };\n\t}\n\n\t/**\n\t * Gives the selected (or given) nodes an envelope distortion, or changes the one they have: a\n\t * warp preset (`style` with `bend`, `horizontal` and `vertical` in percent and `orientation`) or\n\t * a mesh (`style: 'mesh'` with `rows` and `columns`, and optionally its `mesh` points). The\n\t * envelope stays live (an `envelope` effect: edit it with the Envelope tool, Expand Appearance\n\t * bakes it). One undo step (with `merge`, consecutive changes fold into one, as a slider drag\n\t * should). Returns the envelope of each node.\n\t */\n\tenvelope(params: Partial<VectorEnvelopeParams> = {}, options: { ids?: readonly string[]; merge?: boolean } = {}): { nodeId: string; effectId: string }[] {\n\t\tconst targets = this.envelopeTargets(options.ids);\n\t\tif (!targets.length) return [];\n\t\tconst out: { nodeId: string; effectId: string }[] = [];\n\t\tconst next = new Map<string, VectorNode>();\n\t\tconst raw = params as Record<string, unknown>;\n\t\tfor (const node of targets) {\n\t\t\tconst list = node.effects ?? [];\n\t\t\tconst current = (list.find((e) => e.type === 'envelope') as Extract<VectorEffect, { type: 'envelope' }> | undefined) ?? null;\n\t\t\tif (current) {\n\t\t\t\tconst p = normalizeVectorEffectParams('envelope', this.envelopeParams(node, current, raw), current.params)!;\n\t\t\t\tconst { enabled: _e, ...rest } = current;\n\t\t\t\tnext.set(node.id, { ...node, effects: list.map((e) => (e === current ? { ...rest, params: p } : e)) } as VectorNode);\n\t\t\t\tout.push({ nodeId: node.id, effectId: current.id });\n\t\t\t} else {\n\t\t\t\tlet k = list.length + 1;\n\t\t\t\twhile (list.some((e) => e.id === `fx${k}`)) k++;\n\t\t\t\tconst effect = createVectorEffect('envelope', `fx${k}`, raw)!;\n\t\t\t\tnext.set(node.id, { ...node, effects: [...list, effect] } as VectorNode);\n\t\t\t\tout.push({ nodeId: node.id, effectId: effect.id });\n\t\t\t}\n\t\t}\n\t\tconst made = out.some((o) => !targets.find((n) => n.id === o.nodeId)?.effects?.some((e) => e.id === o.effectId));\n\t\tconst merge = options.merge ? `envelope:${out.map((o) => o.nodeId).join(',')}` : undefined;\n\t\tthis.change(made ? (params.style === 'mesh' ? 'Make Envelope with Mesh' : 'Make Envelope with Warp') : 'Edit Envelope', () => this.setDoc(mapTree(this.doc.nodes, (n) => next.get(n.id) ?? n)), merge);\n\t\treturn out;\n\t}\n\n\t/**\n\t * Make with Top Object: the topmost selected object's outline (a closed path) becomes the envelope\n\t * of the other selected objects (grouped when there are several; they must share a parent), as a\n\t * mesh of `columns × rows` patches whose border follows the outline. The top object is used up.\n\t * One undo step; returns the id of the enveloped node.\n\t */\n\tmakeEnvelopeWithTopObject(options: { ids?: readonly string[]; rows?: number; columns?: number } = {}): VectorActionResult & { nodeId?: string } {\n\t\tconst ids = options.ids ?? this.selectedRoots();\n\t\tif (ids.length < 2) return { ok: false, message: 'Select the objects and, on top of them, the shape to use as the envelope.' };\n\t\tconst order = new Map<string, number>();\n\t\twalkVectorNodes(this.doc.nodes, (n) => order.set(n.id, order.size));\n\t\tconst sorted = ids.filter((id) => order.has(id)).sort((a, b) => order.get(a)! - order.get(b)!);\n\t\tconst topId = sorted[sorted.length - 1];\n\t\tconst top = this.getNode(topId);\n\t\tconst outline = top?.type === 'path' ? top.subpaths.find((sp) => sp.closed && sp.anchors.length > 1) : undefined;\n\t\tif (!top || !outline) return { ok: false, message: 'The top object must be a path with a closed outline.' };\n\t\tconst contents = sorted.slice(0, -1).map((id) => this.getNode(id)!);\n\t\tif (contents.some((n) => n.locked)) return { ok: false, message: 'Locked objects cannot be enveloped.' };\n\t\tconst parents = contents.map((n) => locateVectorNode(this.doc.nodes, n.id)?.parent?.id ?? null);\n\t\tif (parents.some((p) => p !== parents[0])) return { ok: false, message: 'The objects to envelope must be in the same group.' };\n\t\t// The node that gets the envelope: the one object, or a new group of them in place of the topmost.\n\t\tconst target: VectorNode =\n\t\t\tcontents.length === 1 ? contents[0] : { id: this.newId(), type: 'group', name: 'Envelope', children: contents.map((n) => n) };\n\t\tconst parentToDoc = parents[0] ? this.worldOf(parents[0]) : IDENTITY_MATRIX;\n\t\tconst targetToDoc = multiplyMatrices(nodeMatrix(target), parentToDoc);\n\t\tconst toLocal = multiplyMatrices(this.worldOf(topId), invertMatrix(targetToDoc));\n\t\tconst local = {\n\t\t\tclosed: true,\n\t\t\tanchors: outline.anchors.map((a) => ({\n\t\t\t\t...applyMatrix(toLocal, a),\n\t\t\t\t...(a.in ? { in: applyMatrix(toLocal, a.in) } : {}),\n\t\t\t\t...(a.out ? { out: applyMatrix(toLocal, a.out) } : {})\n\t\t\t}))\n\t\t};\n\t\tconst mesh = vectorMeshFromOutline(local, options.columns ?? 4, options.rows ?? 4);\n\t\tif (!mesh) return { ok: false, message: 'The top object’s outline is too small to use as an envelope.' };\n\t\tconst list = target.effects ?? [];\n\t\tlet k = list.length + 1;\n\t\twhile (list.some((e) => e.id === `fx${k}`)) k++;\n\t\tconst effect = createVectorEffect('envelope', `fx${k}`, { style: 'mesh', mesh })!;\n\t\tconst enveloped = { ...target, effects: [...list, effect] } as VectorNode;\n\t\tthis.change('Make Envelope with Top Object', () => {\n\t\t\tlet nodes = spliceTree(this.doc.nodes, topId, []);\n\t\t\tif (contents.length === 1) nodes = spliceTree(nodes, target.id, [enveloped]);\n\t\t\telse {\n\t\t\t\tconst last = contents[contents.length - 1].id;\n\t\t\t\tfor (const n of contents.slice(0, -1)) nodes = spliceTree(nodes, n.id, []);\n\t\t\t\tnodes = spliceTree(nodes, last, [enveloped]);\n\t\t\t}\n\t\t\tthis.setDoc(nodes);\n\t\t\tthis.selection = [target.id];\n\t\t\tthis.anchors = [];\n\t\t\tthis.editingId = null;\n\t\t});\n\t\treturn { ok: true, nodeId: target.id };\n\t}\n\n\t/**\n\t * Release: the selected (or given) nodes lose their envelope, and its shape (the mesh's border)\n\t * comes back as a path above each of them. One undo step; returns the new paths' ids.\n\t */\n\treleaseEnvelope(options: { ids?: readonly string[] } = {}): string[] {\n\t\tconst targets = this.envelopeTargets(options.ids).filter((n) => n.effects?.some((e) => e.type === 'envelope'));\n\t\tif (!targets.length) return [];\n\t\tconst created: string[] = [];\n\t\tconst replace = new Map<string, VectorNode[]>();\n\t\tfor (const node of targets) {\n\t\t\tconst effect = node.effects!.find((e) => e.type === 'envelope')!;\n\t\t\tconst frame = vectorEnvelopeFrame(node, effect.id);\n\t\t\tconst rest = node.effects!.filter((e) => e !== effect);\n\t\t\tconst { effects: _e, ...plain } = node;\n\t\t\tconst kept = (rest.length ? { ...plain, effects: rest } : plain) as VectorNode;\n\t\t\tif (!frame) {\n\t\t\t\treplace.set(node.id, [kept]);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst id = this.newId();\n\t\t\tcreated.push(id);\n\t\t\tconst shape: VectorPathNode = {\n\t\t\t\tid,\n\t\t\t\ttype: 'path',\n\t\t\t\tname: 'Envelope',\n\t\t\t\t...(node.transform ? { transform: node.transform } : {}),\n\t\t\t\tsubpaths: [vectorMeshOutline(frame.mesh)],\n\t\t\t\tfill: null,\n\t\t\t\tstroke: '#808080',\n\t\t\t\tstrokeWidth: 1\n\t\t\t};\n\t\t\treplace.set(node.id, [kept, shape]);\n\t\t}\n\t\tthis.change('Release Envelope', () => {\n\t\t\tlet nodes = this.doc.nodes;\n\t\t\tfor (const [id, list] of replace) nodes = spliceTree(nodes, id, list);\n\t\t\tthis.setDoc(nodes);\n\t\t\tthis.selection = [...targets.map((n) => n.id), ...created];\n\t\t\tthis.anchors = [];\n\t\t});\n\t\treturn created;\n\t}\n\n\t/** The selected node the Envelope tool edits, with its mesh (node space) and node → document matrix. */\n\tprivate envelopeTarget() {\n\t\tconst target = getVectorEnvelopeTarget(this.doc, this.selection);\n\t\treturn target && !target.node.locked ? target : null;\n\t}\n\n\t/** The envelope mesh point under `screen` (handles first), or a spot inside the mesh. */\n\tprivate hitEnvelope(screen: Vec2, p: Vec2): { target: NonNullable<ReturnType<VectorEditorController['envelopeTarget']>>; index: number | null; s: number; t: number } | null {\n\t\tconst target = this.envelopeTarget();\n\t\tif (!target) return null;\n\t\tconst { mesh } = target.frame;\n\t\tconst pts = vectorMeshEditPoints(mesh);\n\t\tlet best: number | null = null;\n\t\tlet bestD = Infinity;\n\t\tfor (const index of [...pts.handles.map((h) => h.index), ...pts.anchors]) {\n\t\t\tconst s = this.documentToScreen(applyMatrix(target.world, mesh.points[index]));\n\t\t\tconst d = Math.hypot(s.x - screen.x, s.y - screen.y);\n\t\t\t// Handles come first, so an anchor wins only when it is clearly nearer.\n\t\t\tif (d <= this.hitRadius && d < bestD - 0.5) {\n\t\t\t\tbest = index;\n\t\t\t\tbestD = d;\n\t\t\t}\n\t\t}\n\t\tif (best !== null) return { target, index: best, s: 0, t: 0 };\n\t\tconst local = applyMatrix(invertMatrix(target.world), p);\n\t\tconst at = vectorMeshParamAt(mesh, local);\n\t\tif (at.distance > this.tolerance()) return null;\n\t\treturn { target, index: null, s: at.s, t: at.t };\n\t}\n\n\tprivate downEnvelope(screen: Vec2, p: Vec2, input: VectorPointerInput) {\n\t\tconst hit = this.hitEnvelope(screen, p);\n\t\tif (hit) {\n\t\t\tconst { node, frame } = hit.target;\n\t\t\tthis.drag = {\n\t\t\t\tkind: 'envelope',\n\t\t\t\tnodeId: node.id,\n\t\t\t\teffectId: frame.effectId,\n\t\t\t\tbase: frame.mesh,\n\t\t\t\tbaseDoc: this.doc,\n\t\t\t\tbefore: this.snapshot(),\n\t\t\t\tindex: hit.index,\n\t\t\t\ts: hit.s,\n\t\t\t\tt: hit.t,\n\t\t\t\tstart: this.toLocal(node.id, p),\n\t\t\t\tmoved: false\n\t\t\t};\n\t\t\tthis.interaction = { type: 'envelope', nodeId: node.id, index: hit.index };\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\t// Elsewhere: pick the object to edit.\n\t\tconst node = hitTestVectorNodes(this.doc.nodes, p, this.tolerance(), { entered: this.enteredGroups() });\n\t\tif (node) this.selection = input.shift ? (this.selection.includes(node.id) ? this.selection.filter((id) => id !== node.id) : [...this.selection, node.id]) : [node.id];\n\t\telse if (!input.shift) this.selection = [];\n\t\tthis.anchors = [];\n\t\tthis.notify();\n\t}\n\n\tprivate moveEnvelope(drag: Extract<Drag, { kind: 'envelope' }>, p: Vec2) {\n\t\tconst local = this.toLocal(drag.nodeId, p, drag.baseDoc);\n\t\tconst d = { x: local.x - drag.start.x, y: local.y - drag.start.y };\n\t\tif (!drag.moved && Math.hypot(d.x, d.y) * this.viewport.zoom < 1) return;\n\t\tdrag.moved = true;\n\t\tconst mesh = drag.index !== null ? dragVectorMeshPoint(drag.base, drag.index, d) : pullVectorMesh(drag.base, drag.s, drag.t, d);\n\t\tconst nodes = mapTree(drag.baseDoc.nodes, (n) => {\n\t\t\tif (n.id !== drag.nodeId) return n;\n\t\t\tconst effects = (n.effects ?? []).map((e) =>\n\t\t\t\te.id === drag.effectId && e.type === 'envelope'\n\t\t\t\t\t? { ...e, params: { ...e.params, style: 'mesh' as const, mesh, columns: mesh.us.length - 1, rows: mesh.vs.length - 1 } }\n\t\t\t\t\t: e\n\t\t\t);\n\t\t\treturn { ...n, effects } as VectorNode;\n\t\t});\n\t\tthis.doc = { ...drag.baseDoc, nodes };\n\t\tthis.notify(true);\n\t}\n\n\t/** Applies a matrix to the selected (or given) nodes as one undo step. */\n\ttransform(matrix: AffineMatrix, options: { ids?: readonly string[]; label?: string } = {}): VectorActionResult {\n\t\tconst ids = new Set(options.ids ?? this.selectedRoots());\n\t\tif (!ids.size) return { ok: false, message: 'Nothing is selected.' };\n\t\tthis.change(options.label ?? 'Transform', () => {\n\t\t\tthis.doc = this.transformed(this.doc, ids, matrix);\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** `doc` with nodes `ids` moved by `m`, given in document space. */\n\tprivate transformed(doc: VectorDocument, ids: ReadonlySet<string>, m: AffineMatrix): VectorDocument {\n\t\tconst visit = (list: readonly VectorNode[], parent: AffineMatrix): VectorNode[] => {\n\t\t\tlet changed = false;\n\t\t\tconst out = list.map((n) => {\n\t\t\t\tlet next = n;\n\t\t\t\tif (ids.has(n.id) && !n.locked) {\n\t\t\t\t\t// `m` in the node's parent space: parent → document, m, document → parent.\n\t\t\t\t\tconst local = isRoot(parent) ? m : multiplyMatrices(multiplyMatrices(parent, m), invertMatrix(parent));\n\t\t\t\t\tnext = transformVectorNode(n, local);\n\t\t\t\t} else if (n.type === 'group') {\n\t\t\t\t\tconst children = visit(n.children, multiplyMatrices(nodeMatrix(n), parent));\n\t\t\t\t\tif (children !== n.children) next = { ...n, children };\n\t\t\t\t}\n\t\t\t\tif (next !== n) changed = true;\n\t\t\t\treturn next;\n\t\t\t});\n\t\t\treturn changed ? out : (list as VectorNode[]);\n\t\t};\n\t\tconst nodes = visit(doc.nodes, IDENTITY_MATRIX);\n\t\treturn nodes === doc.nodes ? doc : { ...doc, nodes };\n\t}\n\n\ttranslate(dx: number, dy: number, options: { ids?: readonly string[] } = {}): VectorActionResult {\n\t\tif (this.tool === 'direct' && this.anchors.length && !options.ids) {\n\t\t\tthis.change('Move Points', () => {\n\t\t\t\tthis.doc = this.movedAnchors(this.doc, this.anchors, dx, dy);\n\t\t\t});\n\t\t\treturn { ok: true };\n\t\t}\n\t\treturn this.transform(translateMatrix(dx, dy), { ids: options.ids, label: 'Move' });\n\t}\n\n\t/** The selection frame's centre in document space. */\n\tprivate frameCenter(frame: VectorSelectionFrame): Vec2 {\n\t\treturn applyMatrix(rotateMatrix(frame.angle), { x: frame.x + frame.width / 2, y: frame.y + frame.height / 2 });\n\t}\n\n\t/**\n\t * Scales along the selection box's own axes (rotated with the selection when its nodes\n\t * share a rotation) around `origin` (default: the box centre, in document space).\n\t */\n\tscale(sx: number, sy: number = sx, options: { ids?: readonly string[]; origin?: Vec2 } = {}): VectorActionResult {\n\t\tconst frame = this.frameOf(options.ids ?? this.selectedRoots());\n\t\tif (!frame) return { ok: false, message: 'Nothing is selected.' };\n\t\tconst origin = applyMatrix(rotateMatrix(-frame.angle), options.origin ?? this.frameCenter(frame));\n\t\treturn this.transform(this.inFrame(frame.angle, scaleMatrix(sx, sy, origin)), { ids: options.ids, label: 'Scale' });\n\t}\n\n\t/** Rotates clockwise by `degrees` around `origin` (default: the selection centre). */\n\trotate(degrees: number, options: { ids?: readonly string[]; origin?: Vec2 } = {}): VectorActionResult {\n\t\tconst frame = this.frameOf(options.ids ?? this.selectedRoots());\n\t\tif (!frame) return { ok: false, message: 'Nothing is selected.' };\n\t\tconst origin = options.origin ?? this.frameCenter(frame);\n\t\treturn this.transform(rotateMatrix(degrees, origin), { ids: options.ids, label: 'Rotate' });\n\t}\n\n\t/** Sets each selected (or given) node's rotation to `degrees`, turning it around its own centre. */\n\tsetRotation(degrees: number, options: { ids?: readonly string[] } = {}): VectorActionResult {\n\t\tconst ids = options.ids ?? this.selectedRoots();\n\t\tif (!ids.length) return { ok: false, message: 'Nothing is selected.' };\n\t\tthis.change('Set Rotation', () => {\n\t\t\tfor (const id of ids) {\n\t\t\t\tconst frame = this.frameOf([id]);\n\t\t\t\tif (!frame) continue;\n\t\t\t\tconst delta = normalizeAngle(degrees - frame.angle);\n\t\t\t\tif (Math.abs(delta) > 1e-9) this.doc = this.transformed(this.doc, new Set([id]), rotateMatrix(delta, this.frameCenter(frame)));\n\t\t\t}\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** Mirrors along the selection box's own axes. */\n\tflip(axis: 'horizontal' | 'vertical', options: { ids?: readonly string[] } = {}): VectorActionResult {\n\t\tconst frame = this.frameOf(options.ids ?? this.selectedRoots());\n\t\tif (!frame) return { ok: false, message: 'Nothing is selected.' };\n\t\tconst c = { x: frame.x + frame.width / 2, y: frame.y + frame.height / 2 };\n\t\tconst m = axis === 'horizontal' ? scaleMatrix(-1, 1, c) : scaleMatrix(1, -1, c);\n\t\treturn this.transform(this.inFrame(frame.angle, m), { ids: options.ids, label: axis === 'horizontal' ? 'Flip Horizontal' : 'Flip Vertical' });\n\t}\n\n\t/**\n\t * Aligns the selected nodes to their combined bounds, or to the artboard when one node is\n\t * selected (or `to: 'artboard'`).\n\t */\n\talign(edge: VectorAlign, options: { ids?: readonly string[]; to?: 'selection' | 'artboard' } = {}): VectorActionResult {\n\t\tconst ids = options.ids ?? this.selectedRoots();\n\t\tif (!ids.length) return { ok: false, message: 'Nothing is selected.' };\n\t\tconst target = options.to === 'artboard' || ids.length === 1 ? { x: 0, y: 0, width: this.doc.width, height: this.doc.height } : this.boundsOf(ids)!;\n\t\tthis.change(`Align ${edge[0].toUpperCase()}${edge.slice(1)}`, () => {\n\t\t\tfor (const id of ids) {\n\t\t\t\tconst b = this.boundsOf([id]);\n\t\t\t\tif (!b) continue;\n\t\t\t\tlet dx = 0;\n\t\t\t\tlet dy = 0;\n\t\t\t\tif (edge === 'left') dx = target.x - b.x;\n\t\t\t\tif (edge === 'center') dx = target.x + target.width / 2 - (b.x + b.width / 2);\n\t\t\t\tif (edge === 'right') dx = target.x + target.width - (b.x + b.width);\n\t\t\t\tif (edge === 'top') dy = target.y - b.y;\n\t\t\t\tif (edge === 'middle') dy = target.y + target.height / 2 - (b.y + b.height / 2);\n\t\t\t\tif (edge === 'bottom') dy = target.y + target.height - (b.y + b.height);\n\t\t\t\tif (dx || dy) this.doc = this.transformed(this.doc, new Set([id]), translateMatrix(dx, dy));\n\t\t\t}\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** Spaces three or more selected nodes evenly (equal gaps). */\n\tdistribute(direction: VectorDistribute, options: { ids?: readonly string[] } = {}): VectorActionResult {\n\t\tconst ids = options.ids ?? this.selectedRoots();\n\t\tconst items = ids.map((id) => ({ id, b: this.boundsOf([id]) })).filter((i): i is { id: string; b: VectorBounds } => !!i.b);\n\t\tif (items.length < 3) return { ok: false, message: 'Select at least three objects to distribute.' };\n\t\tconst horizontal = direction === 'horizontal';\n\t\tconst pos = (b: VectorBounds) => (horizontal ? b.x : b.y);\n\t\tconst size = (b: VectorBounds) => (horizontal ? b.width : b.height);\n\t\titems.sort((a, b) => pos(a.b) - pos(b.b));\n\t\tconst first = items[0].b;\n\t\tconst last = items[items.length - 1].b;\n\t\tconst total = pos(last) + size(last) - pos(first);\n\t\tconst gap = (total - items.reduce((s, i) => s + size(i.b), 0)) / (items.length - 1);\n\t\tthis.change(horizontal ? 'Distribute Horizontally' : 'Distribute Vertically', () => {\n\t\t\tlet cursor = pos(first);\n\t\t\tfor (const item of items) {\n\t\t\t\tconst d = cursor - pos(item.b);\n\t\t\t\tif (Math.abs(d) > 1e-9) this.doc = this.transformed(this.doc, new Set([item.id]), horizontal ? translateMatrix(d, 0) : translateMatrix(0, d));\n\t\t\t\tcursor += size(item.b) + gap;\n\t\t\t}\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Structure\n\t// -------------------------------------------------------------------------\n\n\t/** Copies the selection (offset by `offset`, default 10, 10) on top of each original. */\n\tduplicate(options: { ids?: readonly string[]; offset?: Vec2 } = {}): string[] {\n\t\tconst ids = options.ids ?? this.selectedRoots();\n\t\tconst offset = options.offset ?? { x: 10, y: 10 };\n\t\tconst created: string[] = [];\n\t\tif (!ids.length) return created;\n\t\tthis.change('Duplicate', () => {\n\t\t\tthis.doc = this.withDuplicates(this.doc, ids, offset, created);\n\t\t\tthis.selection = created;\n\t\t\tthis.anchors = [];\n\t\t});\n\t\treturn created;\n\t}\n\n\tprivate withDuplicates(doc: VectorDocument, ids: readonly string[], offset: Vec2, created: string[]): VectorDocument {\n\t\tconst set = new Set(ids);\n\t\tconst m = translateMatrix(offset.x, offset.y);\n\t\tconst insert = (list: readonly VectorNode[]): VectorNode[] => {\n\t\t\tconst out: VectorNode[] = [];\n\t\t\tfor (const node of list) {\n\t\t\t\tconst n = node.type === 'group' ? { ...node, children: insert(node.children) } : node;\n\t\t\t\tout.push(n);\n\t\t\t\tif (set.has(node.id)) {\n\t\t\t\t\tconst copy = transformVectorNode(this.freshIds(node), m);\n\t\t\t\t\tcreated.push(copy.id);\n\t\t\t\t\tout.push(copy);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn out;\n\t\t};\n\t\treturn { ...doc, nodes: insert(doc.nodes) };\n\t}\n\n\t/** Groups the selected nodes (they must share a parent). Returns the group id. */\n\tgroup(options: { ids?: readonly string[]; name?: string } = {}): string | null {\n\t\tconst ids = options.ids ?? this.selectedRoots();\n\t\tif (!ids.length) return null;\n\t\tconst locs = ids.map((id) => locateVectorNode(this.doc.nodes, id));\n\t\tif (locs.some((l) => !l)) return null;\n\t\tconst parent = locs[0]!.parent?.id ?? null;\n\t\tif (locs.some((l) => (l!.parent?.id ?? null) !== parent)) return null;\n\t\tconst id = this.newId();\n\t\tthis.change('Group', () => {\n\t\t\tconst set = new Set(ids);\n\t\t\tconst regroup = (list: readonly VectorNode[]): VectorNode[] => {\n\t\t\t\tconst members = list.filter((n) => set.has(n.id));\n\t\t\t\tconst top = Math.max(...members.map((m) => list.indexOf(m)));\n\t\t\t\tconst out: VectorNode[] = [];\n\t\t\t\tlist.forEach((n, i) => {\n\t\t\t\t\tif (i === top) out.push({ id, type: 'group', name: options.name ?? 'Group', children: members });\n\t\t\t\t\tif (!set.has(n.id)) out.push(n);\n\t\t\t\t});\n\t\t\t\treturn out;\n\t\t\t};\n\t\t\tif (parent === null) this.setDoc(regroup(this.doc.nodes));\n\t\t\telse this.setDoc(mapTree(this.doc.nodes, (n) => (n.id === parent && n.type === 'group' ? { ...n, children: regroup(n.children) } : n)));\n\t\t\tthis.selection = [id];\n\t\t\tthis.anchors = [];\n\t\t});\n\t\treturn id;\n\t}\n\n\t/** Replaces selected groups with their children. */\n\tungroup(options: { ids?: readonly string[] } = {}): VectorActionResult {\n\t\tconst groups = (options.ids ?? this.selection).map((id) => this.getNode(id)).filter((n): n is VectorGroupNode => n?.type === 'group');\n\t\tif (!groups.length) return { ok: false, message: 'Select a group to ungroup.' };\n\t\tconst set = new Set(groups.map((g) => g.id));\n\t\tthis.change('Ungroup', () => {\n\t\t\tconst flatten = (list: readonly VectorNode[]): VectorNode[] => {\n\t\t\t\tconst out: VectorNode[] = [];\n\t\t\t\tlet changed = false;\n\t\t\t\tfor (const n of list) {\n\t\t\t\t\tif (set.has(n.id) && n.type === 'group') {\n\t\t\t\t\t\tchanged = true;\n\t\t\t\t\t\tconst opacity = n.opacity;\n\t\t\t\t\t\tconst m = nodeMatrix(n);\n\t\t\t\t\t\tout.push(\n\t\t\t\t\t\t\t...n.children\n\t\t\t\t\t\t\t\t.map((c) => (c.blendStep ? (({ blendStep: _s, locked: _l, ...rest }) => rest as VectorNode)(c) : c))\n\t\t\t\t\t\t\t\t.map((c) => (c.booleanOperation ? (({ booleanOperation: _b, ...rest }) => rest as VectorNode)(c) : c))\n\t\t\t\t\t\t\t\t.map((c) => (n.transform ? transformVectorNode(c, m) : c))\n\t\t\t\t\t\t\t\t.map((c) => (opacity !== undefined && opacity < 1 ? { ...c, opacity: (c.opacity ?? 1) * opacity } : c))\n\t\t\t\t\t\t);\n\t\t\t\t\t} else if (n.type === 'group') {\n\t\t\t\t\t\tconst children = flatten(n.children);\n\t\t\t\t\t\tif (children !== n.children) changed = true;\n\t\t\t\t\t\tout.push(children !== n.children ? { ...n, children } : n);\n\t\t\t\t\t} else out.push(n);\n\t\t\t\t}\n\t\t\t\treturn changed ? out : (list as VectorNode[]);\n\t\t\t};\n\t\t\tthis.setDoc(flatten(this.doc.nodes));\n\t\t\tthis.selection = groups.flatMap((g) => g.children.map((c) => c.id));\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** Changes stacking order within each node's parent. */\n\tarrange(op: VectorArrange, options: { ids?: readonly string[] } = {}): VectorActionResult {\n\t\tconst set = new Set(options.ids ?? this.selectedRoots());\n\t\tif (!set.size) return { ok: false, message: 'Nothing is selected.' };\n\t\tconst reorder = (list: readonly VectorNode[]): VectorNode[] => {\n\t\t\tconst out = list.map((n) => (n.type === 'group' ? { ...n, children: reorder(n.children) } : n));\n\t\t\tif (op === 'front') return [...out.filter((n) => !set.has(n.id)), ...out.filter((n) => set.has(n.id))];\n\t\t\tif (op === 'back') return [...out.filter((n) => set.has(n.id)), ...out.filter((n) => !set.has(n.id))];\n\t\t\tif (op === 'forward') {\n\t\t\t\tfor (let i = out.length - 2; i >= 0; i--) if (set.has(out[i].id) && !set.has(out[i + 1].id)) [out[i], out[i + 1]] = [out[i + 1], out[i]];\n\t\t\t} else {\n\t\t\t\tfor (let i = 1; i < out.length; i++) if (set.has(out[i].id) && !set.has(out[i - 1].id)) [out[i], out[i - 1]] = [out[i - 1], out[i]];\n\t\t\t}\n\t\t\treturn out;\n\t\t};\n\t\tconst labels = { front: 'Bring to Front', forward: 'Bring Forward', backward: 'Send Backward', back: 'Send to Back' };\n\t\tthis.change(labels[op], () => this.setDoc(reorder(this.doc.nodes)));\n\t\treturn { ok: true };\n\t}\n\n\t/**\n\t * Combines the selected paths (two or more) into one with a boolean operation. `subtract`\n\t * removes the upper shapes from the lowest one. The result takes the lowest shape's style\n\t * and place. Shapes take part as drawn (path effects and live booleans applied).\n\t *\n\t * With `live`, the operands stay editable instead: a lone live boolean group gets the\n\t * operation (its operands follow it again), operands of live boolean groups get it as their\n\t * own, and anything else (two or more nodes with one parent) is wrapped in a new live boolean\n\t * group. Returns the result's (or the live group's) id, or null.\n\t */\n\tbooleanOp(op: VectorBooleanOperation, options: { ids?: readonly string[]; live?: boolean } = {}): string | null {\n\t\tconst roots = options.ids ?? this.selectedRoots();\n\t\tif (options.live) return this.liveBooleanOp(op, roots);\n\t\tconst entries = vectorPathEntries(this.doc, roots, { locked: true });\n\t\tif (entries.length < 2) return null;\n\t\tconst result = pathBoolean(\n\t\t\tBOOLEAN_OPS[op],\n\t\t\tentries.map((e) => ({\n\t\t\t\tpath: vectorPathWorldBezier({ ...e.node, subpaths: e.node.subpaths.filter((s) => s.anchors.length > 1).map((s) => ({ ...s, closed: true })) }, e.parent),\n\t\t\t\tfillRule: e.node.fillRule ?? 'nonzero'\n\t\t\t}))\n\t\t);\n\t\tconst base = entries[0].node;\n\t\tconst anchorRoot = roots.find((r) => this.holds(r, base.id)) ?? base.id;\n\t\tconst subpaths = bezierToSubpaths(this.intoParentSpace(anchorRoot, result.path));\n\t\tif (result.error || !subpaths.length) return null;\n\t\tconst id = this.newId();\n\t\tconst { transform: _t, effects: _e, booleanOperation: _b, ...baseRest } = cloneVectorNode(base);\n\t\tconst node: VectorPathNode = { ...baseRest, id, name: BOOLEAN_LABELS[op], subpaths };\n\t\tconst drop = new Set([...entries.map((e) => e.node.id), ...roots]);\n\t\tthis.change(BOOLEAN_LABELS[op], () => {\n\t\t\tthis.setDoc(mapTree(this.doc.nodes, (n) => (n.id === anchorRoot ? node : drop.has(n.id) ? null : n)));\n\t\t\tthis.selection = [id];\n\t\t\tthis.anchors = [];\n\t\t\tthis.editingId = null;\n\t\t});\n\t\treturn id;\n\t}\n\n\t/** Whether stored node `rootId` holds `id` as drawn (`id` may be an expanded path inside it). */\n\tprivate holds(rootId: string, id: string): boolean {\n\t\tconst n = this.getNode(rootId);\n\t\treturn !!n && (!!findVectorNode([n], id) || !!findVectorNode([expandVectorAppearance(n)], id) || !!findVectorNode([vectorMaskedGeometry(n)], id));\n\t}\n\n\tprivate liveBooleanOp(op: VectorBooleanOperation, roots: readonly string[]): string | null {\n\t\tconst nodes = roots.map((id) => this.getNode(id));\n\t\tif (!nodes.length || nodes.some((n) => !n)) return null;\n\t\tconst label = `Live ${BOOLEAN_LABELS[op]}`;\n\t\tconst lone = nodes.length === 1 ? nodes[0] : null;\n\t\tif (isVectorLiveBoolean(lone)) {\n\t\t\tconst children = lone.children.map((c) => (c.booleanOperation ? (({ booleanOperation: _b, ...rest }) => rest as VectorNode)(c) : c));\n\t\t\tthis.change(label, () => this.replaceNode(lone.id, { ...lone, liveBoolean: op, children }));\n\t\t\treturn lone.id;\n\t\t}\n\t\tconst parents = roots.map((id) => locateVectorNode(this.doc.nodes, id)?.parent ?? null);\n\t\tif (parents.every((g) => isVectorLiveBoolean(g))) {\n\t\t\tconst set = new Set(roots);\n\t\t\tthis.change(label, () =>\n\t\t\t\tthis.setDoc(\n\t\t\t\t\tmapTree(this.doc.nodes, (n) => {\n\t\t\t\t\t\tif (n.type !== 'group' || !n.liveBoolean || !n.children.some((c) => set.has(c.id))) return n;\n\t\t\t\t\t\tconst group = n;\n\t\t\t\t\t\tconst children = group.children.map((c) => {\n\t\t\t\t\t\t\tif (!set.has(c.id)) return c;\n\t\t\t\t\t\t\tconst { booleanOperation: _b, ...rest } = c;\n\t\t\t\t\t\t\treturn (op === group.liveBoolean ? rest : { ...rest, booleanOperation: op }) as VectorNode;\n\t\t\t\t\t\t});\n\t\t\t\t\t\treturn { ...group, children };\n\t\t\t\t\t})\n\t\t\t\t)\n\t\t\t);\n\t\t\treturn parents[0]!.id;\n\t\t}\n\t\tif (nodes.length < 2) return null;\n\t\tconst parent = parents[0]?.id ?? null;\n\t\tif (parents.some((g) => (g?.id ?? null) !== parent)) return null;\n\t\tconst id = this.newId();\n\t\tthis.change(label, () => {\n\t\t\tconst set = new Set(roots);\n\t\t\tconst wrap = (list: readonly VectorNode[]): VectorNode[] => {\n\t\t\t\tconst members = list.filter((n) => set.has(n.id)).map((c) => (c.booleanOperation ? (({ booleanOperation: _b, ...rest }) => rest as VectorNode)(c) : c));\n\t\t\t\tconst top = Math.max(...list.map((n, i) => (set.has(n.id) ? i : -1)));\n\t\t\t\tconst out: VectorNode[] = [];\n\t\t\t\tlist.forEach((n, i) => {\n\t\t\t\t\tif (i === top) out.push({ id, type: 'group', name: BOOLEAN_LABELS[op], liveBoolean: op, children: members });\n\t\t\t\t\tif (!set.has(n.id)) out.push(n);\n\t\t\t\t});\n\t\t\t\treturn out;\n\t\t\t};\n\t\t\tif (parent === null) this.setDoc(wrap(this.doc.nodes));\n\t\t\telse this.setDoc(mapTree(this.doc.nodes, (n) => (n.id === parent && n.type === 'group' ? { ...n, children: wrap(n.children) } : n)));\n\t\t\tthis.selection = [id];\n\t\t\tthis.anchors = [];\n\t\t\tthis.editingId = null;\n\t\t});\n\t\treturn id;\n\t}\n\n\t/** Joins the selected paths into one compound path (subpaths kept, lowest style). */\n\tcombine(options: { ids?: readonly string[] } = {}): string | null {\n\t\tconst roots = options.ids ?? this.selectedRoots();\n\t\tconst entries = vectorPathEntries(this.doc, roots, { locked: true });\n\t\tif (entries.length < 2) return null;\n\t\tconst paths = entries.map((e) => e.node);\n\t\tconst id = this.newId();\n\t\tconst first = roots.find((r) => this.holds(r, paths[0].id)) ?? paths[0].id;\n\t\tconst subpaths = entries.flatMap((e) => bezierToSubpaths(this.intoParentSpace(first, vectorPathWorldBezier(e.node, e.parent))));\n\t\tconst { transform: _t, effects: _e, booleanOperation: _b, ...baseRest } = cloneVectorNode(paths[0]);\n\t\tconst node: VectorPathNode = { ...baseRest, id, name: 'Compound Path', subpaths };\n\t\tconst drop = new Set([...paths.map((p) => p.id), ...roots]);\n\t\tthis.change('Combine', () => {\n\t\t\tthis.setDoc(mapTree(this.doc.nodes, (n) => (n.id === first ? node : drop.has(n.id) ? null : n)));\n\t\t\tthis.selection = [id];\n\t\t\tthis.anchors = [];\n\t\t});\n\t\treturn id;\n\t}\n\n\t/** A document-space path mapped into the parent space of node `id`. */\n\tprivate intoParentSpace(id: string, path: BezierPath): BezierPath {\n\t\tconst parent = vectorParentMatrix(this.doc.nodes, id) ?? IDENTITY_MATRIX;\n\t\treturn isRoot(parent) ? path : transformBezierPath(path, invertMatrix(parent));\n\t}\n\n\t/** Splits selected compound paths into one path per subpath. */\n\tbreakApart(options: { ids?: readonly string[] } = {}): VectorActionResult {\n\t\tconst paths = (options.ids ?? this.selection).map((id) => this.getNode(id)).filter((n): n is VectorPathNode => n?.type === 'path' && n.subpaths.length > 1);\n\t\tif (!paths.length) return { ok: false, message: 'Select a compound path to break apart.' };\n\t\tconst created: string[] = [];\n\t\tconst set = new Set(paths.map((p) => p.id));\n\t\tthis.change('Break Apart', () => {\n\t\t\tconst split = (list: readonly VectorNode[]): VectorNode[] =>\n\t\t\t\tlist.flatMap((n): VectorNode[] => {\n\t\t\t\t\tif (n.type === 'group') return [{ ...n, children: split(n.children) }];\n\t\t\t\t\tif (n.type !== 'path' || !set.has(n.id)) return [n];\n\t\t\t\t\treturn n.subpaths.map((sp, i) => {\n\t\t\t\t\t\tconst id = i === 0 ? n.id : this.newId();\n\t\t\t\t\t\tcreated.push(id);\n\t\t\t\t\t\treturn { ...n, id, subpaths: [sp] };\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\tthis.setDoc(split(this.doc.nodes));\n\t\t\tthis.selection = created;\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Pathfinder, Shape Builder and paint bucket\n\t// -------------------------------------------------------------------------\n\n\t/**\n\t * Pathfinder: Divide, Trim, Merge, Crop, Outline or Minus Back over the selected (or given)\n\t * paths, in document space (rotated nodes and groups included, live corners cut). Unfilled\n\t * stroked paths take part as the area their stroke paints (Divide: open ones cut instead).\n\t * The results replace the operands at the place of the top-most one, grouped when there are\n\t * several; each keeps the style of the operand whose area it is. Returns the new path ids,\n\t * or an empty list when nothing came out.\n\t */\n\tpathfinder(op: VectorPathfinderOperation, options: { ids?: readonly string[] } = {}): string[] {\n\t\tconst roots = options.ids ?? this.selectedRoots();\n\t\tconst entries = vectorPathEntries(this.doc, roots);\n\t\tif (entries.length < (op === 'divide' || op === 'outline' ? 1 : 2)) return [];\n\t\tconst shapes = entries.map((e) => vectorPathfinderShape(e, op));\n\t\tconst result = runPathfinder(op, shapes);\n\t\tif (result.error || !result.pieces.length) return [];\n\t\tconst last = entries[entries.length - 1].node.id;\n\t\tconst topRoot = [...roots].reverse().find((r) => this.holds(r, last)) ?? last;\n\t\tconst nodes: VectorPathNode[] = [];\n\t\tfor (const piece of result.pieces) {\n\t\t\tconst subpaths = bezierToSubpaths(this.intoParentSpace(topRoot, piece.path)).map((sp) => (piece.open ? { ...sp, closed: false } : sp));\n\t\t\tif (!subpaths.length) continue;\n\t\t\tconst source = entries[piece.source].node;\n\t\t\tconst style = vectorResultStyle(source, shapes[piece.source].paint, op === 'outline');\n\t\t\tnodes.push({ ...style, type: 'path', id: this.newId(), name: source.name ?? 'Path', subpaths });\n\t\t}\n\t\tif (!nodes.length) return [];\n\t\tconst label = VECTOR_PATHFINDER_LABELS[op];\n\t\tconst container: VectorNode = nodes.length > 1 ? { id: this.newId(), type: 'group', name: label, children: nodes } : nodes[0];\n\t\tconst drop = new Set([...entries.map((e) => e.node.id), ...roots]);\n\t\tthis.change(label, () => {\n\t\t\tthis.setDoc(mapTree(this.doc.nodes, (n) => (n.id === topRoot ? container : drop.has(n.id) ? null : n)));\n\t\t\tthis.selection = [container.id];\n\t\t\tthis.anchors = [];\n\t\t\tthis.editingId = null;\n\t\t});\n\t\treturn nodes.map((n) => n.id);\n\t}\n\n\t/** The Shape Builder map of the selected (or given) paths, cached per document and selection. */\n\t// -------------------------------------------------------------------------\n\t// Blends\n\t// -------------------------------------------------------------------------\n\n\t/** Selected (or given) blend groups; a selected key or step counts as its blend. */\n\tprivate blendTargets(ids?: readonly string[]): (VectorGroupNode & { blend: VectorBlend })[] {\n\t\tconst out = new Map<string, VectorGroupNode & { blend: VectorBlend }>();\n\t\tfor (const id of ids ?? this.selection) {\n\t\t\tconst node = this.getNode(id);\n\t\t\tconst group = isVectorBlendGroup(node) ? node : locateVectorNode(this.doc.nodes, id)?.parent;\n\t\t\tif (isVectorBlendGroup(group)) out.set(group.id, group);\n\t\t}\n\t\treturn [...out.values()];\n\t}\n\n\t/**\n\t * Blends two or more selected (or given) paths or groups: they become the keys of a live\n\t * blend group (bottom to top, or in the order given with `keepOrder`) whose in-betweens are\n\t * regenerated whenever a key or the options change. The keys must share a parent. Returns\n\t * the blend group's id, or null.\n\t */\n\tblend(input: VectorBlendInput = {}): string | null {\n\t\tconst ids = [...new Set(input.ids ?? this.selectedRoots())].filter((id) => {\n\t\t\tconst n = this.getNode(id);\n\t\t\treturn !!n && !n.blendStep && !n.locked;\n\t\t});\n\t\tif (ids.length < 2) return null;\n\t\tconst locs = ids.map((id) => locateVectorNode(this.doc.nodes, id)!);\n\t\tconst parent = locs[0].parent?.id ?? null;\n\t\tif (locs.some((l) => (l.parent?.id ?? null) !== parent)) return null;\n\t\tconst order = input.keepOrder ? ids : ids.slice().sort((a, b) => locs[ids.indexOf(a)].index - locs[ids.indexOf(b)].index);\n\t\tconst blend: VectorBlend = blendOptionsOf({ ...this.blendDefaults, ...input });\n\t\tif (input.starts) {\n\t\t\tconst starts = Object.fromEntries(Object.entries(input.starts).filter(([k, v]) => order.includes(k) && Number.isInteger(v) && v >= 0));\n\t\t\tif (Object.keys(starts).length) blend.starts = starts;\n\t\t}\n\t\tconst id = this.newId();\n\t\tthis.change('Blend', () => {\n\t\t\tconst set = new Set(order);\n\t\t\tconst regroup = (list: readonly VectorNode[]): VectorNode[] => {\n\t\t\t\tconst members = order.map((m) => list.find((n) => n.id === m)!);\n\t\t\t\tconst top = Math.max(...members.map((m) => list.indexOf(m)));\n\t\t\t\tconst out: VectorNode[] = [];\n\t\t\t\tlist.forEach((n, i) => {\n\t\t\t\t\tif (i === top) out.push({ id, type: 'group', name: input.name ?? 'Blend', blend, children: members });\n\t\t\t\t\tif (!set.has(n.id)) out.push(n);\n\t\t\t\t});\n\t\t\t\treturn out;\n\t\t\t};\n\t\t\tif (parent === null) this.setDoc(regroup(this.doc.nodes));\n\t\t\telse this.setDoc(mapTree(this.doc.nodes, (n) => (n.id === parent && n.type === 'group' ? { ...n, children: regroup(n.children) } : n)));\n\t\t\tthis.selection = [id];\n\t\t\tthis.anchors = [];\n\t\t\tthis.editingId = null;\n\t\t});\n\t\treturn id;\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Masks\n\t// -------------------------------------------------------------------------\n\n\t/**\n\t * Make Clipping Mask (`clip`) / Make Opacity Mask (`alpha`, `luminance`): groups the selected (or\n\t * given) objects into a mask group whose mask is the topmost of them (or `maskId`), moved to the\n\t * bottom of the group. They must be two or more, unlocked, with one parent. One undo step.\n\t * Returns the group's id, or why it could not.\n\t */\n\tmakeMask(type: VectorMaskType, options: { ids?: readonly string[]; maskId?: string; invert?: boolean; name?: string } = {}): VectorMaskResult {\n\t\tif (!isVectorMaskType(type)) return { ok: false, message: `Unknown mask type \"${type}\": use clip, alpha or luminance.` };\n\t\tconst ids = [...new Set(options.ids ?? this.selectedRoots())];\n\t\tif (ids.length < 2) return { ok: false, message: 'Select at least two objects: the topmost becomes the mask, the ones below show through it.' };\n\t\tfor (const id of ids) {\n\t\t\tconst n = this.getNode(id);\n\t\t\tif (!n) return { ok: false, message: `There is no node \"${id}\".` };\n\t\t\tif (n.locked) return { ok: false, message: `\"${id}\" is locked; unlock it first.` };\n\t\t\tif (n.blendStep) return { ok: false, message: `\"${id}\" is a blend step; select its blend instead.` };\n\t\t}\n\t\tconst locs = ids.map((id) => locateVectorNode(this.doc.nodes, id)!);\n\t\tconst parent = locs[0].parent?.id ?? null;\n\t\tif (locs.some((l) => (l.parent?.id ?? null) !== parent)) return { ok: false, message: 'The objects must share a parent (group them, or pick objects at one level).' };\n\t\tconst order = ids.slice().sort((a, b) => locs[ids.indexOf(a)].index - locs[ids.indexOf(b)].index);\n\t\tconst maskId = options.maskId ?? order[order.length - 1];\n\t\tif (!order.includes(maskId)) return { ok: false, message: `The mask \"${maskId}\" must be one of the objects (${order.map((id) => `\"${id}\"`).join(', ')}).` };\n\t\tconst id = this.newId();\n\t\tconst mask: VectorMask = options.invert ? { type, invert: true } : { type };\n\t\tthis.change(type === 'clip' ? 'Make Clipping Mask' : 'Make Opacity Mask', () => {\n\t\t\tconst set = new Set(order);\n\t\t\tconst regroup = (list: readonly VectorNode[]): VectorNode[] => {\n\t\t\t\tconst members = order.map((m) => list.find((n) => n.id === m)!);\n\t\t\t\tconst top = Math.max(...members.map((m) => list.indexOf(m)));\n\t\t\t\tconst children = [members.find((m) => m.id === maskId)!, ...members.filter((m) => m.id !== maskId)];\n\t\t\t\tconst out: VectorNode[] = [];\n\t\t\t\tlist.forEach((n, i) => {\n\t\t\t\t\tif (i === top) out.push({ id, type: 'group', name: options.name ?? (type === 'clip' ? 'Clip Group' : 'Opacity Mask'), mask, children });\n\t\t\t\t\tif (!set.has(n.id)) out.push(n);\n\t\t\t\t});\n\t\t\t\treturn out;\n\t\t\t};\n\t\t\tif (parent === null) this.setDoc(regroup(this.doc.nodes));\n\t\t\telse this.setDoc(mapTree(this.doc.nodes, (n) => (n.id === parent && n.type === 'group' ? { ...n, children: regroup(n.children) } : n)));\n\t\t\tthis.selection = [id];\n\t\t\tthis.anchors = [];\n\t\t\tthis.editingId = null;\n\t\t});\n\t\treturn { ok: true, id };\n\t}\n\n\t// -----------------------------------------------------------------------------------------\n\t// Object snap tracking and typed entry (after cadcraft's snap/tracking.rs and its direct\n\t// distance entry)\n\n\tprivate trackingPoints: VectorTrackingPoint[] = [];\n\tprivate lastSnap: { kind: string; point: Vec2 } | null = null;\n\tprivate dwellTimer: ReturnType<typeof setTimeout> | null = null;\n\tprivate dwellAt: Vec2 | null = null;\n\tprivate dwellToggled: Vec2 | null = null;\n\tprivate typed: string | null = null;\n\n\t/** How long (ms) the pointer rests on a point before it is acquired or released for tracking. */\n\tstatic readonly TRACKING_DWELL_MS = 400;\n\n\t/** The pen's polar angles: measured from its last segment with `polarRelative`, else the page's (`snap.angles`). */\n\tprivate penAngles(): { angles?: readonly number[] } {\n\t\tif (!this.snap.polarRelative || !this.pen) return {};\n\t\tconst node = this.getNode(this.pen.nodeId) as VectorPathNode | undefined;\n\t\tconst sp = node?.subpaths[this.pen.subpath];\n\t\tif (!sp || sp.anchors.length < 2) return {};\n\t\tconst n = sp.anchors.length;\n\t\tconst a = getAnchorWorld(this.doc, { nodeId: this.pen.nodeId, subpath: this.pen.subpath, index: n - 2 })!;\n\t\tconst b = getAnchorWorld(this.doc, { nodeId: this.pen.nodeId, subpath: this.pen.subpath, index: n - 1 })!;\n\t\tif (Math.hypot(b.x - a.x, b.y - a.y) < 1e-9) return {};\n\t\tconst base = (Math.atan2(-(b.y - a.y), b.x - a.x) * 180) / Math.PI;\n\t\treturn { angles: this.snap.angles.map((x) => x + base) };\n\t}\n\n\t/** Pointer rested on the snapped anchor, centre or crossing: starts (or keeps) the dwell that acquires it. */\n\tprivate dwell(): void {\n\t\tconst hit = this.snap.tracking !== false && this.lastSnap && (this.lastSnap.kind === 'anchor' || this.lastSnap.kind === 'centre' || this.lastSnap.kind === 'intersect') ? this.lastSnap.point : null;\n\t\tconst same = (a: Vec2 | null, b: Vec2 | null) => !!a && !!b && Math.hypot(a.x - b.x, a.y - b.y) < 1e-9;\n\t\tif (!hit) {\n\t\t\tthis.dwellAt = null;\n\t\t\tthis.dwellToggled = null;\n\t\t\tif (this.dwellTimer) clearTimeout(this.dwellTimer);\n\t\t\tthis.dwellTimer = null;\n\t\t\treturn;\n\t\t}\n\t\tif (same(hit, this.dwellToggled) || same(hit, this.dwellAt)) return;\n\t\tthis.dwellToggled = null;\n\t\tthis.dwellAt = hit;\n\t\tif (this.dwellTimer) clearTimeout(this.dwellTimer);\n\t\tthis.dwellTimer = setTimeout(() => {\n\t\t\tthis.dwellTimer = null;\n\t\t\tconst at = this.dwellAt;\n\t\t\tthis.dwellAt = null;\n\t\t\tif (!at) return;\n\t\t\tthis.toggleTracking(at);\n\t\t\tthis.dwellToggled = at;\n\t\t\tthis.notify();\n\t\t}, VectorEditorController.TRACKING_DWELL_MS);\n\t}\n\n\t/** The direction an open path's end at `p` (document space) would extend in, or null when no open path ends there. */\n\tprivate extensionAt(p: Vec2): Vec2 | null {\n\t\tlet out: Vec2 | null = null;\n\t\twalkVectorNodes(this.doc.nodes, (n) => {\n\t\t\tif (out || n.type !== 'path' || n.visible === false) return;\n\t\t\tn.subpaths.forEach((sp, s) => {\n\t\t\t\tif (out || sp.closed || sp.anchors.length < 2) return;\n\t\t\t\tfor (const end of [0, sp.anchors.length - 1]) {\n\t\t\t\t\tconst a = getAnchorWorld(this.doc, { nodeId: n.id, subpath: s, index: end })!;\n\t\t\t\t\tif (Math.hypot(a.x - p.x, a.y - p.y) > 1e-6) continue;\n\t\t\t\t\tconst nextIndex = end === 0 ? 1 : end - 1;\n\t\t\t\t\tconst handle = end === 0 ? a.out : a.in;\n\t\t\t\t\tconst b = handle && Math.hypot(handle.x - a.x, handle.y - a.y) > 1e-9 ? handle : getAnchorWorld(this.doc, { nodeId: n.id, subpath: s, index: nextIndex })!;\n\t\t\t\t\tconst d = { x: a.x - b.x, y: a.y - b.y };\n\t\t\t\t\tif (Math.hypot(d.x, d.y) > 1e-9) out = d;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t\treturn out;\n\t}\n\n\tprivate toggleTracking(p: Vec2): void {\n\t\tconst i = this.trackingPoints.findIndex((t) => Math.hypot(t.x - p.x, t.y - p.y) < 1e-9);\n\t\tif (i >= 0) this.trackingPoints = this.trackingPoints.filter((_, k) => k !== i);\n\t\telse {\n\t\t\tconst ext = this.extensionAt(p);\n\t\t\tthis.trackingPoints = [...this.trackingPoints, { x: p.x, y: p.y, ...(ext ? { extension: ext } : {}) }].slice(-VECTOR_MAX_TRACKING_POINTS);\n\t\t}\n\t}\n\n\t/**\n\t * Acquires the point at `x, y` (document units) for object snap tracking, or releases it when\n\t * it is acquired already, as resting the pointer on it does. An open path's end also sends its\n\t * extension. At most seven are kept, the oldest dropped. Returns the acquired points.\n\t */\n\tacquireTrackingPoint(p: Vec2): readonly VectorTrackingPoint[] {\n\t\tif (!Number.isFinite(p.x) || !Number.isFinite(p.y)) return this.trackingPoints;\n\t\tthis.toggleTracking(p);\n\t\tthis.notify();\n\t\treturn this.trackingPoints;\n\t}\n\n\t/** Releases every acquired tracking point. */\n\tclearTracking(notify = true): void {\n\t\tconst had = this.trackingPoints.length > 0;\n\t\tthis.trackingPoints = [];\n\t\tthis.dwellAt = null;\n\t\tthis.dwellToggled = null;\n\t\tif (this.dwellTimer) clearTimeout(this.dwellTimer);\n\t\tthis.dwellTimer = null;\n\t\tif (had && notify) this.notify();\n\t}\n\n\t/** A key typed while the pen draws: digits and `. , < @ -` build a typed entry, Enter places it, Backspace and Escape edit or drop it. */\n\tprivate typedKey(input: VectorKeyInput): boolean {\n\t\tif (input.mod || this.tool !== 'pen' || !this.pen || this.drag) return false;\n\t\tconst key = input.key;\n\t\tif (this.typed === null) {\n\t\t\tif (!/^[0-9.@-]$/.test(key)) return false;\n\t\t\tthis.typed = key;\n\t\t\tthis.notify();\n\t\t\treturn true;\n\t\t}\n\t\tif (/^[0-9.,<@-]$/.test(key)) this.typed += key;\n\t\telse if (key === 'Backspace') this.typed = this.typed.length > 1 ? this.typed.slice(0, -1) : null;\n\t\telse if (key === 'Escape') this.typed = null;\n\t\telse if (key === 'Enter') {\n\t\t\tconst text = this.typed;\n\t\t\tthis.typed = null;\n\t\t\tconst r = this.placePenPoint(text);\n\t\t\tif (!r.ok) this.notify();\n\t\t\treturn true;\n\t\t} else return false;\n\t\tthis.notify();\n\t\treturn true;\n\t}\n\n\t/**\n\t * Places the pen's next point from typed text, as CAD direct distance entry does: `120` is that\n\t * far from the last point toward the pointer (along the path it tracks), `120<30` that far at 30°\n\t * (counter-clockwise on the page, from the last segment with `snap.polarRelative`), `@40,-10`\n\t * an offset from the last point and `200,150` a position. One undo step. Fails without a path\n\t * being drawn with the pen.\n\t */\n\tplacePenPoint(text: string): VectorActionResult {\n\t\tconst pen = this.pen;\n\t\tconst node = pen ? (this.getNode(pen.nodeId) as VectorPathNode | undefined) : undefined;\n\t\tconst sp = pen && node ? node.subpaths[pen.subpath] : undefined;\n\t\tif (!pen || !node || !sp?.anchors.length) return { ok: false, message: 'Start a path with the pen first: typed entry places its next point.' };\n\t\tconst last = getAnchorWorld(this.doc, { nodeId: pen.nodeId, subpath: pen.subpath, index: sp.anchors.length - 1 })!;\n\t\tconst num = '(-?(?:\\\\d+\\\\.?\\\\d*|\\\\.\\\\d+))';\n\t\tconst t = text.replace(/\\s+/g, '');\n\t\tlet target: Vec2 | null = null;\n\t\tlet m: RegExpMatchArray | null;\n\t\tif ((m = t.match(new RegExp(`^@${num},${num}$`)))) target = { x: last.x + parseFloat(m[1]), y: last.y + parseFloat(m[2]) };\n\t\telse if ((m = t.match(new RegExp(`^${num},${num}$`)))) target = { x: parseFloat(m[1]), y: parseFloat(m[2]) };\n\t\telse if ((m = t.match(new RegExp(`^${num}<${num}$`)))) {\n\t\t\tconst base = this.snap.polarRelative ? (this.penAngles().angles?.[0] ?? this.snap.angles[0] ?? 0) - (this.snap.angles[0] ?? 0) : 0;\n\t\t\tconst dir = vectorAngleDirection(parseFloat(m[2]) + base);\n\t\t\tconst len = parseFloat(m[1]);\n\t\t\ttarget = { x: last.x + dir.x * len, y: last.y + dir.y * len };\n\t\t} else if ((m = t.match(new RegExp(`^${num}$`)))) {\n\t\t\tconst toward = this.pointer;\n\t\t\tconst d = toward ? Math.hypot(toward.x - last.x, toward.y - last.y) : 0;\n\t\t\tif (!toward || d < 1e-9) return { ok: false, message: 'Point the pointer the way the distance should go (or type length<angle).' };\n\t\t\tconst len = parseFloat(m[1]);\n\t\t\ttarget = { x: last.x + ((toward.x - last.x) / d) * len, y: last.y + ((toward.y - last.y) / d) * len };\n\t\t}\n\t\tif (!target || !Number.isFinite(target.x) || !Number.isFinite(target.y)) return { ok: false, message: `Cannot read \"${text}\": type a distance (120), length<angle (120<30), an offset (@40,-10) or a position (200,150).` };\n\t\tconst local = this.toLocal(pen.nodeId, target);\n\t\tconst anchors = [...sp.anchors, { x: local.x, y: local.y }];\n\t\tthis.change('Add Point', () => {\n\t\t\tthis.replaceNode(pen.nodeId, { ...node, subpaths: node.subpaths.map((s2, i) => (i === pen.subpath ? { ...s2, anchors } : s2)) });\n\t\t\tthis.anchors = [{ nodeId: pen.nodeId, subpath: pen.subpath, index: anchors.length - 1 }];\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/**\n\t * Adds an open (or closed) path drawn by dimensions, the way a CAD user types them: from\n\t * `start`, each step goes `length` at `angle` degrees (counter-clockwise on the page), by an\n\t * offset `dx, dy`, or to a position `x, y` (document units). Default style. One undo step;\n\t * selects it. Returns its id and the anchors placed.\n\t */\n\taddPathByDimensions(input: { start: Vec2; steps: readonly ({ length: number; angle: number } | { dx: number; dy: number } | { x: number; y: number })[]; closed?: boolean; name?: string }): VectorMaskResult & { anchors?: Vec2[] } {\n\t\tif (!input.start || !Number.isFinite(input.start.x) || !Number.isFinite(input.start.y)) return { ok: false, message: 'start must be { x, y } in document units.' };\n\t\tif (!input.steps?.length) return { ok: false, message: 'Give at least one step.' };\n\t\tconst pts: Vec2[] = [{ x: input.start.x, y: input.start.y }];\n\t\tfor (const [i, step] of input.steps.entries()) {\n\t\t\tconst at = pts[pts.length - 1];\n\t\t\tconst st = step as Record<string, number>;\n\t\t\tlet q: Vec2 | null = null;\n\t\t\tif ('length' in st && 'angle' in st) {\n\t\t\t\tconst dir = vectorAngleDirection(st.angle);\n\t\t\t\tq = { x: at.x + dir.x * st.length, y: at.y + dir.y * st.length };\n\t\t\t} else if ('dx' in st && 'dy' in st) q = { x: at.x + st.dx, y: at.y + st.dy };\n\t\t\telse if ('x' in st && 'y' in st) q = { x: st.x, y: st.y };\n\t\t\tif (!q || !Number.isFinite(q.x) || !Number.isFinite(q.y)) return { ok: false, message: `Step ${i} must be { length, angle }, { dx, dy } or { x, y } with numbers.` };\n\t\t\tpts.push(q);\n\t\t}\n\t\tconst id = this.newId();\n\t\tconst closed = !!input.closed;\n\t\tconst node: VectorPathNode = { id, type: 'path', name: input.name ?? 'Path', ...this.styleFor(!closed), subpaths: [{ closed, anchors: pts.map((p) => ({ x: p.x, y: p.y })) }] };\n\t\tthis.change('Add Path', () => {\n\t\t\tthis.setDoc([...this.doc.nodes, node]);\n\t\t\tthis.selection = [id];\n\t\t\tthis.anchors = [];\n\t\t\tthis.editingId = null;\n\t\t});\n\t\treturn { ok: true, id, anchors: pts };\n\t}\n\n\t// -----------------------------------------------------------------------------------------\n\t// Connectors\n\n\t/** How near (document units) a connector end snaps to a connection site. */\n\tprivate connectorRadius(): number {\n\t\treturn 12 / this.viewport.zoom;\n\t}\n\n\tprivate downConnector(p: Vec2) {\n\t\tconst before = this.snapshot();\n\t\tconst target = vectorConnectorTargetAt(this.doc.nodes, p, this.connectorRadius());\n\t\tconst at = target?.site ?? p;\n\t\tconst node: VectorPathNode = {\n\t\t\tid: this.newId(),\n\t\t\ttype: 'path',\n\t\t\tname: 'Connector',\n\t\t\t...this.styleFor(true),\n\t\t\t...(this.defaults.endArrow === undefined ? { endArrow: 'triangle' as const } : {}),\n\t\t\tconnector: { start: target ? { nodeId: target.nodeId, ...(target.side && target.side !== 'auto' ? { side: target.side } : {}) } : null, end: null, route: this.connectorRoute },\n\t\t\tsubpaths: [{ closed: false, anchors: [{ x: at.x, y: at.y }, { x: at.x, y: at.y }] }]\n\t\t};\n\t\tthis.setDoc([...this.doc.nodes, node]);\n\t\tthis.drag = { kind: 'connector', nodeId: node.id, start: { x: at.x, y: at.y }, before };\n\t\tthis.interaction = { type: 'draw', tool: 'connector', nodeId: node.id, bounds: { x: at.x, y: at.y, width: 0, height: 0 } };\n\t\tthis.notify(true);\n\t}\n\n\t/** The route new connectors get (the Connector tool and `addConnector`). Default straight. */\n\tprivate connectorRoute: VectorConnectorRoute = 'straight';\n\n\t/** Sets the route the Connector tool draws with. */\n\tsetConnectorRoute(route: VectorConnectorRoute): VectorActionResult {\n\t\tif (!VECTOR_CONNECTOR_ROUTES.includes(route)) return { ok: false, message: `Unknown route \"${route}\": use ${VECTOR_CONNECTOR_ROUTES.join(', ')}.` };\n\t\tthis.connectorRoute = route;\n\t\tthis.notify();\n\t\treturn { ok: true };\n\t}\n\n\t/** The route the Connector tool draws with. */\n\tgetConnectorRoute(): VectorConnectorRoute {\n\t\treturn this.connectorRoute;\n\t}\n\n\t/** Checks a connector end given by an API caller: the object must exist and not be a connector. */\n\tprivate checkConnectorEnd(end: VectorConnectorEnd, self?: string): string | null {\n\t\tif (!end || typeof end.nodeId !== 'string') return 'A glued end needs a nodeId.';\n\t\tconst n = this.getNode(end.nodeId);\n\t\tif (!n) return `There is no node \"${end.nodeId}\".`;\n\t\tif (end.nodeId === self) return 'A connector cannot be glued to itself.';\n\t\tif (isVectorConnector(n)) return `\"${end.nodeId}\" is a connector: glue ends to shapes, text, groups or instances.`;\n\t\tif (end.side !== undefined && !VECTOR_CONNECTOR_SIDES.includes(end.side)) return `Unknown side \"${end.side}\": use ${VECTOR_CONNECTOR_SIDES.join(', ')}.`;\n\t\tif (!vectorConnectionSites(this.doc.nodes, end.nodeId).length) return `\"${end.nodeId}\" has no extent (or is hidden) to connect to.`;\n\t\treturn null;\n\t}\n\n\t/**\n\t * Adds a connector from `start` to `end`, each glued to an object (`{ nodeId, side? }`, side\n\t * default `auto`) or a free point (`{ x, y }`, document units). It is routed now and after every\n\t * later edit, so it follows the objects. Stroked in the default stroke with an arrowhead at the\n\t * end unless given. One undo step; selects it.\n\t */\n\taddConnector(input: {\n\t\tstart: VectorConnectorEnd | Vec2;\n\t\tend: VectorConnectorEnd | Vec2;\n\t\troute?: VectorConnectorRoute;\n\t\tradius?: number;\n\t\tgap?: number;\n\t\tname?: string;\n\t\tstroke?: VectorPaint;\n\t\tstrokeWidth?: number;\n\t\tstartArrow?: VectorArrowhead | null;\n\t\tendArrow?: VectorArrowhead | null;\n\t}): VectorMaskResult {\n\t\tconst glued = (e: VectorConnectorEnd | Vec2): e is VectorConnectorEnd => !!e && typeof (e as VectorConnectorEnd).nodeId === 'string';\n\t\tfor (const e of [input.start, input.end]) {\n\t\t\tif (glued(e)) {\n\t\t\t\tconst problem = this.checkConnectorEnd(e);\n\t\t\t\tif (problem) return { ok: false, message: problem };\n\t\t\t} else if (!e || !Number.isFinite((e as Vec2).x) || !Number.isFinite((e as Vec2).y)) return { ok: false, message: 'Each end is { nodeId, side? } or a point { x, y }.' };\n\t\t}\n\t\tif (glued(input.start) && glued(input.end) && input.start.nodeId === input.end.nodeId) return { ok: false, message: 'Both ends are glued to the same object; connect two different objects.' };\n\t\tconst route = input.route ?? this.connectorRoute;\n\t\tif (!VECTOR_CONNECTOR_ROUTES.includes(route)) return { ok: false, message: `Unknown route \"${route}\": use ${VECTOR_CONNECTOR_ROUTES.join(', ')}.` };\n\t\tif (input.radius !== undefined && !(Number.isFinite(input.radius) && input.radius >= 0)) return { ok: false, message: 'radius must be zero or more.' };\n\t\tif (input.gap !== undefined && !(Number.isFinite(input.gap) && input.gap >= 0)) return { ok: false, message: 'gap must be zero or more.' };\n\t\tconst point = (e: VectorConnectorEnd | Vec2): Vec2 => (glued(e) ? { x: 0, y: 0 } : { x: (e as Vec2).x, y: (e as Vec2).y });\n\t\tconst endOf = (e: VectorConnectorEnd | Vec2) => (glued(e) ? { nodeId: e.nodeId, ...(e.side && e.side !== 'auto' ? { side: e.side } : {}) } : null);\n\t\tconst id = this.newId();\n\t\tconst style = this.styleFor(true);\n\t\tconst node: VectorPathNode = {\n\t\t\tid,\n\t\t\ttype: 'path',\n\t\t\tname: input.name ?? 'Connector',\n\t\t\t...style,\n\t\t\tstroke: input.stroke !== undefined ? input.stroke : (style.stroke ?? '#000000'),\n\t\t\t...(input.strokeWidth !== undefined ? { strokeWidth: input.strokeWidth } : {}),\n\t\t\t...(input.startArrow !== undefined ? { startArrow: input.startArrow } : {}),\n\t\t\tendArrow: input.endArrow !== undefined ? input.endArrow : (style.endArrow ?? 'triangle'),\n\t\t\tconnector: {\n\t\t\t\tstart: endOf(input.start),\n\t\t\t\tend: endOf(input.end),\n\t\t\t\t...(route !== 'straight' ? { route } : {}),\n\t\t\t\t...(input.radius ? { radius: input.radius } : {}),\n\t\t\t\t...(input.gap !== undefined ? { gap: input.gap } : {})\n\t\t\t},\n\t\t\tsubpaths: [{ closed: false, anchors: [point(input.start), point(input.end)] }]\n\t\t};\n\t\tthis.change('Connect', () => {\n\t\t\tthis.setDoc([...this.doc.nodes, node]);\n\t\t\tthis.selection = [id];\n\t\t\tthis.anchors = [];\n\t\t\tthis.editingId = null;\n\t\t});\n\t\treturn { ok: true, id };\n\t}\n\n\t/**\n\t * Changes connectors (default: the selected ones): `start` / `end` glue an end to an object\n\t * (`{ nodeId, side? }`) or unglue it (`null`, the end staying where it is), and `route`,\n\t * `radius`, `gap` set how they run. Plain open paths given by id become connectors. One undo step.\n\t */\n\tsetConnector(\n\t\tpatch: { start?: VectorConnectorEnd | null; end?: VectorConnectorEnd | null; route?: VectorConnectorRoute; radius?: number; gap?: number },\n\t\toptions: { ids?: readonly string[] } = {}\n\t): VectorActionResult {\n\t\tconst ids = options.ids ?? this.selection;\n\t\tif (!ids.length) return { ok: false, message: 'Select a connector first.' };\n\t\tif (patch.route !== undefined && !VECTOR_CONNECTOR_ROUTES.includes(patch.route)) return { ok: false, message: `Unknown route \"${patch.route}\": use ${VECTOR_CONNECTOR_ROUTES.join(', ')}.` };\n\t\tif (patch.radius !== undefined && !(Number.isFinite(patch.radius) && patch.radius >= 0)) return { ok: false, message: 'radius must be zero or more.' };\n\t\tif (patch.gap !== undefined && !(Number.isFinite(patch.gap) && patch.gap >= 0)) return { ok: false, message: 'gap must be zero or more.' };\n\t\tfor (const id of ids) {\n\t\t\tconst n = this.getNode(id);\n\t\t\tif (!n) return { ok: false, message: `There is no node \"${id}\".` };\n\t\t\tif (n.locked) return { ok: false, message: `\"${id}\" is locked; unlock it first.` };\n\t\t\tif (!isVectorConnector(n) && !(n.type === 'path' && n.subpaths.length === 1 && !n.subpaths[0].closed && n.subpaths[0].anchors.length >= 2))\n\t\t\t\treturn { ok: false, message: `\"${id}\" is not a connector or an open path (draw one with vector.connect or the Connector tool).` };\n\t\t\tfor (const key of ['start', 'end'] as const) {\n\t\t\t\tconst e = patch[key];\n\t\t\t\tif (e) {\n\t\t\t\t\tconst problem = this.checkConnectorEnd(e, id);\n\t\t\t\t\tif (problem) return { ok: false, message: problem };\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tconst set = new Set(ids);\n\t\tconst endOf = (e: VectorConnectorEnd | null) => (e ? { nodeId: e.nodeId, ...(e.side && e.side !== 'auto' ? { side: e.side } : {}) } : null);\n\t\tthis.change('Connector', () => {\n\t\t\tthis.setDoc(\n\t\t\t\tmapTree(this.doc.nodes, (n) => {\n\t\t\t\t\tif (!set.has(n.id) || n.type !== 'path') return n;\n\t\t\t\t\tconst c: VectorConnector = { ...(n.connector ?? {}) };\n\t\t\t\t\tif (patch.start !== undefined) c.start = endOf(patch.start);\n\t\t\t\t\tif (patch.end !== undefined) c.end = endOf(patch.end);\n\t\t\t\t\tif (patch.route !== undefined) {\n\t\t\t\t\t\tif (patch.route === 'straight') delete c.route;\n\t\t\t\t\t\telse c.route = patch.route;\n\t\t\t\t\t}\n\t\t\t\t\tif (patch.radius !== undefined) {\n\t\t\t\t\t\tif (patch.radius > 0) c.radius = patch.radius;\n\t\t\t\t\t\telse delete c.radius;\n\t\t\t\t\t}\n\t\t\t\t\tif (patch.gap !== undefined) c.gap = patch.gap;\n\t\t\t\t\treturn { ...n, connector: c };\n\t\t\t\t})\n\t\t\t);\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** Every connector: its ends (glued object and resolved side, or the free point) and route. */\n\tgetConnectors(): VectorConnectorListing[] {\n\t\tconst out: VectorConnectorListing[] = [];\n\t\twalkVectorNodes(this.doc.nodes, (n) => {\n\t\t\tif (!isVectorConnector(n)) return;\n\t\t\tconst r = routeVectorConnector(this.doc.nodes, n.id);\n\t\t\tconst world = vectorWorldMatrix(this.doc.nodes, n.id) ?? IDENTITY_MATRIX;\n\t\t\tconst sp = n.subpaths[0];\n\t\t\tconst first = sp?.anchors[0];\n\t\t\tconst last = sp?.anchors[sp.anchors.length - 1];\n\t\t\tconst end = (e: VectorConnectorEnd | null | undefined, side: string | null, a: VectorAnchor | undefined) => {\n\t\t\t\tconst at = a ? applyMatrix(world, a) : null;\n\t\t\t\treturn { ...(e ? { nodeId: e.nodeId, side: e.side ?? 'auto', resolvedSide: side } : {}), ...(at ? { x: at.x, y: at.y } : {}) };\n\t\t\t};\n\t\t\tout.push({\n\t\t\t\tid: n.id,\n\t\t\t\t...(n.name ? { name: n.name } : {}),\n\t\t\t\troute: n.connector.route ?? 'straight',\n\t\t\t\t...(n.connector.radius ? { radius: n.connector.radius } : {}),\n\t\t\t\tstart: end(n.connector.start, r?.startSide ?? null, first),\n\t\t\t\tend: end(n.connector.end, r?.endSide ?? null, last)\n\t\t\t});\n\t\t});\n\t\treturn out;\n\t}\n\n\t/** The connection sites of node `id` in document space (top, right, bottom, left, centre), or null when there is no such node. */\n\tgetConnectionSites(id: string): VectorConnectionSite[] | null {\n\t\tif (!this.getNode(id)) return null;\n\t\treturn vectorConnectionSites(this.doc.nodes, id);\n\t}\n\n\t// -----------------------------------------------------------------------------------------\n\t// Frames (artboards) and resizing constraints\n\n\t/** A name for a new frame: \"Frame <n>\" after the frames there are. */\n\tprivate frameName(): string {\n\t\tlet n = 0;\n\t\twalkVectorNodes(this.doc.nodes, (x) => {\n\t\t\tif (isVectorFrame(x)) n++;\n\t\t});\n\t\treturn `Frame ${n + 1}`;\n\t}\n\n\t/**\n\t * Adds an empty frame (an artboard when top-level) with its rectangle at `x, y, width, height`\n\t * (document units), filled white by default and clipping its contents. With `adopt`, the\n\t * top-level objects it fully encloses move into it. One undo step; selects it.\n\t */\n\taddFrame(input: { x: number; y: number; width: number; height: number; name?: string; fill?: VectorPaint; clip?: boolean; adopt?: boolean }): VectorMaskResult {\n\t\tconst { x, y, width, height } = input;\n\t\tif (![x, y, width, height].every(Number.isFinite)) return { ok: false, message: 'x, y, width and height must be numbers.' };\n\t\tif (!(width > 0 && height > 0)) return { ok: false, message: 'A frame needs a positive width and height.' };\n\t\tconst id = this.newId();\n\t\tconst rect: VectorBounds = { x, y, width, height };\n\t\tconst adopted = input.adopt ? this.doc.nodes.filter((n) => !n.locked && n.visible !== false && !n.blendStep && vectorBoundsInside(vectorNodeBounds(n), rect)) : [];\n\t\tconst frame: VectorFrameGroup = {\n\t\t\tid,\n\t\t\ttype: 'group',\n\t\t\tname: input.name ?? this.frameName(),\n\t\t\tframe: { x, y, width, height, fill: input.fill === undefined ? '#ffffff' : input.fill, ...(input.clip === false ? { clip: false } : {}) },\n\t\t\tchildren: adopted\n\t\t};\n\t\tconst taken = new Set(adopted.map((n) => n.id));\n\t\tthis.change('Add Frame', () => {\n\t\t\tthis.setDoc([...this.doc.nodes.filter((n) => !taken.has(n.id)), frame]);\n\t\t\tthis.selection = [id];\n\t\t\tthis.anchors = [];\n\t\t\tthis.editingId = null;\n\t\t});\n\t\treturn { ok: true, id };\n\t}\n\n\t/**\n\t * Frame Selection: puts the selected (or given) objects, which must share a parent, into a new\n\t * frame fitted to their bounds plus `padding` (document units, default 0), in their place in the\n\t * stacking order. No fill unless `fill` is given; clips unless `clip: false`. One undo step;\n\t * selects the frame.\n\t */\n\tframeSelection(options: { ids?: readonly string[]; padding?: number; name?: string; fill?: VectorPaint; clip?: boolean } = {}): VectorMaskResult {\n\t\tconst ids = [...new Set(options.ids ?? this.selectedRoots())];\n\t\tif (!ids.length) return { ok: false, message: 'Select the objects to frame first.' };\n\t\tconst padding = options.padding ?? 0;\n\t\tif (!(Number.isFinite(padding) && padding >= 0)) return { ok: false, message: 'padding must be zero or more.' };\n\t\tfor (const id of ids) {\n\t\t\tconst n = this.getNode(id);\n\t\t\tif (!n) return { ok: false, message: `There is no node \"${id}\".` };\n\t\t\tif (n.locked) return { ok: false, message: `\"${id}\" is locked; unlock it first.` };\n\t\t\tif (n.blendStep) return { ok: false, message: `\"${id}\" is a blend step; select its blend instead.` };\n\t\t}\n\t\tconst locs = ids.map((id) => locateVectorNode(this.doc.nodes, id)!);\n\t\tconst parent = locs[0].parent ?? null;\n\t\tif (locs.some((l) => (l.parent?.id ?? null) !== (parent?.id ?? null))) return { ok: false, message: 'The objects must share a parent (pick objects at one level).' };\n\t\tconst b = unionBounds(ids.map((id) => vectorNodeBounds(this.getNode(id)!)));\n\t\tif (!b) return { ok: false, message: 'The objects have no extent to frame.' };\n\t\tconst id = this.newId();\n\t\tconst order = ids.slice().sort((a, c) => locs[ids.indexOf(a)].index - locs[ids.indexOf(c)].index);\n\t\tconst set = new Set(order);\n\t\tconst frame = {\n\t\t\tx: b.x - padding,\n\t\t\ty: b.y - padding,\n\t\t\twidth: b.width + 2 * padding,\n\t\t\theight: b.height + 2 * padding,\n\t\t\t...(options.fill !== undefined && options.fill !== null ? { fill: options.fill } : {}),\n\t\t\t...(options.clip === false ? { clip: false } : {})\n\t\t};\n\t\tthis.change('Frame Selection', () => {\n\t\t\tconst regroup = (list: readonly VectorNode[]): VectorNode[] => {\n\t\t\t\tconst members = order.map((m) => list.find((n) => n.id === m)!);\n\t\t\t\tconst top = Math.max(...members.map((m) => list.indexOf(m)));\n\t\t\t\tconst out: VectorNode[] = [];\n\t\t\t\tlist.forEach((n, i) => {\n\t\t\t\t\tif (i === top) out.push({ id, type: 'group', name: options.name ?? this.frameName(), frame, children: members });\n\t\t\t\t\tif (!set.has(n.id)) out.push(n);\n\t\t\t\t});\n\t\t\t\treturn out;\n\t\t\t};\n\t\t\tif (!parent) this.setDoc(regroup(this.doc.nodes));\n\t\t\telse this.setDoc(mapTree(this.doc.nodes, (n) => (n.id === parent.id && n.type === 'group' ? { ...n, children: regroup(n.children) } : n)));\n\t\t\tthis.selection = [id];\n\t\t\tthis.anchors = [];\n\t\t\tthis.editingId = null;\n\t\t});\n\t\treturn { ok: true, id };\n\t}\n\n\t/** Frames among `ids` (default: the selection), or holding one of them directly. */\n\tprivate frameTargets(ids?: readonly string[]): VectorFrameGroup[] {\n\t\tconst out = new Map<string, VectorFrameGroup>();\n\t\tfor (const id of ids ?? this.selection) {\n\t\t\tconst n = this.getNode(id);\n\t\t\tif (isVectorFrame(n)) out.set(n.id, n);\n\t\t\telse if (!ids) {\n\t\t\t\tconst parent = locateVectorNode(this.doc.nodes, id)?.parent;\n\t\t\t\tif (isVectorFrame(parent)) out.set(parent.id, parent);\n\t\t\t}\n\t\t}\n\t\treturn [...out.values()];\n\t}\n\n\t/**\n\t * Changes frames (default: the selected ones, or the frame holding the selection): `x`, `y`,\n\t * `width`, `height` move and resize the rectangle (in the frame's own space), and its children\n\t * follow their resizing constraints; `fill` (null for none) and `clip` set how it draws.\n\t * One undo step (merged while `merge` is the same key).\n\t */\n\tsetFrame(patch: { x?: number; y?: number; width?: number; height?: number; fill?: VectorPaint; clip?: boolean }, options: { ids?: readonly string[]; merge?: string } = {}): VectorActionResult {\n\t\tfor (const k of ['x', 'y', 'width', 'height'] as const) if (patch[k] !== undefined && !Number.isFinite(patch[k])) return { ok: false, message: `${k} must be a number.` };\n\t\tif ((patch.width !== undefined && !(patch.width > 0)) || (patch.height !== undefined && !(patch.height > 0))) return { ok: false, message: 'A frame needs a positive width and height.' };\n\t\tif (patch.clip !== undefined && typeof patch.clip !== 'boolean') return { ok: false, message: 'clip must be true or false.' };\n\t\tif (options.ids) for (const id of options.ids) if (!isVectorFrame(this.getNode(id))) return { ok: false, message: `\"${id}\" is not a frame (use frameSelection or addFrame to make one).` };\n\t\tconst frames = this.frameTargets(options.ids);\n\t\tif (!frames.length) return { ok: false, message: 'Select a frame (or something in one).' };\n\t\tconst locked = frames.find((f) => f.locked);\n\t\tif (locked) return { ok: false, message: `\"${locked.id}\" is locked; unlock it first.` };\n\t\tconst next = new Map<string, VectorNode>();\n\t\tfor (const f of frames) {\n\t\t\tconst from = f.frame;\n\t\t\tconst to = { x: patch.x ?? from.x, y: patch.y ?? from.y, width: patch.width ?? from.width, height: patch.height ?? from.height };\n\t\t\tlet node: VectorFrameGroup = f;\n\t\t\tif (to.x !== from.x || to.y !== from.y || to.width !== from.width || to.height !== from.height) {\n\t\t\t\t// The rectangle's map in the frame's own space, carried into its parent's space.\n\t\t\t\tconst sx = from.width > 1e-9 ? to.width / from.width : 1;\n\t\t\t\tconst sy = from.height > 1e-9 ? to.height / from.height : 1;\n\t\t\t\tconst local: AffineMatrix = [sx, 0, 0, sy, to.x - sx * from.x, to.y - sy * from.y];\n\t\t\t\tconst own = nodeMatrix(f);\n\t\t\t\tnode = transformVectorNode(f, multiplyMatrices(multiplyMatrices(invertMatrix(own), local), own));\n\t\t\t\t// Exact numbers (the map can round); the placement is unchanged.\n\t\t\t\tnode = { ...node, frame: { ...node.frame, ...to }, ...(f.transform ? { transform: f.transform } : {}) };\n\t\t\t}\n\t\t\tconst frame = { ...node.frame };\n\t\t\tif (patch.fill !== undefined) {\n\t\t\t\tif (patch.fill === null || patch.fill === 'none') delete frame.fill;\n\t\t\t\telse frame.fill = patch.fill;\n\t\t\t}\n\t\t\tif (patch.clip !== undefined) {\n\t\t\t\tif (patch.clip) delete frame.clip;\n\t\t\t\telse frame.clip = false;\n\t\t\t}\n\t\t\tnext.set(f.id, { ...node, frame });\n\t\t}\n\t\tthis.change(\n\t\t\t'Frame',\n\t\t\t() => {\n\t\t\t\tthis.setDoc(mapTree(this.doc.nodes, (n) => next.get(n.id) ?? n));\n\t\t\t},\n\t\t\toptions.merge\n\t\t);\n\t\treturn { ok: true };\n\t}\n\n\t/**\n\t * Sets the resizing constraints of the selected (or given) objects, which must be children of\n\t * frames: how each follows its frame when the frame is resized. A missing axis keeps its value.\n\t * One undo step.\n\t */\n\tsetResizing(resizing: VectorResizing, options: { ids?: readonly string[] } = {}): VectorActionResult {\n\t\tif (resizing.horizontal !== undefined && !VECTOR_HORIZONTAL_RESIZING.includes(resizing.horizontal)) return { ok: false, message: `Unknown horizontal constraint \"${resizing.horizontal}\": use ${VECTOR_HORIZONTAL_RESIZING.join(', ')}.` };\n\t\tif (resizing.vertical !== undefined && !VECTOR_VERTICAL_RESIZING.includes(resizing.vertical)) return { ok: false, message: `Unknown vertical constraint \"${resizing.vertical}\": use ${VECTOR_VERTICAL_RESIZING.join(', ')}.` };\n\t\tif (!resizing.horizontal && !resizing.vertical) return { ok: false, message: 'Pass horizontal, vertical or both.' };\n\t\tconst ids = options.ids ?? this.selection;\n\t\tif (!ids.length) return { ok: false, message: 'Select objects inside a frame first.' };\n\t\tfor (const id of ids) {\n\t\t\tif (!this.getNode(id)) return { ok: false, message: `There is no node \"${id}\".` };\n\t\t\tif (!isVectorFrame(locateVectorNode(this.doc.nodes, id)?.parent)) return { ok: false, message: `\"${id}\" is not directly inside a frame: resizing constraints only apply to a frame's children.` };\n\t\t}\n\t\tconst set = new Set(ids);\n\t\tthis.change('Constraints', () => {\n\t\t\tthis.setDoc(mapTree(this.doc.nodes, (n) => (set.has(n.id) ? { ...n, resizing: { ...n.resizing, ...resizing } } : n)));\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** Every frame in the document (top-level ones are artboards), with its rectangle, parent and children's constraints. */\n\tgetFrames(): VectorFrameListing[] {\n\t\tconst out: VectorFrameListing[] = [];\n\t\tconst visit = (list: readonly VectorNode[], parent: string | null) => {\n\t\t\tfor (const n of list) {\n\t\t\t\tif (isVectorFrame(n)) {\n\t\t\t\t\tout.push({\n\t\t\t\t\t\tid: n.id,\n\t\t\t\t\t\tname: n.name ?? 'Frame',\n\t\t\t\t\t\tparentId: parent,\n\t\t\t\t\t\tartboard: parent === null,\n\t\t\t\t\t\tframe: n.frame,\n\t\t\t\t\t\tbounds: vectorNodeBounds(n, vectorParentMatrix(this.doc.nodes, n.id) ?? IDENTITY_MATRIX),\n\t\t\t\t\t\tchildren: n.children.map((c) => ({ id: c.id, type: c.type, ...(c.name ? { name: c.name } : {}), horizontal: c.resizing?.horizontal ?? 'left', vertical: c.resizing?.vertical ?? 'top' }))\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tif (n.type === 'group') visit(n.children, n.id);\n\t\t\t}\n\t\t};\n\t\tvisit(this.doc.nodes, null);\n\t\treturn out;\n\t}\n\n\t/** One frame as an SVG document of its own size (its corner at the origin), or null when `id` is not a frame. */\n\tframeToSvg(id: string, options: VectorSvgExportOptions = {}): string | null {\n\t\tconst doc = vectorFrameDocument(this.storedDocument(), id, findVectorNode);\n\t\treturn doc ? vectorDocumentToSvg(doc, options) : null;\n\t}\n\n\t/** Mask groups among `ids` (default: the selection), or holding one of them directly. */\n\tprivate maskTargets(ids?: readonly string[]): VectorGroupNode[] {\n\t\tconst out = new Map<string, VectorGroupNode>();\n\t\tfor (const id of ids ?? this.selection) {\n\t\t\tconst n = this.getNode(id);\n\t\t\tif (isVectorMaskGroup(n)) out.set(n.id, n);\n\t\t\telse {\n\t\t\t\tconst parent = locateVectorNode(this.doc.nodes, id)?.parent;\n\t\t\t\tif (isVectorMaskGroup(parent)) out.set(parent.id, parent);\n\t\t\t}\n\t\t}\n\t\treturn [...out.values()];\n\t}\n\n\t/**\n\t * Release: the selected (or given) mask groups stop masking. The mask goes back on top of the\n\t * contents, drawn as itself again. A group with appearance of its own (filters, a blend mode,\n\t * path effects) stays a plain group; otherwise it is replaced by its children. One undo step.\n\t * Returns the released nodes' ids (the mask last).\n\t */\n\treleaseMask(options: { ids?: readonly string[] } = {}): string[] {\n\t\tconst groups = this.maskTargets(options.ids);\n\t\tif (!groups.length) return [];\n\t\tconst released: string[] = [];\n\t\tthis.change('Release Mask', () => {\n\t\t\tfor (const g of groups) {\n\t\t\t\tconst [mask, ...contents] = g.children;\n\t\t\t\tconst children = [...contents, mask];\n\t\t\t\treleased.push(...children.map((c) => c.id));\n\t\t\t\tconst { mask: _m, ...rest } = g;\n\t\t\t\tconst keep = !!(g.filters?.length || (g.blendMode && g.blendMode !== 'normal') || g.effects?.length);\n\t\t\t\tif (keep) {\n\t\t\t\t\tthis.replaceNode(g.id, { ...rest, children });\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst m = nodeMatrix(g);\n\t\t\t\tconst opacity = g.opacity;\n\t\t\t\tconst flat = children\n\t\t\t\t\t.map((c) => (g.transform ? transformVectorNode(c, m) : c))\n\t\t\t\t\t.map((c) => (opacity !== undefined && opacity < 1 ? { ...c, opacity: (c.opacity ?? 1) * opacity } : c));\n\t\t\t\tthis.setDoc(spliceTree(this.doc.nodes, g.id, flat));\n\t\t\t}\n\t\t\tthis.selection = released;\n\t\t\tthis.anchors = [];\n\t\t\tthis.editingId = null;\n\t\t});\n\t\treturn released;\n\t}\n\n\t/**\n\t * Changes the type or inversion of the selected (or given) mask groups, or turns their masks off\n\t * (`enabled: false` hides the mask, so the contents draw whole) and on. One undo step.\n\t */\n\tsetMaskOptions(patch: Partial<VectorMask> & { enabled?: boolean }, options: { ids?: readonly string[] } = {}): VectorActionResult {\n\t\tif (patch.type !== undefined && !isVectorMaskType(patch.type)) return { ok: false, message: `Unknown mask type \"${patch.type}\": use clip, alpha or luminance.` };\n\t\tif (patch.invert !== undefined && typeof patch.invert !== 'boolean') return { ok: false, message: 'invert must be true or false.' };\n\t\tif (patch.enabled !== undefined && typeof patch.enabled !== 'boolean') return { ok: false, message: 'enabled must be true or false.' };\n\t\tconst groups = this.maskTargets(options.ids);\n\t\tif (!groups.length) return { ok: false, message: 'Select a mask group (made with Make Clipping Mask or Make Opacity Mask).' };\n\t\tconst set = new Set(groups.map((g) => g.id));\n\t\tthis.change('Mask Options', () => {\n\t\t\tthis.setDoc(\n\t\t\t\tmapTree(this.doc.nodes, (n) => {\n\t\t\t\t\tif (!set.has(n.id) || !isVectorMaskGroup(n)) return n;\n\t\t\t\t\tconst type = patch.type ?? n.mask.type;\n\t\t\t\t\tconst invert = patch.invert ?? !!n.mask.invert;\n\t\t\t\t\tconst [source, ...rest] = n.children;\n\t\t\t\t\tconst shown = patch.enabled === undefined || patch.enabled === (source.visible !== false) ? source : patch.enabled ? (({ visible: _v, ...r }) => r as VectorNode)(source) : { ...source, visible: false };\n\t\t\t\t\treturn { ...n, mask: invert ? { type, invert } : { type }, ...(shown !== source ? { children: [shown, ...rest] } : {}) };\n\t\t\t\t})\n\t\t\t);\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/**\n\t * Edit Mask: selects the mask of the selected (or given) mask group, or of the group holding the\n\t * selection. A path mask opens in the direct tool for anchor editing (`editPath: false` keeps the\n\t * tool). Returns the mask's id, or null.\n\t */\n\teditMask(options: { id?: string; editPath?: boolean } = {}): string | null {\n\t\tconst group = this.maskTargets(options.id ? [options.id] : undefined)[0];\n\t\tif (!group) return null;\n\t\tconst mask = group.children[0];\n\t\tthis.selection = [mask.id];\n\t\tthis.anchors = [];\n\t\tthis.editingId = null;\n\t\tif (mask.type === 'path' && options.editPath !== false && !mask.locked) {\n\t\t\tthis.tool = 'direct';\n\t\t\tthis.editingId = mask.id;\n\t\t}\n\t\tthis.notify();\n\t\treturn mask.id;\n\t}\n\n\t/** Edit Contents: selects what the selected (or given) mask group masks. Returns their ids. */\n\teditMaskContents(options: { id?: string } = {}): string[] {\n\t\tconst group = this.maskTargets(options.id ? [options.id] : undefined)[0];\n\t\tif (!group) return [];\n\t\tthis.selection = group.children.slice(1).map((c) => c.id);\n\t\tthis.anchors = [];\n\t\tthis.editingId = null;\n\t\tthis.notify();\n\t\treturn [...this.selection];\n\t}\n\n\t/** Sets the options new blends get (spacing, steps, distance, orientation, morph). */\n\tsetBlendDefaults(patch: Partial<VectorBlendOptions>): void {\n\t\tthis.blendDefaults = { ...this.blendDefaults, ...blendOptionsOf(patch) };\n\t\tthis.notify();\n\t}\n\n\t/**\n\t * Adds a node as the last key of a blend (it must share the blend's parent); `start` is the\n\t * anchor of its first subpath the blend starts from. One undo step.\n\t */\n\taddBlendKey(blendId: string, nodeId: string, options: { start?: number | null } = {}): VectorActionResult {\n\t\tconst group = this.getNode(blendId);\n\t\tconst node = this.getNode(nodeId);\n\t\tif (!isVectorBlendGroup(group)) return { ok: false, message: `\"${blendId}\" is not a blend.` };\n\t\tif (!node || node.blendStep || node.locked || node.id === blendId) return { ok: false, message: `\"${nodeId}\" cannot be a key.` };\n\t\tconst parentOf = (id: string) => locateVectorNode(this.doc.nodes, id)?.parent?.id ?? null;\n\t\tif (parentOf(blendId) !== parentOf(nodeId)) return { ok: false, message: 'The new key must sit beside the blend (same parent).' };\n\t\tconst start = options.start;\n\t\tthis.change('Blend', () => {\n\t\t\tconst blend = start !== null && start !== undefined ? { ...group.blend, starts: { ...group.blend.starts, [nodeId]: start } } : group.blend;\n\t\t\t// The key leaves its place and joins the blend.\n\t\t\tconst m = invertMatrix(nodeMatrix(group));\n\t\t\tconst key = group.transform ? transformVectorNode(node, m) : node;\n\t\t\t// Two passes: the key must not be removed again from inside the blend.\n\t\t\tconst without = mapTree(this.doc.nodes, (n) => (n.id === nodeId ? null : n));\n\t\t\tthis.setDoc(spliceTree(without, blendId, [{ ...group, blend, children: [...group.children, key] }]));\n\t\t\tthis.selection = [blendId];\n\t\t\tthis.anchors = [];\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** Changes the spacing, steps, distance, orientation or morph of the selected (or given) blends. */\n\tsetBlendOptions(patch: Partial<VectorBlendOptions>, options: { ids?: readonly string[] } = {}): VectorActionResult {\n\t\tconst groups = this.blendTargets(options.ids);\n\t\tif (!groups.length) return { ok: false, message: 'Select a blend.' };\n\t\tconst clean = blendOptionsOf(patch);\n\t\tif (!Object.keys(clean).length) return { ok: false, message: 'No valid blend options were given.' };\n\t\tconst set = new Set(groups.map((g) => g.id));\n\t\tthis.change('Blend Options', () => {\n\t\t\tthis.setDoc(mapTree(this.doc.nodes, (n) => (set.has(n.id) && n.type === 'group' && n.blend ? { ...n, blend: { ...n.blend, ...clean } } : n)));\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** Expand: turns the selected (or given) blends into plain groups of their keys and steps. Returns their ids. */\n\texpandBlend(options: { ids?: readonly string[] } = {}): string[] {\n\t\tconst groups = this.blendTargets(options.ids);\n\t\tif (!groups.length) return [];\n\t\tconst set = new Set(groups.map((g) => g.id));\n\t\tconst used = new Set<string>();\n\t\twalkVectorNodes(this.doc.nodes, (n) => used.add(n.id));\n\t\tconst fresh = () => {\n\t\t\tlet id = this.createId();\n\t\t\twhile (used.has(id)) id = this.createId();\n\t\t\tused.add(id);\n\t\t\treturn id;\n\t\t};\n\t\tthis.change('Expand Blend', () => {\n\t\t\tthis.setDoc(mapTree(this.doc.nodes, (n) => (set.has(n.id) && n.type === 'group' ? expandVectorBlend(n, fresh) : n)));\n\t\t\tthis.selection = [...set];\n\t\t\tthis.anchors = [];\n\t\t});\n\t\treturn [...set];\n\t}\n\n\t/** Release: replaces the selected (or given) blends with their keys (and a replaced spine as a path). */\n\treleaseBlend(options: { ids?: readonly string[] } = {}): string[] {\n\t\tconst groups = this.blendTargets(options.ids);\n\t\tif (!groups.length) return [];\n\t\tconst released: string[] = [];\n\t\tthis.change('Release Blend', () => {\n\t\t\tfor (const g of groups) {\n\t\t\t\tconst nodes = releaseVectorBlend(g, () => this.newId());\n\t\t\t\treleased.push(...nodes.map((n) => n.id));\n\t\t\t\tthis.setDoc(spliceTree(this.doc.nodes, g.id, nodes));\n\t\t\t}\n\t\t\tthis.selection = released;\n\t\t\tthis.anchors = [];\n\t\t});\n\t\treturn released;\n\t}\n\n\t/**\n\t * Replace Spine: the selected blend's steps follow the selected path instead of the straight\n\t * lines between its keys. The path's first subpath becomes the spine (the path is removed)\n\t * and the keys move onto it, spread evenly by arc length.\n\t */\n\treplaceBlendSpine(options: { ids?: readonly string[] } = {}): VectorActionResult {\n\t\tconst roots = options.ids ?? this.selectedRoots();\n\t\tconst groups = this.blendTargets(roots);\n\t\tconst paths = roots.map((id) => this.getNode(id)).filter((n): n is VectorPathNode => n?.type === 'path' && !n.blendStep && !isVectorBlendGroup(locateVectorNode(this.doc.nodes, n.id)?.parent));\n\t\tif (groups.length !== 1 || paths.length !== 1) return { ok: false, message: 'Select one blend and one path to be its spine.' };\n\t\tconst group = groups[0];\n\t\tconst path = paths[0];\n\t\tconst first = path.subpaths.find((sp) => sp.anchors.length >= 2);\n\t\tif (!first) return { ok: false, message: 'The spine needs a path with at least two points.' };\n\t\t// The path's first subpath in the blend group's own space.\n\t\tconst m = multiplyMatrices(this.worldOf(path.id), invertMatrix(this.worldOf(group.id)));\n\t\tconst spine = transformVectorNode<VectorPathNode>({ id: '', type: 'path', subpaths: [first] }, m).subpaths[0];\n\t\tthis.change('Replace Spine', () => {\n\t\t\tconst withSpine: VectorGroupNode = { ...group, blend: { ...group.blend, spine } };\n\t\t\tconst targets = vectorBlendKeyTargets(withSpine);\n\t\t\tlet k = 0;\n\t\t\tconst children = withSpine.children.map((c) => {\n\t\t\t\tif (c.blendStep) return c;\n\t\t\t\tconst target = targets[k++];\n\t\t\t\tconst b = vectorNodeBounds(c);\n\t\t\t\tif (!target || !b) return c;\n\t\t\t\tconst dx = target[0] - (b.x + b.width / 2);\n\t\t\t\tconst dy = target[1] - (b.y + b.height / 2);\n\t\t\t\treturn Math.hypot(dx, dy) > 1e-9 ? transformVectorNode(c, translateMatrix(dx, dy)) : c;\n\t\t\t});\n\t\t\tthis.setDoc(mapTree(this.doc.nodes, (n) => (n.id === path.id ? null : n.id === group.id ? { ...withSpine, children } : n)));\n\t\t\tthis.selection = [group.id];\n\t\t\tthis.anchors = [];\n\t\t\tif (this.editingId === path.id) this.editingId = null;\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\tprivate shapeBuilderMap(ids?: readonly string[]): VectorShapeBuilderMap | null {\n\t\tconst roots = ids ?? this.selectedRoots();\n\t\tconst key = roots.join('\\u0000');\n\t\tif (this.builderCache?.doc === this.doc && this.builderCache.key === key) return this.builderCache.map;\n\t\t// The Shape Builder edits the shapes it takes area from: their stored geometry.\n\t\tconst entries = roots.length ? vectorPathEntries(this.doc, roots, { expand: false }) : [];\n\t\tconst map = entries.length ? vectorShapeBuilderMap(entries) : null;\n\t\tthis.builderCache = { doc: this.doc, key, map };\n\t\treturn map;\n\t}\n\n\t/** The faces the Shape Builder offers for the selected (or given) paths (document units). */\n\tgetShapeBuilderFaces(options: { ids?: readonly string[] } = {}): VectorShapeBuilderFace[] {\n\t\tconst m = this.shapeBuilderMap(options.ids);\n\t\tif (!m) return [];\n\t\treturn m.map.faces.map((f, index) => {\n\t\t\tconst p = m.map.faceInterior(index);\n\t\t\treturn {\n\t\t\t\tindex,\n\t\t\t\td: bezierPathToSvg(f.path, { precision: 3 }),\n\t\t\t\tarea: f.area,\n\t\t\t\tsources: m.map.faceSources(index).map((i) => m.entries[i].node.id),\n\t\t\t\tpoint: p ? { x: p[0], y: p[1] } : null\n\t\t\t};\n\t\t});\n\t}\n\n\t/** Pick distance of the Shape Builder's erase for line pieces (document units). */\n\tprivate builderLineTolerance(): number {\n\t\treturn Math.max(this.hitRadius, 4) / this.viewport.zoom;\n\t}\n\n\t/**\n\t * Shape Builder gesture along `points` (document units; one point is a click) over the faces\n\t * the selected (or given) paths make. Merges the faces touched into one shape styled like the\n\t * top-most shape over the first face (the current fill for a face only outlines enclose);\n\t * with `erase` deletes them, the pieces of unfilled open paths and the edges of other outlines\n\t * (cut where paths meet) within `tolerance`: a deleted edge opens a closed subpath there,\n\t * splits an open one or takes a subpath it runs all the way round, and edges bounding a face\n\t * deleted in the same gesture go with the face. Every shape covering a touched face loses\n\t * that area; paths keep their own transforms. One undo step.\n\t */\n\tshapeBuilder(points: readonly Vec2[], options: { ids?: readonly string[]; erase?: boolean; tolerance?: number } = {}): VectorShapeBuilderResult {\n\t\tconst none = (message: string): VectorShapeBuilderResult => ({ ok: false, message, merged: null, faces: 0, lines: 0, edges: 0 });\n\t\tif (!points.length || !points.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))) return none('Give the points of the gesture.');\n\t\tconst m = this.shapeBuilderMap(options.ids);\n\t\tif (!m) return none('Select paths that make faces.');\n\t\tconst erase = !!options.erase;\n\t\tconst tol = erase ? (options.tolerance ?? this.builderLineTolerance()) : null;\n\t\tconst touched: VectorBuilderTouched = { faces: [], lines: [], edges: [] };\n\t\tconst size = m.map.faces.reduce((s, f) => Math.max(s, f.bounds.maxX - f.bounds.minX, f.bounds.maxY - f.bounds.minY), 1);\n\t\tfor (const p of samplePolyline(points, Math.max(size / 2000, 0.25 / this.viewport.zoom))) addBuilderHit(touched, vectorShapeBuilderHit(m, p, tol));\n\t\tif (!touched.faces.length && !touched.lines.length && !touched.edges.length) return none('There is no face under the pointer.');\n\t\treturn this.applyShapeBuilder(m, touched, erase);\n\t}\n\n\tprivate applyShapeBuilder(m: VectorShapeBuilderMap, touched: VectorBuilderTouched, erase: boolean): VectorShapeBuilderResult {\n\t\tconst plan = planShapeBuilder(m, touched, erase);\n\t\tlet mergedId: string | null = null;\n\t\tconst created = new Map<string, string[]>();\n\t\tthis.change(erase ? 'Shape Builder Delete' : 'Shape Builder', () => {\n\t\t\tlet nodes = this.doc.nodes;\n\t\t\tm.entries.forEach((entry, i) => {\n\t\t\t\tconst node = entry.node;\n\t\t\t\tconst toLocal = (path: BezierPath) => transformBezierPath(path, invertMatrix(multiplyMatrices(nodeMatrix(node), entry.parent)));\n\t\t\t\tconst list: VectorNode[] = [];\n\t\t\t\tconst r = plan.replace.get(i);\n\t\t\t\tif (r === undefined) list.push(node);\n\t\t\t\telse if (r?.kind === 'fill') list.push({ ...node, fillRule: 'nonzero', subpaths: bezierToSubpaths(toLocal(r.path)) });\n\t\t\t\telse if (r?.kind === 'outline') list.push({ ...node, subpaths: bezierToSubpaths(toLocal(r.path)) });\n\t\t\t\telse if (r?.kind === 'pieces') {\n\t\t\t\t\tr.paths.forEach((path, k) =>\n\t\t\t\t\t\tlist.push({ ...node, id: k === 0 ? node.id : this.newId(), subpaths: bezierToSubpaths(toLocal(path)).map((sp) => ({ ...sp, closed: false })) })\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (plan.merged && plan.merged.style === i) {\n\t\t\t\t\tmergedId = this.newId();\n\t\t\t\t\tconst merged: VectorPathNode = {\n\t\t\t\t\t\t...vectorResultStyle(node, 'fill'),\n\t\t\t\t\t\ttype: 'path',\n\t\t\t\t\t\tid: mergedId,\n\t\t\t\t\t\tname: 'Shape',\n\t\t\t\t\t\t...(node.transform ? { transform: node.transform } : {}),\n\t\t\t\t\t\tsubpaths: bezierToSubpaths(toLocal(plan.merged.path))\n\t\t\t\t\t};\n\t\t\t\t\tif (plan.merged.currentFill || !hasPaint(merged.fill)) merged.fill = hasPaint(this.defaults.fill) ? this.defaults.fill! : '#808080';\n\t\t\t\t\tlist.push(merged);\n\t\t\t\t}\n\t\t\t\tif (r === undefined && list.length === 1) return;\n\t\t\t\tcreated.set(node.id, list.map((n) => n.id));\n\t\t\t\tnodes = spliceTree(nodes, node.id, list);\n\t\t\t});\n\t\t\tthis.setDoc(nodes);\n\t\t\t// The shapes stay selected so the next gesture works on them too.\n\t\t\tconst selection = this.selection.flatMap((id) => created.get(id) ?? [id]);\n\t\t\tif (mergedId && !selection.includes(mergedId)) selection.push(mergedId);\n\t\t\tthis.selection = selection.filter((id, k) => !!this.getNode(id) && selection.indexOf(id) === k);\n\t\t\tthis.anchors = [];\n\t\t\tthis.editingId = null;\n\t\t});\n\t\tthis.builderCache = null;\n\t\treturn { ok: true, merged: mergedId, faces: touched.faces.length, lines: touched.lines.length, edges: erase ? vectorBuilderErasedEdges(m, touched).length : 0 };\n\t}\n\n\t/** The paint bucket's map of the visible outlines, cached per document and gap. */\n\tprivate fillMap(gap: number): PlanarMap | null {\n\t\tif (this.fillCache?.doc === this.doc && this.fillCache.gap === gap) return this.fillCache.map;\n\t\tconst map = vectorFillMap(this.doc, gap);\n\t\tthis.fillCache = { doc: this.doc, gap, map };\n\t\treturn map;\n\t}\n\n\t/**\n\t * Paint bucket: the region the visible outlines enclose around `point` (document units),\n\t * crossings split and islands left as holes, becomes a new path filled with the current fill\n\t * default, placed just below the selection (or on top when nothing is selected). `gap`\n\t * (default `editing.fillGap`) closes gaps up to that size between loose ends. Returns the new\n\t * path's id, or null when nothing encloses the point.\n\t */\n\tfillRegion(point: Vec2, options: { gap?: number } = {}): string | null {\n\t\tif (!Number.isFinite(point.x) || !Number.isFinite(point.y)) return null;\n\t\tconst region = this.fillMap(options.gap ?? this.editing.fillGap)?.regionAt([point.x, point.y]);\n\t\tif (!region?.length) return null;\n\t\t// Below the lowest selected node, in its parent.\n\t\tlet below: string | null = null;\n\t\tconst roots = new Set(this.selectedRoots());\n\t\twalkVectorNodes(this.doc.nodes, (n) => {\n\t\t\tif (!below && roots.has(n.id)) below = n.id;\n\t\t});\n\t\tconst subpaths = bezierToSubpaths(below ? this.intoParentSpace(below, region) : region);\n\t\tif (!subpaths.length) return null;\n\t\tconst id = this.newId();\n\t\tconst node: VectorPathNode = { id, type: 'path', name: 'Fill', ...this.styleFor(false), stroke: null, fillRule: 'nonzero', subpaths };\n\t\tif (!hasPaint(node.fill)) node.fill = '#808080';\n\t\tconst target = below as string | null;\n\t\tthis.change('Fill Region', () => {\n\t\t\tif (target) {\n\t\t\t\tconst current = this.getNode(target)!;\n\t\t\t\tthis.setDoc(spliceTree(this.doc.nodes, target, [node, current]));\n\t\t\t} else this.setDoc([...this.doc.nodes, node]);\n\t\t\tif (!target) this.selection = [id];\n\t\t\tthis.anchors = [];\n\t\t\tthis.editingId = null;\n\t\t});\n\t\treturn id;\n\t}\n\n\t/** Updates the Shape Builder / paint bucket highlight for the pointer at `p`; true when it changed. */\n\tprivate hoverBuilder(p: Vec2, alt: boolean): boolean {\n\t\tif (this.tool === 'paintBucket') {\n\t\t\tconst region = this.fillMap(this.editing.fillGap)?.regionAt([p.x, p.y]) ?? null;\n\t\t\tconst key = region ? bezierPathToSvg(region, { precision: 2 }) : null;\n\t\t\tif (key === this.builderHoverKey) return false;\n\t\t\tthis.builderHoverKey = key;\n\t\t\tthis.builderPreview = region ? { mode: 'fill', faces: [region], lines: [], trail: null } : null;\n\t\t\treturn true;\n\t\t}\n\t\tconst m = this.selectedRoots().length ? this.shapeBuilderMap() : null;\n\t\tconst hit = m ? vectorShapeBuilderHit(m, p, alt ? this.builderLineTolerance() : null) : null;\n\t\tconst key = hit ? `${alt ? 'erase' : 'merge'}:${JSON.stringify(hit)}` : null;\n\t\tif (key === this.builderHoverKey) return false;\n\t\tthis.builderHoverKey = key;\n\t\tconst touched: VectorBuilderTouched = { faces: [], lines: [], edges: [] };\n\t\taddBuilderHit(touched, hit);\n\t\tthis.builderPreview = hit && m ? this.builderPreviewOf(m, touched, alt, null) : null;\n\t\treturn true;\n\t}\n\n\tprivate builderPreviewOf(m: VectorShapeBuilderMap, touched: VectorBuilderTouched, erase: boolean, trail: Vec2[] | null): VectorBuilderPreview {\n\t\tconst faces = m.map.faces;\n\t\treturn {\n\t\t\tmode: erase ? 'erase' : 'merge',\n\t\t\tfaces: touched.faces.map((f) => faces[f].path),\n\t\t\t// Edges bounding a face touched too go with the face, so they are not shown apart.\n\t\t\tlines: [...touched.lines.map((k) => m.pieces[k].path), ...(erase ? vectorBuilderErasedEdges(m, touched).map((k) => m.edges[k].path) : [])],\n\t\t\ttrail\n\t\t};\n\t}\n\n\tprivate downBuilder(p: Vec2, input: VectorPointerInput) {\n\t\tthis.builderHoverKey = null;\n\t\tif (this.tool === 'paintBucket') {\n\t\t\tthis.fillRegion(p);\n\t\t\tthis.builderPreview = null;\n\t\t\tthis.hoverBuilder(p, false);\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tconst m = this.selectedRoots().length ? this.shapeBuilderMap() : null;\n\t\tif (!m) return;\n\t\tconst erase = !!input.alt;\n\t\tconst touched: VectorBuilderTouched = { faces: [], lines: [], edges: [] };\n\t\taddBuilderHit(touched, vectorShapeBuilderHit(m, p, erase ? this.builderLineTolerance() : null));\n\t\tthis.drag = { kind: 'builder', erase, points: [p], touched, map: m };\n\t\tthis.interaction = { type: 'builder', mode: erase ? 'erase' : 'merge', points: [p] };\n\t\tthis.builderPreview = this.builderPreviewOf(m, touched, erase, [p]);\n\t\tthis.notify();\n\t}\n\n\tprivate moveBuilder(drag: Extract<Drag, { kind: 'builder' }>, p: Vec2) {\n\t\tconst last = drag.points[drag.points.length - 1];\n\t\tif (Math.hypot(p.x - last.x, p.y - last.y) * this.viewport.zoom < 1) return;\n\t\tconst tol = drag.erase ? this.builderLineTolerance() : null;\n\t\tfor (const q of samplePolyline([last, p], 2 / this.viewport.zoom)) addBuilderHit(drag.touched, vectorShapeBuilderHit(drag.map, q, tol));\n\t\tdrag.points.push(p);\n\t\tthis.interaction = { type: 'builder', mode: drag.erase ? 'erase' : 'merge', points: drag.points.slice() };\n\t\tthis.builderPreview = this.builderPreviewOf(drag.map, drag.touched, drag.erase, drag.points.slice());\n\t\tthis.notify();\n\t}\n\n\tprivate upBuilder(drag: Extract<Drag, { kind: 'builder' }>) {\n\t\tthis.builderPreview = null;\n\t\tthis.builderHoverKey = null;\n\t\tif (!drag.touched.faces.length && !drag.touched.lines.length && !drag.touched.edges.length) {\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tthis.applyShapeBuilder(drag.map, drag.touched, drag.erase);\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Anchor editing\n\t// -------------------------------------------------------------------------\n\n\tprivate updateAnchors(doc: VectorDocument, refs: readonly VectorAnchorRef[], fn: (a: VectorAnchor, ref: VectorAnchorRef) => VectorAnchor): VectorDocument {\n\t\tconst byNode = new Map<string, VectorAnchorRef[]>();\n\t\tfor (const r of refs) byNode.set(r.nodeId, [...(byNode.get(r.nodeId) ?? []), r]);\n\t\tconst nodes = mapTree(doc.nodes, (n) => {\n\t\t\tconst list = byNode.get(n.id);\n\t\t\tif (!list || n.type !== 'path' || n.locked) return n;\n\t\t\tconst subpaths = n.subpaths.map((sp, s) => {\n\t\t\t\tconst hits = list.filter((r) => r.subpath === s);\n\t\t\t\tif (!hits.length) return sp;\n\t\t\t\treturn { closed: sp.closed, anchors: sp.anchors.map((a, i) => (hits.some((r) => r.index === i) ? fn(a, { nodeId: n.id, subpath: s, index: i }) : a)) };\n\t\t\t});\n\t\t\treturn { ...n, subpaths };\n\t\t});\n\t\treturn nodes === doc.nodes ? doc : { ...doc, nodes };\n\t}\n\n\t/** Moves anchors by a document-space offset (converted into each node's own space). */\n\tprivate movedAnchors(doc: VectorDocument, refs: readonly VectorAnchorRef[], dx: number, dy: number): VectorDocument {\n\t\tconst local = new Map<string, Vec2>();\n\t\tfor (const r of refs) if (!local.has(r.nodeId)) local.set(r.nodeId, applyMatrixLinear(invertMatrix(this.worldOf(r.nodeId, doc)), { x: dx, y: dy }));\n\t\treturn this.updateAnchors(doc, refs, (a, ref) => {\n\t\t\tconst d = local.get(ref.nodeId)!;\n\t\t\treturn translateAnchor(a, d.x, d.y);\n\t\t});\n\t}\n\n\t/**\n\t * Moves one anchor to a document position. Its handles follow; with `curvature` (default:\n\t * the `curvatureMoves` setting) its two segments bend instead, as the Curvature tool does.\n\t */\n\tmoveAnchor(ref: VectorAnchorRef, to: Vec2, options: { curvature?: boolean } = {}): VectorActionResult {\n\t\tconst a = getAnchorWorld(this.doc, ref);\n\t\tif (!a) return { ok: false, message: 'There is no such anchor.' };\n\t\tconst curvature = options.curvature ?? this.editing.curvatureMoves;\n\t\tthis.change('Move Point', () => {\n\t\t\tthis.doc = curvature ? this.curvatureMoved(this.doc, ref, to) : this.movedAnchors(this.doc, [ref], to.x - a.x, to.y - a.y);\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** `doc` with anchor `ref` moved to document point `to` by a Curvature-tool move. */\n\tprivate curvatureMoved(doc: VectorDocument, ref: VectorAnchorRef, to: Vec2): VectorDocument {\n\t\tconst node = findVectorNode(doc.nodes, ref.nodeId);\n\t\tif (node?.type !== 'path' || node.locked || !node.subpaths[ref.subpath]) return doc;\n\t\tconst q = this.toLocal(ref.nodeId, to, doc);\n\t\tconst subpaths = node.subpaths.map((sp, i) => (i === ref.subpath ? curvatureMoveAnchor(sp, ref.index, q) : sp));\n\t\tconst nodes = mapTree(doc.nodes, (n) => (n.id === ref.nodeId ? { ...node, subpaths } : n));\n\t\treturn { ...doc, nodes };\n\t}\n\n\t/** `doc` with segment `ref` bent through document point `p` at parameter `t`. */\n\tprivate bent(doc: VectorDocument, ref: VectorSegmentRef, t: number, p: Vec2): VectorDocument {\n\t\tconst node = findVectorNode(doc.nodes, ref.nodeId);\n\t\tif (node?.type !== 'path' || node.locked || !node.subpaths[ref.subpath]) return doc;\n\t\tconst q = this.toLocal(ref.nodeId, p, doc);\n\t\tconst subpaths = node.subpaths.map((sp, i) => (i === ref.subpath ? bendSubpathSegment(sp, ref.segment, t, q) : sp));\n\t\treturn { ...doc, nodes: mapTree(doc.nodes, (n) => (n.id === ref.nodeId ? { ...node, subpaths } : n)) };\n\t}\n\n\t/**\n\t * Bends a segment so the curve passes through document point `to` at parameter `t`\n\t * (default 0.5, clamped to 0.05–0.95); see `bendSegment` in `pathEdit`.\n\t */\n\tbendSegment(ref: VectorSegmentRef, to: Vec2, t = 0.5): VectorActionResult {\n\t\tconst node = this.getNode(ref.nodeId);\n\t\tconst sp = node?.type === 'path' ? node.subpaths[ref.subpath] : undefined;\n\t\tif (!sp || ref.segment < 0 || ref.segment >= (sp.closed ? sp.anchors.length : sp.anchors.length - 1) || sp.anchors.length < 2) return { ok: false, message: 'There is no such segment.' };\n\t\tif (node!.locked) return { ok: false, message: 'The path is locked.' };\n\t\tthis.change('Bend Segment', () => {\n\t\t\tthis.doc = this.bent(this.doc, ref, t, to);\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** Moves one handle to a document position (see `moveAnchorHandle` for how the opposite handle follows). */\n\tmoveHandle(ref: VectorHandleRef, to: Vec2, options: { breakHandles?: boolean } = {}): VectorActionResult {\n\t\tif (!getAnchor(this.doc, ref)) return { ok: false, message: 'There is no such anchor.' };\n\t\tconst local = this.toLocal(ref.nodeId, to);\n\t\tthis.change('Move Handle', () => {\n\t\t\tthis.doc = this.updateAnchors(this.doc, [ref], (a) => moveAnchorHandle(a, ref.handle, local, options.breakHandles));\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** Converts the selected (or given) anchors between corner, smooth and symmetric. */\n\tconvertAnchors(type: VectorAnchorType, refs: readonly VectorAnchorRef[] = this.anchors): VectorActionResult {\n\t\tif (!refs.length) return { ok: false, message: 'Select points to convert.' };\n\t\tthis.change(type === 'corner' ? 'Make Corner' : 'Make Smooth', () => {\n\t\t\tconst nodes = mapTree(this.doc.nodes, (n) => {\n\t\t\t\tif (n.type !== 'path') return n;\n\t\t\t\tconst mine = refs.filter((r) => r.nodeId === n.id);\n\t\t\t\tif (!mine.length) return n;\n\t\t\t\tconst subpaths = n.subpaths.map((sp, s) => mine.filter((r) => r.subpath === s).reduce((acc, r) => convertAnchor(acc, r.index, type), sp));\n\t\t\t\treturn { ...n, subpaths };\n\t\t\t});\n\t\t\tthis.setDoc(nodes);\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** Adds an anchor on the nearest segment of `nodeId` to document point `point` (shape unchanged). */\n\tinsertAnchorAt(nodeId: string, point: Vec2): VectorAnchorRef | null {\n\t\tconst node = this.getNode(nodeId);\n\t\tif (node?.type !== 'path') return null;\n\t\tconst hit = nearestSegment(node, this.toLocal(nodeId, point));\n\t\tif (!hit) return null;\n\t\tconst { subpath, index } = insertAnchor(node.subpaths[hit.subpath], hit.segment, hit.t);\n\t\tconst ref = { nodeId, subpath: hit.subpath, index };\n\t\tthis.change('Add Point', () => {\n\t\t\tthis.replaceNode(nodeId, { ...node, subpaths: node.subpaths.map((sp, i) => (i === hit.subpath ? subpath : sp)) });\n\t\t\tthis.editingId = nodeId;\n\t\t\tthis.anchors = [ref];\n\t\t});\n\t\treturn ref;\n\t}\n\n\t/**\n\t * Deletes the selected (or given) anchors; subpaths left with one anchor and empty paths go\n\t * too. With `keepShape` (default: the `keepShapeOnDelete` setting, on) the neighbours'\n\t * handles are refitted so the path keeps its shape; off, the neighbours are joined as they are.\n\t */\n\tdeleteAnchors(refs: readonly VectorAnchorRef[] = this.anchors, options: { keepShape?: boolean } = {}): VectorActionResult {\n\t\tif (!refs.length) return { ok: false, message: 'Select points to delete.' };\n\t\tconst keepShape = options.keepShape ?? this.editing.keepShapeOnDelete;\n\t\tthis.change('Delete Points', () => {\n\t\t\tconst nodes = mapTree(this.doc.nodes, (n) => {\n\t\t\t\tif (n.type !== 'path' || n.locked) return n;\n\t\t\t\tconst mine = refs.filter((r) => r.nodeId === n.id);\n\t\t\t\tif (!mine.length) return n;\n\t\t\t\tconst subpaths = n.subpaths\n\t\t\t\t\t.map((sp, s) => {\n\t\t\t\t\t\tconst indices = mine.filter((r) => r.subpath === s).map((r) => r.index);\n\t\t\t\t\t\tif (!indices.length) return sp;\n\t\t\t\t\t\treturn keepShape ? removeAnchorsKeepingShape(sp, indices) : removeAnchors(sp, indices);\n\t\t\t\t\t})\n\t\t\t\t\t.filter((sp) => sp.anchors.length > 1);\n\t\t\t\treturn subpaths.length ? { ...n, subpaths } : null;\n\t\t\t});\n\t\t\tthis.setDoc(nodes);\n\t\t\tthis.anchors = [];\n\t\t\tthis.selection = this.selection.filter((id) => this.getNode(id));\n\t\t\tif (this.editingId && !this.getNode(this.editingId)) this.editingId = null;\n\t\t\tif (this.pen && !this.getNode(this.pen.nodeId)) this.pen = null;\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** Opens or closes the subpaths that contain selected anchors (or all subpaths of `nodeId`). */\n\tsetClosed(closed: boolean, nodeId?: string): VectorActionResult {\n\t\tconst targets = nodeId\n\t\t\t? (() => {\n\t\t\t\t\tconst n = this.getNode(nodeId);\n\t\t\t\t\treturn n?.type === 'path' ? n.subpaths.map((_, s) => ({ nodeId, subpath: s })) : [];\n\t\t\t\t})()\n\t\t\t: this.anchors.map((r) => ({ nodeId: r.nodeId, subpath: r.subpath }));\n\t\tif (!targets.length) return { ok: false, message: 'Select a path or points.' };\n\t\tthis.change(closed ? 'Close Path' : 'Open Path', () => {\n\t\t\tthis.setDoc(\n\t\t\t\tmapTree(this.doc.nodes, (n) =>\n\t\t\t\t\tn.type === 'path' && targets.some((t) => t.nodeId === n.id)\n\t\t\t\t\t\t? { ...n, subpaths: n.subpaths.map((sp, s) => (targets.some((t) => t.nodeId === n.id && t.subpath === s) ? { ...sp, closed } : sp)) }\n\t\t\t\t\t\t: n\n\t\t\t\t)\n\t\t\t);\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** Reverses the direction of every subpath of the selected paths. */\n\treversePaths(ids: readonly string[] = this.selection): VectorActionResult {\n\t\tconst set = new Set(ids.flatMap((id) => (this.getNode(id) ? collectPathNodes(this.getNode(id)!) : [])).map((p) => p.id));\n\t\tif (!set.size) return { ok: false, message: 'Select a path.' };\n\t\tthis.change('Reverse Path', () => {\n\t\t\tthis.setDoc(\n\t\t\t\tmapTree(this.doc.nodes, (n) =>\n\t\t\t\t\tn.type === 'path' && set.has(n.id)\n\t\t\t\t\t\t? { ...n, subpaths: n.subpaths.map(reverseSubpath), ...(n.strokeWidthProfile?.length ? { strokeWidthProfile: reverseWidthProfile(n.strokeWidthProfile) } : {}) }\n\t\t\t\t\t\t: n\n\t\t\t\t)\n\t\t\t);\n\t\t\tthis.anchors = [];\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Path clean-up, Scissors, Knife and Eraser\n\t// -------------------------------------------------------------------------\n\n\t/** Unlocked path nodes in the given roots (default: the paths being edited, else the selection). */\n\tprivate pathTargets(ids?: readonly string[]): VectorPathNode[] {\n\t\tconst roots = ids ?? (this.tool === 'direct' && this.anchors.length ? [...new Set(this.anchors.map((r) => r.nodeId))] : this.selection);\n\t\tconst out = new Map<string, VectorPathNode>();\n\t\tfor (const id of roots) {\n\t\t\tconst node = this.getNode(id);\n\t\t\tif (node && !node.locked) for (const p of collectPathNodes(node)) if (!p.locked) out.set(p.id, p);\n\t\t}\n\t\t// Document order, bottom to top.\n\t\tconst order: string[] = [];\n\t\twalkVectorNodes(this.doc.nodes, (n) => {\n\t\t\tif (out.has(n.id)) order.push(n.id);\n\t\t});\n\t\treturn order.map((id) => out.get(id)!);\n\t}\n\n\t/** Subpaths of node `from` mapped into node `to`'s own space. */\n\tprivate subpathsIn(from: VectorPathNode, to: string): VectorSubpath[] {\n\t\tconst m = multiplyMatrices(this.worldOf(from.id), invertMatrix(this.worldOf(to)));\n\t\tif (isRoot(m)) return from.subpaths;\n\t\tconst map = (p: Vec2) => applyMatrix(m, p);\n\t\treturn from.subpaths.map((sp) => ({\n\t\t\tclosed: sp.closed,\n\t\t\tanchors: sp.anchors.map((a) => ({ ...map(a), ...(a.in ? { in: map(a.in) } : {}), ...(a.out ? { out: map(a.out) } : {}), ...(a.type ? { type: a.type } : {}), ...(a.corner ? { corner: { ...a.corner } } : {}), ...(a.id ? { id: a.id } : {}) }))\n\t\t}));\n\t}\n\n\t/**\n\t * Replaces a path with pieces: each entry becomes one path (the first keeps the node's id and\n\t * place, the others go just above it with the same style). No pieces removes the path.\n\t * Returns the ids of the paths now standing in for it.\n\t */\n\tprivate replaceWithPieces(node: VectorPathNode, pieces: readonly VectorSubpath[][]): string[] {\n\t\tconst nodes = pieces.filter((p) => p.length).map((subpaths, i) => ({ ...node, id: i === 0 ? node.id : this.newId(), subpaths }) as VectorPathNode);\n\t\tthis.setDoc(spliceTree(this.doc.nodes, node.id, nodes));\n\t\treturn nodes.map((n) => n.id);\n\t}\n\n\t/** Clears anchor selections, editing and pen state that no longer point at anything. */\n\tprivate dropStaleRefs() {\n\t\tthis.selection = this.selection.filter((id) => this.getNode(id));\n\t\tthis.anchors = this.anchors.filter((r) => getAnchor(this.doc, r));\n\t\tif (this.editingId && !this.getNode(this.editingId)) this.editingId = null;\n\t\tif (this.pen && !this.getNode(this.pen.nodeId)) this.pen = null;\n\t}\n\n\t/**\n\t * Join: the open subpaths of the selected (or given) paths become one open path, joined\n\t * nearest endpoints first (ends within `tolerance` document units merge into one anchor,\n\t * others get a straight segment); a single open subpath is closed. The result replaces the\n\t * lowest path, in its space and style. Default tolerance: the pick distance.\n\t */\n\tjoinPaths(options: { ids?: readonly string[]; tolerance?: number } = {}): VectorActionResult {\n\t\tconst paths = this.pathTargets(options.ids);\n\t\tif (!paths.length) return { ok: false, message: 'Select paths to join.' };\n\t\tconst base = paths[0];\n\t\tconst all = paths.flatMap((p) => this.subpathsIn(p, base.id));\n\t\tif (!all.some((sp) => !sp.closed && sp.anchors.length > 1)) return { ok: false, message: 'There are no open paths to join.' };\n\t\tconst subpaths = joinSubpaths(all, options.tolerance ?? this.tolerance());\n\t\tconst drop = new Set(paths.slice(1).map((p) => p.id));\n\t\tthis.change('Join', () => {\n\t\t\tthis.setDoc(mapTree(this.doc.nodes, (n) => (n.id === base.id ? { ...base, subpaths } : drop.has(n.id) ? null : n)));\n\t\t\tthis.selection = [base.id];\n\t\t\tthis.anchors = [];\n\t\t\tif (this.editingId && drop.has(this.editingId)) this.editingId = base.id;\n\t\t\tthis.dropStaleRefs();\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** Average: moves the selected (or given) anchors onto their average position along `axis`, in document space. */\n\taverageAnchors(axis: VectorAverageAxis = 'both', refs: readonly VectorAnchorRef[] = this.anchors): VectorActionResult {\n\t\tconst points = refs.map((r) => ({ ref: r, at: getAnchorWorld(this.doc, r) })).filter((p): p is { ref: VectorAnchorRef; at: NonNullable<ReturnType<typeof getAnchorWorld>> } => !!p.at);\n\t\tif (points.length < 2) return { ok: false, message: 'Select at least two points to average.' };\n\t\tconst cx = points.reduce((s, p) => s + p.at.x, 0) / points.length;\n\t\tconst cy = points.reduce((s, p) => s + p.at.y, 0) / points.length;\n\t\tthis.change('Average', () => {\n\t\t\tfor (const { ref, at } of points) {\n\t\t\t\tconst dx = axis === 'horizontal' ? 0 : cx - at.x;\n\t\t\t\tconst dy = axis === 'vertical' ? 0 : cy - at.y;\n\t\t\t\tif (dx || dy) this.doc = this.movedAnchors(this.doc, [ref], dx, dy);\n\t\t\t}\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/** Applies `fn` to every subpath of the target paths as one undo step. */\n\tprivate editSubpaths(label: string, ids: readonly string[] | undefined, fn: (sp: VectorSubpath, node: VectorPathNode, index: number) => VectorSubpath): VectorActionResult {\n\t\tconst paths = this.pathTargets(ids);\n\t\tif (!paths.length) return { ok: false, message: 'Select a path.' };\n\t\tconst set = new Set(paths.map((p) => p.id));\n\t\tthis.change(label, () => {\n\t\t\tthis.setDoc(mapTree(this.doc.nodes, (n) => (n.type === 'path' && set.has(n.id) ? { ...n, subpaths: n.subpaths.map((sp, i) => fn(sp, n, i)) } : n)));\n\t\t\tthis.dropStaleRefs();\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/**\n\t * Remove Redundant Points: drops anchors of the selected (or given) paths that can go\n\t * without moving the outline more than `tolerance` document units (default 0.5).\n\t */\n\tremoveRedundantPoints(options: { ids?: readonly string[]; tolerance?: number } = {}): VectorActionResult {\n\t\tconst tolerance = options.tolerance ?? 0.5;\n\t\treturn this.editSubpaths('Remove Redundant Points', options.ids, (sp) => removeRedundantSubpathPoints(sp, tolerance));\n\t}\n\n\t/**\n\t * Smooth: pulls handles towards smooth tangents by `amount` (0–1, default 1). With the direct\n\t * tool and points selected, only those points change.\n\t */\n\tsmoothPaths(options: { ids?: readonly string[]; amount?: number; refs?: readonly VectorAnchorRef[] } = {}): VectorActionResult {\n\t\tconst refs = options.refs ?? (!options.ids && this.tool === 'direct' && this.anchors.length ? this.anchors : null);\n\t\tconst ids = options.ids ?? (refs ? [...new Set(refs.map((r) => r.nodeId))] : undefined);\n\t\treturn this.editSubpaths('Smooth', ids, (sp, node, i) => {\n\t\t\tconst only = refs ? new Set(refs.filter((r) => r.nodeId === node.id && r.subpath === i).map((r) => r.index)) : undefined;\n\t\t\treturn only && !only.size ? sp : smoothSubpath(sp, options.amount ?? 1, only);\n\t\t});\n\t}\n\n\t/** Splits a cut subpath into pieces: separate paths for a one-subpath node, subpaths in place for a compound one. */\n\tprivate applyCuts(node: VectorPathNode, cut: (sp: VectorSubpath, index: number) => VectorSubpath[] | null): string[] | null {\n\t\tconst results = node.subpaths.map((sp, i) => cut(sp, i));\n\t\tif (results.every((r) => !r)) return null;\n\t\tconst subpaths = node.subpaths.flatMap((sp, i) => results[i] ?? [sp]);\n\t\tif (node.subpaths.length === 1) return this.replaceWithPieces(node, subpaths.map((sp) => [sp]));\n\t\tthis.replaceNode(node.id, { ...node, subpaths });\n\t\treturn [node.id];\n\t}\n\n\t/**\n\t * Scissors: cuts path `nodeId` at the outline point nearest document point `point` (at an\n\t * anchor when within the pick distance of one). A closed subpath opens there; an open one\n\t * splits in two (a one-subpath path becomes two paths). Returns the ids of the paths.\n\t */\n\tcutPathAt(nodeId: string, point: Vec2): string[] | null {\n\t\tconst node = this.getNode(nodeId);\n\t\tif (node?.type !== 'path' || node.locked) return null;\n\t\tconst local = this.toLocal(nodeId, point);\n\t\tconst hit = nearestSegment(node, local);\n\t\tif (!hit) return null;\n\t\tconst sp = node.subpaths[hit.subpath];\n\t\tconst n = sp.anchors.length;\n\t\tconst near = [hit.segment, (hit.segment + 1) % n].find((i) => Math.hypot(sp.anchors[i].x - hit.point.x, sp.anchors[i].y - hit.point.y) <= this.tolerance());\n\t\tif (!sp.closed && n > 1 && (near === 0 || near === n - 1) && !(hit.t > 1e-6 && hit.t < 1 - 1e-6)) return null;\n\t\tlet ids: string[] | null = null;\n\t\tthis.change('Cut Path', () => {\n\t\t\tids = this.applyCuts(node, (s, i) => (i !== hit.subpath ? null : near !== undefined ? cutSubpathAt(s, [near]) : splitSubpathAt(s, hit.segment, hit.t)));\n\t\t\tif (ids) {\n\t\t\t\tthis.selection = ids;\n\t\t\t\tthis.anchors = [];\n\t\t\t\tif (this.editingId === nodeId) this.editingId = null;\n\t\t\t}\n\t\t\tthis.dropStaleRefs();\n\t\t});\n\t\treturn ids;\n\t}\n\n\t/** Scissors on the selected (or given) anchors: each cuts its subpath there (see `cutPathAt`). */\n\tcutAtAnchors(refs: readonly VectorAnchorRef[] = this.anchors): VectorActionResult {\n\t\tconst byNode = new Map<string, VectorAnchorRef[]>();\n\t\tfor (const r of refs) if (getAnchor(this.doc, r)) byNode.set(r.nodeId, [...(byNode.get(r.nodeId) ?? []), r]);\n\t\tif (!byNode.size) return { ok: false, message: 'Select points to cut at.' };\n\t\tconst created: string[] = [];\n\t\tthis.change('Cut Path', () => {\n\t\t\tfor (const [id, list] of byNode) {\n\t\t\t\tconst node = this.getNode(id) as VectorPathNode;\n\t\t\t\tif (node.locked) continue;\n\t\t\t\tconst ids = this.applyCuts(node, (sp, i) => {\n\t\t\t\t\tconst at = list.filter((r) => r.subpath === i).map((r) => r.index);\n\t\t\t\t\tconst pieces = at.length ? cutSubpathAt(sp, at) : null;\n\t\t\t\t\treturn pieces && !(pieces.length === 1 && pieces[0].closed === sp.closed) ? pieces : null;\n\t\t\t\t});\n\t\t\t\tif (ids) created.push(...ids);\n\t\t\t}\n\t\t\tif (created.length) this.selection = created;\n\t\t\tthis.anchors = [];\n\t\t\tthis.dropStaleRefs();\n\t\t});\n\t\treturn created.length ? { ok: true } : { ok: false, message: 'Points at the ends of open paths do not cut.' };\n\t}\n\n\t/** Visible, unlocked paths a stroke over `points` can reach: in `ids`, the selection, or (with neither) any whose bounds it crosses. */\n\tprivate strokeTargets(points: readonly Vec2[], ids: readonly string[] | undefined, pad: number): VectorPathNode[] {\n\t\tconst blocked = new Set<string>();\n\t\twalkVectorNodes(this.doc.nodes, (n, parent) => {\n\t\t\tif (n.visible === false || n.locked || (parent && blocked.has(parent.id))) blocked.add(n.id);\n\t\t});\n\t\tconst visible = (p: VectorPathNode) => !blocked.has(p.id);\n\t\tif (ids?.length || this.selection.length) return this.pathTargets(ids ?? this.selection).filter(visible);\n\t\tconst box = polylineBounds(points.map(toPoint), pad);\n\t\tif (!box) return [];\n\t\tconst out: VectorPathNode[] = [];\n\t\twalkVectorNodes(this.doc.nodes, (n) => {\n\t\t\tif (n.type !== 'path' || !visible(n)) return;\n\t\t\tconst b = vectorNodeBounds(n, vectorParentMatrix(this.doc.nodes, n.id) ?? IDENTITY_MATRIX);\n\t\t\tif (b && boundsIntersect(b, box)) out.push(n);\n\t\t});\n\t\treturn out;\n\t}\n\n\t/** A stroke's points in node `id`'s own space. */\n\tprivate localPoints(id: string, points: readonly Vec2[]): Point[] {\n\t\tconst inv = invertMatrix(this.worldOf(id));\n\t\treturn points.map((p) => toPoint(applyMatrix(inv, p)));\n\t}\n\n\t/**\n\t * Knife: cuts closed paths along the polyline `points` (document space) wherever it crosses\n\t * them from edge to edge, with no gap between the pieces. It cuts the given paths, the\n\t * selection, or (with neither) every path it crosses. Returns the ids of the pieces (empty\n\t * when nothing was cut).\n\t */\n\tknife(points: readonly Vec2[], options: { ids?: readonly string[] } = {}): string[] {\n\t\tconst pts = simplifyPolyline(points.map(toPoint), 0.25 / this.viewport.zoom).map(([x, y]) => ({ x, y }));\n\t\tif (pts.length < 2) return [];\n\t\tconst plan: { node: VectorPathNode; pieces: VectorSubpath[][] }[] = [];\n\t\tfor (const node of this.strokeTargets(pts, options.ids, 0)) {\n\t\t\tconst closed = node.subpaths.filter((sp) => sp.anchors.length > 1);\n\t\t\tif (!closed.length || closed.some((sp) => !sp.closed)) continue;\n\t\t\tconst result = knifePath(subpathsToBezier(closed), node.fillRule ?? 'nonzero', this.localPoints(node.id, pts));\n\t\t\tif (result) plan.push({ node, pieces: result.map((path) => bezierToSubpaths(path)) });\n\t\t}\n\t\tif (!plan.length) return [];\n\t\tconst created: string[] = [];\n\t\tthis.change('Knife', () => {\n\t\t\tfor (const { node, pieces } of plan) created.push(...this.replaceWithPieces(node, pieces));\n\t\t\tthis.selection = created;\n\t\t\tthis.anchors = [];\n\t\t\tthis.dropStaleRefs();\n\t\t});\n\t\treturn created;\n\t}\n\n\t/**\n\t * Eraser: rubs out a round stroke of diameter `size` (default: the `eraserSize` setting)\n\t * along `points` (document space). Filled closed paths lose that area (pieces that come\n\t * apart become separate paths); outlines are cut where they pass under it. It erases from\n\t * the given paths, the selection, or (with neither) every path it reaches. Returns the ids\n\t * of the paths it changed or created.\n\t */\n\terase(points: readonly Vec2[], options: { ids?: readonly string[]; size?: number } = {}): string[] {\n\t\tconst width = options.size ?? this.editing.eraserSize;\n\t\tif (!(width > 0) || !points.length) return [];\n\t\tconst pts = simplifyPolyline(points.map(toPoint), Math.min(width * 0.05, 0.25 / this.viewport.zoom));\n\t\tconst region = eraserRegion(pts, width);\n\t\tif (!region.length) return [];\n\t\tconst plan: { node: VectorPathNode; pieces: VectorSubpath[][] }[] = [];\n\t\tconst rb = bezierPathBounds(region);\n\t\tconst reach = rb ? [{ x: rb.minX, y: rb.minY }, { x: rb.maxX, y: rb.maxY }] : points;\n\t\tfor (const node of this.strokeTargets(reach, options.ids, 0)) {\n\t\t\tconst local = transformBezierPath(region, invertMatrix(this.worldOf(node.id)));\n\t\t\tconst live = node.subpaths.filter((sp) => sp.anchors.length > 1);\n\t\t\tif (!live.length) continue;\n\t\t\tif (hasPaint(node.fill) && live.every((sp) => sp.closed)) {\n\t\t\t\tconst faces = eraseFromFill(subpathsToBezier(live), node.fillRule ?? 'nonzero', local);\n\t\t\t\tif (faces) plan.push({ node, pieces: faces.map((f) => bezierToSubpaths(f)) });\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst results = live.map((sp) => eraseFromOutline(sp, local));\n\t\t\tif (results.every((r) => !r)) continue;\n\t\t\tconst subpaths = live.flatMap((sp, i) => results[i] ?? [sp]);\n\t\t\tplan.push({ node, pieces: live.length === 1 ? subpaths.map((sp) => [sp]) : [subpaths] });\n\t\t}\n\t\tif (!plan.length) return [];\n\t\tconst changed: string[] = [];\n\t\tthis.change('Erase', () => {\n\t\t\tfor (const { node, pieces } of plan) changed.push(...this.replaceWithPieces(node, pieces));\n\t\t\tthis.anchors = [];\n\t\t\tthis.dropStaleRefs();\n\t\t\tif (this.selection.length || options.ids) this.selection = [...new Set([...this.selection, ...changed.filter((id) => this.getNode(id))])];\n\t\t});\n\t\treturn changed;\n\t}\n\n\t/** Ends the pen path being drawn (a lone anchor is removed). */\n\tfinishPath(): void {\n\t\tif (!this.pen) return;\n\t\tconst node = this.getNode(this.pen.nodeId) as VectorPathNode | undefined;\n\t\tconst sp = node?.subpaths[this.pen.subpath];\n\t\tconst pen = this.pen;\n\t\tthis.pen = null;\n\t\tthis.pointer = null;\n\t\tif (node && sp && sp.anchors.length < 2) {\n\t\t\tconst subpaths = node.subpaths.filter((_, i) => i !== pen.subpath);\n\t\t\tthis.change('Delete Path', () => {\n\t\t\t\tthis.replaceNode(node.id, subpaths.length ? { ...node, subpaths } : null);\n\t\t\t\tif (!subpaths.length) {\n\t\t\t\t\tthis.selection = this.selection.filter((id) => id !== node.id);\n\t\t\t\t\tif (this.editingId === node.id) this.editingId = null;\n\t\t\t\t}\n\t\t\t\tthis.anchors = [];\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tthis.notify();\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Clipboard\n\t// -------------------------------------------------------------------------\n\n\t/** Copies the selection. Returns JSON text for the system clipboard (or null). */\n\tcopy(): string | null {\n\t\tconst nodes = this.selectedRoots()\n\t\t\t.map((id) => this.getNode(id))\n\t\t\t.filter((n): n is VectorNode => !!n)\n\t\t\t.map((n) => cloneVectorNode(n));\n\t\tif (!nodes.length) return null;\n\t\tthis.clipboard = nodes;\n\t\treturn JSON.stringify({ type: VECTOR_CLIPBOARD_TYPE, nodes });\n\t}\n\n\tcut(): string | null {\n\t\tconst text = this.copy();\n\t\tif (text) this.removeNodes(this.selectedRoots(), { label: 'Cut' });\n\t\treturn text;\n\t}\n\n\t/**\n\t * Pastes `text` (from `copy()`, or SVG markup) or the internal clipboard, offset by 10, 10.\n\t * Returns the new ids.\n\t */\n\tpaste(text?: string, options: { offset?: Vec2 } = {}): string[] {\n\t\tlet nodes: VectorNode[] | null = null;\n\t\tif (text) {\n\t\t\tconst trimmed = text.trim();\n\t\t\tif (trimmed.startsWith('{')) {\n\t\t\t\ttry {\n\t\t\t\t\tconst data = JSON.parse(trimmed);\n\t\t\t\t\tif (data?.type === VECTOR_CLIPBOARD_TYPE && Array.isArray(data.nodes)) nodes = data.nodes;\n\t\t\t\t} catch {\n\t\t\t\t\tnodes = null;\n\t\t\t\t}\n\t\t\t} else if (/<svg[\\s>]/i.test(trimmed)) {\n\t\t\t\ttry {\n\t\t\t\t\tnodes = parseVectorSvg(trimmed).nodes;\n\t\t\t\t} catch {\n\t\t\t\t\tnodes = null;\n\t\t\t\t}\n\t\t\t}\n\t\t} else nodes = this.clipboard;\n\t\tif (!nodes?.length) return [];\n\t\tconst offset = options.offset ?? (text && !text.trim().startsWith('{') ? { x: 0, y: 0 } : { x: 10, y: 10 });\n\t\tconst copies = nodes.map((n) => transformVectorNode(this.freshIds(n), translateMatrix(offset.x, offset.y)));\n\t\tthis.change('Paste', () => {\n\t\t\tthis.setDoc([...this.doc.nodes, ...copies]);\n\t\t\tthis.selection = copies.map((n) => n.id);\n\t\t\tthis.anchors = [];\n\t\t});\n\t\tif (!text) this.clipboard = copies.map((n) => cloneVectorNode(n));\n\t\treturn copies.map((n) => n.id);\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Undo\n\t// -------------------------------------------------------------------------\n\n\tundo(): boolean {\n\t\tthis.cancelDrag();\n\t\tthis.pen = null;\n\t\treturn this.history.undo();\n\t}\n\n\tredo(): boolean {\n\t\tthis.cancelDrag();\n\t\treturn this.history.redo();\n\t}\n\n\t/** Deletes selected anchors (direct tool) or nodes. */\n\tdeleteSelection(): VectorActionResult {\n\t\tif (this.tool === 'direct' && this.anchors.length) return this.deleteAnchors();\n\t\treturn this.removeNodes(this.selectedRoots());\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Pointer input\n\t// -------------------------------------------------------------------------\n\n\t/** The fields the anchor / handle helpers read, without building a full state. */\n\tprivate view() {\n\t\treturn { document: this.doc, selection: this.selection, editingId: this.editingId, tool: this.tool, pen: this.pen, selectedAnchors: this.anchors };\n\t}\n\n\tprivate tolerance() {\n\t\treturn this.hitRadius / this.viewport.zoom;\n\t}\n\n\tprivate hitTransformHandle(screen: Vec2): VectorTransformHandle | null {\n\t\tif (this.tool !== 'select' || !this.selection.length) return null;\n\t\tconst frame = this.frameOf(this.selectedRoots());\n\t\tif (!frame) return null;\n\t\tconst { box, handles } = getVectorSelectionBoxGeometry(frame, this.viewport);\n\t\tconst r = this.hitRadius + 2;\n\t\t// Tiny boxes: only corners and rotation, so the box can still be grabbed.\n\t\tconst usable = box.width < 2 * r || box.height < 2 * r ? handles.filter((h) => h.handle.length === 2 || h.handle === 'rotate') : handles;\n\t\tfor (const h of usable) if (Math.hypot(h.screen.x - screen.x, h.screen.y - screen.y) <= r * Math.SQRT2) return h.handle;\n\t\treturn null;\n\t}\n\n\tprivate hitHandle(screen: Vec2): VectorHandleRef | null {\n\t\tfor (const ref of getVisibleHandles(this.view()).reverse()) {\n\t\t\tconst a = getAnchorWorld(this.doc, ref);\n\t\t\tconst h = a?.[ref.handle];\n\t\t\tif (!h) continue;\n\t\t\tconst s = this.documentToScreen(h);\n\t\t\tif (Math.hypot(s.x - screen.x, s.y - screen.y) <= this.hitRadius) return ref;\n\t\t}\n\t\treturn null;\n\t}\n\n\tprivate cornerView() {\n\t\treturn { ...this.view(), viewport: this.viewport };\n\t}\n\n\tprivate hitCornerWidget(screen: Vec2): VectorCornerWidget | null {\n\t\tlet best: VectorCornerWidget | null = null;\n\t\tlet bestD = Infinity;\n\t\tfor (const w of getVectorCornerWidgets(this.cornerView())) {\n\t\t\tconst s = this.documentToScreen(w);\n\t\t\tconst d = Math.hypot(s.x - screen.x, s.y - screen.y);\n\t\t\tif (d <= this.hitRadius && d < bestD) {\n\t\t\t\tbest = w;\n\t\t\t\tbestD = d;\n\t\t\t}\n\t\t}\n\t\treturn best;\n\t}\n\n\tprivate hitAnchor(screen: Vec2, refs: readonly VectorAnchorRef[]): VectorAnchorRef | null {\n\t\tlet best: VectorAnchorRef | null = null;\n\t\tlet bestD = Infinity;\n\t\tfor (const ref of refs) {\n\t\t\tconst a = getAnchorWorld(this.doc, ref);\n\t\t\tif (!a) continue;\n\t\t\tconst s = this.documentToScreen(a);\n\t\t\tconst d = Math.hypot(s.x - screen.x, s.y - screen.y);\n\t\t\tif (d <= this.hitRadius && d < bestD) {\n\t\t\t\tbest = ref;\n\t\t\t\tbestD = d;\n\t\t\t}\n\t\t}\n\t\treturn best;\n\t}\n\n\t/** Deepest unlocked visible path under `p`. */\n\t/** Live groups being edited (selected, or holding the selection or the edited path): their children are hit as stored. */\n\tprivate enteredGroups(): Set<string> {\n\t\treturn vectorEnteredGroups(this.doc.nodes, this.editingId ? [...this.selection, this.editingId] : this.selection);\n\t}\n\n\tprivate hitPath(p: Vec2): VectorPathNode | null {\n\t\tconst tol = this.tolerance();\n\t\tconst entered = this.enteredGroups();\n\t\tconst search = (list: readonly VectorNode[], parent: AffineMatrix): VectorPathNode | null => {\n\t\t\tfor (let i = list.length - 1; i >= 0; i--) {\n\t\t\t\tconst n = list[i];\n\t\t\t\tif (n.visible === false || n.locked) continue;\n\t\t\t\tif (n.type === 'group') {\n\t\t\t\t\t// A live group not being edited is hit where it draws (a click in a subtracted\n\t\t\t\t\t// hole goes through), then picks the child under the pointer.\n\t\t\t\t\t// A mask group likewise, where its mask lets the contents through.\n\t\t\t\t\tif ((hasOwnVectorAppearance(n) || isVectorMaskActive(n)) && !entered.has(n.id) && !hitTestVectorNode(n, p, tol, parent, { entered })) continue;\n\t\t\t\t\tconst found = search(n.children, multiplyMatrices(nodeMatrix(n), parent));\n\t\t\t\t\tif (found) return found;\n\t\t\t\t} else if (n.type === 'path' && hitTestVectorNode(n, p, tol, parent, { entered })) return n;\n\t\t\t}\n\t\t\treturn null;\n\t\t};\n\t\treturn search(this.doc.nodes, IDENTITY_MATRIX);\n\t}\n\n\t/** Endpoints of open subpaths of unlocked visible paths (pen tool resumes from them). */\n\tprivate openEndpoints(): VectorAnchorRef[] {\n\t\tconst out: VectorAnchorRef[] = [];\n\t\twalkVectorNodes(this.doc.nodes, (n) => {\n\t\t\tif (n.type !== 'path' || n.locked || n.visible === false) return;\n\t\t\tn.subpaths.forEach((sp, s) => {\n\t\t\t\tif (sp.closed || sp.anchors.length < 1) return;\n\t\t\t\tout.push({ nodeId: n.id, subpath: s, index: 0 });\n\t\t\t\tif (sp.anchors.length > 1) out.push({ nodeId: n.id, subpath: s, index: sp.anchors.length - 1 });\n\t\t\t});\n\t\t});\n\t\treturn out;\n\t}\n\n\tprivate computeHover(screen: Vec2): VectorHover | null {\n\t\tconst p = this.screenToDocument(screen);\n\t\tswitch (this.tool) {\n\t\t\tcase 'select': {\n\t\t\t\tconst handle = this.hitTransformHandle(screen);\n\t\t\t\tif (handle) return { nodeId: null, transformHandle: handle };\n\t\t\t\tconst node = hitTestVectorNodes(this.doc.nodes, p, this.tolerance(), { entered: this.enteredGroups() });\n\t\t\t\treturn node ? { nodeId: node.id } : null;\n\t\t\t}\n\t\t\tcase 'direct': {\n\t\t\t\tconst handle = this.hitHandle(screen);\n\t\t\t\tif (handle) return { nodeId: handle.nodeId, handle };\n\t\t\t\tconst anchor = this.hitAnchor(screen, getVisibleAnchors(this.view()));\n\t\t\t\tif (anchor) return { nodeId: anchor.nodeId, anchor };\n\t\t\t\tconst widget = this.hitCornerWidget(screen);\n\t\t\t\tif (widget) return { nodeId: widget.ref.nodeId, corner: widget.ref };\n\t\t\t\tfor (const id of getEditablePathIds(this.view())) {\n\t\t\t\t\tconst node = this.getNode(id) as VectorPathNode;\n\t\t\t\t\tconst seg = nearestSegment(node, this.toLocal(id, p));\n\t\t\t\t\tif (seg && seg.distance <= this.tolerance()) {\n\t\t\t\t\t\tconst point = applyMatrix(this.worldOf(id), seg.point);\n\t\t\t\t\t\treturn { nodeId: id, segment: { nodeId: id, subpath: seg.subpath, segment: seg.segment, t: seg.t, point } };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst path = this.hitPath(p);\n\t\t\t\treturn path ? { nodeId: path.id } : null;\n\t\t\t}\n\t\t\tcase 'pen': {\n\t\t\t\tconst refs = this.pen ? [{ nodeId: this.pen.nodeId, subpath: this.pen.subpath, index: 0 }] : this.openEndpoints();\n\t\t\t\tconst anchor = this.hitAnchor(screen, refs);\n\t\t\t\treturn anchor ? { nodeId: anchor.nodeId, anchor } : null;\n\t\t\t}\n\t\t\tcase 'scissors': {\n\t\t\t\tconst seg = this.nearestOutline(p);\n\t\t\t\treturn seg ? { nodeId: seg.nodeId, segment: seg } : null;\n\t\t\t}\n\t\t\tcase 'blend': {\n\t\t\t\tconst hit = this.blendHit(screen, p);\n\t\t\t\tif (!hit) return null;\n\t\t\t\treturn hit.anchor === null ? { nodeId: hit.nodeId } : { nodeId: hit.nodeId, anchor: { nodeId: hit.nodeId, subpath: 0, index: hit.anchor } };\n\t\t\t}\n\t\t\tcase 'width': {\n\t\t\t\tconst hit = this.hitWidthHandle(screen);\n\t\t\t\tif (hit) return { nodeId: hit.ref.nodeId, widthPoint: hit.ref };\n\t\t\t\tconst spot = this.strokeSpotAt(p);\n\t\t\t\treturn spot ? { nodeId: spot.nodeId, strokeSpot: spot } : null;\n\t\t\t}\n\t\t\tcase 'gradient': {\n\t\t\t\tconst a = this.getGradientAnnotator();\n\t\t\t\tconst part = a ? hitVectorGradientAnnotator(a, screen) : null;\n\t\t\t\tif (a && part) return { nodeId: a.nodeId, gradient: part };\n\t\t\t\tconst path = this.hitPath(p);\n\t\t\t\treturn path ? { nodeId: path.id } : null;\n\t\t\t}\n\t\t\tcase 'type': {\n\t\t\t\tconst hit = this.hitText(p);\n\t\t\t\treturn hit ? { nodeId: hit.id } : null;\n\t\t\t}\n\t\t\tcase 'typePath': {\n\t\t\t\tconst hit = this.hitText(p) ?? this.hitPath(p);\n\t\t\t\treturn hit ? { nodeId: hit.id } : null;\n\t\t\t}\n\t\t\tcase 'envelope': {\n\t\t\t\tconst hit = this.hitEnvelope(screen, p);\n\t\t\t\tif (hit) return { nodeId: hit.target.node.id, envelope: { nodeId: hit.target.node.id, index: hit.index } };\n\t\t\t\tconst node = hitTestVectorNodes(this.doc.nodes, p, this.tolerance(), { entered: this.enteredGroups() });\n\t\t\t\treturn node ? { nodeId: node.id } : null;\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\treturn null;\n\t\t}\n\t}\n\n\tprivate widthView(): Pick<VectorEditorState, 'document' | 'selection' | 'tool' | 'hover' | 'interaction' | 'selectedWidthPoints'> {\n\t\treturn { document: this.doc, selection: this.selection, tool: this.tool, hover: this.hover, interaction: this.interaction, selectedWidthPoints: this.widthPoints };\n\t}\n\n\t/** The width point centre or side end under `screen` (width tool). */\n\tprivate hitWidthHandle(screen: Vec2): { ref: VectorWidthPointRef & { part: 'center' | 'left' | 'right' } } | null {\n\t\tconst shown = this.hitWidthHandleIn(screen, this.widthView());\n\t\tif (shown) return shown;\n\t\t// The points of the stroke under the pointer, when no hover has shown them yet.\n\t\tconst spot = this.strokeSpotAt(this.screenToDocument(screen));\n\t\treturn spot && spot.nodeId !== this.hover?.nodeId ? this.hitWidthHandleIn(screen, { ...this.widthView(), hover: { nodeId: spot.nodeId } }) : null;\n\t}\n\n\tprivate hitWidthHandleIn(screen: Vec2, view: Pick<VectorEditorState, 'document' | 'selection' | 'tool' | 'hover' | 'interaction' | 'selectedWidthPoints'>): { ref: VectorWidthPointRef & { part: 'center' | 'left' | 'right' } } | null {\n\t\tlet best: { ref: VectorWidthPointRef & { part: 'center' | 'left' | 'right' } } | null = null;\n\t\tlet bestD = Infinity;\n\t\tfor (const h of getVectorWidthHandles(view)) {\n\t\t\tfor (const [part, q] of [['center', h], ['left', h.left], ['right', h.right]] as const) {\n\t\t\t\tconst s = this.documentToScreen(q);\n\t\t\t\tconst d = Math.hypot(s.x - screen.x, s.y - screen.y);\n\t\t\t\t// Ties go to the centre (listed first), so a zero-width point can still be moved.\n\t\t\t\tif (d <= this.hitRadius && d < bestD - 1e-9) {\n\t\t\t\t\tbest = { ref: { ...h.ref, part } };\n\t\t\t\t\tbestD = d;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn best;\n\t}\n\n\t/** The nearest stroked, unlocked visible path whose stroke is under document point `p` (width tool). */\n\tprivate strokeSpotAt(p: Vec2): VectorStrokeSpot | null {\n\t\tlet best: (VectorStrokeSpot & { distance: number }) | null = null;\n\t\tconst tol = this.tolerance();\n\t\twalkVectorNodes(this.doc.nodes, (n) => {\n\t\t\tif (n.type !== 'path' || n.visible === false || n.locked) return;\n\t\t\tif (hiddenOrLocked(this.doc.nodes, n.id)) return;\n\t\t\tconst g = vectorStrokeWidthGeometry(n);\n\t\t\tif (!g) return;\n\t\t\tconst m = this.worldOf(n.id);\n\t\t\tconst local = applyMatrix(invertMatrix(m), p);\n\t\t\tconst near = nearestStrokePosition(g.line, [local.x, local.y]);\n\t\t\tif (!near) return;\n\t\t\tconst profile = strokeWidthProfileOf(n);\n\t\t\tconst w = widthProfileAt(profile, near.t);\n\t\t\tconst reach = Math.max(near.side === 'left' ? w.left : w.right, 0) * g.unit + tol;\n\t\t\tif (near.distance > reach || near.distance > widthProfileMaxScale(profile) * g.unit + tol) return;\n\t\t\tif (best && near.distance >= best.distance) return;\n\t\t\tconst nx = near.tangent[1];\n\t\t\tconst ny = -near.tangent[0];\n\t\t\tconst [cx, cy] = near.point;\n\t\t\tbest = {\n\t\t\t\tnodeId: n.id,\n\t\t\t\tsubpath: near.contour,\n\t\t\t\tt: near.t,\n\t\t\t\tpoint: applyMatrix(m, { x: cx, y: cy }),\n\t\t\t\tleft: applyMatrix(m, { x: cx + nx * w.left * g.unit, y: cy + ny * w.left * g.unit }),\n\t\t\t\tright: applyMatrix(m, { x: cx - nx * w.right * g.unit, y: cy - ny * w.right * g.unit }),\n\t\t\t\tdistance: near.distance\n\t\t\t};\n\t\t});\n\t\tif (!best) return null;\n\t\tconst { distance: _d, ...spot } = best as VectorStrokeSpot & { distance: number };\n\t\treturn spot;\n\t}\n\n\tprivate downWidth(screen: Vec2, p: Vec2, input: VectorPointerInput) {\n\t\tconst hit = this.hitWidthHandle(screen);\n\t\tconst spot = hit ? null : this.strokeSpotAt(p);\n\t\tconst nodeId = hit?.ref.nodeId ?? spot?.nodeId;\n\t\tconst node = nodeId ? this.getNode(nodeId) : undefined;\n\t\tconst g = node?.type === 'path' ? vectorStrokeWidthGeometry(node) : null;\n\t\tif (this.widthEdit) this.widthEdit = null;\n\t\tif (!node || node.type !== 'path' || !g) {\n\t\t\t// A click away from strokes deselects (Shift keeps the selection).\n\t\t\tif (!input.shift) this.widthPoints = [];\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tconst before = this.snapshot();\n\t\tconst local = this.toLocal(node.id, p);\n\t\tlet profile: VectorWidthPoint[];\n\t\tlet ref: VectorWidthPointRef;\n\t\tlet mode: 'width' | 'move' | 'copy' = 'width';\n\t\tlet side: 'both' | 'left' | 'right' = 'both';\n\t\tlet group: number[] = [];\n\t\tlet shift = false;\n\t\tlet t: number;\n\t\tlet subpath: number;\n\t\tif (hit) {\n\t\t\tprofile = sanitizeWidthProfile(node.strokeWidthProfile);\n\t\t\tref = { nodeId: node.id, index: hit.ref.index, subpath: hit.ref.subpath };\n\t\t\tt = profile[ref.index]?.t ?? 0;\n\t\t\tsubpath = hit.ref.subpath;\n\t\t\tif (hit.ref.part === 'center') mode = input.alt && profile.length < VECTOR_WIDTH_PROFILE_MAX_POINTS ? 'copy' : 'move';\n\t\t\telse if (input.alt) side = hit.ref.part;\n\t\t\tconst same = this.widthPoints.length > 0 && this.widthPoints[0].nodeId === node.id;\n\t\t\tconst selected = same && this.widthPoints.some((r) => r.index === ref.index);\n\t\t\tif (input.shift && mode !== 'copy') {\n\t\t\t\t// Shift-click adds the point to the selection, or takes it out (and drags nothing).\n\t\t\t\tif (selected) {\n\t\t\t\t\tthis.widthPoints = this.widthPoints.filter((r) => r.index !== ref.index);\n\t\t\t\t\tthis.notify();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.widthPoints = same ? [...this.widthPoints, ref] : [ref];\n\t\t\t\tshift = true;\n\t\t\t} else if (mode === 'copy' || !selected) this.widthPoints = [ref];\n\t\t\tif (mode !== 'copy') group = this.widthPoints.filter((r) => r.index !== ref.index).map((r) => r.index);\n\t\t} else {\n\t\t\tconst near = nearestStrokePosition(g.line, [local.x, local.y])!;\n\t\t\tconst inserted = insertWidthPoint(strokeWidthProfileOf(node), near.t);\n\t\t\tprofile = inserted.points;\n\t\t\tsubpath = near.contour;\n\t\t\tt = near.t;\n\t\t\tref = { nodeId: node.id, index: inserted.index, subpath };\n\t\t\tif (input.alt) side = near.side;\n\t\t\tthis.widthPoints = [ref];\n\t\t}\n\t\tconst at = strokePositionPoint(g.line, subpath, t);\n\t\tif (!at) return;\n\t\tconst base = { ...this.doc, nodes: mapTree(this.doc.nodes, (n) => (n.id === node.id ? { ...(n as VectorPathNode), strokeWidthProfile: profile } : n)) };\n\t\tthis.doc = base;\n\t\tthis.drag = {\n\t\t\tkind: 'width',\n\t\t\tref,\n\t\t\tmode,\n\t\t\tside,\n\t\t\tsource: ref.index,\n\t\t\tgroup,\n\t\t\tshift,\n\t\t\trenumber: null,\n\t\t\torigin: { x: at.point[0], y: at.point[1] },\n\t\t\tnormal: { x: at.tangent[1], y: -at.tangent[0] },\n\t\t\tunit: g.unit,\n\t\t\tprofile,\n\t\t\tbefore,\n\t\t\tbase,\n\t\t\tstart: p,\n\t\t\tmoved: false,\n\t\t\tcreated: !hit\n\t\t};\n\t\tconst w = profile[ref.index];\n\t\tthis.interaction = { type: 'width', ref, mode, width: (w.left + w.right) * g.unit };\n\t\tthis.notify(!hit);\n\t}\n\n\t/**\n\t * A width-tool slide to `t` on the dragged point's subpath lands exactly on another width\n\t * point (not `skip`) within the pick distance on screen: dropping a point onto another makes\n\t * a discontinuous point (two points at one `t`, the width steps there).\n\t */\n\tprivate snapWidthT(drag: Extract<Drag, { kind: 'width' }>, node: VectorPathNode, line: BezierPath, t: number, skip: number): number {\n\t\tconst m = this.worldOf(node.id, drag.base);\n\t\tconst at = strokePositionPoint(line, drag.ref.subpath, t);\n\t\tif (!at) return t;\n\t\tconst here = this.documentToScreen(applyMatrix(m, { x: at.point[0], y: at.point[1] }));\n\t\tlet best = t;\n\t\tlet bestD = this.hitRadius;\n\t\tdrag.profile.forEach((w, i) => {\n\t\t\tif (i === skip || drag.group.includes(i)) return;\n\t\t\tconst q = strokePositionPoint(line, drag.ref.subpath, w.t);\n\t\t\tif (!q) return;\n\t\t\tconst s = this.documentToScreen(applyMatrix(m, { x: q.point[0], y: q.point[1] }));\n\t\t\tconst d = Math.hypot(s.x - here.x, s.y - here.y);\n\t\t\tif (d <= bestD) {\n\t\t\t\tbest = w.t;\n\t\t\t\tbestD = d;\n\t\t\t}\n\t\t});\n\t\treturn best;\n\t}\n\n\t/** The nearest outline point of any visible, unlocked path within the pick distance of document point `p`. */\n\tprivate nearestOutline(p: Vec2): NonNullable<VectorHover['segment']> | null {\n\t\tlet best: (NonNullable<VectorHover['segment']> & { distance: number }) | null = null;\n\t\tconst tol = this.tolerance();\n\t\tconst visit = (list: readonly VectorNode[], parent: AffineMatrix) => {\n\t\t\tfor (let i = list.length - 1; i >= 0; i--) {\n\t\t\t\tconst n = list[i];\n\t\t\t\tif (n.visible === false || n.locked) continue;\n\t\t\t\tconst m = multiplyMatrices(nodeMatrix(n), parent);\n\t\t\t\tif (n.type === 'group') {\n\t\t\t\t\tvisit(n.children, m);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (n.type !== 'path') continue;\n\t\t\t\tconst hit = nearestSegment(n, applyMatrix(invertMatrix(m), p));\n\t\t\t\t// Node matrices are rigid, so local distances are document distances.\n\t\t\t\tif (hit && hit.distance <= tol && (!best || hit.distance < best.distance - 1e-9)) {\n\t\t\t\t\tbest = { nodeId: n.id, subpath: hit.subpath, segment: hit.segment, t: hit.t, point: applyMatrix(m, hit.point), distance: hit.distance };\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t\tvisit(this.doc.nodes, IDENTITY_MATRIX);\n\t\tif (!best) return null;\n\t\tconst { distance: _d, ...seg } = best as NonNullable<VectorHover['segment']> & { distance: number };\n\t\treturn seg;\n\t}\n\n\tprivate sameHover(a: VectorHover | null, b: VectorHover | null): boolean {\n\t\treturn JSON.stringify(a) === JSON.stringify(b);\n\t}\n\n\t/** Primary button down on the canvas at `screen` (pixels relative to the canvas). */\n\tpointerDown(screen: Vec2, input: VectorPointerInput = {}): void {\n\t\tthis.cancelDrag();\n\t\tif (input.button === 1 || this.spaceHeld) {\n\t\t\tthis.drag = { kind: 'pan', start: screen, viewport: this.viewport };\n\t\t\tthis.interaction = { type: 'pan' };\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tif (input.button !== undefined && input.button !== 0) return;\n\t\tconst p = this.screenToDocument(screen);\n\t\tthis.pointer = p;\n\t\tswitch (this.tool) {\n\t\t\tcase 'select':\n\t\t\t\treturn this.downSelect(screen, p, input);\n\t\t\tcase 'direct':\n\t\t\t\treturn this.downDirect(screen, p, input);\n\t\t\tcase 'pen':\n\t\t\t\treturn this.downPen(screen, p, input);\n\t\t\tcase 'pencil':\n\t\t\t\treturn this.downPencil(p, input);\n\t\t\tcase 'width':\n\t\t\t\treturn this.downWidth(screen, p, input);\n\t\t\tcase 'shapeBuilder':\n\t\t\tcase 'paintBucket':\n\t\t\t\treturn this.downBuilder(p, input);\n\t\t\tcase 'blend':\n\t\t\t\treturn this.downBlend(screen, p);\n\t\t\tcase 'gradient':\n\t\t\t\treturn this.downGradient(screen, p, input);\n\t\t\tcase 'envelope':\n\t\t\t\treturn this.downEnvelope(screen, p, input);\n\t\t\tcase 'type':\n\t\t\tcase 'typePath':\n\t\t\t\treturn this.downType(p, input);\n\t\t\tcase 'scissors': {\n\t\t\t\tconst seg = this.nearestOutline(p);\n\t\t\t\tif (seg) this.cutPathAt(seg.nodeId, seg.point);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'knife':\n\t\t\tcase 'eraser':\n\t\t\t\tthis.drag = { kind: 'stroke', tool: this.tool, points: [p], before: this.snapshot() };\n\t\t\t\tthis.interaction = { type: 'stroke', tool: this.tool, points: [p], width: this.tool === 'eraser' ? this.editing.eraserSize : 0 };\n\t\t\t\tthis.notify();\n\t\t\t\treturn;\n\t\t\tcase 'connector':\n\t\t\t\treturn this.downConnector(p);\n\t\t\tdefault:\n\t\t\t\treturn this.downShape(p, input);\n\t\t}\n\t}\n\n\t/** The shape under the pointer for the Blend tool, and the anchor of its first subpath under it. */\n\tprivate blendHit(screen: Vec2, p: Vec2): VectorBlendStart | null {\n\t\tconst node = hitTestVectorNodes(this.doc.nodes, p, this.tolerance());\n\t\tif (!node) return null;\n\t\tlet anchor: number | null = null;\n\t\tif (node.type === 'path' && node.subpaths[0]) {\n\t\t\tconst refs = node.subpaths[0].anchors.map((_, index) => ({ nodeId: node.id, subpath: 0, index }));\n\t\t\tanchor = this.hitAnchor(screen, refs)?.index ?? null;\n\t\t}\n\t\treturn { nodeId: node.id, anchor };\n\t}\n\n\t/** Blend tool: the first click picks the shape to blend from, the next one blends it into the shape clicked. */\n\tprivate downBlend(screen: Vec2, p: Vec2) {\n\t\tconst hit = this.blendHit(screen, p);\n\t\tconst start = this.blendStart;\n\t\tif (!hit) {\n\t\t\tif (start) {\n\t\t\t\tthis.blendStart = null;\n\t\t\t\tthis.notify();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tif (!start || start.nodeId === hit.nodeId) {\n\t\t\tthis.blendStart = hit;\n\t\t\tthis.selection = [hit.nodeId];\n\t\t\tthis.anchors = [];\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tthis.blendStart = null;\n\t\tconst first = this.getNode(start.nodeId);\n\t\tif (isVectorBlendGroup(first)) {\n\t\t\t// Clicking on from a blend adds the shape as its next key.\n\t\t\tif (!this.addBlendKey(first.id, hit.nodeId, { start: hit.anchor }).ok) this.notify();\n\t\t\treturn;\n\t\t}\n\t\tconst starts: Record<string, number> = {};\n\t\tif (start.anchor !== null) starts[start.nodeId] = start.anchor;\n\t\tif (hit.anchor !== null) starts[hit.nodeId] = hit.anchor;\n\t\tif (!this.blend({ ids: [start.nodeId, hit.nodeId], keepOrder: true, starts })) this.notify();\n\t}\n\n\tprivate downSelect(screen: Vec2, p: Vec2, input: VectorPointerInput) {\n\t\tconst handle = this.hitTransformHandle(screen);\n\t\tif (handle) {\n\t\t\tconst frame = this.frameOf(this.selectedRoots())!;\n\t\t\tconst before = this.snapshot();\n\t\t\tconst center = applyMatrix(rotateMatrix(frame.angle), { x: frame.x + frame.width / 2, y: frame.y + frame.height / 2 });\n\t\t\tthis.drag =\n\t\t\t\thandle === 'rotate'\n\t\t\t\t\t? { kind: 'rotate', start: p, center, before, base: this.doc }\n\t\t\t\t\t: { kind: 'scale', handle, start: p, frame, before, base: this.doc };\n\t\t\tthis.interaction = handle === 'rotate' ? { type: 'rotate', angle: 0 } : { type: 'scale', handle, sx: 1, sy: 1 };\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tconst node = hitTestVectorNodes(this.doc.nodes, p, this.tolerance(), { entered: this.enteredGroups() });\n\t\tif (node) {\n\t\t\tconst selected = this.selection.includes(node.id);\n\t\t\tif (input.shift) {\n\t\t\t\tthis.selection = selected ? this.selection.filter((id) => id !== node.id) : [...this.selection, node.id];\n\t\t\t\tif (selected) {\n\t\t\t\t\tthis.notify();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} else if (!selected) this.selection = [node.id];\n\t\t\tthis.anchors = [];\n\t\t\tconst before = this.snapshot();\n\t\t\tthis.drag = {\n\t\t\t\tkind: 'move',\n\t\t\t\tstart: p,\n\t\t\t\torigin: this.boundsOf(this.selectedRoots()),\n\t\t\t\tbefore,\n\t\t\t\tbase: this.doc,\n\t\t\t\tmoved: false,\n\t\t\t\tduplicated: false,\n\t\t\t\tclickId: selected && !input.shift ? node.id : null\n\t\t\t};\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tif (!input.shift) this.selection = [];\n\t\tthis.drag = { kind: 'marquee', start: p, base: this.selection, baseAnchors: [], anchorsMode: false };\n\t\tthis.interaction = { type: 'marquee', bounds: { x: p.x, y: p.y, width: 0, height: 0 } };\n\t\tthis.notify();\n\t}\n\n\tprivate downDirect(screen: Vec2, p: Vec2, input: VectorPointerInput) {\n\t\tconst handle = this.hitHandle(screen);\n\t\tif (handle) {\n\t\t\tthis.drag = { kind: 'handle', ref: handle, before: this.snapshot(), base: this.doc };\n\t\t\tthis.interaction = { type: 'handle', ref: handle };\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tconst anchor = this.hitAnchor(screen, getVisibleAnchors(this.view()));\n\t\tif (anchor) {\n\t\t\tconst selected = this.anchors.some((a) => sameRef(a, anchor));\n\t\t\tthis.editingId = anchor.nodeId;\n\t\t\tif (input.alt) {\n\t\t\t\t// Alt-drag an anchor pulls out fresh symmetric handles.\n\t\t\t\tthis.anchors = [anchor];\n\t\t\t\tthis.drag = { kind: 'pull', ref: anchor, before: this.snapshot(), base: this.doc, label: 'Make Smooth', outOnly: false, closing: false, altBreaks: false };\n\t\t\t\tthis.notify();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (input.shift) {\n\t\t\t\tthis.anchors = selected ? this.anchors.filter((a) => !sameRef(a, anchor)) : [...this.anchors, anchor];\n\t\t\t\tif (selected) {\n\t\t\t\t\tthis.notify();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} else if (!selected) this.anchors = [anchor];\n\t\t\tthis.drag = { kind: 'anchors', start: p, before: this.snapshot(), base: this.doc, moved: false, clickRef: selected && !input.shift ? anchor : null, lead: anchor };\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tconst widget = this.hitCornerWidget(screen);\n\t\tif (widget) {\n\t\t\tconst refs = getVectorCornerWidgets(this.cornerView()).map((w) => w.ref);\n\t\t\tthis.drag = { kind: 'corner', widget, refs, start: p, before: this.snapshot(), base: this.doc, moved: false, alt: !!input.alt, shift: !!input.shift };\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tfor (const id of getEditablePathIds(this.view())) {\n\t\t\tconst node = this.getNode(id) as VectorPathNode;\n\t\t\tconst seg = nearestSegment(node, this.toLocal(id, p));\n\t\t\tif (!seg || seg.distance > this.tolerance()) continue;\n\t\t\tconst sp = node.subpaths[seg.subpath];\n\t\t\tif (input.alt) {\n\t\t\t\tconst before = this.snapshot();\n\t\t\t\tconst { subpath, index } = insertAnchor(sp, seg.segment, seg.t);\n\t\t\t\tthis.replaceNode(id, { ...node, subpaths: node.subpaths.map((s, i) => (i === seg.subpath ? subpath : s)) });\n\t\t\t\tconst ref = { nodeId: id, subpath: seg.subpath, index };\n\t\t\t\tthis.anchors = [ref];\n\t\t\t\tthis.editingId = id;\n\t\t\t\tthis.drag = { kind: 'anchors', start: p, before, base: this.doc, moved: true, clickRef: null, lead: ref };\n\t\t\t\tthis.notify(true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// A click selects the segment's anchors; a drag bends it through the pointer.\n\t\t\tconst a = { nodeId: id, subpath: seg.subpath, index: seg.segment };\n\t\t\tconst b = { nodeId: id, subpath: seg.subpath, index: (seg.segment + 1) % sp.anchors.length };\n\t\t\tthis.anchors = input.shift ? [...this.anchors.filter((r) => !sameRef(r, a) && !sameRef(r, b)), a, b] : [a, b];\n\t\t\tthis.editingId = id;\n\t\t\tthis.drag = { kind: 'bend', ref: { nodeId: id, subpath: seg.subpath, segment: seg.segment }, t: seg.t, start: p, before: this.snapshot(), base: this.doc, moved: false };\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tconst path = this.hitPath(p);\n\t\tif (path) {\n\t\t\tthis.editingId = path.id;\n\t\t\tif (!input.shift) this.selection = [path.id];\n\t\t\telse if (!this.selection.includes(path.id)) this.selection = [...this.selection, path.id];\n\t\t\tthis.anchors = path.subpaths.flatMap((sp, s) => sp.anchors.map((_, i) => ({ nodeId: path.id, subpath: s, index: i })));\n\t\t\tthis.drag = { kind: 'anchors', start: p, before: this.snapshot(), base: this.doc, moved: false, clickRef: null, lead: null };\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tif (!input.shift) {\n\t\t\tthis.anchors = [];\n\t\t}\n\t\tthis.drag = { kind: 'marquee', start: p, base: this.selection, baseAnchors: this.anchors, anchorsMode: getEditablePathIds(this.view()).length > 0 };\n\t\tthis.interaction = { type: 'marquee', bounds: { x: p.x, y: p.y, width: 0, height: 0 } };\n\t\tthis.notify();\n\t}\n\n\tprivate downPen(screen: Vec2, p: Vec2, input: VectorPointerInput) {\n\t\tconst before = this.snapshot();\n\t\tconst sp = this.pen ? (this.getNode(this.pen.nodeId) as VectorPathNode | undefined)?.subpaths[this.pen.subpath] : undefined;\n\t\tif (this.pen && sp) {\n\t\t\tconst pen = this.pen;\n\t\t\tconst first = { nodeId: pen.nodeId, subpath: pen.subpath, index: 0 };\n\t\t\tif (sp.anchors.length >= 2 && this.hitAnchor(screen, [first])) {\n\t\t\t\tconst node = this.getNode(pen.nodeId) as VectorPathNode;\n\t\t\t\tthis.replaceNode(pen.nodeId, { ...node, subpaths: node.subpaths.map((s, i) => (i === pen.subpath ? { ...s, closed: true } : s)) });\n\t\t\t\tthis.drag = { kind: 'pull', ref: first, before, base: this.doc, label: 'Close Path', outOnly: false, closing: true, altBreaks: true };\n\t\t\t\tthis.interaction = { type: 'pen', nodeId: pen.nodeId };\n\t\t\t\tthis.notify(true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst last = getAnchorWorld(this.doc, { nodeId: pen.nodeId, subpath: pen.subpath, index: sp.anchors.length - 1 })!;\n\t\t\tconst leave = { at: { x: last.x, y: last.y }, constrain: input.shift ? ('angle' as const) : null, ...this.penAngles() };\n\t\t\tlet pt = this.snapDrawn(p, leave, input, () => this.drawingTargets(pen.nodeId));\n\t\t\tpt = this.toLocal(pen.nodeId, pt);\n\t\t\tconst node = this.getNode(pen.nodeId) as VectorPathNode;\n\t\t\tconst anchors = [...sp.anchors, { x: pt.x, y: pt.y }];\n\t\t\tthis.replaceNode(pen.nodeId, { ...node, subpaths: node.subpaths.map((s, i) => (i === pen.subpath ? { ...s, anchors } : s)) });\n\t\t\tconst ref = { nodeId: pen.nodeId, subpath: pen.subpath, index: anchors.length - 1 };\n\t\t\tthis.anchors = [ref];\n\t\t\tthis.drag = { kind: 'pull', ref, before, base: this.doc, label: 'Add Point', outOnly: false, closing: false, altBreaks: true };\n\t\t\tthis.interaction = { type: 'pen', nodeId: pen.nodeId };\n\t\t\tthis.notify(true);\n\t\t\treturn;\n\t\t}\n\t\tconst endpoint = this.hitAnchor(screen, this.openEndpoints());\n\t\tif (endpoint) {\n\t\t\tlet node = this.getNode(endpoint.nodeId) as VectorPathNode;\n\t\t\tlet index = endpoint.index;\n\t\t\tif (index === 0 && node.subpaths[endpoint.subpath].anchors.length > 1) {\n\t\t\t\tnode = { ...node, subpaths: node.subpaths.map((s, i) => (i === endpoint.subpath ? reverseSubpath(s) : s)) };\n\t\t\t\tthis.replaceNode(node.id, node);\n\t\t\t\tindex = node.subpaths[endpoint.subpath].anchors.length - 1;\n\t\t\t}\n\t\t\tthis.pen = { nodeId: node.id, subpath: endpoint.subpath };\n\t\t\tthis.selection = [node.id];\n\t\t\tthis.editingId = node.id;\n\t\t\tconst ref = { nodeId: node.id, subpath: endpoint.subpath, index };\n\t\t\tthis.anchors = [ref];\n\t\t\tthis.drag = { kind: 'pull', ref, before, base: this.doc, label: 'Continue Path', outOnly: true, closing: false, altBreaks: true };\n\t\t\tthis.interaction = { type: 'pen', nodeId: node.id };\n\t\t\tthis.notify(this.doc !== before.doc);\n\t\t\treturn;\n\t\t}\n\t\tconst pt = this.snapDrawn(p, null, input, () => this.drawingTargets(null));\n\t\tconst node: VectorPathNode = { id: this.newId(), type: 'path', name: 'Path', ...this.styleFor(true), subpaths: [{ anchors: [{ x: pt.x, y: pt.y }], closed: false }] };\n\t\tthis.setDoc([...this.doc.nodes, node]);\n\t\tthis.pen = { nodeId: node.id, subpath: 0 };\n\t\tthis.selection = [node.id];\n\t\tthis.editingId = node.id;\n\t\tconst ref = { nodeId: node.id, subpath: 0, index: 0 };\n\t\tthis.anchors = [ref];\n\t\tthis.drag = { kind: 'pull', ref, before, base: this.doc, label: 'Start Path', outOnly: false, closing: false, altBreaks: true };\n\t\tthis.interaction = { type: 'pen', nodeId: node.id };\n\t\tthis.notify(true);\n\t}\n\n\tprivate downPencil(p: Vec2, input: VectorPointerInput = {}) {\n\t\tconst before = this.snapshot();\n\t\tconst node: VectorPathNode = {\n\t\t\tid: this.newId(),\n\t\t\ttype: 'path',\n\t\t\tname: 'Pencil',\n\t\t\t...this.styleFor(true),\n\t\t\tstrokeLinecap: this.defaults.strokeLinecap ?? 'round',\n\t\t\tstrokeLinejoin: this.defaults.strokeLinejoin ?? 'round',\n\t\t\tsubpaths: [{ anchors: [{ x: p.x, y: p.y }], closed: false }]\n\t\t};\n\t\tthis.setDoc([...this.doc.nodes, node]);\n\t\tconst lazy = new LazyMouse((this.editing.pencilSmoothing * VECTOR_PENCIL_STRING_PX) / this.viewport.zoom);\n\t\tlazy.push([p.x, p.y]);\n\t\tconst mode = this.editing.pencilPressure;\n\t\tconst hasPressure = typeof input.pressure === 'number' && Number.isFinite(input.pressure);\n\t\tconst record = hasPressure && (mode === 'all' || (mode === 'pen' && input.pointerType === 'pen'));\n\t\tconst pressure = hasPressure ? input.pressure! : 0.5;\n\t\tthis.drag = { kind: 'pencil', nodeId: node.id, points: [[p.x, p.y]], before, lazy, pressures: record ? [pressure] : null, pressure };\n\t\tthis.interaction = { type: 'draw', tool: 'pencil', nodeId: node.id, bounds: { x: p.x, y: p.y, width: 0, height: 0 } };\n\t\tthis.notify(true);\n\t}\n\n\tprivate downShape(p: Vec2, input: VectorPointerInput) {\n\t\tconst before = this.snapshot();\n\t\t// The targets are gathered before the new shape joins the document, and kept for the drag.\n\t\tconst targets = this.drawingTargets(null);\n\t\tconst start = this.snapDrawn(p, null, input, () => targets);\n\t\t// The Frame tool drags out a rectangle that becomes the frame when released.\n\t\tconst kind = (this.tool === 'frame' ? 'rect' : this.tool) as VectorShapeInput['kind'];\n\t\tconst node = this.createShapeNode({ kind, x: start.x, y: start.y, width: 0, height: 0 });\n\t\tif (this.tool === 'frame') Object.assign(node, { fill: '#ffffff', stroke: '#a3a3a3', strokeWidth: 1 / this.viewport.zoom });\n\t\tthis.setDoc([...this.doc.nodes, node]);\n\t\tthis.drag = { kind: 'shape', tool: this.tool, start, nodeId: node.id, before, targets };\n\t\tthis.interaction = { type: 'draw', tool: this.tool, nodeId: node.id, bounds: { x: start.x, y: start.y, width: 0, height: 0 } };\n\t\tthis.notify(true);\n\t}\n\n\t/** Pointer moved over the canvas (with or without a button down). */\n\tpointerMove(screen: Vec2, input: VectorPointerInput = {}): void {\n\t\tconst p = this.screenToDocument(screen);\n\t\tconst drag = this.drag;\n\t\tif (!drag && (this.tool === 'shapeBuilder' || this.tool === 'paintBucket')) {\n\t\t\tthis.pointer = p;\n\t\t\tif (this.hoverBuilder(p, !!input.alt)) this.notify();\n\t\t\treturn;\n\t\t}\n\t\tif (!drag && this.tool === 'connector') {\n\t\t\t// The sites under the pointer show while hovering (see the overlay's connectorSites).\n\t\t\tthis.pointer = p;\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tif (!drag) {\n\t\t\tconst hover = this.computeHover(screen);\n\t\t\tconst pointerMatters = this.tool === 'pen' && !!this.pen;\n\t\t\tconst guides = this.guides;\n\t\t\tthis.hover = hover;\n\t\t\tconst pointer = this.hoverSnap(p, input);\n\t\t\tif (this.sameHover(hover, this.state.hover) && !pointerMatters && JSON.stringify(guides) === JSON.stringify(this.guides)) return;\n\t\t\tthis.pointer = pointer;\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tthis.pointer = p;\n\t\tswitch (drag.kind) {\n\t\t\tcase 'textBox':\n\t\t\t\tdrag.moved ||= Math.hypot(p.x - drag.start.x, p.y - drag.start.y) * this.viewport.zoom > 3;\n\t\t\t\tthis.interaction = { type: 'textBox', bounds: boundsFromPoints(drag.start, p) };\n\t\t\t\tthis.notify();\n\t\t\t\treturn;\n\t\t\tcase 'textSelect': {\n\t\t\t\tconst node = this.getNode(drag.nodeId);\n\t\t\t\tif (node?.type === 'text' && this.textEdit) {\n\t\t\t\t\tthis.textEdit = { ...this.textEdit, focus: vectorTextIndexAt(node, this.toLocal(node.id, p)) };\n\t\t\t\t\tthis.notify();\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'pan':\n\t\t\t\tthis.viewport = { ...drag.viewport, x: drag.viewport.x + screen.x - drag.start.x, y: drag.viewport.y + screen.y - drag.start.y };\n\t\t\t\tthis.notify();\n\t\t\t\treturn;\n\t\t\tcase 'gradient':\n\t\t\t\treturn this.moveGradient(drag, screen, p, input);\n\t\t\tcase 'envelope':\n\t\t\t\treturn this.moveEnvelope(drag, p);\n\t\t\tcase 'move': {\n\t\t\t\tlet dx = p.x - drag.start.x;\n\t\t\t\tlet dy = p.y - drag.start.y;\n\t\t\t\tif (!drag.moved && Math.hypot(dx, dy) * this.viewport.zoom < 2) return;\n\t\t\t\tdrag.moved = true;\n\t\t\t\tif (input.shift) Math.abs(dx) > Math.abs(dy) ? (dy = 0) : (dx = 0);\n\t\t\t\tif (input.alt && !drag.duplicated) {\n\t\t\t\t\tdrag.duplicated = true;\n\t\t\t\t\tconst created: string[] = [];\n\t\t\t\t\tdrag.base = this.withDuplicates(drag.base, this.selectedRoots(), { x: 0, y: 0 }, created);\n\t\t\t\t\tthis.selection = created;\n\t\t\t\t\t// The originals stay behind as targets.\n\t\t\t\t\tdrag.targets = undefined;\n\t\t\t\t}\n\t\t\t\t({ dx, dy } = this.snapMove(drag, dx, dy, input));\n\t\t\t\tthis.doc = this.transformed(drag.base, new Set(this.selectedRoots()), translateMatrix(dx, dy));\n\t\t\t\tthis.interaction = { type: 'move', dx, dy };\n\t\t\t\tthis.notify(true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'scale': {\n\t\t\t\tif (this.freeDistort && drag.handle !== 'rotate') return this.moveDistort(drag, p, input);\n\t\t\t\t// Scale in the box's own (possibly rotated) frame.\n\t\t\t\tconst angle = drag.frame.angle;\n\t\t\t\tconst q = angle ? applyMatrix(rotateMatrix(-angle), p) : p;\n\t\t\t\tlet { sx, sy, origin } = scaleFor(drag.handle, drag.frame, q, input);\n\t\t\t\tif (this.snap.objects && !input.mod && drag.handle !== 'rotate') {\n\t\t\t\t\tdrag.targets ??= VectorSnapTargets.forResize(drag.base, this.selectedRoots());\n\t\t\t\t\tconst res = drag.targets.snapScale(drag.frame, drag.handle, sx, sy, input, this.snapTolerance());\n\t\t\t\t\t({ sx, sy } = res);\n\t\t\t\t\tthis.setGuides(res);\n\t\t\t\t} else this.setGuides(null);\n\t\t\t\tconst ids = new Set(this.selectedRoots());\n\t\t\t\tthis.doc = this.transformed(drag.base, ids, this.inFrame(angle, scaleMatrix(sx, sy, origin)));\n\t\t\t\tthis.interaction = { type: 'scale', handle: drag.handle, sx, sy };\n\t\t\t\tthis.notify(true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'rotate': {\n\t\t\t\tconst a0 = Math.atan2(drag.start.y - drag.center.y, drag.start.x - drag.center.x);\n\t\t\t\tconst a1 = Math.atan2(p.y - drag.center.y, p.x - drag.center.x);\n\t\t\t\tlet angle = ((a1 - a0) * 180) / Math.PI;\n\t\t\t\tif (input.shift) angle = Math.round(angle / 15) * 15;\n\t\t\t\tthis.doc = this.transformed(drag.base, new Set(this.selectedRoots()), rotateMatrix(angle, drag.center));\n\t\t\t\tthis.interaction = { type: 'rotate', angle };\n\t\t\t\tthis.notify(true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'marquee': {\n\t\t\t\tconst bounds = boundsFromPoints(drag.start, p);\n\t\t\t\tthis.interaction = { type: 'marquee', bounds };\n\t\t\t\tif (drag.anchorsMode) {\n\t\t\t\t\tconst inside = getVisibleAnchors(this.view()).filter((r) => {\n\t\t\t\t\t\tconst a = getAnchorWorld(this.doc, r)!;\n\t\t\t\t\t\treturn a.x >= bounds.x && a.x <= bounds.x + bounds.width && a.y >= bounds.y && a.y <= bounds.y + bounds.height;\n\t\t\t\t\t});\n\t\t\t\t\tthis.anchors = [...drag.baseAnchors, ...inside.filter((r) => !drag.baseAnchors.some((b) => sameRef(b, r)))];\n\t\t\t\t} else {\n\t\t\t\t\tconst hits = vectorNodesNear(this.doc.nodes, bounds)\n\t\t\t\t\t\t.map((i) => this.doc.nodes[i])\n\t\t\t\t\t\t.filter((n) => !n.locked && n.visible !== false)\n\t\t\t\t\t\t.filter((n) => {\n\t\t\t\t\t\t\tconst b = vectorNodeBounds(n);\n\t\t\t\t\t\t\treturn b && boundsIntersect(b, bounds);\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.map((n) => n.id);\n\t\t\t\t\tthis.selection = [...new Set([...drag.base, ...hits])];\n\t\t\t\t}\n\t\t\t\tthis.notify();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'anchors': {\n\t\t\t\tlet dx = p.x - drag.start.x;\n\t\t\t\tlet dy = p.y - drag.start.y;\n\t\t\t\tif (!drag.moved && Math.hypot(dx, dy) * this.viewport.zoom < 2) return;\n\t\t\t\tdrag.moved = true;\n\t\t\t\tconst lead = this.leadAnchor(drag);\n\t\t\t\tif (lead) {\n\t\t\t\t\t// The grabbed (or nearest) anchor snaps; the others move with it.\n\t\t\t\t\tconst leave = input.shift ? { at: lead, constrain: 'axis' as const, curves: false } : null;\n\t\t\t\t\tconst q = this.snapDrawn({ x: lead.x + dx, y: lead.y + dy }, leave, input, () => (drag.targets ??= VectorSnapTargets.forAnchorDrag(drag.base, this.anchors)));\n\t\t\t\t\tdx = q.x - lead.x;\n\t\t\t\t\tdy = q.y - lead.y;\n\t\t\t\t} else if (input.shift) Math.abs(dx) > Math.abs(dy) ? (dy = 0) : (dx = 0);\n\t\t\t\tconst single = this.anchors.length === 1 ? this.anchors[0] : null;\n\t\t\t\tconst from = single ? getAnchorWorld(drag.base, single) : undefined;\n\t\t\t\t// A lone point bends its segments with the Curvature setting (Alt held flips it).\n\t\t\t\tif (single && from && this.editing.curvatureMoves !== !!input.alt) this.doc = this.curvatureMoved(drag.base, single, { x: from.x + dx, y: from.y + dy });\n\t\t\t\telse this.doc = this.movedAnchors(drag.base, this.anchors, dx, dy);\n\t\t\t\tthis.interaction = { type: 'anchors', dx, dy };\n\t\t\t\tthis.notify(true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'handle': {\n\t\t\t\tconst anchor = getAnchorWorld(drag.base, drag.ref);\n\t\t\t\tconst leave = anchor ? { at: { x: anchor.x, y: anchor.y }, constrain: input.shift ? ('angle' as const) : null } : null;\n\t\t\t\tconst pt = this.snapDrawn(p, leave, input, () => (drag.targets ??= VectorSnapTargets.forHandle(drag.base, drag.ref.nodeId)));\n\t\t\t\tconst local = this.toLocal(drag.ref.nodeId, pt, drag.base);\n\t\t\t\tthis.doc = this.updateAnchors(drag.base, [drag.ref], (a) => moveAnchorHandle(a, drag.ref.handle, local, input.alt));\n\t\t\t\tthis.interaction = { type: 'handle', ref: drag.ref };\n\t\t\t\tthis.notify(true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'corner': {\n\t\t\t\tif (!drag.moved && Math.hypot(p.x - drag.start.x, p.y - drag.start.y) * this.viewport.zoom < 3) return;\n\t\t\t\tdrag.moved = true;\n\t\t\t\tconst c = drag.widget.corner;\n\t\t\t\tconst inv = invertMatrix(this.worldOf(drag.widget.ref.nodeId, drag.base));\n\t\t\t\tconst d = applyMatrixLinear(inv, { x: p.x - drag.start.x, y: p.y - drag.start.y });\n\t\t\t\tconst radius = Math.max(0, Math.min(c.maxRadius, drag.widget.drawnRadius + cornerRadiusChange(c, d)));\n\t\t\t\tthis.doc = this.updateAnchors(drag.base, drag.refs, (a) => setAnchorCorner(a, radius));\n\t\t\t\tthis.interaction = { type: 'corner', ref: drag.widget.ref, radius };\n\t\t\t\tthis.notify(true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'bend': {\n\t\t\t\tif (!drag.moved && Math.hypot(p.x - drag.start.x, p.y - drag.start.y) * this.viewport.zoom < 2) return;\n\t\t\t\tdrag.moved = true;\n\t\t\t\tthis.doc = this.bent(drag.base, drag.ref, drag.t, p);\n\t\t\t\tthis.interaction = { type: 'bend', ref: drag.ref };\n\t\t\t\tthis.notify(true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'builder':\n\t\t\t\treturn this.moveBuilder(drag, p);\n\t\t\tcase 'stroke': {\n\t\t\t\tconst last = drag.points[drag.points.length - 1];\n\t\t\t\tif (Math.hypot(p.x - last.x, p.y - last.y) * this.viewport.zoom < 1.5) return;\n\t\t\t\tdrag.points.push(p);\n\t\t\t\tthis.interaction = { type: 'stroke', tool: drag.tool, points: drag.points.slice(), width: drag.tool === 'eraser' ? this.editing.eraserSize : 0 };\n\t\t\t\tthis.notify();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'pull': {\n\t\t\t\tconst anchor = getAnchorWorld(drag.base, drag.ref);\n\t\t\t\tif (!anchor) return;\n\t\t\t\tif (Math.hypot(p.x - anchor.x, p.y - anchor.y) * this.viewport.zoom < 2) {\n\t\t\t\t\tthis.doc = drag.base;\n\t\t\t\t\tthis.notify(true);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.guides = null;\n\t\t\t\tlet pt = p;\n\t\t\t\tif (input.shift) pt = constrain45(anchor, pt);\n\t\t\t\tconst side = drag.closing ? 'in' : 'out';\n\t\t\t\t// Dragging out of the closing anchor pulls its incoming handle toward the pointer's mirror.\n\t\t\t\tconst pos = this.toLocal(drag.ref.nodeId, drag.closing ? { x: 2 * anchor.x - pt.x, y: 2 * anchor.y - pt.y } : pt, drag.base);\n\t\t\t\tthis.doc = this.updateAnchors(drag.base, [drag.ref], (a) => {\n\t\t\t\t\tif (drag.outOnly || (drag.altBreaks && input.alt)) return moveAnchorHandle(a, side, pos, true);\n\t\t\t\t\treturn moveAnchorHandle({ ...a, type: 'symmetric' }, side, pos);\n\t\t\t\t});\n\t\t\t\tthis.notify(true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'connector': {\n\t\t\t\tconst node = this.getNode(drag.nodeId);\n\t\t\t\tif (!isVectorConnector(node)) return;\n\t\t\t\tconst target = vectorConnectorTargetAt(this.doc.nodes, p, this.connectorRadius(), drag.nodeId);\n\t\t\t\tconst startGlue = node.connector.start;\n\t\t\t\tconst end = target && target.nodeId !== startGlue?.nodeId ? { nodeId: target.nodeId, ...(target.side && target.side !== 'auto' ? { side: target.side } : {}) } : null;\n\t\t\t\tconst first = node.subpaths[0]?.anchors[0] ?? drag.start;\n\t\t\t\tthis.replaceNode(drag.nodeId, { ...node, connector: { ...node.connector, end }, subpaths: [{ closed: false, anchors: [{ x: first.x, y: first.y }, { x: p.x, y: p.y }] }] });\n\t\t\t\tthis.notify(true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'shape': {\n\t\t\t\tconst node = this.getNode(drag.nodeId);\n\t\t\t\tif (node?.type !== 'path') return;\n\t\t\t\tconst leave = { at: drag.start, constrain: input.shift ? (drag.tool === 'line' ? ('angle' as const) : ('square' as const)) : null, curves: false };\n\t\t\t\tlet end = this.snapDrawn(p, leave, input, () => (drag.targets ??= this.drawingTargets(null)));\n\t\t\t\tif (!this.snappedToObject && drag.tool !== 'line' && !input.shift && !input.mod && this.snap.objects && drag.targets) end = this.matchShapeSize(drag.targets, drag.start, end, input.alt);\n\t\t\t\tlet start = drag.start;\n\t\t\t\tlet bounds: VectorBounds;\n\t\t\t\tif (drag.tool === 'line') {\n\t\t\t\t\tif (input.shift) end = constrain45(start, end);\n\t\t\t\t\tif (input.alt) start = { x: 2 * start.x - end.x, y: 2 * start.y - end.y };\n\t\t\t\t\tbounds = { x: start.x, y: start.y, width: end.x - start.x, height: end.y - start.y };\n\t\t\t\t} else {\n\t\t\t\t\tlet w = end.x - start.x;\n\t\t\t\t\tlet h = end.y - start.y;\n\t\t\t\t\tif (input.shift) {\n\t\t\t\t\t\tconst s = Math.max(Math.abs(w), Math.abs(h));\n\t\t\t\t\t\tw = Math.sign(w || 1) * s;\n\t\t\t\t\t\th = Math.sign(h || 1) * s;\n\t\t\t\t\t}\n\t\t\t\t\tif (input.alt) start = { x: start.x - w, y: start.y - h };\n\t\t\t\t\tconst x2 = drag.start.x + w;\n\t\t\t\t\tconst y2 = drag.start.y + h;\n\t\t\t\t\tbounds = boundsFromPoints(start, { x: x2, y: y2 });\n\t\t\t\t}\n\t\t\t\tconst shaped = this.createShapeNode({ kind: (drag.tool === 'frame' ? 'rect' : drag.tool) as VectorShapeInput['kind'], ...bounds });\n\t\t\t\tthis.replaceNode(drag.nodeId, { ...node, subpaths: shaped.subpaths });\n\t\t\t\tthis.interaction = { type: 'draw', tool: drag.tool, nodeId: drag.nodeId, bounds };\n\t\t\t\tthis.notify(true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'width': {\n\t\t\t\tif (!drag.moved && Math.hypot(p.x - drag.start.x, p.y - drag.start.y) * this.viewport.zoom < 2) return;\n\t\t\t\tdrag.moved = true;\n\t\t\t\tconst node = findVectorNode(drag.base.nodes, drag.ref.nodeId) as VectorPathNode;\n\t\t\t\tconst local = this.toLocal(node.id, p, drag.base);\n\t\t\t\tconst src = drag.profile[drag.source];\n\t\t\t\tlet profile: VectorWidthPoint[];\n\t\t\t\tlet index = drag.source;\n\t\t\t\tif (drag.mode !== 'width') {\n\t\t\t\t\tconst g = vectorStrokeWidthGeometry(node);\n\t\t\t\t\tconst near = g ? nearestStrokePosition(g.line, [local.x, local.y], drag.ref.subpath) : null;\n\t\t\t\t\tif (!g || !near) return;\n\t\t\t\t\tconst t = drag.group.length ? near.t : this.snapWidthT(drag, node, g.line, near.t, drag.source);\n\t\t\t\t\tif (drag.mode === 'copy') {\n\t\t\t\t\t\t// The source stays; a copy of it slides (and may pass other points).\n\t\t\t\t\t\t({ points: profile, index } = insertWidthPoint(drag.profile, t, { left: src.left, right: src.right }, { step: true }));\n\t\t\t\t\t} else if (drag.group.length) {\n\t\t\t\t\t\tconst shifted = shiftWidthPoints(drag.profile, [drag.source, ...drag.group], t - src.t);\n\t\t\t\t\t\tprofile = shifted.points;\n\t\t\t\t\t\tdrag.renumber = shifted.renumber;\n\t\t\t\t\t\tindex = shifted.renumber.get(drag.source) ?? drag.source;\n\t\t\t\t\t} else profile = setWidthPoint(drag.profile, drag.source, { t });\n\t\t\t\t} else {\n\t\t\t\t\tconst off = ((local.x - drag.origin.x) * drag.normal.x + (local.y - drag.origin.y) * drag.normal.y) / drag.unit;\n\t\t\t\t\tconst patch =\n\t\t\t\t\t\tdrag.side === 'both' ? { left: Math.abs(off), right: Math.abs(off) } : drag.side === 'left' ? { left: Math.max(0, off) } : { right: Math.max(0, -off) };\n\t\t\t\t\tprofile = setWidthPoint(drag.profile, drag.source, patch);\n\t\t\t\t\tif (drag.group.length) {\n\t\t\t\t\t\t// Every selected point widens by as much as the dragged one.\n\t\t\t\t\t\tconst dl = profile[drag.source].left - src.left;\n\t\t\t\t\t\tconst dr = profile[drag.source].right - src.right;\n\t\t\t\t\t\tfor (const i of drag.group) profile = setWidthPoint(profile, i, { left: Math.max(0, drag.profile[i].left + dl), right: Math.max(0, drag.profile[i].right + dr) });\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tthis.doc = { ...drag.base, nodes: mapTree(drag.base.nodes, (n) => (n.id === node.id ? { ...(n as VectorPathNode), strokeWidthProfile: profile } : n)) };\n\t\t\t\tdrag.ref = { ...drag.ref, index };\n\t\t\t\tconst w = profile[index];\n\t\t\t\tthis.interaction = { type: 'width', ref: drag.ref, mode: drag.mode, width: (w.left + w.right) * drag.unit };\n\t\t\t\tthis.notify(true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'pencil': {\n\t\t\t\tconst last = drag.points[drag.points.length - 1];\n\t\t\t\tlet moved = false;\n\t\t\t\tif (typeof input.pressure === 'number' && Number.isFinite(input.pressure)) drag.pressure = input.pressure;\n\t\t\t\tfor (const q of drag.lazy.push([p.x, p.y])) {\n\t\t\t\t\tif (Math.hypot(q[0] - last[0], q[1] - last[1]) * this.viewport.zoom < 1.5) continue;\n\t\t\t\t\tdrag.points.push(q);\n\t\t\t\t\tdrag.pressures?.push(drag.pressure);\n\t\t\t\t\tmoved = true;\n\t\t\t\t}\n\t\t\t\tconst string = drag.lazy.string();\n\t\t\t\tconst node = this.getNode(drag.nodeId);\n\t\t\t\tif (node?.type !== 'path') return;\n\t\t\t\tif (!moved) {\n\t\t\t\t\tif (string && this.interaction?.type === 'draw') {\n\t\t\t\t\t\tthis.interaction = { ...this.interaction, string: [{ x: string[0][0], y: string[0][1] }, { x: string[1][0], y: string[1][1] }] };\n\t\t\t\t\t\tthis.notify();\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.replaceNode(drag.nodeId, { ...node, subpaths: [{ anchors: drag.points.map(([x, y]) => ({ x, y })), closed: false }] });\n\t\t\t\tconst xs = drag.points.map((q) => q[0]);\n\t\t\t\tconst ys = drag.points.map((q) => q[1]);\n\t\t\t\tthis.interaction = {\n\t\t\t\t\ttype: 'draw',\n\t\t\t\t\ttool: 'pencil',\n\t\t\t\t\tnodeId: drag.nodeId,\n\t\t\t\t\tbounds: { x: Math.min(...xs), y: Math.min(...ys), width: Math.max(...xs) - Math.min(...xs), height: Math.max(...ys) - Math.min(...ys) },\n\t\t\t\t\t...(string ? { string: [{ x: string[0][0], y: string[0][1] }, { x: string[1][0], y: string[1][1] }] as [Vec2, Vec2] } : {})\n\t\t\t\t};\n\t\t\t\tthis.notify(true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Snaps a move of the selection by (dx, dy): its bounds line up with targets or space evenly, else go to the grid. */\n\tprivate snapMove(drag: Extract<Drag, { kind: 'move' }>, dx: number, dy: number, input: VectorModifiers): { dx: number; dy: number } {\n\t\tconst origin = drag.origin;\n\t\tif (input.mod || !origin) {\n\t\t\tthis.setGuides(null);\n\t\t\treturn { dx, dy };\n\t\t}\n\t\t// A Shift-locked axis stays put.\n\t\tconst axes = { x: !input.shift || dx !== 0, y: !input.shift || dy !== 0 };\n\t\tlet res = null;\n\t\tif (this.snap.objects) {\n\t\t\tdrag.targets ??= VectorSnapTargets.forMove(drag.base, this.selectedRoots());\n\t\t\tres = drag.targets.snapRect({ ...origin, x: origin.x + dx, y: origin.y + dy }, this.snapTolerance(), { spacing: true, axes });\n\t\t}\n\t\tthis.setGuides(res);\n\t\treturn {\n\t\t\tdx: res?.x ? dx + res.dx : axes.x ? this.gridValue(origin.x + dx) - origin.x : dx,\n\t\t\tdy: res?.y ? dy + res.dy : axes.y ? this.gridValue(origin.y + dy) - origin.y : dy\n\t\t};\n\t}\n\n\t/** The anchor a point drag snaps by, where it started: the grabbed one, else the moving one nearest the press. */\n\tprivate leadAnchor(drag: Extract<Drag, { kind: 'anchors' }>): Vec2 | null {\n\t\tconst ref = drag.lead && this.anchors.some((a) => sameRef(a, drag.lead!)) ? drag.lead : null;\n\t\tif (ref) {\n\t\t\tconst a = getAnchorWorld(drag.base, ref);\n\t\t\treturn a ? { x: a.x, y: a.y } : null;\n\t\t}\n\t\tlet best: Vec2 | null = null;\n\t\tfor (const r of this.anchors) {\n\t\t\tconst a = getAnchorWorld(drag.base, r);\n\t\t\tif (a && (!best || Math.hypot(a.x - drag.start.x, a.y - drag.start.y) < Math.hypot(best.x - drag.start.x, best.y - drag.start.y))) best = { x: a.x, y: a.y };\n\t\t}\n\t\treturn best;\n\t}\n\n\t/** A shape's dragged corner moved so its width or height matches another object's, when one is within reach. */\n\tprivate matchShapeSize(targets: VectorSnapTargets, start: Vec2, end: Vec2, fromCentre = false): Vec2 {\n\t\t// Dragged from the centre, the shape is twice as large as the drag.\n\t\tconst k = fromCentre ? 2 : 1;\n\t\tconst tol = this.snapTolerance() * k;\n\t\tconst out = { ...end };\n\t\tconst guides: VectorSnapGuides = { lines: [], labels: [] };\n\t\tfor (const axis of ['x', 'y'] as const) {\n\t\t\tconst d = end[axis] - start[axis];\n\t\t\tconst len = Math.abs(d) > 1e-9 ? targets.matchSize(Math.abs(d) * k, tol) : null;\n\t\t\tif (len === null || len <= 0) continue;\n\t\t\tout[axis] = start[axis] + (Math.sign(d) * len) / k;\n\t\t\tconst from = { ...out, [axis]: fromCentre ? 2 * start[axis] - out[axis] : start[axis] };\n\t\t\tguides.lines.push({ kind: 'size', x1: from.x, y1: from.y, x2: out.x, y2: out.y });\n\t\t\tguides.labels.push({ kind: 'size', x: (from.x + out.x) / 2, y: (from.y + out.y) / 2, text: `${axis === 'x' ? 'W' : 'H'} ${Math.round(len * 100) / 100}` });\n\t\t}\n\t\tthis.setGuides(guides);\n\t\treturn out;\n\t}\n\n\t/** Primary button released. */\n\tpointerUp(screen?: Vec2, input: VectorPointerInput = {}): void {\n\t\t// A pen reports no pressure as it lifts: keep the last pressure for the final move.\n\t\tif (screen && this.drag && this.drag.kind !== 'pan') this.pointerMove(screen, { ...input, pressure: undefined });\n\t\tconst drag = this.drag;\n\t\tthis.drag = null;\n\t\tthis.interaction = null;\n\t\tthis.guides = null;\n\t\tif (!drag) return;\n\t\tswitch (drag.kind) {\n\t\t\tcase 'textBox':\n\t\t\t\treturn this.upTypeBox(drag);\n\t\t\tcase 'textSelect':\n\t\t\t\tthis.notify();\n\t\t\t\treturn;\n\t\t\tcase 'pan':\n\t\t\tcase 'marquee':\n\t\t\t\tthis.notify();\n\t\t\t\treturn;\n\t\t\tcase 'move':\n\t\t\t\tif (!drag.moved) {\n\t\t\t\t\tif (drag.clickId) this.selection = [drag.clickId];\n\t\t\t\t\tthis.notify();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\treturn this.commit(drag.duplicated ? 'Duplicate' : 'Move', drag.before);\n\t\t\tcase 'scale':\n\t\t\t\treturn this.commit(drag.op ?? 'Scale', drag.before);\n\t\t\tcase 'envelope':\n\t\t\t\tif (!drag.moved) {\n\t\t\t\t\tthis.notify();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\treturn this.commit('Edit Envelope', drag.before);\n\t\t\tcase 'rotate':\n\t\t\t\treturn this.commit('Rotate', drag.before);\n\t\t\tcase 'anchors':\n\t\t\t\tif (!drag.moved) {\n\t\t\t\t\tif (drag.clickRef) this.anchors = [drag.clickRef];\n\t\t\t\t\tif (drag.before.doc !== this.doc) return this.commit('Add Point', drag.before);\n\t\t\t\t\tthis.notify();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\treturn this.commit(drag.before.doc !== drag.base ? 'Add Point' : 'Move Points', drag.before);\n\t\t\tcase 'handle':\n\t\t\t\treturn this.commit('Move Handle', drag.before);\n\t\t\tcase 'bend':\n\t\t\t\tif (!drag.moved) {\n\t\t\t\t\tthis.notify();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\treturn this.commit('Bend Segment', drag.before);\n\t\t\tcase 'corner': {\n\t\t\t\tif (drag.moved) return this.commit('Corner Radius', drag.before);\n\t\t\t\tif (drag.alt) {\n\t\t\t\t\tconst kind = nextVectorCornerKind(drag.widget.kind);\n\t\t\t\t\tthis.doc = this.updateAnchors(drag.base, drag.refs, (a) => setAnchorCorner(a, a.corner?.radius ?? 0, kind));\n\t\t\t\t\treturn this.commit('Corner Kind', drag.before);\n\t\t\t\t}\n\t\t\t\t// A click selects the widget's corner, so the next drag rounds that corner alone.\n\t\t\t\tconst ref = drag.widget.ref;\n\t\t\t\tconst has = this.anchors.some((a) => sameRef(a, ref));\n\t\t\t\tthis.editingId = ref.nodeId;\n\t\t\t\tthis.anchors = drag.shift ? (has ? this.anchors.filter((a) => !sameRef(a, ref)) : [...this.anchors, ref]) : [ref];\n\t\t\t\tthis.notify();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'width': {\n\t\t\t\tif (drag.moved) {\n\t\t\t\t\tconst several = drag.group.length > 0;\n\t\t\t\t\tif (drag.mode === 'copy') this.widthPoints = [drag.ref];\n\t\t\t\t\telse if (drag.mode === 'move' && several) {\n\t\t\t\t\t\t// Points moved together stay selected (re-sorting may renumber them).\n\t\t\t\t\t\tthis.widthPoints = [drag.source, ...drag.group].map((i) => ({ ...drag.ref, index: drag.renumber?.get(i) ?? i }));\n\t\t\t\t\t} else if (!several) this.widthPoints = [drag.ref];\n\t\t\t\t\tconst label = drag.mode === 'copy' ? 'Copy Width Point' : drag.mode === 'move' ? (several ? 'Move Width Points' : 'Move Width Point') : several ? 'Width Points' : 'Width Point';\n\t\t\t\t\treturn this.commit(label, drag.before);\n\t\t\t\t}\n\t\t\t\t// A click adds nothing; on a width point it selects it (just it, among several selected).\n\t\t\t\tif (drag.created) {\n\t\t\t\t\tthis.doc = drag.before.doc;\n\t\t\t\t\tthis.widthPoints = [];\n\t\t\t\t\tthis.notify(true);\n\t\t\t\t} else {\n\t\t\t\t\tif (!drag.shift && drag.group.length) this.widthPoints = [drag.ref];\n\t\t\t\t\tthis.notify();\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'builder':\n\t\t\t\treturn this.upBuilder(drag);\n\t\t\tcase 'gradient':\n\t\t\t\treturn this.upGradient(drag);\n\t\t\tcase 'stroke': {\n\t\t\t\tthis.notify();\n\t\t\t\tif (drag.tool === 'knife') this.knife(drag.points);\n\t\t\t\telse this.erase(drag.points);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'pull':\n\t\t\t\tif (drag.closing) this.pen = null;\n\t\t\t\treturn this.commit(drag.label, drag.before);\n\t\t\tcase 'connector': {\n\t\t\t\tconst node = this.getNode(drag.nodeId);\n\t\t\t\tconst b = node ? vectorNodeBounds(node) : null;\n\t\t\t\tconst glued = isVectorConnector(node) && !!node.connector.end;\n\t\t\t\tif (!b || (!glued && Math.hypot(b.width, b.height) < 2 / this.viewport.zoom)) {\n\t\t\t\t\tthis.doc = drag.before.doc;\n\t\t\t\t\tthis.notify(true);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.selection = [drag.nodeId];\n\t\t\t\tthis.anchors = [];\n\t\t\t\tthis.tool = 'select';\n\t\t\t\treturn this.commit('Draw Connector', drag.before);\n\t\t\t}\n\t\t\tcase 'shape': {\n\t\t\t\tconst node = this.getNode(drag.nodeId);\n\t\t\t\tconst b = node ? vectorNodeBounds(node) : null;\n\t\t\t\tconst min = 2 / this.viewport.zoom;\n\t\t\t\tif (!b || (b.width < min && b.height < min)) {\n\t\t\t\t\tthis.doc = drag.before.doc;\n\t\t\t\t\tthis.notify(true);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (drag.tool === 'frame') {\n\t\t\t\t\t// The rectangle becomes a frame holding the top-level objects it encloses.\n\t\t\t\t\tconst rest = this.doc.nodes.filter((n) => n.id !== drag.nodeId);\n\t\t\t\t\tconst adopted = rest.filter((n) => !n.locked && n.visible !== false && !n.blendStep && vectorBoundsInside(vectorNodeBounds(n), b));\n\t\t\t\t\tconst taken = new Set(adopted.map((n) => n.id));\n\t\t\t\t\tconst frame: VectorFrameGroup = { id: drag.nodeId, type: 'group', name: this.frameName(), frame: { ...b, fill: '#ffffff' }, children: adopted };\n\t\t\t\t\tthis.setDoc([...rest.filter((n) => !taken.has(n.id)), frame]);\n\t\t\t\t}\n\t\t\t\tthis.selection = [drag.nodeId];\n\t\t\t\tthis.anchors = [];\n\t\t\t\tthis.tool = 'select';\n\t\t\t\treturn this.commit(`Draw ${TOOL_LABELS[drag.tool]}`, drag.before);\n\t\t\t}\n\t\t\tcase 'pencil': {\n\t\t\t\tconst node = this.getNode(drag.nodeId);\n\t\t\t\tif (node?.type !== 'path' || drag.points.length < 2) {\n\t\t\t\t\tthis.doc = drag.before.doc;\n\t\t\t\t\tthis.notify(true);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tfor (const q of drag.lazy.finish()) {\n\t\t\t\t\tdrag.points.push(q);\n\t\t\t\t\tdrag.pressures?.push(drag.pressure);\n\t\t\t\t}\n\t\t\t\tconst shape = this.editing.recognizeShapes ? recognizeInkShape(drag.points, { minSize: 6 / this.viewport.zoom }) : null;\n\t\t\t\tif (shape) {\n\t\t\t\t\tthis.replaceNode(drag.nodeId, { ...node, name: inkShapeLabel(shape), subpaths: [inkShapeSubpath(shape)] });\n\t\t\t\t\tthis.selection = [drag.nodeId];\n\t\t\t\t\treturn this.commit(`Draw ${inkShapeLabel(shape)}`, drag.before);\n\t\t\t\t}\n\t\t\t\tconst fitted = fitCubics(drag.points, 1.5 / this.viewport.zoom, { cornerAngleDeg: 60 });\n\t\t\t\tconst anchors: VectorAnchor[] = [];\n\t\t\t\tfor (const c of fitted) {\n\t\t\t\t\tif (!anchors.length) anchors.push({ x: c.p0[0], y: c.p0[1] });\n\t\t\t\t\tanchors[anchors.length - 1].out = { x: c.p1[0], y: c.p1[1] };\n\t\t\t\t\tanchors.push({ x: c.p3[0], y: c.p3[1], in: { x: c.p2[0], y: c.p2[1] } });\n\t\t\t\t}\n\t\t\t\tconst subpaths = anchors.length > 1 ? bezierToSubpaths(subpathsToBezier([{ anchors, closed: false }])) : node.subpaths;\n\t\t\t\tconst profile = drag.pressures ? pencilPressureProfile(drag.points, drag.pressures) : [];\n\t\t\t\tthis.replaceNode(drag.nodeId, { ...node, subpaths, ...(profile.length ? { strokeWidthProfile: profile } : {}) });\n\t\t\t\tthis.selection = [drag.nodeId];\n\t\t\t\treturn this.commit('Draw', drag.before);\n\t\t\t}\n\t\t}\n\t}\n\n\t/** The pointer left the canvas. */\n\tpointerLeave(): void {\n\t\tif (this.drag) return;\n\t\tif (!this.hover && !this.pointer && !this.guides) return;\n\t\tthis.hover = null;\n\t\tthis.pointer = null;\n\t\tthis.guides = null;\n\t\tthis.notify();\n\t}\n\n\t/** Abandons the drag in progress (Escape, lost capture). */\n\tcancelDrag(): void {\n\t\tconst drag = this.drag;\n\t\tif (!drag) return;\n\t\tthis.drag = null;\n\t\tthis.interaction = null;\n\t\tthis.guides = null;\n\t\tif (drag.kind === 'textBox' || drag.kind === 'textSelect') {\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tif (drag.kind === 'pan' || drag.kind === 'builder') {\n\t\t\tif (drag.kind === 'builder') this.builderPreview = null;\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tif (drag.kind === 'marquee') {\n\t\t\tthis.selection = drag.base;\n\t\t\tthis.anchors = drag.baseAnchors;\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tthis.restore(drag.before);\n\t}\n\n\t/**\n\t * Double click: select tool enters path editing, direct tool toggles an anchor or adds one,\n\t * width tool opens the width-point editor (`widthPointEdit`).\n\t */\n\tdoubleClick(screen: Vec2): void {\n\t\tconst p = this.screenToDocument(screen);\n\t\tif (this.tool === 'select') {\n\t\t\t// Into a symbol instance: Edit Symbol in isolation.\n\t\t\tconst instance = this.hitOfType(p, 'instance');\n\t\t\tif (instance) {\n\t\t\t\tthis.editSymbol(instance.id);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tif (this.tool === 'select' || this.tool === 'type' || this.tool === 'typePath') {\n\t\t\tconst text = this.hitText(p);\n\t\t\tif (text) {\n\t\t\t\t// Into the text: the Type tool, the caret where it was clicked (twice inside the text: the word).\n\t\t\t\tif (this.tool === 'select') this.setTool('type');\n\t\t\t\tconst index = vectorTextIndexAt(text, this.toLocal(text.id, p));\n\t\t\t\tif (this.textEdit?.nodeId === text.id) {\n\t\t\t\t\tconst [a, b] = wordAt(text.text, index);\n\t\t\t\t\tthis.textEdit = { ...this.textEdit, anchor: a, focus: b };\n\t\t\t\t\tthis.notify();\n\t\t\t\t} else this.editText(text.id, { index });\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tif (this.tool === 'select') {\n\t\t\tconst path = this.hitPath(p);\n\t\t\tif (!path) return;\n\t\t\tthis.tool = 'direct';\n\t\t\tthis.selection = [path.id];\n\t\t\tthis.editingId = path.id;\n\t\t\tthis.anchors = [];\n\t\t\tthis.notify();\n\t\t\treturn;\n\t\t}\n\t\tif (this.tool === 'direct') {\n\t\t\tconst anchor = this.hitAnchor(screen, getVisibleAnchors(this.view()));\n\t\t\tif (anchor) {\n\t\t\t\tconst a = getAnchor(this.doc, anchor)!;\n\t\t\t\tthis.convertAnchors(a.in || a.out ? 'corner' : 'smooth', [anchor]);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst id = this.hover?.segment?.nodeId ?? this.editingId;\n\t\t\tif (id) {\n\t\t\t\tconst node = this.getNode(id) as VectorPathNode | undefined;\n\t\t\t\tconst seg = node ? nearestSegment(node, this.toLocal(id, p)) : null;\n\t\t\t\tif (seg && seg.distance <= this.tolerance()) this.insertAnchorAt(id, p);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tif (this.tool === 'width') {\n\t\t\t// Width Point Edit: on a width point, or on a stroke (applying it adds a point there).\n\t\t\tconst hit = this.hitWidthHandle(screen);\n\t\t\tif (hit) {\n\t\t\t\tthis.openWidthPointEdit(hit.ref);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst spot = this.strokeSpotAt(p);\n\t\t\tif (spot) this.openWidthPointEdit({ nodeId: spot.nodeId, subpath: spot.subpath, t: spot.t });\n\t\t\treturn;\n\t\t}\n\t\tif (this.tool === 'pen') this.finishPath();\n\t}\n\n\n\t// -------------------------------------------------------------------------\n\t// Text\n\t// -------------------------------------------------------------------------\n\n\t/**\n\t * Sets the text shaper (shared by every vector editor on the page, see `setVectorTextShaper`) and\n\t * lays every text node out again with it. Not an undo step.\n\t */\n\tsetTextShaper(shaper: TextShaper | null): void {\n\t\tsetVectorTextShaper(shaper);\n\t\t// New node objects so views keyed by the document draw the text again.\n\t\tthis.doc = { ...this.doc, nodes: mapTree(this.doc.nodes, (n) => (n.type === 'text' ? { ...n } : n)) };\n\t\tthis.notify(true);\n\t}\n\n\t/** The text shaper in use. */\n\tgetTextShaper(): TextShaper {\n\t\treturn getVectorTextShaper();\n\t}\n\n\t/** Changes the font and alignment new text gets (validated; throws on a bad value). */\n\tsetTextDefaults(patch: Partial<VectorTextDefaults>): void {\n\t\tconst { align, ...font } = patch;\n\t\tconst next: VectorTextDefaults = patchVectorCharacterStyle({ ...this.textDefaults }, font as Record<string, unknown>, 'textDefaults');\n\t\tif (align !== undefined) {\n\t\t\tconst p = patchVectorParagraphStyle({}, { align });\n\t\t\tif (p.align) next.align = p.align;\n\t\t\telse delete next.align;\n\t\t}\n\t\tthis.textDefaults = next;\n\t\tthis.notify();\n\t}\n\n\t/** The paint new text gets: black, no stroke (`addText` and the style panel change it). */\n\tprivate textStyle(): VectorStyle {\n\t\treturn { fill: '#000000', stroke: null, strokeWidth: this.defaults.strokeWidth ?? 1 };\n\t}\n\n\t/** Builds a text node from `input` with the text defaults (not added). Throws a `VectorTextError` on bad input. */\n\tcreateTextNode(input: VectorTextInput): VectorTextNode {\n\t\tif (typeof input.text !== 'string') throw new VectorTextError('text must be a string.');\n\t\tconst { align, ...defaultFont } = this.textDefaults;\n\t\tconst font = patchVectorCharacterStyle({ ...defaultFont }, (input.font ?? {}) as Record<string, unknown>, 'font');\n\t\tconst paragraph = patchVectorParagraphStyle(align ? { align } : {}, (input.paragraph ?? {}) as Record<string, unknown>);\n\t\tlet frame: VectorTextFrame;\n\t\tif (input.path !== undefined) {\n\t\t\tconst sp = typeof input.path === 'string' ? pathTextFrameFromBezier(toBezierPathLoose(input.path)) : input.path;\n\t\t\tframe = sanitizeVectorTextFrame({ kind: 'path', path: sp, ...(input.pathOptions ?? {}) });\n\t\t} else if (input.width !== undefined || input.height !== undefined) {\n\t\t\tframe = sanitizeVectorTextFrame({ kind: 'area', x: input.x ?? 0, y: input.y ?? 0, width: input.width, height: input.height });\n\t\t} else frame = sanitizeVectorTextFrame({ kind: 'point', x: input.x ?? 0, y: input.y ?? 0 });\n\t\tconst own: VectorStyle = {};\n\t\tfor (const key of VECTOR_STYLE_KEYS) if (input[key] !== undefined) (own as Record<string, unknown>)[key] = input[key];\n\t\tconst text = input.text.replace(/\\r\\n?/g, '\\n');\n\t\tconst node: VectorTextNode = {\n\t\t\tid: this.newId(),\n\t\t\ttype: 'text',\n\t\t\tname: input.name ?? vectorTextName(text),\n\t\t\t...this.textStyle(),\n\t\t\ttext,\n\t\t\tfont,\n\t\t\t...(Object.keys(paragraph).length ? { paragraph } : {}),\n\t\t\tframe\n\t\t};\n\t\treturn withStyle(node as unknown as VectorPathNode, own) as unknown as VectorTextNode;\n\t}\n\n\t/** Adds text (point, area or on a path, see `VectorTextInput`). Returns its id. */\n\taddText(input: VectorTextInput, options: AddVectorNodeOptions = {}): string {\n\t\tconst node = this.createTextNode(input);\n\t\treturn this.addNode(node, { label: 'Add Text', ...options });\n\t}\n\n\t/** The text node `id`, or an error message. */\n\tprivate textNode(id: string): VectorTextNode | string {\n\t\tconst node = this.getNode(id);\n\t\tif (!node) return `There is no node \"${id}\".`;\n\t\tif (node.type !== 'text') return `\"${id}\" is a ${node.type}, not text; vector.list shows each node's type.`;\n\t\tif (node.blendStep) return `\"${id}\" is a blend step; edit the blend's keys instead.`;\n\t\treturn node;\n\t}\n\n\t/** Replaces text node `id` by `fn(node)` as one undo step (merged with others of `merge`). */\n\tprivate editTextNode(id: string, label: string, fn: (node: VectorTextNode) => VectorTextNode, merge?: string): VectorActionResult {\n\t\tconst node = this.textNode(id);\n\t\tif (typeof node === 'string') return { ok: false, message: node };\n\t\tlet next: VectorTextNode;\n\t\ttry {\n\t\t\tnext = fn(node);\n\t\t} catch (e) {\n\t\t\treturn { ok: false, message: e instanceof Error ? e.message : String(e) };\n\t\t}\n\t\tif (next === node) return { ok: true };\n\t\t// The edit session's pending creation folds into its first change.\n\t\tconst pending = this.textEdit?.nodeId === id ? this.textEdit.before : null;\n\t\tconst before = pending ?? this.snapshot();\n\t\tif (pending && this.textEdit) this.textEdit.before = null;\n\t\tthis.replaceNode(id, next);\n\t\tthis.commit(label, before, merge);\n\t\treturn { ok: true };\n\t}\n\n\t/** Replaces [start, end) of text node `id` (default: all of it) with `text`. One undo step. */\n\tsetText(id: string, text: string, range: { start?: number; end?: number } = {}, options: { merge?: string } = {}): VectorActionResult {\n\t\tconst node = this.textNode(id);\n\t\tif (typeof node === 'string') return { ok: false, message: node };\n\t\treturn this.editTextNode(\n\t\t\tid,\n\t\t\t'Edit Text',\n\t\t\t(n) => {\n\t\t\t\tconst [a, b] = vectorTextRange(n.text, range.start, range.end);\n\t\t\t\tconst next = replaceVectorText(n, a, b, text);\n\t\t\t\t// A name that followed the text keeps following it.\n\t\t\t\treturn n.name === vectorTextName(n.text) ? { ...next, name: vectorTextName(next.text) } : next;\n\t\t\t},\n\t\t\toptions.merge\n\t\t);\n\t}\n\n\t/**\n\t * Applies character style fields (`fontFamily`, `fontWeight`, `italic`, `fontSize`, `tracking`,\n\t * `leading`, `baselineShift`, `underline`, `strikethrough`, `null` to clear) and `fill` to\n\t * [start, end) of text node `id` (default: all of it, which changes its own style). One undo step.\n\t */\n\tsetTextStyle(id: string, patch: Record<string, unknown>, range: { start?: number; end?: number } = {}, options: { merge?: string } = {}): VectorActionResult {\n\t\treturn this.editTextNode(\n\t\t\tid,\n\t\t\t'Character Style',\n\t\t\t(n) => {\n\t\t\t\tconst [a, b] = vectorTextRange(n.text, range.start, range.end);\n\t\t\t\treturn styleVectorText(n, a, b, patch);\n\t\t\t},\n\t\t\toptions.merge\n\t\t);\n\t}\n\n\t/**\n\t * Applies character style fields (and `fill`) to the whole of every text node in `ids` (default:\n\t * the selected text, including text inside selected groups), or a paragraph patch with\n\t * `paragraph`. One undo step for all of them.\n\t */\n\tstyleTexts(patch: { style?: Record<string, unknown>; paragraph?: Record<string, unknown> }, options: { ids?: readonly string[]; merge?: string } = {}): VectorActionResult {\n\t\tconst targets: VectorTextNode[] = [];\n\t\tfor (const id of options.ids ?? this.selection) {\n\t\t\tconst n = this.getNode(id);\n\t\t\tif (!n) return { ok: false, message: `There is no node \"${id}\".` };\n\t\t\twalkVectorNodes([n], (c) => {\n\t\t\t\tif (c.type === 'text' && !c.blendStep && !targets.includes(c)) targets.push(c);\n\t\t\t});\n\t\t}\n\t\tif (!targets.length) return { ok: false, message: 'Select text to style.' };\n\t\tconst next = new Map<string, VectorNode>();\n\t\ttry {\n\t\t\tfor (const t of targets) {\n\t\t\t\tlet n = t;\n\t\t\t\tif (patch.style) n = styleVectorText(n, 0, n.text.length, patch.style);\n\t\t\t\tif (patch.paragraph) {\n\t\t\t\t\tconst paragraph = patchVectorParagraphStyle(n.paragraph ?? {}, patch.paragraph);\n\t\t\t\t\tn = { ...n, paragraph };\n\t\t\t\t\tif (!Object.keys(paragraph).length) delete (n as Partial<VectorTextNode>).paragraph;\n\t\t\t\t}\n\t\t\t\tnext.set(t.id, n);\n\t\t\t}\n\t\t} catch (e) {\n\t\t\treturn { ok: false, message: e instanceof Error ? e.message : String(e) };\n\t\t}\n\t\tthis.change(patch.paragraph && !patch.style ? 'Paragraph Style' : 'Character Style', () => this.setDoc(mapTree(this.doc.nodes, (n) => next.get(n.id) ?? n)), options.merge);\n\t\treturn { ok: true };\n\t}\n\n\t/** Changes paragraph fields (`align`, `autoLeading`, indents, spacing; `null` clears) of text node `id`. One undo step. */\n\tsetParagraphStyle(id: string, patch: Record<string, unknown>, options: { merge?: string } = {}): VectorActionResult {\n\t\treturn this.editTextNode(\n\t\t\tid,\n\t\t\t'Paragraph Style',\n\t\t\t(n) => {\n\t\t\t\tconst paragraph = patchVectorParagraphStyle(n.paragraph ?? {}, patch);\n\t\t\t\tconst next = { ...n, paragraph };\n\t\t\t\tif (!Object.keys(paragraph).length) delete (next as Partial<VectorTextNode>).paragraph;\n\t\t\t\treturn next;\n\t\t\t},\n\t\t\toptions.merge\n\t\t);\n\t}\n\n\t/**\n\t * Changes how text node `id` is set: `point` (keeps where its first line starts), `area` (a box:\n\t * `width` / `height` default to its current size, `x` / `y` to its top left) or `path` options\n\t * (`start`, `end`, `flip`, `orientation`, `baseline`; a new `path` needs path data or a subpath in\n\t * the node's own space). One undo step.\n\t */\n\tsetTextFrame(id: string, patch: Record<string, unknown>): VectorActionResult {\n\t\treturn this.editTextNode(id, 'Text Frame', (n) => {\n\t\t\tconst kind = (patch.kind as string | undefined) ?? n.frame.kind;\n\t\t\tconst layout = layoutVectorText(n);\n\t\t\tconst cur = n.frame;\n\t\t\tconst box = layout.bounds ?? { x: 0, y: 0, width: 0, height: 0 };\n\t\t\tlet frame: VectorTextFrame;\n\t\t\tif (kind === 'point') {\n\t\t\t\tconst first = layout.lines[0];\n\t\t\t\tconst x = (patch.x as number | undefined) ?? (cur.kind === 'point' ? cur.x : (first?.x ?? box.x));\n\t\t\t\tconst y = (patch.y as number | undefined) ?? (cur.kind === 'point' ? cur.y : (first?.baseline ?? box.y));\n\t\t\t\tframe = sanitizeVectorTextFrame({ kind: 'point', x, y });\n\t\t\t\t// Point text has no wrap: the left edge stays where the lines started.\n\t\t\t\tif (cur.kind !== 'point' && first && n.paragraph?.align && n.paragraph.align !== 'left') frame = sanitizeVectorTextFrame({ kind: 'point', x: box.x, y });\n\t\t\t} else if (kind === 'area') {\n\t\t\t\tconst base = cur.kind === 'area' ? cur : { x: box.x, y: box.y, width: Math.max(box.width, n.font.fontSize), height: Math.max(box.height, n.font.fontSize * 1.2) };\n\t\t\t\tframe = sanitizeVectorTextFrame({ kind: 'area', x: patch.x ?? base.x, y: patch.y ?? base.y, width: patch.width ?? base.width, height: patch.height ?? base.height });\n\t\t\t} else if (kind === 'path') {\n\t\t\t\tlet path = patch.path ?? (cur.kind === 'path' ? cur.path : undefined);\n\t\t\t\tif (typeof path === 'string') path = pathTextFrameFromBezier(toBezierPathLoose(path)) ?? undefined;\n\t\t\t\tif (!path) throw new VectorTextError('Pass path (SVG path data or a subpath) to set text on a path, or use vector.typeOnPath with a path node.');\n\t\t\t\tconst { kind: _k, path: _p, ...options } = patch;\n\t\t\t\tconst keep = cur.kind === 'path' ? { start: cur.start, end: cur.end, flip: cur.flip, orientation: cur.orientation, baseline: cur.baseline } : {};\n\t\t\t\tframe = sanitizeVectorTextFrame({ ...keep, ...options, kind: 'path', path });\n\t\t\t} else throw new VectorTextError(`kind must be point, area or path, got ${JSON.stringify(kind)}.`);\n\t\t\treturn { ...n, frame };\n\t\t});\n\t}\n\n\t/**\n\t * Turns path `pathId` into text set along it (Type on a Path): a text node in its place, its\n\t * anchors the text's path, its paint dropped for the text defaults (or `input`'s). Returns the\n\t * text's id, or an error.\n\t */\n\ttypeOnPath(pathId: string, input: Partial<VectorTextInput> = {}): VectorActionResult & { id?: string } {\n\t\tconst node = this.getNode(pathId);\n\t\tif (!node) return { ok: false, message: `There is no node \"${pathId}\".` };\n\t\tif (node.type !== 'path' || !node.subpaths.length || node.subpaths[0].anchors.length < 2) return { ok: false, message: `\"${pathId}\" is not a path with at least two anchors; type on a path needs one.` };\n\t\tlet text: VectorTextNode;\n\t\ttry {\n\t\t\ttext = this.createTextNode({ ...input, text: input.text ?? '', path: node.subpaths[0] });\n\t\t} catch (e) {\n\t\t\treturn { ok: false, message: e instanceof Error ? e.message : String(e) };\n\t\t}\n\t\tconst placed: VectorTextNode = { ...text, ...(node.transform ? { transform: node.transform } : {}), ...(node.name && node.name !== 'Path' ? { name: node.name } : {}) };\n\t\tthis.change('Type on a Path', () => {\n\t\t\tthis.replaceNode(pathId, placed);\n\t\t\tthis.selection = [placed.id];\n\t\t\tthis.anchors = [];\n\t\t\tif (this.editingId === pathId) this.editingId = null;\n\t\t});\n\t\treturn { ok: true, id: placed.id };\n\t}\n\n\t/**\n\t * Create Outlines: turns text into paths of its glyph outlines (one compound path, or a group\n\t * with one per run fill), keeping its paint, effects, filters and placement. Needs an outline\n\t * text shaper. Returns the new nodes' ids, or an error.\n\t */\n\tcreateOutlines(options: { ids?: readonly string[] } = {}): VectorActionResult & { ids?: string[] } {\n\t\tconst list = options.ids ?? this.selection;\n\t\tconst targets: VectorTextNode[] = [];\n\t\tfor (const id of list) {\n\t\t\tconst n = this.getNode(id);\n\t\t\tif (!n) return { ok: false, message: `There is no node \"${id}\".` };\n\t\t\tif (n.type === 'text') targets.push(n);\n\t\t\telse walkVectorNodes([n], (c) => {\n\t\t\t\tif (c.type === 'text') targets.push(c);\n\t\t\t});\n\t\t}\n\t\tif (!targets.length) return { ok: false, message: 'Select text to outline.' };\n\t\tif (!vectorTextHasOutlines()) {\n\t\t\treturn {\n\t\t\t\tok: false,\n\t\t\t\tmessage: `The text shaper \"${getVectorTextShaper().id}\" gives no glyph outlines. Load a font file and set an outline shaper (createOutlineTextShaper with parseOpenTypeFont) to create outlines.`\n\t\t\t};\n\t\t}\n\t\tconst replaced = new Map<string, VectorNode>();\n\t\tfor (const t of targets) {\n\t\t\tconst parts = vectorTextPaths(t).parts;\n\t\t\tconst keep = {\n\t\t\t\t...(t.name ? { name: t.name } : {}),\n\t\t\t\t...(t.transform ? { transform: t.transform } : {}),\n\t\t\t\t...(t.visible === false ? { visible: false } : {}),\n\t\t\t\t...(t.locked ? { locked: true } : {}),\n\t\t\t\t...(t.opacity !== undefined ? { opacity: t.opacity } : {}),\n\t\t\t\t...(t.blendMode ? { blendMode: t.blendMode } : {}),\n\t\t\t\t...(t.filters?.length ? { filters: t.filters } : {}),\n\t\t\t\t...(t.effects?.length ? { effects: t.effects } : {}),\n\t\t\t\t...(t.booleanOperation ? { booleanOperation: t.booleanOperation } : {})\n\t\t\t};\n\t\t\tconst pathOf = (fill: VectorTextNode['fill'] | undefined, path: BezierPath, id: string): VectorPathNode => {\n\t\t\t\tconst { transform: _t, visible: _v, locked: _l, opacity: _o, blendMode: _b, filters: _f, effects: _e, booleanOperation: _bo, name: _n, ...style } = vectorTextPaintSource(t, fill, bezierToSubpaths(path));\n\t\t\t\treturn { ...style, id };\n\t\t\t};\n\t\t\tif (parts.length <= 1) replaced.set(t.id, { ...pathOf(parts[0]?.fill, parts[0]?.path ?? [], t.id), ...keep });\n\t\t\telse replaced.set(t.id, { id: t.id, type: 'group', ...keep, children: parts.map((p) => ({ ...pathOf(p.fill, p.path, this.newId()), name: 'Outlines' })) });\n\t\t}\n\t\tif (this.textEdit && replaced.has(this.textEdit.nodeId)) this.endTextEdit();\n\t\tthis.change('Create Outlines', () => this.setDoc(mapTree(this.doc.nodes, (n) => replaced.get(n.id) ?? n)));\n\t\treturn { ok: true, ids: [...replaced.keys()] };\n\t}\n\n\t/** The layout of text node `id` (lines, glyphs, bounds, overflow), or null. */\n\tgetTextLayout(id: string): VectorTextLayout | null {\n\t\tconst node = this.getNode(id);\n\t\treturn node?.type === 'text' ? layoutVectorText(node) : null;\n\t}\n\n\t// --- Editing on the canvas ------------------------------------------------\n\n\t/**\n\t * Starts editing text node `id` on the canvas (the caret at `index`, default the end; or a\n\t * selection from `anchor` to `focus`). Typing goes to `textInput`, keys to `keyDown`.\n\t */\n\teditText(id: string, options: { index?: number; anchor?: number; focus?: number } = {}): VectorActionResult {\n\t\tconst node = this.textNode(id);\n\t\tif (typeof node === 'string') return { ok: false, message: node };\n\t\tif (this.textEdit?.nodeId !== id) this.endTextEdit();\n\t\tconst n = node.text.length;\n\t\tconst clamp = (v: number | undefined, d: number) => Math.max(0, Math.min(n, v ?? d));\n\t\tconst focus = clamp(options.focus ?? options.index, n);\n\t\tconst anchor = clamp(options.anchor, focus);\n\t\tthis.textEdit = { nodeId: id, anchor, focus, before: this.textEdit?.nodeId === id ? this.textEdit.before : null, key: this.textEdit?.nodeId === id ? this.textEdit.key : `text-edit:${id}:${++this.textEditCount}`, created: this.textEdit?.nodeId === id ? this.textEdit.created : false };\n\t\tthis.selection = [id];\n\t\tthis.anchors = [];\n\t\tthis.notify();\n\t\treturn { ok: true };\n\t}\n\n\t/** Moves the caret or selection of the text being edited. */\n\tselectText(anchor: number, focus: number = anchor): void {\n\t\tconst t = this.textEdit;\n\t\tconst node = t ? this.getNode(t.nodeId) : null;\n\t\tif (!t || node?.type !== 'text') return;\n\t\tconst n = node.text.length;\n\t\tthis.textEdit = { ...t, anchor: Math.max(0, Math.min(n, anchor)), focus: Math.max(0, Math.min(n, focus)) };\n\t\tthis.notify();\n\t}\n\n\t/**\n\t * Ends the text edit session. Text it created and left empty is removed (with no undo step when\n\t * nothing was typed); other emptied text is deleted as its own step.\n\t */\n\tendTextEdit(): void {\n\t\tconst t = this.textEdit;\n\t\tif (!t) return;\n\t\tthis.textEdit = null;\n\t\tthis.history.endGesture();\n\t\tconst node = this.getNode(t.nodeId);\n\t\tif (node?.type === 'text' && !node.text) {\n\t\t\tif (t.before) {\n\t\t\t\tthis.doc = t.before.doc;\n\t\t\t\tthis.selection = t.before.selection.filter((id) => this.getNode(id));\n\t\t\t\tthis.notify(true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (t.created) {\n\t\t\t\tthis.change('Delete Text', () => {\n\t\t\t\t\tthis.setDoc(mapTree(this.doc.nodes, (n) => (n.id === t.nodeId ? null : n)));\n\t\t\t\t\tthis.selection = this.selection.filter((id) => id !== t.nodeId);\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tthis.notify();\n\t}\n\n\t/** Types `text` over the selection of the text being edited (one undo step per session). */\n\ttextInput(text: string): VectorActionResult {\n\t\tconst t = this.textEdit;\n\t\tif (!t) return { ok: false, message: 'No text is being edited.' };\n\t\tconst a = Math.min(t.anchor, t.focus);\n\t\tconst b = Math.max(t.anchor, t.focus);\n\t\tconst clean = text.replace(/\\r\\n?/g, '\\n');\n\t\tconst r = this.setText(t.nodeId, clean, { start: a, end: b }, { merge: t.key });\n\t\tif (r.ok && this.textEdit) {\n\t\t\tthis.textEdit = { ...this.textEdit, anchor: a + clean.length, focus: a + clean.length };\n\t\t\tthis.notify();\n\t\t}\n\t\treturn r;\n\t}\n\n\t/** Deletes the selection, or one character (a whole surrogate pair) before or after the caret. */\n\tprivate textDelete(forward: boolean, word = false): void {\n\t\tconst t = this.textEdit;\n\t\tconst node = t ? this.getNode(t.nodeId) : null;\n\t\tif (!t || node?.type !== 'text') return;\n\t\tlet a = Math.min(t.anchor, t.focus);\n\t\tlet b = Math.max(t.anchor, t.focus);\n\t\tif (a === b) {\n\t\t\tif (forward) b = word ? wordEdge(node.text, b, 1) : nextIndex(node.text, b);\n\t\t\telse a = word ? wordEdge(node.text, a, -1) : prevIndex(node.text, a);\n\t\t}\n\t\tif (a === b) return;\n\t\tif (this.setText(t.nodeId, '', { start: a, end: b }, { merge: t.key }).ok && this.textEdit) {\n\t\t\tthis.textEdit = { ...this.textEdit, anchor: a, focus: a };\n\t\t\tthis.notify();\n\t\t}\n\t}\n\n\t/** Keys while text is being edited: caret movement, deleting, Enter, Escape and style shortcuts. */\n\tprivate textKey(input: VectorKeyInput): boolean {\n\t\tconst t = this.textEdit!;\n\t\tconst node = this.getNode(t.nodeId);\n\t\tif (node?.type !== 'text') {\n\t\t\tthis.textEdit = null;\n\t\t\treturn false;\n\t\t}\n\t\tconst text = node.text;\n\t\tconst key = input.key;\n\t\tconst move = (to: number) => {\n\t\t\tconst focus = Math.max(0, Math.min(text.length, to));\n\t\t\tthis.textEdit = { ...t, focus, anchor: input.shift ? t.anchor : focus };\n\t\t\tthis.notify();\n\t\t\treturn true;\n\t\t};\n\t\tconst a = Math.min(t.anchor, t.focus);\n\t\tconst b = Math.max(t.anchor, t.focus);\n\t\tconst collapsed = a === b;\n\t\tif (input.mod) {\n\t\t\tconst lower = key.toLowerCase();\n\t\t\tif (lower === 'a') {\n\t\t\t\tthis.textEdit = { ...t, anchor: 0, focus: text.length };\n\t\t\t\tthis.notify();\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tconst toggle = lower === 'b' ? 'bold' : lower === 'i' ? 'italic' : lower === 'u' ? 'underline' : null;\n\t\t\tif (toggle && !collapsed) {\n\t\t\t\tconst style = vectorTextStyleOf(node, a);\n\t\t\t\tconst patch = toggle === 'bold' ? { fontWeight: style.fontWeight >= 600 ? 400 : 700 } : toggle === 'italic' ? { italic: !style.italic } : { underline: !style.underline };\n\t\t\t\tthis.setTextStyle(node.id, patch, { start: a, end: b });\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (key === 'ArrowLeft' || key === 'ArrowRight' || key === 'ArrowUp' || key === 'ArrowDown') return move(key === 'ArrowLeft' || key === 'ArrowUp' ? 0 : text.length);\n\t\t\tif (key === 'Backspace' || key === 'Delete') {\n\t\t\t\tthis.textDelete(key === 'Delete', true);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\t// Undo, redo and the clipboard keep working; other shortcuts wait until editing ends.\n\t\t\treturn !['z', 'y', 'c', 'x', 'v'].includes(lower);\n\t\t}\n\t\tswitch (key) {\n\t\t\tcase 'Escape':\n\t\t\t\tthis.endTextEdit();\n\t\t\t\tif (this.getNode(t.nodeId)) this.select([t.nodeId]);\n\t\t\t\treturn true;\n\t\t\tcase 'Enter':\n\t\t\t\tif (input.alt || input.shift) return false;\n\t\t\t\tthis.textInput('\\n');\n\t\t\t\treturn true;\n\t\t\tcase 'Tab':\n\t\t\t\tthis.textInput('\\t');\n\t\t\t\treturn true;\n\t\t\tcase 'Backspace':\n\t\t\t\tthis.textDelete(false, !!input.alt);\n\t\t\t\treturn true;\n\t\t\tcase 'Delete':\n\t\t\t\tthis.textDelete(true, !!input.alt);\n\t\t\t\treturn true;\n\t\t\tcase 'ArrowLeft':\n\t\t\t\tif (!collapsed && !input.shift) return move(a);\n\t\t\t\treturn move(input.alt ? wordEdge(text, t.focus, -1) : prevIndex(text, t.focus));\n\t\t\tcase 'ArrowRight':\n\t\t\t\tif (!collapsed && !input.shift) return move(b);\n\t\t\t\treturn move(input.alt ? wordEdge(text, t.focus, 1) : nextIndex(text, t.focus));\n\t\t\tcase 'ArrowUp':\n\t\t\tcase 'ArrowDown':\n\t\t\t\treturn move(this.lineMove(node, t.focus, key === 'ArrowUp' ? -1 : 1));\n\t\t\tcase 'Home':\n\t\t\tcase 'End': {\n\t\t\t\tconst layout = layoutVectorText(node);\n\t\t\t\tconst line = layout.text.lines[vectorTextCaret(node, t.focus).line];\n\t\t\t\tif (!line) return move(key === 'Home' ? 0 : text.length);\n\t\t\t\treturn move(key === 'Home' ? line.start : line.end);\n\t\t\t}\n\t\t}\n\t\t// Printable characters normally arrive through `textInput` (the canvases' hidden textarea);\n\t\t// typed into the canvas itself, they go in here.\n\t\tif (key.length === 1) {\n\t\t\tthis.textInput(key);\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t/** The selected characters of the text being edited ('' when the selection is collapsed), or null when not editing. */\n\tgetSelectedText(): string | null {\n\t\tconst t = this.textEdit;\n\t\tconst node = t ? this.getNode(t.nodeId) : null;\n\t\tif (!t || node?.type !== 'text') return null;\n\t\treturn node.text.slice(Math.min(t.anchor, t.focus), Math.max(t.anchor, t.focus));\n\t}\n\n\t/** Cut while editing text: returns the selected characters and deletes them (null when not editing). */\n\tcutText(): string | null {\n\t\tconst text = this.getSelectedText();\n\t\tif (text) this.textInput('');\n\t\treturn text;\n\t}\n\n\t/** The index one line up or down from `index`, at the same x (start or end of the text past the first or last line). */\n\tprivate lineMove(node: VectorTextNode, index: number, dir: -1 | 1): number {\n\t\tconst layout = layoutVectorText(node);\n\t\tif (node.frame.kind === 'path') return dir < 0 ? 0 : node.text.length;\n\t\tconst c = vectorTextCaret(node, index);\n\t\tconst lines = layout.text.lines;\n\t\tconst target = lines[c.line + dir];\n\t\tif (!target) return dir < 0 ? 0 : node.text.length;\n\t\treturn vectorTextIndexAt(node, { x: c.top.x, y: layout.origin.y + target.baseline });\n\t}\n\n\t/** The topmost node of `type` under `p` (inside groups too, hit where it draws), or null. */\n\tprivate hitOfType<K extends 'instance'>(p: Vec2, type: K): Extract<VectorNode, { type: K }> | null {\n\t\tconst tol = this.tolerance();\n\t\tconst search = (list: readonly VectorNode[], parent: AffineMatrix): Extract<VectorNode, { type: K }> | null => {\n\t\t\tfor (let i = list.length - 1; i >= 0; i--) {\n\t\t\t\tconst n = list[i];\n\t\t\t\tif (n.visible === false || n.locked) continue;\n\t\t\t\tif (n.type === type) {\n\t\t\t\t\tif (hitTestVectorNode(n, p, tol, parent)) return n as Extract<VectorNode, { type: K }>;\n\t\t\t\t} else if (n.type === 'group' && hitTestVectorNode(n, p, tol, parent)) {\n\t\t\t\t\tconst found = search(n.children, multiplyMatrices(nodeMatrix(n), parent));\n\t\t\t\t\tif (found) return found;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn null;\n\t\t};\n\t\treturn search(this.doc.nodes, IDENTITY_MATRIX);\n\t}\n\n\t/** The topmost text node under `p` (inside groups too), or null. */\n\tprivate hitText(p: Vec2): VectorTextNode | null {\n\t\tconst tol = this.tolerance();\n\t\tconst search = (list: readonly VectorNode[], parent: AffineMatrix): VectorTextNode | null => {\n\t\t\tfor (let i = list.length - 1; i >= 0; i--) {\n\t\t\t\tconst n = list[i];\n\t\t\t\tif (n.visible === false || n.locked || n.blendStep) continue;\n\t\t\t\tif (n.type === 'group') {\n\t\t\t\t\tconst found = search(n.children, multiplyMatrices(nodeMatrix(n), parent));\n\t\t\t\t\tif (found) return found;\n\t\t\t\t} else if (n.type === 'text') {\n\t\t\t\t\t// Text is picked by its glyph boxes or (area text) its frame, so gaps between letters still hit.\n\t\t\t\t\tconst local = applyMatrix(invertMatrix(multiplyMatrices(nodeMatrix(n), parent)), p);\n\t\t\t\t\tconst b = layoutVectorText(n).bounds;\n\t\t\t\t\tif (b && local.x >= b.x - tol && local.x <= b.x + b.width + tol && local.y >= b.y - tol && local.y <= b.y + b.height + tol) {\n\t\t\t\t\t\tif (n.frame.kind !== 'path' || hitTestVectorNode(n, p, tol + 4, parent)) return n;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn null;\n\t\t};\n\t\treturn search(this.doc.nodes, IDENTITY_MATRIX);\n\t}\n\n\t/** A text node created for the edit session (no undo step until something is typed). */\n\tprivate startNewText(node: VectorTextNode, replace?: string): void {\n\t\tthis.endTextEdit();\n\t\tconst before = this.snapshot();\n\t\tif (replace) this.setDoc(mapTree(this.doc.nodes, (n) => (n.id === replace ? node : n)));\n\t\telse this.setDoc([...this.doc.nodes, node]);\n\t\tthis.selection = [node.id];\n\t\tthis.anchors = [];\n\t\tthis.textEdit = { nodeId: node.id, anchor: 0, focus: 0, before, key: `text-edit:${node.id}:${++this.textEditCount}`, created: true };\n\t\tthis.notify();\n\t}\n\n\tprivate downType(p: Vec2, input: VectorPointerInput) {\n\t\tconst text = this.hitText(p);\n\t\tif (text) {\n\t\t\tconst index = vectorTextIndexAt(text, this.toLocal(text.id, p));\n\t\t\tif (this.textEdit?.nodeId === text.id) {\n\t\t\t\tthis.textEdit = { ...this.textEdit, focus: index, anchor: input.shift ? this.textEdit.anchor : index };\n\t\t\t\tthis.notify();\n\t\t\t} else this.editText(text.id, { index });\n\t\t\tthis.drag = { kind: 'textSelect', nodeId: text.id };\n\t\t\tthis.interaction = { type: 'textSelect', nodeId: text.id };\n\t\t\treturn;\n\t\t}\n\t\tif (this.textEdit) {\n\t\t\t// A click outside the text being edited ends the session.\n\t\t\tthis.endTextEdit();\n\t\t\tif (this.tool === 'type') {\n\t\t\t\tthis.clearSelection();\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tif (this.tool === 'typePath') {\n\t\t\tconst path = this.hitPath(p);\n\t\t\tif (!path || path.subpaths[0]?.anchors.length < 2 || hiddenOrLocked(this.doc.nodes, path.id)) return;\n\t\t\t// The path's own space is the text's: its anchors become the text's path as they are.\n\t\t\tlet node: VectorTextNode;\n\t\t\ttry {\n\t\t\t\tnode = this.createTextNode({ text: '', path: path.subpaths[0] });\n\t\t\t} catch {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.startNewText({ ...node, ...(path.transform ? { transform: path.transform } : {}) }, path.id);\n\t\t\treturn;\n\t\t}\n\t\tthis.drag = { kind: 'textBox', start: p, moved: false };\n\t\tthis.interaction = { type: 'textBox', bounds: { x: p.x, y: p.y, width: 0, height: 0 } };\n\t\tthis.notify();\n\t}\n\n\tprivate upTypeBox(drag: Extract<Drag, { kind: 'textBox' }>) {\n\t\tconst p = this.pointer ?? drag.start;\n\t\tconst box = boundsFromPoints(drag.start, p);\n\t\tconst area = drag.moved && box.width > 2 && box.height > 2;\n\t\tconst node = this.createTextNode(area ? { text: '', x: box.x, y: box.y, width: box.width, height: box.height } : { text: '', x: drag.start.x, y: drag.start.y });\n\t\tthis.startNewText(node);\n\t}\n\n\n\t// -------------------------------------------------------------------------\n\t// Symbols and instances\n\t// -------------------------------------------------------------------------\n\n\t/** The whole document: in isolation mode, the host with the edited symbol's art. */\n\tprivate storedDocument(): VectorDocument {\n\t\tconst edit = this.symbolEdit;\n\t\tif (!edit) return this.doc;\n\t\tconst symbols = (edit.host.symbols ?? []).map((s) => (s.id === edit.symbolId ? { ...s, nodes: this.doc.nodes } : (this.doc.symbols?.find((x) => x.id === s.id) ?? s)));\n\t\treturn resolveVectorSymbols({ ...edit.host, symbols });\n\t}\n\n\tprivate symbolEditState(): VectorEditorState['symbolEdit'] {\n\t\tconst edit = this.symbolEdit;\n\t\tif (!edit) return null;\n\t\tconst host = this.storedDocument();\n\t\tconst sym = host.symbols?.find((s) => s.id === edit.symbolId);\n\t\t// The document in symbol space, lined up with the instance by its origin and scale (the art\n\t\t// shows unrotated and unskewed: a view cannot turn).\n\t\tconst hostTransform: VectorMatrix | null = edit.origin ? ([1 / edit.scale, 0, 0, 1 / edit.scale, -edit.origin.x / edit.scale + 0, -edit.origin.y / edit.scale + 0] as const) : null;\n\t\treturn { symbolId: edit.symbolId, name: sym?.name ?? edit.symbolId, instanceId: edit.instanceId, host, hostTransform };\n\t}\n\n\t/** The document's symbols (the stored ones, also while one is being edited). */\n\tgetSymbols(): readonly VectorSymbol[] {\n\t\treturn this.storedDocument().symbols ?? [];\n\t}\n\n\t/** Ids of the instances of symbol `symbolId` in the whole document (inside other symbols too). */\n\tgetSymbolInstances(symbolId: string): string[] {\n\t\tconst doc = this.storedDocument();\n\t\tconst out: string[] = [];\n\t\tconst visit = (list: readonly VectorNode[]) => {\n\t\t\tfor (const n of list) {\n\t\t\t\tif (n.type === 'instance' && n.symbolId === symbolId) out.push(n.id);\n\t\t\t\tif (n.type === 'group') visit(n.children);\n\t\t\t}\n\t\t};\n\t\tvisit(doc.nodes);\n\t\tfor (const s of doc.symbols ?? []) visit(s.nodes);\n\t\treturn out;\n\t}\n\n\tprivate newSymbolId(): string {\n\t\tconst used = new Set(this.getSymbols().map((s) => s.id));\n\t\tlet id = `symbol-${this.createId()}`;\n\t\twhile (used.has(id)) id = `symbol-${this.createId()}`;\n\t\treturn id;\n\t}\n\n\t/**\n\t * Make Symbol: turns the selected (or given) objects, which must share a parent, into a new\n\t * symbol whose registration point is their centre, and puts an instance in their place. One\n\t * undo step. Returns the symbol's and the instance's ids.\n\t */\n\tmakeSymbol(options: { ids?: readonly string[]; name?: string } = {}): VectorActionResult & { symbolId?: string; instanceId?: string } {\n\t\tconst ids = options.ids ?? this.selectedRoots();\n\t\tif (!ids.length) return { ok: false, message: 'Select the objects to make a symbol from.' };\n\t\tconst locs = ids.map((id) => locateVectorNode(this.doc.nodes, id));\n\t\tif (locs.some((l) => !l)) return { ok: false, message: `There is no node \"${ids[locs.findIndex((l) => !l)]}\".` };\n\t\tconst parent = locs[0]!.parent?.id ?? null;\n\t\tif (locs.some((l) => (l!.parent?.id ?? null) !== parent)) return { ok: false, message: 'The objects must share a parent (the same group) to make one symbol.' };\n\t\tif (ids.some((id) => hiddenOrLocked(this.doc.nodes, id))) return { ok: false, message: 'Unlock and show the objects first.' };\n\t\tconst parentMatrix = parent ? this.worldOf(parent) : IDENTITY_MATRIX;\n\t\tconst box = unionBounds(ids.map((id) => vectorNodeBounds(this.getNode(id)!, IDENTITY_MATRIX)));\n\t\tif (!box) return { ok: false, message: 'The objects have no geometry to make a symbol from.' };\n\t\tvoid parentMatrix;\n\t\tconst cx = box.x + box.width / 2;\n\t\tconst cy = box.y + box.height / 2;\n\t\tconst symbolId = this.newSymbolId();\n\t\tconst instanceId = this.newId();\n\t\tconst name = options.name?.trim() || `Symbol ${this.getSymbols().length + 1}`;\n\t\tconst set = new Set(ids);\n\t\tthis.change('Make Symbol', () => {\n\t\t\tconst replace = (list: readonly VectorNode[]): VectorNode[] => {\n\t\t\t\tconst members = list.filter((n) => set.has(n.id));\n\t\t\t\tconst top = Math.max(...members.map((m) => list.indexOf(m)));\n\t\t\t\tconst art = members.map((n) => transformVectorNode(n, translateMatrix(-cx, -cy)));\n\t\t\t\tthis.doc = { ...this.doc, symbols: [...(this.doc.symbols ?? []), { id: symbolId, name, nodes: art }] };\n\t\t\t\tconst out: VectorNode[] = [];\n\t\t\t\tlist.forEach((n, i) => {\n\t\t\t\t\tif (i === top) out.push({ id: instanceId, type: 'instance', name, symbolId, transform: translateMatrix(cx, cy) });\n\t\t\t\t\tif (!set.has(n.id)) out.push(n);\n\t\t\t\t});\n\t\t\t\treturn out;\n\t\t\t};\n\t\t\tif (parent === null) this.setDoc(replace(this.doc.nodes));\n\t\t\telse this.setDoc(mapTree(this.doc.nodes, (n) => (n.id === parent && n.type === 'group' ? { ...n, children: replace(n.children) } : n)));\n\t\t\tthis.selection = [instanceId];\n\t\t\tthis.anchors = [];\n\t\t});\n\t\treturn { ok: true, symbolId, instanceId };\n\t}\n\n\t/**\n\t * Places an instance of symbol `symbolId` with its registration point at (`x`, `y`) (default the\n\t * artboard's centre), or through `transform`. One undo step. Returns its id.\n\t */\n\tplaceSymbol(symbolId: string, options: { x?: number; y?: number; transform?: VectorMatrix; name?: string; parentId?: string } = {}): VectorActionResult & { id?: string } {\n\t\tconst sym = this.getSymbols().find((s) => s.id === symbolId);\n\t\tif (!sym) return { ok: false, message: `There is no symbol \"${symbolId}\"; vector.listSymbols lists them.` };\n\t\tif (this.symbolEdit && (symbolId === this.symbolEdit.symbolId || vectorSymbolUses(this.getSymbols(), symbolId, this.symbolEdit.symbolId))) {\n\t\t\treturn { ok: false, message: `\"${sym.name}\" would contain itself: a symbol cannot hold an instance of itself.` };\n\t\t}\n\t\tconst t = options.transform ?? translateMatrix(options.x ?? this.doc.width / 2, options.y ?? this.doc.height / 2);\n\t\tif (!t.every(Number.isFinite)) return { ok: false, message: 'transform must be six finite numbers [a, b, c, d, e, f].' };\n\t\tconst node: VectorInstanceNode = { id: this.newId(), type: 'instance', name: options.name ?? sym.name, symbolId, ...(isIdentityMatrix(t) ? {} : { transform: [...t] as unknown as VectorMatrix }) };\n\t\tconst id = this.addNode(node, { label: 'Place Symbol', ...(options.parentId ? { parentId: options.parentId } : {}) });\n\t\treturn { ok: true, id };\n\t}\n\n\tprivate instanceTargets(ids: readonly string[] | undefined): VectorInstanceNode[] | string {\n\t\tconst list = ids ?? this.selection;\n\t\tif (!list.length) return 'Select an instance.';\n\t\tconst out: VectorInstanceNode[] = [];\n\t\tfor (const id of list) {\n\t\t\tconst n = this.getNode(id);\n\t\t\tif (!n) return `There is no node \"${id}\".`;\n\t\t\tif (n.type !== 'instance') return `\"${id}\" is a ${n.type}, not a symbol instance.`;\n\t\t\tout.push(n);\n\t\t}\n\t\treturn out;\n\t}\n\n\t/**\n\t * Detach: replaces instances by groups of their symbol's art as drawn (placed, strokes scaled,\n\t * overrides applied), unlinked from the symbol. One undo step. Returns the groups' ids (the\n\t * instances' own).\n\t */\n\tdetachInstances(options: { ids?: readonly string[] } = {}): VectorActionResult & { ids?: string[] } {\n\t\tconst targets = this.instanceTargets(options.ids);\n\t\tif (typeof targets === 'string') return { ok: false, message: targets };\n\t\tconst next = new Map<string, VectorNode>();\n\t\tfor (const t of targets) {\n\t\t\tconst drawn = vectorInstanceDrawn(t);\n\t\t\tconst fresh = this.freshIds(drawn);\n\t\t\tnext.set(t.id, { ...fresh, id: t.id, name: t.name ?? 'Group' });\n\t\t}\n\t\tthis.change('Detach Instance', () => this.setDoc(mapTree(this.doc.nodes, (n) => next.get(n.id) ?? n)));\n\t\treturn { ok: true, ids: [...next.keys()] };\n\t}\n\n\t/** Replace Symbol: instances draw symbol `symbolId` instead, keeping their placement. One undo step. */\n\treplaceSymbol(symbolId: string, options: { ids?: readonly string[] } = {}): VectorActionResult {\n\t\tconst targets = this.instanceTargets(options.ids);\n\t\tif (typeof targets === 'string') return { ok: false, message: targets };\n\t\tconst sym = this.getSymbols().find((s) => s.id === symbolId);\n\t\tif (!sym) return { ok: false, message: `There is no symbol \"${symbolId}\"; vector.listSymbols lists them.` };\n\t\tif (this.symbolEdit && (symbolId === this.symbolEdit.symbolId || vectorSymbolUses(this.getSymbols(), symbolId, this.symbolEdit.symbolId))) return { ok: false, message: `\"${sym.name}\" would contain itself.` };\n\t\tconst set = new Set(targets.map((t) => t.id));\n\t\tthis.change('Replace Symbol', () =>\n\t\t\tthis.setDoc(\n\t\t\t\tmapTree(this.doc.nodes, (n) => {\n\t\t\t\t\tif (!set.has(n.id) || n.type !== 'instance') return n;\n\t\t\t\t\tconst { art: _a, ...rest } = n;\n\t\t\t\t\tconst hidden = n.overrides?.hidden;\n\t\t\t\t\t// Hidden art ids belong to the old symbol.\n\t\t\t\t\tconst overrides = n.overrides ? { ...n.overrides } : null;\n\t\t\t\t\tif (overrides && hidden) delete overrides.hidden;\n\t\t\t\t\treturn { ...rest, symbolId, ...(overrides && Object.keys(overrides).length ? { overrides } : { overrides: undefined }) } as VectorNode;\n\t\t\t\t})\n\t\t\t)\n\t\t);\n\t\treturn { ok: true };\n\t}\n\n\t/**\n\t * Instance options: `scaleStrokes` (strokes of the art scale with the instance), and overrides —\n\t * `fill` / `stroke` (every fill or stroke of the art in one paint; `null` clears), `hidden` (art\n\t * node ids not drawn). One undo step.\n\t */\n\tsetInstanceOptions(patch: { scaleStrokes?: boolean; fill?: VectorPaint | null; stroke?: VectorPaint | null; hidden?: string[] | null }, options: { ids?: readonly string[] } = {}): VectorActionResult {\n\t\tconst targets = this.instanceTargets(options.ids);\n\t\tif (typeof targets === 'string') return { ok: false, message: targets };\n\t\tconst next = new Map<string, VectorNode>();\n\t\tfor (const t of targets) {\n\t\t\tconst n: VectorInstanceNode = { ...t };\n\t\t\tif (patch.scaleStrokes !== undefined) {\n\t\t\t\tif (patch.scaleStrokes) delete n.scaleStrokes;\n\t\t\t\telse n.scaleStrokes = false;\n\t\t\t}\n\t\t\tconst o: VectorInstanceOverrides = { ...(t.overrides ?? {}) };\n\t\t\tfor (const key of ['fill', 'stroke'] as const) {\n\t\t\t\tif (patch[key] === undefined) continue;\n\t\t\t\tif (patch[key] === null) delete o[key];\n\t\t\t\telse o[key] = patch[key] as VectorPaint;\n\t\t\t}\n\t\t\tif (patch.hidden !== undefined) {\n\t\t\t\tif (patch.hidden?.length) {\n\t\t\t\t\tconst known = new Set<string>();\n\t\t\t\t\twalkVectorNodes(t.art ?? [], (a) => known.add(a.id));\n\t\t\t\t\tconst unknown = patch.hidden.filter((id) => !known.has(id));\n\t\t\t\t\tif (unknown.length) return { ok: false, message: `The symbol's art has no nodes ${unknown.map((u) => `\"${u}\"`).join(', ')}; vector.listSymbols lists its node ids.` };\n\t\t\t\t\to.hidden = [...patch.hidden];\n\t\t\t\t} else delete o.hidden;\n\t\t\t}\n\t\t\tif (Object.keys(o).length) n.overrides = o;\n\t\t\telse delete n.overrides;\n\t\t\tnext.set(t.id, n);\n\t\t}\n\t\tthis.change('Instance Options', () => this.setDoc(mapTree(this.doc.nodes, (n) => next.get(n.id) ?? n)));\n\t\treturn { ok: true };\n\t}\n\n\t/** Renames symbol `symbolId`. One undo step. */\n\trenameSymbol(symbolId: string, name: string): VectorActionResult {\n\t\tconst trimmed = name.trim();\n\t\tif (!trimmed) return { ok: false, message: 'A symbol needs a name.' };\n\t\tif (!this.getSymbols().some((s) => s.id === symbolId)) return { ok: false, message: `There is no symbol \"${symbolId}\".` };\n\t\tthis.change('Rename Symbol', () => {\n\t\t\tthis.doc = { ...this.doc, symbols: (this.doc.symbols ?? []).map((s) => (s.id === symbolId ? { ...s, name: trimmed } : s)) };\n\t\t\tif (this.symbolEdit) this.symbolEdit = { ...this.symbolEdit, host: { ...this.symbolEdit.host, symbols: (this.symbolEdit.host.symbols ?? []).map((s) => (s.id === symbolId ? { ...s, name: trimmed } : s)) } };\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/**\n\t * Deletes symbol `symbolId`. With instances in use it refuses unless `detach` (they become plain\n\t * groups first). One undo step.\n\t */\n\tdeleteSymbol(symbolId: string, options: { detach?: boolean } = {}): VectorActionResult {\n\t\tif (this.symbolEdit) return { ok: false, message: 'Leave the symbol being edited first (exitSymbolEdit).' };\n\t\tconst sym = this.getSymbols().find((s) => s.id === symbolId);\n\t\tif (!sym) return { ok: false, message: `There is no symbol \"${symbolId}\".` };\n\t\tconst used = this.getSymbolInstances(symbolId);\n\t\tif (used.length && !options.detach) return { ok: false, message: `\"${sym.name}\" has ${used.length} instance${used.length === 1 ? '' : 's'}; pass detach to turn them into groups, or delete them first.` };\n\t\tconst before = this.snapshot();\n\t\tconst detach = (list: VectorNode[]): VectorNode[] =>\n\t\t\tlist.map((n) => {\n\t\t\t\tif (n.type === 'instance' && n.symbolId === symbolId) return { ...this.freshIds(vectorInstanceDrawn(n)), id: n.id, name: n.name ?? 'Group' };\n\t\t\t\tif (n.type === 'group') return { ...n, children: detach(n.children) };\n\t\t\t\treturn n;\n\t\t\t});\n\t\tthis.doc = { ...this.doc, nodes: detach(this.doc.nodes), symbols: (this.doc.symbols ?? []).filter((s) => s.id !== symbolId).map((s) => ({ ...s, nodes: detach(s.nodes) })) };\n\t\tthis.commit('Delete Symbol', before);\n\t\treturn { ok: true };\n\t}\n\n\t/**\n\t * Edit Symbol (isolation mode): the document becomes the art of symbol `id` (or of instance `id`'s\n\t * symbol), in its own space; everything else draws dimmed behind it. Edits are ordinary undo\n\t * steps; `exitSymbolEdit` returns to the document with every instance updated.\n\t */\n\teditSymbol(id: string): VectorActionResult {\n\t\tconst node = this.getNode(id);\n\t\tconst symbolId = node?.type === 'instance' ? node.symbolId : id;\n\t\tconst host = this.storedDocument();\n\t\tconst sym = host.symbols?.find((s) => s.id === symbolId);\n\t\tif (!sym) return { ok: false, message: node ? `\"${id}\" is a ${node.type}, not a symbol instance.` : `There is no symbol or instance \"${id}\".` };\n\t\tif (this.symbolEdit) this.exitSymbolEdit();\n\t\tthis.endTextEdit();\n\t\tthis.finishPath();\n\t\tconst fresh = this.storedDocument();\n\t\tlet origin: Vec2 | null = null;\n\t\tlet scale = 1;\n\t\tconst world = node?.type === 'instance' ? vectorWorldMatrix(fresh.nodes, node.id) : null;\n\t\tif (world) {\n\t\t\tconst det = Math.abs(world[0] * world[3] - world[1] * world[2]);\n\t\t\tif (det > 1e-12) {\n\t\t\t\torigin = { x: world[4], y: world[5] };\n\t\t\t\tscale = Math.sqrt(det);\n\t\t\t}\n\t\t}\n\t\tthis.symbolEdit = { symbolId, instanceId: origin ? node!.id : null, host: fresh, origin, scale };\n\t\t// The view follows: the art's origin where the instance's was, at its scale.\n\t\tif (origin) this.viewport = this.viewInto(this.symbolEdit);\n\t\tthis.doc = { width: fresh.width, height: fresh.height, ...(fresh.background ? { background: fresh.background } : {}), nodes: sym.nodes, symbols: fresh.symbols };\n\t\tthis.selection = [];\n\t\tthis.anchors = [];\n\t\tthis.editingId = null;\n\t\tthis.notify();\n\t\treturn { ok: true };\n\t}\n\n\t/** The view inside isolation mode for the current (document) view. */\n\tprivate viewInto(edit: SymbolEditContext): VectorViewport {\n\t\tconst v = this.viewport;\n\t\tif (!edit.origin) return v;\n\t\treturn { x: v.zoom * edit.origin.x + v.x, y: v.zoom * edit.origin.y + v.y, zoom: v.zoom * edit.scale };\n\t}\n\n\t/** The document's view for the current view inside isolation mode. */\n\tprivate viewOutOf(edit: SymbolEditContext): VectorViewport {\n\t\tconst v = this.viewport;\n\t\tif (!edit.origin) return v;\n\t\tconst zoom = v.zoom / edit.scale;\n\t\treturn { x: v.x - zoom * edit.origin.x, y: v.y - zoom * edit.origin.y, zoom };\n\t}\n\n\t/** Leaves isolation mode: back to the whole document, the symbol's instances showing the edits. */\n\texitSymbolEdit(): void {\n\t\tif (!this.symbolEdit) return;\n\t\tthis.endTextEdit();\n\t\tthis.finishPath();\n\t\tconst { instanceId, origin } = this.symbolEdit;\n\t\t// Back to the document's view, where it was.\n\t\tif (origin) this.viewport = this.viewOutOf(this.symbolEdit);\n\t\tthis.doc = this.storedDocument();\n\t\tthis.symbolEdit = null;\n\t\tthis.selection = instanceId && this.getNode(instanceId) ? [instanceId] : [];\n\t\tthis.anchors = [];\n\t\tthis.editingId = null;\n\t\tthis.history.endGesture();\n\t\tthis.notify(true);\n\t}\n\n\n\t// -------------------------------------------------------------------------\n\t// Constraints\n\t// -------------------------------------------------------------------------\n\n\t/** The document's constraints. */\n\tgetConstraints(): readonly VectorConstraint[] {\n\t\treturn this.doc.constraints ?? [];\n\t}\n\n\t/** The document's named parameters. */\n\tgetParameters(): readonly VectorParameter[] {\n\t\treturn this.doc.parameters ?? [];\n\t}\n\n\t/** Degrees of freedom the constraints leave, and whether any are redundant. */\n\tgetConstraintDof(): VectorDofReport {\n\t\treturn vectorConstraintDof(this.doc);\n\t}\n\n\t/**\n\t * References from loose input to stable ones: anchors given by `subpath` / `index` get ids (the\n\t * path is updated in `nodes`). Throws on a missing path or anchor.\n\t */\n\tprivate resolveRefs(refs: readonly VectorConstraintRefInput[], nodes: Map<string, VectorPathNode>): VectorGeomRef[] {\n\t\treturn refs.map((r, i) => {\n\t\t\tconst node = nodes.get(r.nodeId) ?? (this.getNode(r.nodeId) as VectorNode | undefined);\n\t\t\tif (!node) throw new VectorConstraintError(`refs[${i}]: there is no node \"${r.nodeId}\".`);\n\t\t\tif (node.type !== 'path') throw new VectorConstraintError(`refs[${i}]: \"${r.nodeId}\" is a ${node.type}; constraints refer to paths.`);\n\t\t\tif (r.circle) return { nodeId: r.nodeId, circle: true };\n\t\t\tlet anchor = r.anchor;\n\t\t\tif (anchor === undefined) {\n\t\t\t\tif (r.subpath === undefined || r.index === undefined) throw new VectorConstraintError(`refs[${i}]: give an anchor (subpath and index, or its id), or circle: true.`);\n\t\t\t\tconst sp = node.subpaths[r.subpath];\n\t\t\t\tif (!sp?.anchors[r.index]) throw new VectorConstraintError(`refs[${i}]: \"${r.nodeId}\" has no anchor ${r.index} on subpath ${r.subpath}.`);\n\t\t\t\tconst withIds = withVectorAnchorIds(node);\n\t\t\t\tnodes.set(node.id, withIds);\n\t\t\t\tanchor = withIds.subpaths[r.subpath].anchors[r.index].id!;\n\t\t\t} else if (!findVectorAnchorById(node, anchor)) throw new VectorConstraintError(`refs[${i}]: \"${r.nodeId}\" has no anchor \"${anchor}\".`);\n\t\t\treturn { nodeId: r.nodeId, anchor, ...(r.handle ? { handle: r.handle } : {}), ...(r.segment ? { segment: true } : {}) };\n\t\t});\n\t}\n\n\tprivate newConstraintId(): string {\n\t\tconst used = new Set((this.doc.constraints ?? []).map((c) => c.id));\n\t\tlet id = `k${used.size + 1}`;\n\t\tfor (let n = used.size + 1; used.has(id); n++) id = `k${n}`;\n\t\treturn id;\n\t}\n\n\t/**\n\t * Adds a constraint: refs by anchor (`subpath` / `index`, or its id), handle, segment or circle;\n\t * dimensional kinds take a `value` expression (default: what the geometry measures now) and a\n\t * `name`. Refuses (changing nothing) one that conflicts with the others or repeats them, naming\n\t * them. One undo step. Returns the new constraint's id.\n\t */\n\taddConstraint(input: VectorConstraintInput): VectorActionResult & { id?: string; ids?: string[] } {\n\t\tconst nodes = new Map<string, VectorPathNode>();\n\t\tlet refs: VectorGeomRef[];\n\t\ttry {\n\t\t\trefs = this.resolveRefs(input.refs, nodes);\n\t\t} catch (e) {\n\t\t\treturn { ok: false, message: e instanceof Error ? e.message : String(e) };\n\t\t}\n\t\tconst id = this.newConstraintId();\n\t\tconst withIds = nodes.size ? { ...this.doc, nodes: mapTree(this.doc.nodes, (n) => nodes.get(n.id) ?? n) } : this.doc;\n\t\tlet next: VectorDocument;\n\t\ttry {\n\t\t\tnext = addVectorConstraint(withIds, {\n\t\t\t\tid,\n\t\t\t\tkind: input.kind,\n\t\t\t\trefs,\n\t\t\t\t...(input.value !== undefined ? { value: String(input.value) } : {}),\n\t\t\t\t...(input.name ? { name: input.name } : {}),\n\t\t\t\t...(input.axis && input.axis !== 'aligned' ? { axis: input.axis } : {})\n\t\t\t});\n\t\t} catch (e) {\n\t\t\tif (e instanceof VectorConstraintError) return { ok: false, message: e.message, ids: e.ids };\n\t\t\tthrow e;\n\t\t}\n\t\tthis.change(`Add ${input.kind[0].toUpperCase()}${input.kind.slice(1)} Constraint`, () => {\n\t\t\tthis.doc = next;\n\t\t\tthis.lastSolved = next;\n\t\t});\n\t\treturn { ok: true, id };\n\t}\n\n\t/** Removes constraints by id. One undo step. */\n\tremoveConstraints(ids: readonly string[]): VectorActionResult {\n\t\tconst set = new Set(ids);\n\t\tconst missing = ids.filter((id) => !(this.doc.constraints ?? []).some((c) => c.id === id));\n\t\tif (missing.length) return { ok: false, message: `There is no constraint ${missing.map((m) => `\"${m}\"`).join(', ')}.` };\n\t\tthis.change(ids.length === 1 ? 'Remove Constraint' : 'Remove Constraints', () => {\n\t\t\tconst kept = (this.doc.constraints ?? []).filter((c) => !set.has(c.id));\n\t\t\tthis.doc = { ...this.doc, constraints: kept };\n\t\t\tif (!kept.length) delete this.doc.constraints;\n\t\t});\n\t\treturn { ok: true };\n\t}\n\n\t/**\n\t * Sets a dimensional constraint's value expression (by id or name), or a named parameter's\n\t * (creating it; `null` removes a parameter), then solves. Refuses (changing nothing) a value\n\t * that does not evaluate or that the constraints cannot meet. One undo step.\n\t */\n\tsetConstraintValue(key: string, value: string | number | null, options: { merge?: string } = {}): VectorActionResult & { ids?: string[] } {\n\t\tconst text = value === null ? null : String(value).trim();\n\t\tconst cons = this.doc.constraints ?? [];\n\t\tconst c = cons.find((x) => x.id === key || x.name === key);\n\t\tlet trial: VectorDocument;\n\t\tif (c) {\n\t\t\tif (!VECTOR_DIMENSIONAL_KINDS.has(c.kind)) return { ok: false, message: `\"${key}\" is a ${c.kind} constraint, which has no value.` };\n\t\t\tif (text === null || text === '') return { ok: false, message: 'A dimensional constraint needs a value; remove the constraint instead.' };\n\t\t\ttrial = { ...this.doc, constraints: cons.map((x) => (x === c ? { ...x, value: text } : x)) };\n\t\t} else {\n\t\t\tif (!isValidVectorParameterName(key)) return { ok: false, message: `\"${key}\" is not a constraint or a valid parameter name (a letter or _, then letters, digits or _).` };\n\t\t\tconst params = this.doc.parameters ?? [];\n\t\t\tif (text === null) {\n\t\t\t\tif (!params.some((p) => p.name === key)) return { ok: false, message: `There is no parameter \"${key}\".` };\n\t\t\t\ttrial = { ...this.doc, parameters: params.filter((p) => p.name !== key) };\n\t\t\t} else {\n\t\t\t\ttrial = { ...this.doc, parameters: params.some((p) => p.name === key) ? params.map((p) => (p.name === key ? { ...p, value: text } : p)) : [...params, { name: key, value: text }] };\n\t\t\t}\n\t\t}\n\t\ttry {\n\t\t\tif (text !== null) checkVectorExpression(text);\n\t\t\t// Every dimensional constraint must still evaluate.\n\t\t\tconst env = vectorConstraintEnv(trial);\n\t\t\tfor (const x of trial.constraints ?? []) if (VECTOR_DIMENSIONAL_KINDS.has(x.kind)) (x.name && env.has(x.name) ? env.value(x.name) : env.eval(x.value ?? ''));\n\t\t} catch (e) {\n\t\t\treturn { ok: false, message: `${key}: ${e instanceof Error ? e.message : String(e)}` };\n\t\t}\n\t\tconst r = solveVectorConstraints(trial, null);\n\t\tif (!r.converged) return { ok: false, message: `The constraints cannot meet ${key} = ${text}: it conflicts with ${[...new Set(r.conflicts)].map((i) => `\"${i}\"`).join(', ')}.`, ids: r.conflicts };\n\t\tthis.change(c ? 'Constraint Value' : 'Parameter', () => {\n\t\t\tthis.doc = r.document;\n\t\t\tthis.lastSolved = r.document;\n\t\t}, options.merge);\n\t\treturn { ok: true };\n\t}\n\n\t/**\n\t * Auto-constrain: constraints the paths `ids` (default the selection, or every path) nearly\n\t * satisfy, added one by one (redundant or conflicting ones skipped). One undo step. Returns the\n\t * ids of the constraints added.\n\t */\n\tautoConstrain(options: { ids?: readonly string[] } & VectorInferOptions = {}): VectorActionResult & { ids?: string[] } {\n\t\tconst { ids, ...infer } = options;\n\t\tconst roots = ids ?? (this.selection.length ? this.selection : null);\n\t\tif (roots) for (const id of roots) if (!this.getNode(id)) return { ok: false, message: `There is no node \"${id}\".` };\n\t\tconst used = new Set((this.doc.constraints ?? []).map((c) => c.id));\n\t\tlet n = used.size;\n\t\tconst newId = () => {\n\t\t\tlet id = `k${++n}`;\n\t\t\twhile (used.has(id)) id = `k${++n}`;\n\t\t\tused.add(id);\n\t\t\treturn id;\n\t\t};\n\t\tconst pathIds = roots ? roots.flatMap((id) => collectPathNodes(this.getNode(id)!).map((p) => p.id)) : null;\n\t\tconst r = autoConstrainVector(this.doc, pathIds, newId, infer);\n\t\tif (!r.added.length) return { ok: true, ids: [] };\n\t\tthis.change('Auto Constrain', () => {\n\t\t\tthis.doc = r.document;\n\t\t\tthis.lastSolved = r.document;\n\t\t});\n\t\treturn { ok: true, ids: r.added.map((c) => c.id) };\n\t}\n\n\t// -------------------------------------------------------------------------\n\t// Keyboard input\n\t// -------------------------------------------------------------------------\n\n\t/**\n\t * Handles editor shortcuts. Returns true when the key was used (call `preventDefault`).\n\t * Do not forward keys typed into text inputs.\n\t */\n\tkeyDown(input: VectorKeyInput): boolean {\n\t\tif (this.textEdit && this.textKey(input)) return true;\n\t\tif (this.typedKey(input)) return true;\n\t\tconst key = input.key;\n\t\tconst lower = key.length === 1 ? key.toLowerCase() : key;\n\t\tif (key === ' ') {\n\t\t\tif (!this.spaceHeld) {\n\t\t\t\tthis.spaceHeld = true;\n\t\t\t\tthis.notify();\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\t\tif (input.mod) {\n\t\t\tswitch (lower) {\n\t\t\t\tcase 'z':\n\t\t\t\t\tinput.shift ? this.redo() : this.undo();\n\t\t\t\t\treturn true;\n\t\t\t\tcase 'y':\n\t\t\t\t\tthis.redo();\n\t\t\t\t\treturn true;\n\t\t\t\tcase 'a':\n\t\t\t\t\tthis.selectAll();\n\t\t\t\t\treturn true;\n\t\t\t\tcase 'd':\n\t\t\t\t\tthis.duplicate();\n\t\t\t\t\treturn true;\n\t\t\t\tcase 'g':\n\t\t\t\t\tinput.shift ? this.ungroup() : this.group();\n\t\t\t\t\treturn true;\n\t\t\t\tcase 'j':\n\t\t\t\t\treturn input.alt ? this.averageAnchors('both').ok : this.joinPaths().ok;\n\t\t\t\t// Make Clipping Mask; with Alt, Release (Option-7 types ¶ on a Mac).\n\t\t\t\tcase '7':\n\t\t\t\tcase '¶':\n\t\t\t\t\treturn input.alt || key === '¶' ? this.releaseMask().length > 0 : this.makeMask('clip').ok;\n\t\t\t\tcase ']':\n\t\t\t\t\tthis.arrange(input.alt ? 'front' : 'forward');\n\t\t\t\t\treturn true;\n\t\t\t\tcase '[':\n\t\t\t\t\tthis.arrange(input.alt ? 'back' : 'backward');\n\t\t\t\t\treturn true;\n\t\t\t\tcase '=':\n\t\t\t\tcase '+':\n\t\t\t\t\tthis.setViewport({ zoom: this.viewport.zoom * 1.25 });\n\t\t\t\t\treturn true;\n\t\t\t\tcase '-':\n\t\t\t\t\tthis.setViewport({ zoom: this.viewport.zoom / 1.25 });\n\t\t\t\t\treturn true;\n\t\t\t\tcase '0':\n\t\t\t\t\tthis.setViewport({ zoom: 1 });\n\t\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t\tswitch (key) {\n\t\t\tcase 'Escape':\n\t\t\t\tif (this.drag) this.cancelDrag();\n\t\t\t\telse if (this.trackingPoints.length) this.clearTracking();\n\t\t\t\telse if (this.widthEdit) this.closeWidthPointEdit();\n\t\t\t\telse if (this.tool === 'width' && this.widthPoints.length) {\n\t\t\t\t\tthis.widthPoints = [];\n\t\t\t\t\tthis.notify();\n\t\t\t\t}\n\t\t\t\telse if (this.pen) this.finishPath();\n\t\t\t\telse if (this.blendStart) {\n\t\t\t\t\tthis.blendStart = null;\n\t\t\t\t\tthis.notify();\n\t\t\t\t} else if (this.tool === 'direct' && this.anchors.length) {\n\t\t\t\t\tthis.anchors = [];\n\t\t\t\t\tthis.notify();\n\t\t\t\t} else if (this.tool !== 'select') this.setTool('select');\n\t\t\t\telse if (this.symbolEdit && !this.selection.length) this.exitSymbolEdit();\n\t\t\t\telse this.clearSelection();\n\t\t\t\treturn true;\n\t\t\tcase 'Enter':\n\t\t\t\tif (this.pen) {\n\t\t\t\t\tthis.finishPath();\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\tif (this.tool === 'select' && this.selection.length === 1 && this.getNode(this.selection[0])?.type === 'path') {\n\t\t\t\t\tthis.tool = 'direct';\n\t\t\t\t\tthis.editingId = this.selection[0];\n\t\t\t\t\tthis.notify();\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\tcase 'Delete':\n\t\t\tcase 'Backspace':\n\t\t\t\tif (this.tool === 'width' && this.widthPoints.length) return this.removeWidthPoints().ok;\n\t\t\t\tif (this.tool === 'gradient' && this.gradientEdit.stop !== null) return this.removeGradientStop(this.gradientEdit.stop).ok;\n\t\t\t\tif (this.pen) {\n\t\t\t\t\tconst node = this.getNode(this.pen.nodeId) as VectorPathNode;\n\t\t\t\t\tconst sp = node.subpaths[this.pen.subpath];\n\t\t\t\t\tif (sp.anchors.length > 2) this.deleteAnchors([{ nodeId: node.id, subpath: this.pen.subpath, index: sp.anchors.length - 1 }]);\n\t\t\t\t\telse {\n\t\t\t\t\t\tthis.removeNodes([node.id], { label: 'Delete Path' });\n\t\t\t\t\t\tthis.pen = null;\n\t\t\t\t\t}\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\treturn this.deleteSelection().ok;\n\t\t\tcase 'ArrowLeft':\n\t\t\tcase 'ArrowRight':\n\t\t\tcase 'ArrowUp':\n\t\t\tcase 'ArrowDown': {\n\t\t\t\tconst step = input.shift ? 10 : 1;\n\t\t\t\tconst dx = key === 'ArrowLeft' ? -step : key === 'ArrowRight' ? step : 0;\n\t\t\t\tconst dy = key === 'ArrowUp' ? -step : key === 'ArrowDown' ? step : 0;\n\t\t\t\treturn this.translate(dx, dy).ok;\n\t\t\t}\n\t\t}\n\t\tif (key.length === 1 && !input.alt) {\n\t\t\tconst tool = TOOL_KEYS[lower];\n\t\t\tif (tool) {\n\t\t\t\tthis.setTool(tool);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\tkeyUp(input: VectorKeyInput): void {\n\t\tif (input.key === ' ' && this.spaceHeld) {\n\t\t\tthis.spaceHeld = false;\n\t\t\tthis.notify();\n\t\t}\n\t}\n}\n\n/** \"Rectangle\", \"Square\", \"Ellipse\", \"Circle\", \"Line\", \"Triangle\" or \"Polygon\". */\nfunction inkShapeLabel(shape: VectorInkShape): string {\n\tconst name = vectorInkShapeName(shape);\n\treturn name[0].toUpperCase() + name.slice(1);\n}\n\n/** The subpath of a recognised pencil shape. */\nfunction inkShapeSubpath(shape: VectorInkShape): VectorSubpath {\n\tswitch (shape.kind) {\n\t\tcase 'line':\n\t\t\treturn lineSubpath({ x: shape.a[0], y: shape.a[1] }, { x: shape.b[0], y: shape.b[1] });\n\t\tcase 'rect':\n\t\t\treturn rectSubpath(shape);\n\t\tcase 'ellipse':\n\t\t\treturn ellipseSubpath(shape);\n\t\tcase 'polygon':\n\t\t\treturn { anchors: shape.points.map(([x, y]) => ({ x, y })), closed: true };\n\t}\n}\n\n/** Snaps `p` to the nearest 45° direction from `origin`, keeping its distance. */\nfunction constrain45(origin: Vec2, p: Vec2): Vec2 {\n\tconst dx = p.x - origin.x;\n\tconst dy = p.y - origin.y;\n\tconst len = Math.hypot(dx, dy);\n\tconst a = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);\n\treturn { x: origin.x + Math.cos(a) * len, y: origin.y + Math.sin(a) * len };\n}\n\n/** Scale factors and origin for dragging a selection-box handle to `p`. */\nexport function scaleFor(handle: VectorTransformHandle, b: VectorBounds, p: Vec2, mods: VectorModifiers = {}): { sx: number; sy: number; origin: Vec2 } {\n\tconst west = handle.includes('w');\n\tconst east = handle.includes('e');\n\tconst north = handle.startsWith('n');\n\tconst south = handle.startsWith('s');\n\tconst center = { x: b.x + b.width / 2, y: b.y + b.height / 2 };\n\tconst origin = mods.alt ? center : { x: west ? b.x + b.width : east ? b.x : center.x, y: north ? b.y + b.height : south ? b.y : center.y };\n\tconst reachX = mods.alt ? b.width / 2 : b.width;\n\tconst reachY = mods.alt ? b.height / 2 : b.height;\n\tlet sx = west || east ? (reachX > 1e-9 ? ((p.x - origin.x) * (west ? -1 : 1)) / reachX : 1) : 1;\n\tlet sy = north || south ? (reachY > 1e-9 ? ((p.y - origin.y) * (north ? -1 : 1)) / reachY : 1) : 1;\n\tif (mods.shift) {\n\t\tif ((west || east) && (north || south)) {\n\t\t\tconst s = Math.max(Math.abs(sx), Math.abs(sy));\n\t\t\tsx = Math.sign(sx || 1) * s;\n\t\t\tsy = Math.sign(sy || 1) * s;\n\t\t} else if (west || east) sy = Math.abs(sx);\n\t\telse sx = Math.abs(sy);\n\t}\n\treturn { sx, sy, origin };\n}\n\n/** A style applied to a path; a `null` (or empty) width profile removes it. */\nfunction withStyle(node: VectorPathNode, style: VectorStyle & { opacity?: number }): VectorNode {\n\tconst { fills, strokes, ...shorthand } = style;\n\tlet next = { ...node, ...shorthand } as VectorPathNode;\n\tif ('strokeWidthProfile' in style) {\n\t\tconst p = style.strokeWidthProfile ? sanitizeWidthProfile(style.strokeWidthProfile) : [];\n\t\tif (p.length) next.strokeWidthProfile = p;\n\t\telse delete next.strokeWidthProfile;\n\t}\n\t// Shorthands edit the top layer of a stack; whole stacks given replace them.\n\tnext = normalizeVectorPaintStacks(next, 'shorthand');\n\tif (fills !== undefined) next = withVectorFills(next, sanitizeVectorFills(fills ?? []));\n\tif (strokes !== undefined) next = withVectorStrokes(next, sanitizeVectorStrokes(strokes ?? []));\n\treturn next;\n}\n\n\n/** The resolved character style at `index` of a text node. */\nfunction vectorTextStyleOf(node: VectorTextNode, index: number): VectorCharacterStyle {\n\tlet at = 0;\n\tfor (const r of node.runs ?? []) {\n\t\tif (index < at + r.length) {\n\t\t\tconst { fill: _f, ...fields } = r.style;\n\t\t\treturn { ...node.font, ...fields };\n\t\t}\n\t\tat += r.length;\n\t}\n\treturn node.font;\n}\n\nconst isLowSurrogate = (c: number) => c >= 0xdc00 && c <= 0xdfff;\nconst isHighSurrogate = (c: number) => c >= 0xd800 && c <= 0xdbff;\n/** The index after the character at `i` (a surrogate pair moves two). */\nfunction nextIndex(text: string, i: number): number {\n\tif (i >= text.length) return text.length;\n\treturn isHighSurrogate(text.charCodeAt(i)) && isLowSurrogate(text.charCodeAt(i + 1)) ? i + 2 : i + 1;\n}\n/** The index before the character ending at `i`. */\nfunction prevIndex(text: string, i: number): number {\n\tif (i <= 0) return 0;\n\treturn isLowSurrogate(text.charCodeAt(i - 1)) && isHighSurrogate(text.charCodeAt(i - 2)) ? i - 2 : i - 1;\n}\nconst isWordChar = (c: string) => /[\\p{L}\\p{N}_'’]/u.test(c);\n/** The next word boundary from `i` in direction `dir` (spaces, then a word). */\nfunction wordEdge(text: string, i: number, dir: 1 | -1): number {\n\tlet j = i;\n\tif (dir > 0) {\n\t\twhile (j < text.length && !isWordChar(text[j])) j++;\n\t\twhile (j < text.length && isWordChar(text[j])) j++;\n\t} else {\n\t\twhile (j > 0 && !isWordChar(text[j - 1])) j--;\n\t\twhile (j > 0 && isWordChar(text[j - 1])) j--;\n\t}\n\treturn j;\n}\n/** The word around `i` as [start, end) (a single character when it is not part of a word). */\nfunction wordAt(text: string, i: number): [number, number] {\n\tconst at = Math.min(i, text.length - 1);\n\tif (at < 0) return [0, 0];\n\tif (!isWordChar(text[at])) return [at, at + 1];\n\tlet a = at;\n\tlet b = at;\n\twhile (a > 0 && isWordChar(text[a - 1])) a--;\n\twhile (b < text.length && isWordChar(text[b])) b++;\n\treturn [a, b];\n}\n\n/** SVG path data or Bézier commands read loosely (bad data gives an empty path). */\nfunction toBezierPathLoose(d: string): BezierPath {\n\ttry {\n\t\treturn subpathsToBezier(bezierToSubpaths(d), { corners: false });\n\t} catch {\n\t\treturn [];\n\t}\n}\n\n/** True when node `id` or one of its ancestors is locked or hidden. */\nfunction hiddenOrLocked(nodes: readonly VectorNode[], id: string): boolean {\n\tconst loc = locateVectorNode(nodes as VectorNode[], id);\n\tif (!loc) return false;\n\tconst n = loc.list[loc.index];\n\tif (n.locked || n.visible === false) return true;\n\treturn loc.parent ? hiddenOrLocked(nodes, loc.parent.id) : false;\n}\n\n/** The width profile of pencil samples and their pressures (positions by length along the samples). */\nfunction pencilPressureProfile(points: readonly Point[], pressures: readonly number[]): VectorWidthPoint[] {\n\tconst n = Math.min(points.length, pressures.length);\n\tif (n < 2) return [];\n\tconst s = [0];\n\tfor (let i = 1; i < n; i++) s.push(s[i - 1] + Math.hypot(points[i][0] - points[i - 1][0], points[i][1] - points[i - 1][1]));\n\tconst total = s[n - 1];\n\tif (!(total > 0)) return [];\n\treturn pressureWidthProfile(s.map((d, i) => ({ t: d / total, pressure: pressures[i] })));\n}\n","/**\n * Where to draw constraint glyphs (document space), the constraints panel's data and the actions the\n * current selection allows. Framework-neutral, so React and Svelte draw the same thing.\n */\n\nimport { applyMatrix, multiplyMatrices, IDENTITY_MATRIX } from '../../geom/affine.js';\nimport type { AffineMatrix } from '../../geom/types.js';\nimport { findVectorAnchorById, vectorConstraintDof, vectorConstraintEnv, vectorConstraintResiduals, vectorConstraintValue, vectorPathCircle, VECTOR_DIMENSIONAL_KINDS, type VectorDofReport } from './solve.js';\nimport type { Vec2, VectorAnchorRef, VectorConstraint, VectorConstraintKind, VectorDocument, VectorGeomRef, VectorNode, VectorPathNode } from '../types.js';\n\n/** Short glyphs per kind. */\nexport const VECTOR_CONSTRAINT_GLYPHS: Readonly<Record<VectorConstraintKind, string>> = {\n\tcoincident: '●',\n\tcollinear: '≡',\n\tconcentric: '◎',\n\tfix: '⚓',\n\tparallel: '∥',\n\tperpendicular: '⊥',\n\thorizontal: 'H',\n\tvertical: 'V',\n\ttangent: 'T',\n\tsmooth: 'G2',\n\tsymmetric: '⇹',\n\tequal: '=',\n\tdistance: '↔',\n\tangle: '∠',\n\tradius: 'R',\n\tdiameter: 'Ø'\n};\n\nexport const VECTOR_CONSTRAINT_LABELS: Readonly<Record<VectorConstraintKind, string>> = {\n\tcoincident: 'Coincident',\n\tcollinear: 'Collinear',\n\tconcentric: 'Concentric',\n\tfix: 'Fix',\n\tparallel: 'Parallel',\n\tperpendicular: 'Perpendicular',\n\thorizontal: 'Horizontal',\n\tvertical: 'Vertical',\n\ttangent: 'Tangent',\n\tsmooth: 'Smooth (G2)',\n\tsymmetric: 'Symmetric',\n\tequal: 'Equal',\n\tdistance: 'Distance',\n\tangle: 'Angle',\n\tradius: 'Radius',\n\tdiameter: 'Diameter'\n};\n\n/** A constraint's glyph in document space. */\nexport interface VectorConstraintGlyph {\n\tid: string;\n\tkind: VectorConstraintKind;\n\t/** The glyph, or `name = value` for dimensional constraints. */\n\tlabel: string;\n\t/** Where to draw it (near the middle of what it refers to). */\n\tat: Vec2;\n\t/** The points it refers to (for a highlight). */\n\tpoints: Vec2[];\n\t/** A dimension line between two points (distances). */\n\tdimension: { a: Vec2; b: Vec2 } | null;\n\tsatisfied: boolean;\n}\n\nfunction worlds(doc: VectorDocument): Map<string, { node: VectorPathNode; world: AffineMatrix }> {\n\tconst out = new Map<string, { node: VectorPathNode; world: AffineMatrix }>();\n\tconst visit = (list: readonly VectorNode[], parent: AffineMatrix) => {\n\t\tfor (const n of list) {\n\t\t\tconst m = n.transform ? multiplyMatrices(n.transform as AffineMatrix, parent) : parent;\n\t\t\tif (n.type === 'path') out.set(n.id, { node: n, world: m });\n\t\t\telse if (n.type === 'group') visit(n.children, m);\n\t\t}\n\t};\n\tvisit(doc.nodes, IDENTITY_MATRIX);\n\treturn out;\n}\n\n/** The document-space points a reference stands for (a point, a segment's ends, a circle's centre). */\nfunction refPoints(ref: VectorGeomRef, located: ReturnType<typeof worlds>): Vec2[] {\n\tconst loc = located.get(ref.nodeId);\n\tif (!loc) return [];\n\tif (ref.circle) {\n\t\tconst c = vectorPathCircle(loc.node, loc.world);\n\t\treturn c ? [c.c] : [];\n\t}\n\tif (!ref.anchor) return [];\n\tconst at = findVectorAnchorById(loc.node, ref.anchor);\n\tif (!at) return [];\n\tconst sp = loc.node.subpaths[at.subpath];\n\tconst a = sp.anchors[at.index];\n\tconst p = ref.handle ? (a[ref.handle] ?? a) : a;\n\tconst first = applyMatrix(loc.world, p);\n\tif (!ref.segment) return [first];\n\tconst next = sp.anchors[(at.index + 1) % sp.anchors.length];\n\treturn [first, applyMatrix(loc.world, next)];\n}\n\n/** Glyphs of the constraints that touch `nodeIds` (all when null), document space. */\nexport function vectorConstraintGlyphs(doc: VectorDocument, nodeIds: ReadonlySet<string> | null = null): VectorConstraintGlyph[] {\n\tconst cons = (doc.constraints ?? []).filter((c) => !nodeIds || c.refs.some((r) => nodeIds.has(r.nodeId)));\n\tif (!cons.length) return [];\n\tconst located = worlds(doc);\n\tconst residuals = vectorConstraintResiduals(doc);\n\tconst env = vectorConstraintEnv(doc);\n\tconst scale = Math.max(doc.width, doc.height, 1);\n\treturn cons.flatMap((c): VectorConstraintGlyph[] => {\n\t\tconst groups = c.refs.map((r) => refPoints(r, located));\n\t\tconst points = groups.flat();\n\t\tif (!points.length) return [];\n\t\tconst mids = groups.map((g) => (g.length === 2 ? { x: (g[0].x + g[1].x) / 2, y: (g[0].y + g[1].y) / 2 } : g[0])).filter(Boolean);\n\t\tconst at = { x: mids.reduce((s, p) => s + p.x, 0) / mids.length, y: mids.reduce((s, p) => s + p.y, 0) / mids.length };\n\t\tlet label = VECTOR_CONSTRAINT_GLYPHS[c.kind];\n\t\tif (VECTOR_DIMENSIONAL_KINDS.has(c.kind)) {\n\t\t\tlet v: number | null = null;\n\t\t\ttry {\n\t\t\t\tv = vectorConstraintValue(env, c);\n\t\t\t} catch {\n\t\t\t\tv = null;\n\t\t\t}\n\t\t\tconst shown = v === null ? '?' : `${Math.round(v * 100) / 100}${c.kind === 'angle' ? '°' : ''}`;\n\t\t\tlabel = `${c.name ?? label} = ${shown}`;\n\t\t}\n\t\tconst r = residuals.get(c.id);\n\t\tconst dimension = c.kind === 'distance' && points.length === 2 ? { a: points[0], b: points[1] } : null;\n\t\treturn [{ id: c.id, kind: c.kind, label, at, points, dimension, satisfied: r !== null && r !== undefined && r <= 1e-6 * scale }];\n\t});\n}\n\n/** A constraint the selection allows, with its references ready for `addConstraint`. */\nexport interface VectorConstraintAction {\n\tkind: VectorConstraintKind;\n\tlabel: string;\n\trefs: { nodeId: string; subpath?: number; index?: number; segment?: boolean; circle?: boolean }[];\n\t/** Dimensional: it asks for a value (the current measure is the default). */\n\tdimensional: boolean;\n}\n\n/**\n * Constraints the selected anchors (direct tool) or paths allow: one anchor (fix, tangent and\n * smooth at it), two anchors (a segment when they are neighbours: horizontal, vertical, length,\n * fix; else coincident, horizontal, vertical, distance), three (a point and a segment: on its\n * line), four as two segments (parallel, perpendicular, equal, collinear, angle); one or two round\n * paths (radius, diameter, fix; concentric, equal, tangent).\n */\nexport function vectorConstraintActions(doc: VectorDocument, selection: { anchors: readonly VectorAnchorRef[]; nodes: readonly string[] }): VectorConstraintAction[] {\n\tconst located = worlds(doc);\n\tconst act = (kind: VectorConstraintKind, refs: VectorConstraintAction['refs']): VectorConstraintAction => ({ kind, label: VECTOR_CONSTRAINT_LABELS[kind], refs, dimensional: VECTOR_DIMENSIONAL_KINDS.has(kind) });\n\tconst anchors = selection.anchors.filter((r) => located.get(r.nodeId)?.node.subpaths[r.subpath]?.anchors[r.index]);\n\tif (anchors.length) {\n\t\tconst neighbours = (a: VectorAnchorRef, b: VectorAnchorRef) => {\n\t\t\tif (a.nodeId !== b.nodeId || a.subpath !== b.subpath) return null;\n\t\t\tconst sp = located.get(a.nodeId)!.node.subpaths[a.subpath];\n\t\t\tconst n = sp.anchors.length;\n\t\t\tif (b.index === a.index + 1 || (sp.closed && a.index === n - 1 && b.index === 0)) return a;\n\t\t\tif (a.index === b.index + 1 || (sp.closed && b.index === n - 1 && a.index === 0)) return b;\n\t\t\treturn null;\n\t\t};\n\t\tconst pointRef = (r: VectorAnchorRef) => ({ nodeId: r.nodeId, subpath: r.subpath, index: r.index });\n\t\tconst segRef = (r: VectorAnchorRef) => ({ ...pointRef(r), segment: true });\n\t\tif (anchors.length === 1) return [act('fix', [pointRef(anchors[0])]), act('tangent', [pointRef(anchors[0])]), act('smooth', [pointRef(anchors[0])])];\n\t\tif (anchors.length === 2) {\n\t\t\tconst start = neighbours(anchors[0], anchors[1]);\n\t\t\tif (start) return ['horizontal', 'vertical', 'distance', 'fix'].map((k) => act(k as VectorConstraintKind, [segRef(start)]));\n\t\t\treturn ['coincident', 'horizontal', 'vertical', 'distance'].map((k) => act(k as VectorConstraintKind, anchors.map(pointRef)));\n\t\t}\n\t\tif (anchors.length === 3) {\n\t\t\tfor (let i = 0; i < 3; i++) {\n\t\t\t\tconst others = anchors.filter((_, k) => k !== i);\n\t\t\t\tconst start = neighbours(others[0], others[1]);\n\t\t\t\tif (start) return [act('coincident', [pointRef(anchors[i]), segRef(start)]), act('distance', [pointRef(anchors[i]), segRef(start)])];\n\t\t\t}\n\t\t\treturn [];\n\t\t}\n\t\tif (anchors.length === 4) {\n\t\t\tfor (const [i, j, k, l] of [\n\t\t\t\t[0, 1, 2, 3],\n\t\t\t\t[0, 2, 1, 3],\n\t\t\t\t[0, 3, 1, 2]\n\t\t\t]) {\n\t\t\t\tconst s1 = neighbours(anchors[i], anchors[j]);\n\t\t\t\tconst s2 = neighbours(anchors[k], anchors[l]);\n\t\t\t\tif (s1 && s2) return ['parallel', 'perpendicular', 'equal', 'collinear', 'angle'].map((kind) => act(kind as VectorConstraintKind, [segRef(s1), segRef(s2)]));\n\t\t\t}\n\t\t}\n\t\treturn [];\n\t}\n\tconst circles = selection.nodes.filter((id) => {\n\t\tconst loc = located.get(id);\n\t\treturn loc && vectorPathCircle(loc.node, loc.world);\n\t});\n\tif (circles.length === 1 && selection.nodes.length === 1) return ['radius', 'diameter', 'fix'].map((k) => act(k as VectorConstraintKind, [{ nodeId: circles[0], circle: true }]));\n\tif (circles.length === 2 && selection.nodes.length === 2) return ['concentric', 'equal', 'tangent'].map((k) => act(k as VectorConstraintKind, circles.map((id) => ({ nodeId: id, circle: true }))));\n\treturn [];\n}\n\n/** The constraints panel: degrees of freedom, constraints of the selection (all without one), actions, parameters. */\nexport interface VectorConstraintPanelData {\n\tdof: VectorDofReport;\n\tconstraints: { id: string; kind: VectorConstraintKind; label: string; name?: string; value?: string; satisfied: boolean; conflict: boolean }[];\n\tactions: VectorConstraintAction[];\n\tparameters: { name: string; value: string; result: number | null; error: string | null }[];\n}\n\nexport function vectorConstraintPanel(doc: VectorDocument, selection: { anchors: readonly VectorAnchorRef[]; nodes: readonly string[] }, conflicts: readonly string[] = []): VectorConstraintPanelData {\n\tconst touched = new Set([...selection.nodes, ...selection.anchors.map((a) => a.nodeId)]);\n\tconst glyphs = new Map(vectorConstraintGlyphs(doc, touched.size ? touched : null).map((g) => [g.id, g]));\n\tconst env = vectorConstraintEnv(doc);\n\tconst list = (doc.constraints ?? []).filter((c: VectorConstraint) => glyphs.has(c.id));\n\treturn {\n\t\tdof: vectorConstraintDof(doc),\n\t\tconstraints: list.map((c) => ({\n\t\t\tid: c.id,\n\t\t\tkind: c.kind,\n\t\t\tlabel: VECTOR_CONSTRAINT_LABELS[c.kind],\n\t\t\t...(c.name ? { name: c.name } : {}),\n\t\t\t...(c.value !== undefined ? { value: c.value } : {}),\n\t\t\tsatisfied: glyphs.get(c.id)?.satisfied ?? false,\n\t\t\tconflict: conflicts.includes(c.id)\n\t\t})),\n\t\tactions: vectorConstraintActions(doc, selection),\n\t\tparameters: (doc.parameters ?? []).map((p) => {\n\t\t\ttry {\n\t\t\t\treturn { name: p.name, value: p.value, result: env.value(p.name), error: null };\n\t\t\t} catch (e) {\n\t\t\t\treturn { name: p.name, value: p.value, result: null, error: e instanceof Error ? e.message : String(e) };\n\t\t\t}\n\t\t})\n\t};\n}\n\n/** The constraints panel for an editor state (selection: the direct tool's anchors, else the selected paths). */\nexport function getVectorConstraintPanel(state: { document: VectorDocument; selection: readonly string[]; selectedAnchors: readonly VectorAnchorRef[]; tool: string; constraintIssues: { conflicts: string[] } | null }): VectorConstraintPanelData {\n\tconst anchors = state.tool === 'direct' ? state.selectedAnchors : [];\n\treturn vectorConstraintPanel(state.document, { anchors, nodes: state.selection }, state.constraintIssues?.conflicts ?? []);\n}\n","/**\n * Framework-neutral render data for a vector editor: the scene (document-space SVG paths and\n * gradient defs) and the overlay (screen-space selection box, anchors, handles, marquee and\n * pen preview). React, Svelte or plain DOM code turns these into `<svg>` elements.\n */\nimport { bezierPathToSvg, transformBezierPath, type BezierPath } from '../canvas/pathops/bezier.js';\nimport { hatchPath } from '../canvas/pathops/hatch.js';\nimport type { SvgGradientDef } from '../canvas/gradient.js';\nimport { expandVectorGradientStops, isCentredVectorGradient, isVectorGradient, parseVectorColor, sortedVectorGradientStops, type VectorGradientHandle, type VectorPaintDef } from './gradient.js';\nimport { getVectorGradientAnnotator, VECTOR_GRADIENT_STOP_GAP } from './gradientTool.js';\n\nexport type { SvgGradientDef };\nimport {\n\tgetAnchorWorld,\n\tgetVectorCornerWidgets,\n\tgetVectorEnvelopeTarget,\n\tgetVectorSelectionBoxGeometry,\n\tgetVectorWidthHandles,\n\tgetVisibleAnchors,\n\tgetVisibleHandles,\n\ttype VectorEditorController,\n\ttype VectorEditorState,\n\ttype VectorSelectionBoxGeometry,\n\ttype VectorWidthPointRef\n} from './controller.js';\nimport { sanitizeWidthProfile, widthProfilePresetId, type VectorWidthPreset } from './width.js';\nimport {\n\tfindVectorNode,\n\twalkVectorNodes,\n\tlocateVectorNode,\n\tsubpathsToBezier,\n\tvectorWorldMatrix,\n\tIDENTITY_MATRIX,\n\tinferAnchorType,\n\tmultiplyMatrices,\n\tnodeMatrix,\n\tvectorNodeBounds,\n\tvectorParentMatrix,\n\tvectorPathData,\n\tvectorPathWorldBezier,\n\ttype AffineMatrix\n} from './geometry.js';\nimport { vectorNodeFilterSvg, vectorPaintToSvg, vectorStrokeSvgParts, type VectorRenderStrokeParts } from './svg.js';\nimport { needsVectorPaintLayers, vectorBlendModeCss, vectorFills, vectorPaintLayerNodes, vectorStrokes, vectorTargetPaint, VECTOR_BLEND_MODE_LABELS } from './paintLayers.js';\nimport { VECTOR_FILTER_LABELS, VECTOR_FILTER_PARAM_SPECS, type VectorFilterParamSpec, type VectorRenderFilter } from './filters.js';\nimport { vectorCorners } from './corners.js';\nimport {\n\texpandVectorAppearance,\n\thasOwnVectorAppearance,\n\thasVectorAppearance,\n\tvectorAppearanceOutline,\n\tvectorInstanceDrawn,\n\tvectorEnteredGroups,\n\tVECTOR_EFFECT_LABELS,\n\tVECTOR_EFFECT_PARAM_SPECS,\n\ttype VectorEffectParamSpec,\n\ttype VectorEnvelopeParams\n} from './appearance.js';\nimport { layoutVectorText, vectorTextPaintSource, vectorTextBoxOutline, vectorTextCaret, vectorTextGeometryNode, vectorTextHasOutlines, vectorTextSelectionQuads, vectorTextSpans, vectorTextSpanSvg, type VectorRenderTextSpan } from './text.js';\nimport { applyMatrix } from '../geom/affine.js';\nimport { vectorConstraintGlyphs } from './constraints/glyphs.js';\nimport { isVectorMaskActive, isVectorMaskGroup, vectorMaskRenderData, vectorMaskSummary, type VectorMaskRenderData, type VectorMaskSummary } from './mask.js';\nimport { isVectorFrame, vectorFrameRectPath } from './frames.js';\nimport { vectorConnectionSites, vectorConnectorTargetAt } from './connectors.js';\nimport { vectorMeshEditPoints, vectorMeshGridCurves, VECTOR_WARP_STYLES, VECTOR_WARP_STYLE_LABELS } from './warp.js';\nimport { VECTOR_EFFECT_TYPES, type VectorEffectType } from './types.js';\nimport { isVectorBlendGroup, resolveVectorBlendOptions, vectorBlendRailAt, vectorBlendSpine } from './blendGroup.js';\nimport type { VectorGuideKind, VectorSnapKind } from './snap.js';\nimport { VECTOR_BLEND_MODES } from './types.js';\nimport type {\n\tVectorMaskType,\n\tVectorAnchorRef,\n\tVectorBlendMode,\n\tVectorFilterType,\n\tVectorStrokeLayer,\n\tVectorAnchorType,\n\tVectorArrowAlign,\n\tVectorArrowhead,\n\tVectorBlendOptions,\n\tVectorBlendSpacing,\n\tVectorBounds,\n\tVectorCornerKind,\n\tVectorGradient,\n\tVectorGradientInterpolation,\n\tVectorGradientSpread,\n\tVectorGradientStop,\n\tVectorGradientType,\n\tVectorStrokeGradientMode,\n\tVectorGroupNode,\n\tVectorHandleRef,\n\tVectorNode,\n\tVectorPaint,\n\tVectorPathfinderOperation,\n\tVectorPathNode,\n\tVectorTextNode,\n\tVectorStrokeAlign,\n\tVectorTransformHandle,\n\tVectorWidthPoint,\n\tVec2\n} from './types.js';\n\nexport interface VectorRenderPath {\n\tkind: 'path';\n\tid: string;\n\t/** SVG `transform` (`matrix(…)`) for a rotated node. */\n\ttransform?: string;\n\td: string;\n\t/** `none`, a colour, or `url(#id)` of a gradient in `defs`. */\n\tfill: string;\n\tstroke: string;\n\tstrokeWidth: number;\n\tstrokeLinecap?: string;\n\tstrokeLinejoin?: string;\n\tstrokeDasharray?: string;\n\tfillRule: 'nonzero' | 'evenodd';\n\topacity: number;\n\t/**\n\t * Set when the stroke takes more than one element: an inside / outside alignment or\n\t * arrowheads. Draw `d` with the fill only, then `stroke.d` with the stroke (at\n\t * `stroke.width`, clipped or masked as `stroke.clip` says), then each head filled with\n\t * `stroke`; group them under `id`, `transform` and `opacity`.\n\t */\n\tstrokeParts?: VectorRenderStrokeParts;\n\t/**\n\t * Set when the path paints through its fill and stroke stacks (several layers, or a layer with\n\t * its own opacity, blend mode or visibility): draw a `<g>` with `id`, `transform`, `opacity`,\n\t * `blendMode` and `filter`, holding each layer in order, bottom to top, as a path of `d` (see\n\t * `VectorRenderPaintLayer`). `fill`, `stroke` and `strokeParts` above then describe the top\n\t * drawn layers only, for readers that draw one paint.\n\t */\n\tlayers?: VectorRenderPaintLayer[];\n\t/** CSS `mix-blend-mode` of the whole node (absent: normal). */\n\tblendMode?: string;\n\t/** `url(#id)` of the node's filter in `filters` (shadows, glows, blurs, feather). */\n\tfilter?: string;\n}\n\n/**\n * One drawn layer of a path's paint stack: a fill (`fill` set, `stroke` none) or a stroke with its\n * own settings and parts (`strokeParts` as on `VectorRenderPath`). Draw it with `opacity` and\n * `mix-blend-mode: blendMode` on its element (or on the group of its parts).\n */\nexport interface VectorRenderPaintLayer {\n\tkind: 'fill' | 'stroke';\n\t/** Index in its stack (bottom to top). */\n\tindex: number;\n\tfill: string;\n\tstroke: string;\n\tstrokeWidth: number;\n\tstrokeLinecap?: string;\n\tstrokeLinejoin?: string;\n\tstrokeDasharray?: string;\n\topacity: number;\n\tblendMode?: string;\n\tstrokeParts?: VectorRenderStrokeParts;\n}\n\nexport interface VectorRenderGroup {\n\tkind: 'group';\n\tid: string;\n\t/** SVG `transform` (`matrix(…)`) for a rotated group. */\n\ttransform?: string;\n\topacity: number;\n\t/** CSS `mix-blend-mode` of the group (absent: normal). */\n\tblendMode?: string;\n\t/** `url(#id)` of the group's filter in `filters`. */\n\tfilter?: string;\n\t/**\n\t * Set on a mask group whose mask is on: `children` are then the masked contents only. Draw them\n\t * in an inner `<g>` with `clip-path` / `mask` referencing the `<clipPath>` / `<mask>` that\n\t * `mask` describes (`mask.nodes`: the mask drawn into a `<mask>`). The group's own transform,\n\t * opacity, blend mode and filter stay on the outer `<g>`, so they apply to the masked result.\n\t */\n\tmask?: VectorRenderMask;\n\tchildren: VectorRenderNode[];\n}\n\n/** A mask group's mask as render data (see `VectorMaskRenderData`), with the mask drawn as nodes for opacity masks. */\nexport interface VectorRenderMask extends Omit<VectorMaskRenderData, 'invertFilter'> {\n\t/** The mask as drawn, for the `<mask>` of an opacity mask (in the group's own space). */\n\tnodes?: VectorRenderNode[];\n}\n\n/**\n * Text drawn as SVG text (when the text shaper gives no glyph outlines; with outlines, text renders\n * as a `VectorRenderPath` or group). Draw a `<g>` with `id`, `transform`, `opacity`, `blendMode` and\n * `filter`, holding one `<text>` per layer, bottom to top, each holding every span as a `<tspan>`.\n */\nexport interface VectorRenderText {\n\tkind: 'text';\n\tid: string;\n\ttransform?: string;\n\topacity: number;\n\tblendMode?: string;\n\tfilter?: string;\n\t/**\n\t * Paint layers, bottom to top: a fill (`fill` set, `stroke` none) or a stroke. Draw each as a\n\t * `<text>` with these attributes (and `mix-blend-mode`). `runFills`: spans with their own `fill`\n\t * use it on this layer (the top fill).\n\t */\n\tlayers: Array<{ kind: 'fill' | 'stroke'; fill: string; stroke: string; strokeWidth: number; strokeLinejoin?: string; strokeDasharray?: string; opacity: number; blendMode?: string; runFills?: boolean }>;\n\tspans: VectorRenderTextSpan[];\n}\n\nexport type { VectorRenderTextSpan };\n\nexport type VectorRenderNode = VectorRenderPath | VectorRenderGroup | VectorRenderText;\n\nexport interface VectorSceneRenderData {\n\t/** SVG `transform` for the document layer: `translate(x y) scale(zoom)`. */\n\ttransform: string;\n\twidth: number;\n\theight: number;\n\tbackground?: string;\n\t/**\n\t * Paint servers referenced by `fill` / `stroke` values: `<linearGradient>` /\n\t * `<radialGradient>` descriptors and `pattern`s (conic, diamond and stroke-mode gradients:\n\t * draw each of `gradients`, then each tile filled, inside a `<pattern>`).\n\t */\n\tdefs: VectorPaintDef[];\n\t/** Filters referenced by nodes' `filter` (draw each as an SVG `<filter>`, see `VectorRenderFilter`). */\n\tfilters: VectorRenderFilter[];\n\t/** Visible nodes, bottom to top. */\n\tnodes: VectorRenderNode[];\n}\n\nexport interface VectorRenderAnchor {\n\tref: VectorAnchorRef;\n\t/** Screen position. */\n\tx: number;\n\ty: number;\n\ttype: VectorAnchorType;\n\tselected: boolean;\n\thovered: boolean;\n\t/** The pen tool closes the path when this anchor is clicked. */\n\tcloses: boolean;\n}\n\nexport interface VectorRenderHandle {\n\tref: VectorHandleRef;\n\tx: number;\n\ty: number;\n\t/** Screen position of the handle's anchor (draw a line to it). */\n\tanchorX: number;\n\tanchorY: number;\n\thovered: boolean;\n}\n\n/** A live corner widget in screen pixels. */\nexport interface VectorRenderCornerWidget {\n\tref: VectorAnchorRef;\n\tx: number;\n\ty: number;\n\tradius: number;\n\tkind: VectorCornerKind;\n\thovered: boolean;\n\t/** Its corners are being dragged. */\n\tactive: boolean;\n}\n\n/**\n * A width point of the width tool in screen pixels: a diamond at (`x`, `y`) on the path and a\n * line across the stroke from `left` to `right`, with a handle at each end.\n */\nexport interface VectorRenderWidthPoint {\n\tref: VectorWidthPointRef;\n\tx: number;\n\ty: number;\n\tleft: Vec2;\n\tright: Vec2;\n\tselected: boolean;\n\t/** The part under the pointer, if any. */\n\thovered: 'center' | 'left' | 'right' | null;\n\t/** Being dragged. */\n\tactive: boolean;\n}\n\n/** A smart guide line in screen pixels. */\nexport interface VectorRenderGuideLine {\n\tkind: VectorGuideKind;\n\tx1: number;\n\ty1: number;\n\tx2: number;\n\ty2: number;\n}\n\n/** A smart guide label (\"anchor\", \"intersect\", \"align\", \"centre\", a gap or a size) at a screen position. */\nexport interface VectorRenderGuideLabel {\n\tkind: VectorSnapKind;\n\tx: number;\n\ty: number;\n\ttext: string;\n}\n\nexport interface VectorOverlayRenderData {\n\t/** Outlines (screen-space path data) of hovered and selected nodes. */\n\toutlines: { id: string; d: string; selected: boolean }[];\n\t/**\n\t * Selection box with resize and rotate handles (select tool). `x / y / width / height` and\n\t * each handle's `x / y` are the unrotated box in screen pixels: draw them inside\n\t * `<g transform={transform}>`. `corners` and each handle's `screen` are true positions.\n\t */\n\tselectionBox:\n\t\t| (VectorBounds & { angle: number; transform: string; corners: Vec2[]; handles: VectorSelectionBoxGeometry['handles']; freeDistort: boolean })\n\t\t| null;\n\t/**\n\t * A Free Distort drag (screen space): the quad the selection box's corners make so far (`d`,\n\t * closed path data) and its corners clockwise from the top left. The box is hidden meanwhile.\n\t */\n\tdistort: { mode: 'distort' | 'perspective' | 'skew'; d: string; corners: Vec2[] } | null;\n\t/**\n\t * The Envelope tool's mesh of the selected envelope (screen space): `grid` is the grid lines as\n\t * path data, `anchors` the points where they cross and `handles` the handles along them, each\n\t * with the anchor it belongs to (`index` is the point's index in the mesh). Draw anchors as\n\t * squares and handles as dots joined to their anchors.\n\t */\n\tenvelope: {\n\t\tnodeId: string;\n\t\tgrid: string;\n\t\tanchors: { index: number; x: number; y: number; hovered: boolean; active: boolean }[];\n\t\thandles: { index: number; x: number; y: number; anchorX: number; anchorY: number; hovered: boolean; active: boolean }[];\n\t} | null;\n\tanchors: VectorRenderAnchor[];\n\thandles: VectorRenderHandle[];\n\t/** Marquee rectangle, screen space. */\n\tmarquee: VectorBounds | null;\n\t/** Rubber-band segment from the pen path's last anchor to the pointer, screen space. */\n\tpenPreview: string | null;\n\t/** Artboard rectangle, screen space. */\n\tartboard: VectorBounds;\n\t/** Smart guides of the snap in progress, screen space (empty when nothing snapped). */\n\tguides: { lines: VectorRenderGuideLine[]; labels: VectorRenderGuideLabel[] };\n\t/**\n\t * The knife or eraser stroke being drawn, screen space: `d` is an open polyline; draw the\n\t * eraser's with a round-capped stroke `width` pixels wide (0 for the knife: a thin line).\n\t */\n\ttoolStroke: { tool: 'knife' | 'eraser'; d: string; width: number } | null;\n\t/** The pencil's lazy-mouse string from the pen to the pointer, screen space. */\n\tpencilString: { x1: number; y1: number; x2: number; y2: number } | null;\n\t/** Where the scissors would cut (screen space). */\n\tcutPoint: Vec2 | null;\n\t/**\n\t * Live corner widgets of the direct tool (screen space): drag one to round its corners,\n\t * Alt-click to cycle round / inverted / chamfer, click to pick its corner alone.\n\t */\n\tcornerWidgets: VectorRenderCornerWidget[];\n\t/** Width points of the width tool (screen space). */\n\twidthPoints: VectorRenderWidthPoint[];\n\t/**\n\t * Where a width-tool drag would add a width point (screen space): draw a dashed line from\n\t * `left` to `right` and a hollow diamond at (`x`, `y`).\n\t */\n\twidthSpot: { x: number; y: number; left: Vec2; right: Vec2 } | null;\n\t/**\n\t * Shape Builder and paint bucket highlight (screen space): `faces` are filled paths (fill\n\t * them translucent and outline them), `hatches` the 45° hatch lines over each face\n\t * (`hatchPath`, a few pixels apart; merge and erase only, empty strings for the paint\n\t * bucket: stroke them over the translucent fill), `lines` are the line pieces and outline\n\t * edges an erase deletes, `trail` is the drag's polyline. `mode` is `merge`, `erase` (Alt)\n\t * or `fill` (paint bucket).\n\t */\n\tbuilder: { mode: 'merge' | 'erase' | 'fill'; faces: string[]; hatches: string[]; lines: string[]; trail: string | null } | null;\n\t/**\n\t * Blends (screen space): the spine of each selected blend (`d`, draw it dashed; `keys` are\n\t * where its keys sit on it; `replaced` when it is a replaced spine) and, with the Blend tool,\n\t * the outline of the shape it blends from (`start`, with the clicked anchor) and the anchors\n\t * of the first subpath under the pointer (click one to start the blend there).\n\t */\n\tblend: {\n\t\tspines: { id: string; d: string; keys: Vec2[]; replaced: boolean }[];\n\t\tstart: { d: string; anchor: Vec2 | null } | null;\n\t\tanchors: { x: number; y: number; hovered: boolean }[];\n\t};\n\t/** The gradient tool's annotator over the selection's gradient (screen space), or null. */\n\tgradient: VectorRenderGradientAnnotator | null;\n\t/**\n\t * Mask indicators (screen space): the outline of the mask of each mask group that is selected\n\t * or holds the selection (`d`, draw it dashed), with its type, whether it is inverted, and\n\t * whether the mask itself is selected (`editing`).\n\t */\n\tmasks: { id: string; maskId: string; d: string; type: VectorMaskType; invert: boolean; enabled: boolean; editing: boolean }[];\n\t/**\n\t * Text editing (screen space): the edited text's frame outline (`frame`, draw it thin and\n\t * dashed), the caret (a line, when the selection is collapsed), the selection's quads (`selection`,\n\t * filled translucent) and the overflow marker (a small boxed plus at `overflow` when text does\n\t * not fit). Also set, without caret, for the selected text while a Type tool is active.\n\t */\n\ttext: { nodeId: string; frame: string; caret: { x1: number; y1: number; x2: number; y2: number } | null; selection: string[]; overflow: Vec2 | null; editing: boolean } | null;\n\t/** The Type tool's area text box being dragged out (screen space). */\n\ttextBox: VectorBounds | null;\n\t/**\n\t * Constraint glyphs of the selected or edited paths (screen space): a label at (`x`, `y`) (a\n\t * glyph, or `name = value`), a dimension line for distances, `satisfied` false when the solve\n\t * could not meet it (draw it in a warning colour).\n\t */\n\t/**\n\t * Name labels of the top-level frames (artboards), screen space: draw `name` just above (x, y),\n\t * the frame's top-left corner (bold when `selected`). Clicking a label should select the frame.\n\t */\n\tframes: { id: string; name: string; x: number; y: number; selected: boolean }[];\n\t/**\n\t * Connection sites, screen space, while the Connector tool is on: those of the object under the\n\t * pointer (top, right, bottom, left, centre), `active` on the one an end would glue to.\n\t */\n\tconnectorSites: { nodeId: string; side: string; x: number; y: number; active: boolean }[];\n\t/** Points acquired for object snap tracking, screen space: draw a small `+` at each. */\n\ttrackingPoints: { x: number; y: number }[];\n\t/** The distance or coordinate being typed while the pen draws, and where to show it (screen space, by the pointer), or null. */\n\ttypedEntry: { text: string; x: number; y: number } | null;\n\tconstraints: { id: string; kind: string; label: string; x: number; y: number; dimension: { x1: number; y1: number; x2: number; y2: number } | null; satisfied: boolean }[];\n\t/** A short status, e.g. \"W 120 × H 80\" or \"45°\". */\n\tstatus: string | null;\n}\n\n/**\n * The gradient annotator in screen pixels: draw the `extent` outline (dashed), the bar from\n * `start` to `end`, the handles, each stop's chip (a swatch at `x, y` with a tick to `barX, barY`;\n * `removing`: the dragged stop is off the bar and will be deleted, draw it faded) and the\n * midpoint diamonds.\n */\nexport interface VectorRenderGradientAnnotator {\n\tnodeId: string;\n\ttype: VectorGradientType;\n\tstart: { x: number; y: number; hovered: boolean };\n\t/** `radius` for radial, conic and diamond gradients. */\n\tend: { x: number; y: number; hovered: boolean; handle: 'end' | 'radius' };\n\taspect: { x: number; y: number; hovered: boolean } | null;\n\tfocal: { x: number; y: number; hovered: boolean } | null;\n\tstops: { index: number; x: number; y: number; barX: number; barY: number; color: string; opacity: number; selected: boolean; hovered: boolean }[];\n\tmidpoints: { index: number; x: number; y: number; hovered: boolean }[];\n\textent: string | null;\n\tremoving: boolean;\n}\n\nconst fmt = (n: number) => Math.round(n * 100) / 100;\nconst fmtMatrix = (m: AffineMatrix) => `matrix(${m.map((n) => Math.round(n * 1e6) / 1e6).join(' ')})`;\n/** Distance between the Shape Builder's hatch lines over a face (screen pixels). */\nexport const VECTOR_BUILDER_HATCH_GAP = 6;\n\n/** The blend mode and filter of a node, as render data (its filter pushed to `filters`). */\nfunction nodeFinish(node: VectorNode, filters: VectorRenderFilter[], idPrefix: string): { blendMode?: string; filter?: string } {\n\tconst out: { blendMode?: string; filter?: string } = {};\n\tconst blend = vectorBlendModeCss(node.blendMode);\n\tif (blend) out.blendMode = blend;\n\tconst filter = vectorNodeFilterSvg(node, `${idPrefix}filter-${node.id}`);\n\tif (filter) {\n\t\tfilters.push(filter);\n\t\tout.filter = `url(#${filter.id})`;\n\t}\n\treturn out;\n}\n\n/** The SVG line attributes of a (layer) node's stroke. */\nfunction lineStyle(node: VectorPathNode) {\n\treturn {\n\t\tstrokeWidth: node.strokeWidth ?? 1,\n\t\t...(node.strokeLinecap ? { strokeLinecap: node.strokeLinecap } : {}),\n\t\t...(node.strokeLinejoin ? { strokeLinejoin: node.strokeLinejoin } : {}),\n\t\t...(node.strokeDasharray?.length ? { strokeDasharray: node.strokeDasharray.join(' ') } : {})\n\t};\n}\n\n/** The drawn layers of a path's stacks as render data (paint servers pushed to `defs`). */\nfunction renderLayers(node: VectorPathNode, defs: VectorPaintDef[], idPrefix: string): VectorRenderPaintLayer[] {\n\tconst out: VectorRenderPaintLayer[] = [];\n\tfor (const l of vectorPaintLayerNodes(node)) {\n\t\tif (!l.drawn) continue;\n\t\tconst role = l.kind;\n\t\tconst paint = vectorPaintToSvg(l.layer.paint, `${idPrefix}${role}-${node.id}-${l.index}`, { node: l.node, ...(role === 'stroke' ? { role } : {}) });\n\t\tdefs.push(...paint.defs);\n\t\tconst blend = vectorBlendModeCss(l.layer.blendMode);\n\t\tconst base = { kind: role, index: l.index, opacity: l.layer.opacity ?? 1, ...(blend ? { blendMode: blend } : {}) };\n\t\tif (role === 'fill') out.push({ ...base, fill: paint.value, stroke: 'none', strokeWidth: 0 });\n\t\telse {\n\t\t\tconst strokeParts = vectorStrokeSvgParts(l.node, `${idPrefix}clip-${node.id}-${l.index}`);\n\t\t\tout.push({ ...base, fill: 'none', stroke: paint.value, ...lineStyle(l.node), ...(strokeParts ? { strokeParts } : {}) });\n\t\t}\n\t}\n\treturn out;\n}\n\n/** A text node drawn as SVG text (no glyph outlines here). */\nfunction renderText(node: VectorTextNode, defs: VectorPaintDef[], filters: VectorRenderFilter[], idPrefix: string): VectorRenderText {\n\tconst geometry = vectorTextGeometryNode(node);\n\tconst layers: VectorRenderText['layers'] = [];\n\tconst nodes = vectorPaintLayerNodes(geometry);\n\tconst topFill = nodes.filter((l) => l.kind === 'fill' && l.drawn).at(-1);\n\tfor (const l of nodes) {\n\t\tif (!l.drawn) continue;\n\t\tconst paint = vectorPaintToSvg(l.layer.paint, `${idPrefix}${l.kind}-${node.id}-${l.index}`, { node: l.node, ...(l.kind === 'stroke' ? { role: 'stroke' as const } : {}) });\n\t\tdefs.push(...paint.defs);\n\t\tconst blend = vectorBlendModeCss(l.layer.blendMode);\n\t\tconst base = { kind: l.kind, opacity: l.layer.opacity ?? 1, ...(blend ? { blendMode: blend } : {}) };\n\t\tif (l.kind === 'fill') layers.push({ ...base, fill: paint.value, stroke: 'none', strokeWidth: 0, ...(l === topFill ? { runFills: true } : {}) });\n\t\telse\n\t\t\tlayers.push({\n\t\t\t\t...base,\n\t\t\t\tfill: 'none',\n\t\t\t\tstroke: paint.value,\n\t\t\t\tstrokeWidth: l.node.strokeWidth ?? 1,\n\t\t\t\t...(l.node.strokeLinejoin ? { strokeLinejoin: l.node.strokeLinejoin } : {}),\n\t\t\t\t...(l.node.strokeDasharray?.length ? { strokeDasharray: l.node.strokeDasharray.join(' ') } : {})\n\t\t\t});\n\t}\n\t// Runs with their own fill paint the top fill (or are the only fill when the node has none).\n\tconst spans = vectorTextSpans(node).map((sp, i): VectorRenderTextSpan => {\n\t\tlet fill: string | undefined;\n\t\tif (sp.fill !== undefined) {\n\t\t\tconst paint = vectorPaintToSvg(sp.fill, `${idPrefix}run-${node.id}-${i}`, { node: geometry });\n\t\t\tdefs.push(...paint.defs);\n\t\t\tfill = paint.value;\n\t\t}\n\t\treturn vectorTextSpanSvg(sp, fill);\n\t});\n\tif (!topFill && spans.some((s) => s.fill)) layers.unshift({ kind: 'fill', fill: 'none', stroke: 'none', strokeWidth: 0, opacity: 1, runFills: true });\n\treturn {\n\t\tkind: 'text',\n\t\tid: node.id,\n\t\t...(node.transform ? { transform: fmtMatrix(node.transform) } : {}),\n\t\topacity: node.opacity ?? 1,\n\t\t...nodeFinish(node, filters, idPrefix),\n\t\tlayers,\n\t\tspans\n\t};\n}\n\nfunction renderNode(stored: VectorNode, defs: VectorPaintDef[], filters: VectorRenderFilter[], idPrefix: string): VectorRenderNode | null {\n\tif (stored.visible === false) return null;\n\t// Text without glyph outlines draws as SVG text; with them it draws as its outlines below.\n\tif (stored.type === 'text' && !vectorTextHasOutlines()) return renderText(stored, defs, filters, idPrefix);\n\t// Path effects and live booleans draw as their expanded shapes.\n\tconst node = expandVectorAppearance(stored);\n\tif (node.type === 'group') {\n\t\tconst finish = nodeFinish(node, filters, idPrefix);\n\t\tconst masked = isVectorMaskGroup(node) ? renderMask(node, defs, filters, idPrefix) : null;\n\t\t// A mask group without a working mask draws its contents whole (a hidden mask child stays hidden).\n\t\tconst list = isVectorMaskGroup(node) ? node.children.slice(1) : node.children;\n\t\tconst children = list.map((c) => renderNode(c, defs, filters, idPrefix)).filter((c): c is VectorRenderNode => !!c);\n\t\treturn {\n\t\t\tkind: 'group',\n\t\t\tid: node.id,\n\t\t\t...(node.transform ? { transform: fmtMatrix(node.transform) } : {}),\n\t\t\topacity: node.opacity ?? 1,\n\t\t\t...finish,\n\t\t\t...(masked ? { mask: masked } : {}),\n\t\t\tchildren\n\t\t};\n\t}\n\tconst finish = nodeFinish(node, filters, idPrefix);\n\tconst common = {\n\t\tkind: 'path' as const,\n\t\tid: node.id,\n\t\t...(node.transform ? { transform: fmtMatrix(node.transform) } : {}),\n\t\td: vectorPathData(node),\n\t\tfillRule: node.fillRule ?? ('nonzero' as const),\n\t\topacity: node.opacity ?? 1,\n\t\t...finish\n\t};\n\tif (needsVectorPaintLayers(node)) {\n\t\tconst layers = renderLayers(node, defs, idPrefix);\n\t\tconst fill = layers.filter((l) => l.kind === 'fill').at(-1);\n\t\tconst stroke = layers.filter((l) => l.kind === 'stroke').at(-1);\n\t\treturn {\n\t\t\t...common,\n\t\t\tfill: fill?.fill ?? 'none',\n\t\t\tstroke: stroke?.stroke ?? 'none',\n\t\t\tstrokeWidth: stroke?.strokeWidth ?? node.strokeWidth ?? 1,\n\t\t\t...(stroke?.strokeLinecap ? { strokeLinecap: stroke.strokeLinecap } : {}),\n\t\t\t...(stroke?.strokeLinejoin ? { strokeLinejoin: stroke.strokeLinejoin } : {}),\n\t\t\t...(stroke?.strokeDasharray ? { strokeDasharray: stroke.strokeDasharray } : {}),\n\t\t\t...(stroke?.strokeParts ? { strokeParts: stroke.strokeParts } : {}),\n\t\t\tlayers\n\t\t};\n\t}\n\tconst fill = vectorPaintToSvg(node.fill, `${idPrefix}fill-${node.id}`, { node });\n\tconst stroke = vectorPaintToSvg(node.stroke, `${idPrefix}stroke-${node.id}`, { node, role: 'stroke' });\n\tdefs.push(...fill.defs, ...stroke.defs);\n\tconst strokeParts = stroke.value !== 'none' ? vectorStrokeSvgParts(node, `${idPrefix}clip-${node.id}`) : undefined;\n\treturn {\n\t\t...common,\n\t\t...(strokeParts ? { strokeParts } : {}),\n\t\tfill: fill.value,\n\t\tstroke: stroke.value,\n\t\t...lineStyle(node)\n\t};\n}\n\n/** A mask group's mask as render data (its inverting filter pushed to `filters`), or null while it is off. */\nfunction renderMask(group: VectorGroupNode, defs: VectorPaintDef[], filters: VectorRenderFilter[], idPrefix: string): VectorRenderMask | null {\n\tif (!isVectorMaskActive(group)) return null;\n\tconst data = vectorMaskRenderData(group, `${idPrefix}mask-${group.id}`, `${idPrefix}mask-filter-${group.id}`);\n\tif (!data) return null;\n\tconst { invertFilter, ...mask } = data;\n\tif (invertFilter) filters.push(invertFilter);\n\tif (mask.type === 'clip') return mask;\n\t// The mask draws as itself (shown even when stored hidden would have turned the mask off: it is on here).\n\tconst source = renderNode(group.children[0], defs, filters, idPrefix);\n\treturn { ...mask, nodes: source ? [source] : [] };\n}\n\n/**\n * The document layer. Render the nodes inside `<g transform={transform}>` and `defs` in a\n * `<defs>` block. Pass a unique `idPrefix` when several editors share a page.\n */\nexport function getVectorSceneRenderData(state: Pick<VectorEditorState, 'document' | 'viewport'>, options: { idPrefix?: string } = {}): VectorSceneRenderData {\n\tconst defs: VectorPaintDef[] = [];\n\tconst filters: VectorRenderFilter[] = [];\n\tconst prefix = options.idPrefix ?? 've-';\n\tconst nodes = state.document.nodes.map((n) => renderNode(n, defs, filters, prefix)).filter((n): n is VectorRenderNode => !!n);\n\tconst v = state.viewport;\n\treturn {\n\t\ttransform: `translate(${fmt(v.x)} ${fmt(v.y)}) scale(${v.zoom})`,\n\t\twidth: state.document.width,\n\t\theight: state.document.height,\n\t\t...(state.document.background ? { background: state.document.background } : {}),\n\t\tdefs,\n\t\tfilters,\n\t\tnodes\n\t};\n}\n\nconst toScreen = (state: Pick<VectorEditorState, 'viewport'>, p: Vec2) => ({ x: p.x * state.viewport.zoom + state.viewport.x, y: p.y * state.viewport.zoom + state.viewport.y });\n\nfunction screenOutline(state: VectorEditorState, node: VectorNode, entered: ReadonlySet<string>): string {\n\tconst v = state.viewport;\n\tconst view: AffineMatrix = [v.zoom, 0, 0, v.zoom, v.x, v.y];\n\tconst parent = vectorParentMatrix(state.document.nodes, node.id) ?? IDENTITY_MATRIX;\n\treturn outlinePaths(node, parent, entered)\n\t\t.map((path) => bezierPathToSvg(transformBezierPath(path, view), { precision: 2 }))\n\t\t.join(' ');\n}\n\n/**\n * Document-space outlines of a subtree: paths as stored (their anchors are what is edited; the\n * scene shows their effects), live groups as drawn unless they are being edited (then their\n * children, as figcraft outlines a boolean group's operands).\n */\nfunction outlinePaths(node: VectorNode, parent: AffineMatrix, entered: ReadonlySet<string>): BezierPath[] {\n\tif (node.type === 'path') return [vectorPathWorldBezier(node, parent, { effects: false })];\n\tconst m = multiplyMatrices(nodeMatrix(node), parent);\n\t// Text outlines by its glyph boxes (its frame shows while it is edited).\n\tif (node.type === 'text') return [transformBezierPath(vectorTextBoxOutline(node), m)];\n\t// An instance outlines as its art draws (through its drawn group's rigid transform).\n\tif (node.type === 'instance') {\n\t\tconst g = vectorInstanceDrawn(node);\n\t\treturn [transformBezierPath(vectorAppearanceOutline(g), multiplyMatrices(nodeMatrix(g), parent))];\n\t}\n\t// A frame outlines as its rectangle.\n\tif (isVectorFrame(node)) return [transformBezierPath(subpathsToBezier(vectorFrameRectPath(node.frame)), m)];\n\t// Live and mask groups outline as drawn (masked) unless they are being edited.\n\tif ((hasOwnVectorAppearance(node) || isVectorMaskActive(node)) && !entered.has(node.id)) return [transformBezierPath(vectorAppearanceOutline(node), m)];\n\treturn node.children.filter((c) => c.visible !== false).flatMap((c) => outlinePaths(c, m, entered));\n}\n\n/** Connection sites under the pointer while the Connector tool is on (see `VectorOverlayRenderData.connectorSites`). */\nfunction connectorSiteOverlay(state: VectorEditorState): VectorOverlayRenderData['connectorSites'] {\n\tif (state.tool !== 'connector' || !state.pointer) return [];\n\tconst drawing = state.interaction?.type === 'draw' && state.interaction.tool === 'connector' ? state.interaction.nodeId : undefined;\n\tconst nodes = state.document.nodes;\n\tconst target = vectorConnectorTargetAt(nodes, state.pointer, 12 / state.viewport.zoom, drawing);\n\tif (!target) return [];\n\treturn vectorConnectionSites(nodes, target.nodeId).map((site) => {\n\t\tconst p = toScreen(state, site);\n\t\treturn { nodeId: target.nodeId, side: site.side, x: p.x, y: p.y, active: target.site?.side === site.side };\n\t});\n}\n\n/** Name labels of the top-level frames (see `VectorOverlayRenderData.frames`). */\nfunction frameLabels(state: VectorEditorState): VectorOverlayRenderData['frames'] {\n\tconst out: VectorOverlayRenderData['frames'] = [];\n\tfor (const n of state.document.nodes) {\n\t\tif (!isVectorFrame(n) || n.visible === false) continue;\n\t\tconst corner = applyMatrix(nodeMatrix(n), { x: n.frame.x, y: n.frame.y });\n\t\tconst p = toScreen(state, corner);\n\t\tout.push({ id: n.id, name: n.name ?? 'Frame', x: p.x, y: p.y, selected: state.selection.includes(n.id) });\n\t}\n\treturn out;\n}\n\n/** Constraint glyphs of what is selected or edited (see `VectorOverlayRenderData.constraints`). */\nfunction constraintOverlay(state: VectorEditorState): VectorOverlayRenderData['constraints'] {\n\tif (!state.document.constraints?.length) return [];\n\tconst ids = new Set([...state.selection, ...(state.editingId ? [state.editingId] : []), ...state.selectedAnchors.map((a) => a.nodeId)]);\n\tfor (const id of [...ids]) {\n\t\tconst n = findVectorNode(state.document.nodes, id);\n\t\tif (n?.type === 'group') walkVectorNodes(n.children, (c) => ids.add(c.id));\n\t}\n\tif (!ids.size) return [];\n\tconst conflicts = new Set(state.constraintIssues?.conflicts ?? []);\n\treturn vectorConstraintGlyphs(state.document, ids).map((g) => {\n\t\tconst at = toScreen(state, g.at);\n\t\tconst d = g.dimension ? { a: toScreen(state, g.dimension.a), b: toScreen(state, g.dimension.b) } : null;\n\t\treturn { id: g.id, kind: g.kind, label: g.label, x: at.x, y: at.y - 10, dimension: d ? { x1: d.a.x, y1: d.a.y, x2: d.b.x, y2: d.b.y } : null, satisfied: g.satisfied && !conflicts.has(g.id) };\n\t});\n}\n\n/** The text editing overlay (see `VectorOverlayRenderData.text`). */\nfunction textOverlay(state: VectorEditorState): VectorOverlayRenderData['text'] {\n\tconst edit = state.textEdit;\n\tconst id = edit?.nodeId ?? (state.tool === 'type' || state.tool === 'typePath' ? state.selection.find((s) => findVectorNode(state.document.nodes, s)?.type === 'text') : undefined);\n\tif (!id) return null;\n\tconst node = findVectorNode(state.document.nodes, id);\n\tif (node?.type !== 'text') return null;\n\tconst world = vectorWorldMatrix(state.document.nodes, id) ?? IDENTITY_MATRIX;\n\tconst v = state.viewport;\n\tconst m = multiplyMatrices(world, [v.zoom, 0, 0, v.zoom, v.x, v.y]);\n\tconst at = (p: Vec2) => applyMatrix(m, p);\n\tconst frame = bezierPathToSvg(transformBezierPath(vectorTextBoxOutline(node), m), { precision: 2 });\n\tlet caret: NonNullable<VectorOverlayRenderData['text']>['caret'] = null;\n\tlet selection: string[] = [];\n\tif (edit) {\n\t\tconst a = Math.min(edit.anchor, edit.focus);\n\t\tconst b = Math.max(edit.anchor, edit.focus);\n\t\tif (a === b) {\n\t\t\tconst c = vectorTextCaret(node, edit.focus);\n\t\t\tconst top = at(c.top);\n\t\t\tconst bottom = at(c.bottom);\n\t\t\tcaret = { x1: top.x, y1: top.y, x2: bottom.x, y2: bottom.y };\n\t\t} else selection = vectorTextSelectionQuads(node, a, b).map((q) => `M${q.map((p) => at(p)).map((p) => `${fmt(p.x)} ${fmt(p.y)}`).join('L')}Z`);\n\t}\n\tconst layout = layoutVectorText(node);\n\tlet overflow: Vec2 | null = null;\n\tif (layout.overflow) {\n\t\tconst f = node.frame;\n\t\toverflow = at(f.kind === 'area' ? { x: f.x + f.width, y: f.y + f.height } : (layout.glyphs.at(-1) ? { x: layout.glyphs.at(-1)!.x, y: layout.glyphs.at(-1)!.y } : { x: 0, y: 0 }));\n\t}\n\treturn { nodeId: id, frame, caret, selection, overflow, editing: !!edit };\n}\n\n/** Live groups being edited in `state` (see `vectorEnteredGroups`). */\nexport function getVectorEnteredGroups(state: Pick<VectorEditorState, 'document' | 'selection' | 'editingId'>): Set<string> {\n\treturn vectorEnteredGroups(state.document.nodes, state.editingId ? [...state.selection, state.editingId] : state.selection);\n}\n\n/** Paths (and text, as a path in its style) of a subtree, for style panels. */\nfunction collect(node: VectorNode): VectorPathNode[] {\n\tif (node.type === 'path') return [node];\n\tif (node.type === 'text') return [vectorTextPaintSource(node)];\n\tif (node.type === 'instance') return [];\n\treturn node.children.flatMap(collect);\n}\n\n/** The editing overlay in screen pixels. Render it on top of the scene, untransformed. */\nexport function getVectorOverlayRenderData(state: VectorEditorState): VectorOverlayRenderData {\n\tconst doc = state.document;\n\tconst z = state.viewport.zoom;\n\tconst outlines: VectorOverlayRenderData['outlines'] = [];\n\tconst entered = getVectorEnteredGroups(state);\n\tconst seen = new Set<string>();\n\tconst outline = (id: string, selected: boolean) => {\n\t\tif (seen.has(id)) return;\n\t\tconst node = findVectorNode(doc.nodes, id);\n\t\tif (!node || node.visible === false) return;\n\t\tseen.add(id);\n\t\toutlines.push({ id, d: screenOutline(state, node, entered), selected });\n\t};\n\tfor (const id of state.selection) outline(id, true);\n\tif (state.editingId) outline(state.editingId, true);\n\tif (state.hover?.nodeId && !state.interaction) outline(state.hover.nodeId, false);\n\n\tlet selectionBox: VectorOverlayRenderData['selectionBox'] = null;\n\tif (state.tool === 'select' && state.selectionFrame && state.interaction?.type !== 'marquee' && state.interaction?.type !== 'distort') {\n\t\tconst g = getVectorSelectionBoxGeometry(state.selectionFrame, state.viewport);\n\t\tselectionBox = { ...g.box, angle: g.angle, transform: g.transform, corners: g.corners, handles: g.handles, freeDistort: state.freeDistort };\n\t}\n\n\tconst selected = (r: VectorAnchorRef) => state.selectedAnchors.some((a) => a.nodeId === r.nodeId && a.subpath === r.subpath && a.index === r.index);\n\tconst sameAnchor = (a: VectorAnchorRef | undefined, b: VectorAnchorRef) => !!a && a.nodeId === b.nodeId && a.subpath === b.subpath && a.index === b.index;\n\tconst anchors: VectorRenderAnchor[] = getVisibleAnchors(state).map((ref) => {\n\t\tconst a = getAnchorWorld(doc, ref)!;\n\t\tconst s = toScreen(state, a);\n\t\tconst closes = !!state.pen && state.pen.nodeId === ref.nodeId && state.pen.subpath === ref.subpath && ref.index === 0;\n\t\treturn { ref, x: s.x, y: s.y, type: a.type ?? inferAnchorType(a), selected: selected(ref), hovered: sameAnchor(state.hover?.anchor, ref), closes };\n\t});\n\tif (state.tool === 'pen' && !state.pen && state.hover?.anchor) {\n\t\tconst a = getAnchorWorld(doc, state.hover.anchor);\n\t\tif (a) {\n\t\t\tconst s = toScreen(state, a);\n\t\t\tanchors.push({ ref: state.hover.anchor, x: s.x, y: s.y, type: a.type ?? 'corner', selected: false, hovered: true, closes: false });\n\t\t}\n\t}\n\n\tconst handles: VectorRenderHandle[] = getVisibleHandles(state).flatMap((ref) => {\n\t\tconst a = getAnchorWorld(doc, ref);\n\t\tconst h = a?.[ref.handle];\n\t\tif (!a || !h) return [];\n\t\tconst s = toScreen(state, h);\n\t\tconst sa = toScreen(state, a);\n\t\tconst hovered = !!state.hover?.handle && sameAnchor(state.hover.handle, ref) && state.hover.handle.handle === ref.handle;\n\t\treturn [{ ref, x: s.x, y: s.y, anchorX: sa.x, anchorY: sa.y, hovered }];\n\t});\n\n\tlet marquee: VectorBounds | null = null;\n\tif (state.interaction?.type === 'marquee') {\n\t\tconst b = state.interaction.bounds;\n\t\tconst tl = toScreen(state, b);\n\t\tmarquee = { x: tl.x, y: tl.y, width: b.width * z, height: b.height * z };\n\t}\n\n\tlet penPreview: string | null = null;\n\tif (state.tool === 'pen' && state.pen && state.pointer && !state.interaction) {\n\t\tconst node = findVectorNode(doc.nodes, state.pen.nodeId);\n\t\tconst sp = node?.type === 'path' ? node.subpaths[state.pen.subpath] : undefined;\n\t\tconst last = sp ? getAnchorWorld(doc, { nodeId: state.pen.nodeId, subpath: state.pen.subpath, index: sp.anchors.length - 1 }) : undefined;\n\t\tif (last) {\n\t\t\tconst a = toScreen(state, last);\n\t\t\tconst p = toScreen(state, state.pointer);\n\t\t\tconst c = last.out ? toScreen(state, last.out) : null;\n\t\t\tpenPreview = c ? `M${fmt(a.x)} ${fmt(a.y)}Q${fmt(c.x)} ${fmt(c.y)} ${fmt(p.x)} ${fmt(p.y)}` : `M${fmt(a.x)} ${fmt(a.y)}L${fmt(p.x)} ${fmt(p.y)}`;\n\t\t}\n\t}\n\n\tconst origin = toScreen(state, { x: 0, y: 0 });\n\tlet artboard = { x: origin.x, y: origin.y, width: doc.width * z, height: doc.height * z };\n\t// Isolation mode: the document's artboard where the dimmed document draws (none without it).\n\tconst se = state.symbolEdit;\n\tif (se) {\n\t\tconst m = se.hostTransform;\n\t\tif (m) {\n\t\t\tconst tl = toScreen(state, { x: m[4], y: m[5] });\n\t\t\tartboard = { x: tl.x, y: tl.y, width: se.host.width * m[0] * z, height: se.host.height * m[3] * z };\n\t\t} else artboard = { x: origin.x, y: origin.y, width: 0, height: 0 };\n\t}\n\n\tlet status: string | null = null;\n\tconst it = state.interaction;\n\tif (it?.type === 'rotate') status = `${Math.round(it.angle)}°`;\n\telse if (it?.type === 'move' || it?.type === 'anchors') status = `Δ ${fmt(it.dx)}, ${fmt(it.dy)}`;\n\telse if (it?.type === 'scale' || it?.type === 'draw') {\n\t\tconst b = it.type === 'draw' ? (findVectorNode(doc.nodes, it.nodeId) ? vectorNodeBounds(findVectorNode(doc.nodes, it.nodeId)!) : null) : state.selectionFrame;\n\t\tif (b) status = `W ${fmt(b.width)} × H ${fmt(b.height)}`;\n\t}\n\n\tconst guides: VectorOverlayRenderData['guides'] = { lines: [], labels: [] };\n\tif (state.guides) {\n\t\tfor (const l of state.guides.lines) {\n\t\t\tconst a = toScreen(state, { x: l.x1, y: l.y1 });\n\t\t\tconst b = toScreen(state, { x: l.x2, y: l.y2 });\n\t\t\tguides.lines.push({ kind: l.kind, x1: fmt(a.x), y1: fmt(a.y), x2: fmt(b.x), y2: fmt(b.y) });\n\t\t}\n\t\tfor (const l of state.guides.labels) {\n\t\t\tconst p = toScreen(state, l);\n\t\t\tguides.labels.push({ kind: l.kind, x: fmt(p.x), y: fmt(p.y), text: l.text });\n\t\t}\n\t}\n\n\tlet toolStroke: VectorOverlayRenderData['toolStroke'] = null;\n\tif (it?.type === 'stroke') {\n\t\tconst pts = it.points.map((q) => toScreen(state, q));\n\t\tconst d = pts.map((q, i) => `${i ? 'L' : 'M'}${fmt(q.x)} ${fmt(q.y)}`).join('') + (pts.length === 1 ? `L${fmt(pts[0].x)} ${fmt(pts[0].y)}` : '');\n\t\ttoolStroke = { tool: it.tool, d, width: fmt(it.width * z) };\n\t}\n\tlet pencilString: VectorOverlayRenderData['pencilString'] = null;\n\tif (it?.type === 'draw' && it.string) {\n\t\tconst a = toScreen(state, it.string[0]);\n\t\tconst b = toScreen(state, it.string[1]);\n\t\tpencilString = { x1: fmt(a.x), y1: fmt(a.y), x2: fmt(b.x), y2: fmt(b.y) };\n\t}\n\tconst dragged = it?.type === 'corner' ? it.ref : null;\n\tconst cornerWidgets: VectorRenderCornerWidget[] = getVectorCornerWidgets(state).map((w) => {\n\t\tconst s = toScreen(state, w);\n\t\treturn { ref: w.ref, x: fmt(s.x), y: fmt(s.y), radius: w.radius, kind: w.kind, hovered: sameAnchor(state.hover?.corner, w.ref), active: !!dragged };\n\t});\n\tif (it?.type === 'corner') status = `R ${fmt(it.radius)}`;\n\tconst cutPoint = state.tool === 'scissors' && state.hover?.segment && !it ? (({ x, y }) => ({ x: fmt(x), y: fmt(y) }))(toScreen(state, state.hover.segment.point)) : null;\n\n\tconst sc = (p: Vec2) => {\n\t\tconst q = toScreen(state, p);\n\t\treturn { x: fmt(q.x), y: fmt(q.y) };\n\t};\n\tconst sameWidth = (a: VectorWidthPointRef | null | undefined, b: VectorWidthPointRef) => !!a && a.nodeId === b.nodeId && a.index === b.index;\n\tconst widthPoints: VectorRenderWidthPoint[] = getVectorWidthHandles(state).map((h) => {\n\t\tconst c = sc(h);\n\t\tconst hw = state.hover?.widthPoint;\n\t\tconst hovered = hw && sameWidth(hw, h.ref) && hw.subpath === h.ref.subpath ? hw.part : null;\n\t\treturn { ref: h.ref, x: c.x, y: c.y, left: sc(h.left), right: sc(h.right), selected: h.selected, hovered, active: it?.type === 'width' && sameWidth(it.ref, h.ref) };\n\t});\n\tconst spot = state.tool === 'width' && !it ? state.hover?.strokeSpot : null;\n\tconst widthSpot = spot ? { ...sc(spot.point), left: sc(spot.left), right: sc(spot.right) } : null;\n\tif (it?.type === 'width') status = it.mode === 'move' ? 'Move width point' : it.mode === 'copy' ? 'Copy width point' : `W ${fmt(it.width)}`;\n\n\tlet builder: VectorOverlayRenderData['builder'] = null;\n\tif (state.builder) {\n\t\tconst v = state.viewport;\n\t\tconst view: AffineMatrix = [v.zoom, 0, 0, v.zoom, v.x, v.y];\n\t\tconst svg = (path: BezierPath) => bezierPathToSvg(path, { precision: 2 });\n\t\tconst screen = state.builder.faces.map((f) => transformBezierPath(f, view));\n\t\tconst hatch = state.builder.mode !== 'fill';\n\t\tconst trail = state.builder.trail && state.builder.trail.length > 1 ? state.builder.trail.map((q, i) => `${i ? 'L' : 'M'}${sc(q).x} ${sc(q).y}`).join('') : null;\n\t\tbuilder = {\n\t\t\tmode: state.builder.mode,\n\t\t\tfaces: screen.map(svg),\n\t\t\thatches: screen.map((f) => (hatch ? svg(hatchPath(f, { gap: VECTOR_BUILDER_HATCH_GAP })) : '')),\n\t\t\tlines: state.builder.lines.map((l) => svg(transformBezierPath(l, view))),\n\t\t\ttrail\n\t\t};\n\t\tif (state.builder.mode !== 'fill' && it?.type === 'builder') {\n\t\t\tconst n = state.builder.faces.length;\n\t\t\tstatus = state.builder.mode === 'erase' ? `Delete ${n} ${n === 1 ? 'face' : 'faces'}` : `Merge ${n} ${n === 1 ? 'face' : 'faces'}`;\n\t\t}\n\t}\n\n\tconst blend = blendOverlay(state);\n\tif (state.tool === 'blend' && !it) status = state.blendStart ? 'Click another shape to blend into' : 'Click a shape to blend from';\n\n\tlet gradient: VectorRenderGradientAnnotator | null = null;\n\tif (state.tool === 'gradient') {\n\t\tgradient = gradientOverlay(state);\n\t\tif (!gradient && !it) status = 'Drag across a shape to set a gradient';\n\t\telse if (it?.type === 'gradient' && it.removing) status = 'Release to delete the stop';\n\t}\n\n\tlet distort: VectorOverlayRenderData['distort'] = null;\n\tif (it?.type === 'distort') {\n\t\tconst corners = it.corners.map(sc);\n\t\tdistort = { mode: it.mode, corners, d: corners.map((c, i) => `${i ? 'L' : 'M'}${c.x} ${c.y}`).join('') + 'Z' };\n\t\tstatus = it.mode === 'skew' ? 'Skew' : it.mode === 'perspective' ? 'Perspective' : 'Distort';\n\t}\n\n\tconst envelope = state.tool === 'envelope' ? envelopeOverlay(state) : null;\n\tif (state.tool === 'envelope' && !envelope && !it) status = 'Select an object with an envelope to edit its mesh';\n\n\tlet textBox: VectorBounds | null = null;\n\tif (state.interaction?.type === 'textBox') {\n\t\tconst b = state.interaction.bounds;\n\t\tconst tl = toScreen(state, b);\n\t\ttextBox = { x: tl.x, y: tl.y, width: b.width * z, height: b.height * z };\n\t}\n\treturn { outlines, selectionBox, distort, anchors, handles, marquee, penPreview, artboard, guides, status, toolStroke, pencilString, cutPoint, cornerWidgets, widthPoints, widthSpot, builder, blend, gradient, envelope, masks: maskOverlay(state), text: textOverlay(state), textBox, constraints: constraintOverlay(state), frames: frameLabels(state), connectorSites: connectorSiteOverlay(state),\n\t\ttrackingPoints: (state.trackingPoints ?? []).map((p) => toScreen(state, p)),\n\t\ttypedEntry: state.typedEntry ? { text: state.typedEntry, ...(state.pointer ? toScreen(state, state.pointer) : { x: 0, y: 0 }) } : null\n\t};\n}\n\n/** Mask indicators of the mask groups that are selected or hold the selection (see `VectorOverlayRenderData.masks`). */\nfunction maskOverlay(state: VectorEditorState): VectorOverlayRenderData['masks'] {\n\tconst ids = state.editingId ? [...state.selection, state.editingId] : state.selection;\n\tif (!ids.length) return [];\n\tconst want = new Set(ids);\n\tconst v = state.viewport;\n\tconst view: AffineMatrix = [v.zoom, 0, 0, v.zoom, v.x, v.y];\n\tconst out: VectorOverlayRenderData['masks'] = [];\n\tconst visit = (list: readonly VectorNode[], parent: AffineMatrix): boolean => {\n\t\tlet any = false;\n\t\tfor (const n of list) {\n\t\t\tif (n.type !== 'group') {\n\t\t\t\tif (want.has(n.id)) any = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst m = multiplyMatrices(nodeMatrix(n), parent);\n\t\t\tconst inside = visit(n.children, m);\n\t\t\tconst hit = want.has(n.id) || inside;\n\t\t\tif (hit) any = true;\n\t\t\tif (!hit || !isVectorMaskGroup(n)) continue;\n\t\t\tconst summary = vectorMaskSummary(n)!;\n\t\t\tconst d = outlinePaths(n.children[0], m, new Set())\n\t\t\t\t.map((path) => bezierPathToSvg(transformBezierPath(path, view), { precision: 2 }))\n\t\t\t\t.join(' ');\n\t\t\tout.push({ id: n.id, maskId: summary.maskId, d, type: summary.type, invert: summary.invert, enabled: summary.enabled, editing: want.has(summary.maskId) });\n\t\t}\n\t\treturn any;\n\t};\n\tvisit(state.document.nodes, IDENTITY_MATRIX);\n\treturn out;\n}\n\nfunction envelopeOverlay(state: VectorEditorState): VectorOverlayRenderData['envelope'] {\n\tconst target = getVectorEnvelopeTarget(state.document, state.selection);\n\tif (!target) return null;\n\tconst v = state.viewport;\n\tconst m = multiplyMatrices(target.world, [v.zoom, 0, 0, v.zoom, v.x, v.y]);\n\tconst sc = (p: Vec2) => ({ x: fmt(m[0] * p.x + m[2] * p.y + m[4]), y: fmt(m[1] * p.x + m[3] * p.y + m[5]) });\n\tconst { mesh } = target.frame;\n\tconst grid = vectorMeshGridCurves(mesh)\n\t\t.map((c) => {\n\t\t\tconst [a, b, cc, d] = c.map(sc);\n\t\t\treturn `M${a.x} ${a.y}C${b.x} ${b.y} ${cc.x} ${cc.y} ${d.x} ${d.y}`;\n\t\t})\n\t\t.join('');\n\tconst hover = state.hover?.envelope?.nodeId === target.node.id ? state.hover.envelope.index : undefined;\n\tconst it = state.interaction;\n\tconst active = it?.type === 'envelope' && it.nodeId === target.node.id ? it.index : undefined;\n\tconst pts = vectorMeshEditPoints(mesh);\n\treturn {\n\t\tnodeId: target.node.id,\n\t\tgrid,\n\t\tanchors: pts.anchors.map((index) => ({ index, ...sc(mesh.points[index]), hovered: !it && hover === index, active: active === index })),\n\t\thandles: pts.handles.map((h) => {\n\t\t\tconst a = sc(mesh.points[h.anchor]);\n\t\t\treturn { index: h.index, ...sc(mesh.points[h.index]), anchorX: a.x, anchorY: a.y, hovered: !it && hover === h.index, active: active === h.index };\n\t\t})\n\t};\n}\n\n/** Warp choices of an envelope: the fifteen presets, then Mesh. */\nexport const VECTOR_ENVELOPE_STYLE_OPTIONS: readonly { value: VectorEnvelopeParams['style']; label: string }[] = [\n\t...VECTOR_WARP_STYLES.map((value) => ({ value, label: VECTOR_WARP_STYLE_LABELS[value] })),\n\t{ value: 'mesh', label: 'Mesh' }\n];\n\n/** What a Distort / Envelope panel shows for the selection. */\nexport interface VectorEnvelopePanelData {\n\t/** The one selected node, or null when several are selected. */\n\tnodeId: string | null;\n\tcount: number;\n\t/** The selected node's envelope (the first one in its stack), if any. */\n\tenvelope: { effectId: string; enabled: boolean; params: VectorEnvelopeParams } | null;\n\t/** Some selected node can take an envelope (Make with Warp / Mesh). */\n\tcanMake: boolean;\n\t/** Make with Top Object: several objects selected and the topmost is a closed path. */\n\tcanMakeWithTopObject: boolean;\n\t/** Some selected node has an envelope (Release, Expand). */\n\tcanRelease: boolean;\n}\n\n/** The Distort / Envelope panel of the selection, or null when nothing is selected. */\nexport function getVectorEnvelopePanel(state: Pick<VectorEditorState, 'document' | 'selection'>): VectorEnvelopePanelData | null {\n\tconst nodes = state.selection.map((id) => findVectorNode(state.document.nodes, id)).filter((n): n is VectorNode => !!n && !n.blendStep);\n\tif (!nodes.length) return null;\n\tconst one = nodes.length === 1 ? nodes[0] : null;\n\tconst first = one?.effects?.find((e) => e.type === 'envelope');\n\tconst order = new Map<string, number>();\n\twalkVectorNodes(state.document.nodes, (n) => order.set(n.id, order.size));\n\tconst top = nodes.length > 1 ? nodes.slice().sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0))[nodes.length - 1] : null;\n\treturn {\n\t\tnodeId: one?.id ?? null,\n\t\tcount: nodes.length,\n\t\tenvelope: first?.type === 'envelope' ? { effectId: first.id, enabled: first.enabled !== false, params: first.params } : null,\n\t\tcanMake: nodes.some((n) => !n.locked),\n\t\tcanMakeWithTopObject: top?.type === 'path' && top.subpaths.some((sp) => sp.closed && sp.anchors.length > 1),\n\t\tcanRelease: nodes.some((n) => !n.locked && !!n.effects?.some((e) => e.type === 'envelope'))\n\t};\n}\n\nfunction gradientOverlay(state: VectorEditorState): VectorRenderGradientAnnotator | null {\n\tconst a = getVectorGradientAnnotator(state.document, state.selection, state.viewport, state.gradientEdit.target, state.gradientEdit.layer ?? null);\n\tif (!a) return null;\n\tconst hover = state.interaction ? null : state.hover?.gradient;\n\tconst handleHovered = (h: VectorGradientHandle) => hover?.kind === 'handle' && hover.handle === h;\n\tconst pt = (p: Vec2) => ({ x: fmt(p.x), y: fmt(p.y) });\n\tconst centred = isCentredVectorGradient(a.gradient.type);\n\tconst removing = state.interaction?.type === 'gradient' && !!state.interaction.removing;\n\treturn {\n\t\tnodeId: a.nodeId,\n\t\ttype: a.gradient.type,\n\t\tstart: { ...pt(a.start), hovered: handleHovered('start') },\n\t\tend: { ...pt(a.end), hovered: handleHovered(centred ? 'radius' : 'end'), handle: centred ? 'radius' : 'end' },\n\t\taspect: a.aspect ? { ...pt(a.aspect), hovered: handleHovered('aspect') } : null,\n\t\tfocal: a.focal ? { ...pt(a.focal), hovered: handleHovered('focal') } : null,\n\t\tstops: a.stops.map((s) => ({\n\t\t\tindex: s.index,\n\t\t\t...pt(s),\n\t\t\tbarX: fmt(s.x - a.normal.x * VECTOR_GRADIENT_STOP_GAP),\n\t\t\tbarY: fmt(s.y - a.normal.y * VECTOR_GRADIENT_STOP_GAP),\n\t\t\tcolor: s.color,\n\t\t\topacity: s.opacity ?? 1,\n\t\t\tselected: state.gradientEdit.stop === s.index,\n\t\t\thovered: hover?.kind === 'stop' && hover.index === s.index\n\t\t})),\n\t\tmidpoints: a.midpoints.map((m) => ({ index: m.index, ...pt(m), hovered: hover?.kind === 'midpoint' && hover.index === m.index })),\n\t\textent: a.extent,\n\t\tremoving\n\t};\n}\n\n/** Blend groups the selection covers (a selected key counts as its blend). */\nfunction selectedBlends(state: Pick<VectorEditorState, 'document' | 'selection'>): VectorGroupNode[] {\n\tconst out = new Map<string, VectorGroupNode>();\n\tfor (const id of state.selection) {\n\t\tconst node = findVectorNode(state.document.nodes, id);\n\t\tconst group = isVectorBlendGroup(node) ? node : locateVectorNode(state.document.nodes, id)?.parent;\n\t\tif (isVectorBlendGroup(group)) out.set(group.id, group);\n\t}\n\treturn [...out.values()];\n}\n\nfunction blendOverlay(state: VectorEditorState): VectorOverlayRenderData['blend'] {\n\tconst v = state.viewport;\n\tconst view: AffineMatrix = [v.zoom, 0, 0, v.zoom, v.x, v.y];\n\tconst doc = state.document;\n\tconst sc = (p: Vec2, m: AffineMatrix) => {\n\t\tconst q = { x: m[0] * p.x + m[2] * p.y + m[4], y: m[1] * p.x + m[3] * p.y + m[5] };\n\t\treturn { x: fmt(q.x), y: fmt(q.y) };\n\t};\n\tconst spines = selectedBlends(state).flatMap((g) => {\n\t\tconst s = vectorBlendSpine(g);\n\t\tif (!s) return [];\n\t\tconst m = multiplyMatrices(vectorWorldMatrix(doc.nodes, g.id) ?? IDENTITY_MATRIX, view);\n\t\tconst d = bezierPathToSvg(transformBezierPath(subpathsToBezier([s.spine], { corners: false }), m), { precision: 2 });\n\t\tconst keys = s.stops.map((stop) => {\n\t\t\tconst p = vectorBlendRailAt(s.rail, stop).point;\n\t\t\treturn sc({ x: p[0], y: p[1] }, m);\n\t\t});\n\t\treturn [{ id: g.id, d, keys, replaced: s.replaced }];\n\t});\n\tlet start: VectorOverlayRenderData['blend']['start'] = null;\n\tconst anchors: VectorOverlayRenderData['blend']['anchors'] = [];\n\tif (state.tool === 'blend') {\n\t\tconst from = state.blendStart ? findVectorNode(doc.nodes, state.blendStart.nodeId) : undefined;\n\t\tif (from && state.blendStart) {\n\t\t\tconst a = state.blendStart.anchor === null ? undefined : getAnchorWorld(doc, { nodeId: from.id, subpath: 0, index: state.blendStart.anchor });\n\t\t\tstart = { d: screenOutline(state, from, new Set()), anchor: a ? sc(a, view) : null };\n\t\t}\n\t\tconst hovered = state.hover?.nodeId ? findVectorNode(doc.nodes, state.hover.nodeId) : undefined;\n\t\tif (hovered?.type === 'path' && hovered.subpaths[0]) {\n\t\t\thovered.subpaths[0].anchors.forEach((_, index) => {\n\t\t\t\tconst a = getAnchorWorld(doc, { nodeId: hovered.id, subpath: 0, index });\n\t\t\t\tif (a) anchors.push({ ...sc(a, view), hovered: state.hover?.anchor?.index === index });\n\t\t\t});\n\t\t}\n\t}\n\treturn { spines, start, anchors };\n}\n\n/** What the Blend controls of a toolbar or style panel show. */\nexport interface VectorBlendControls {\n\t/** Ids of the selected blends (a selected key counts as its blend). */\n\tids: string[];\n\t/** The first selected blend's options, or the options new blends get. */\n\toptions: Required<VectorBlendOptions>;\n\t/** Two or more shapes that share a parent are selected: Blend can run. */\n\tcanBlend: boolean;\n\t/** One blend and one path are selected: Replace Spine can run. */\n\tcanReplaceSpine: boolean;\n\t/** The first selected blend follows a replaced spine. */\n\thasSpine: boolean;\n}\n\n/** State of the Blend controls for the current selection. */\nexport function getVectorBlendControls(state: Pick<VectorEditorState, 'document' | 'selection' | 'blendDefaults'>): VectorBlendControls {\n\tconst blends = selectedBlends(state);\n\tconst doc = state.document;\n\tconst roots = state.selection.filter((id) => {\n\t\tconst node = findVectorNode(doc.nodes, id);\n\t\treturn !!node && !node.blendStep && !node.locked && !state.selection.includes(locateVectorNode(doc.nodes, id)?.parent?.id ?? '');\n\t});\n\tconst parents = new Set(roots.map((id) => locateVectorNode(doc.nodes, id)?.parent?.id ?? null));\n\tconst paths = roots.filter((id) => findVectorNode(doc.nodes, id)?.type === 'path' && !isVectorBlendGroup(locateVectorNode(doc.nodes, id)?.parent));\n\tconst first = blends[0];\n\treturn {\n\t\tids: blends.map((g) => g.id),\n\t\toptions: resolveVectorBlendOptions(first ? first.blend : state.blendDefaults),\n\t\tcanBlend: roots.length >= 2 && parents.size === 1,\n\t\tcanReplaceSpine: blends.length === 1 && paths.length === 1,\n\t\thasSpine: !!first?.blend?.spine\n\t};\n}\n\n/**\n * Applies blend options from a toolbar: to the selected blends (one undo step) and to the\n * options new blends get, so the controls keep showing what was picked.\n */\nexport function applyVectorBlendOptions(controller: VectorEditorController, patch: Partial<VectorBlendOptions>): void {\n\tcontroller.setBlendDefaults(patch);\n\tif (getVectorBlendControls(controller.getState()).ids.length) controller.setBlendOptions(patch);\n}\n\n/** Labels of the blend spacing modes, for toolbars. */\nexport const VECTOR_BLEND_SPACING_LABELS: Readonly<Record<VectorBlendSpacing, string>> = { steps: 'Specified Steps', distance: 'Specified Distance', smoothColor: 'Smooth Color' };\n\n// ---------------------------------------------------------------------------\n// Event adapters (duck-typed, so they work with DOM, React and Svelte events)\n// ---------------------------------------------------------------------------\n\n/** Colours and sizes (screen pixels) the bundled canvases draw the overlay with. */\nexport const VECTOR_OVERLAY_THEME = {\n\taccent: '#0ea5e9',\n\t/** Smart guide lines and labels. */\n\tguide: '#ff3dfc',\n\thandleFill: '#ffffff',\n\tmarqueeFill: 'rgba(14, 165, 233, 0.08)',\n\t/** The eraser's swath while it is dragged. */\n\teraserFill: 'rgba(239, 68, 68, 0.25)',\n\ttransformHandleSize: 8,\n\tanchorSize: 7,\n\thandleRadius: 3.5,\n\t/** Radius of a live corner widget (a ring with a dot). */\n\tcornerWidgetRadius: 4,\n\t/** Half the diagonal of a width point's diamond. */\n\twidthPointSize: 5,\n\t/** Shape Builder faces to merge and paint bucket regions. */\n\tbuilderFill: 'rgba(14, 165, 233, 0.22)',\n\t/** Shape Builder faces and line pieces an Alt gesture deletes. */\n\tbuilderErase: '#ef4444',\n\tbuilderEraseFill: 'rgba(239, 68, 68, 0.22)',\n\t/** Opacity of the hatch lines over Shape Builder faces (drawn in the face's outline colour). */\n\tbuilderHatchOpacity: 0.6,\n\t/** Mask indicators: the outline of a selected mask group's mask (dashed; solid while the mask is selected). */\n\tmask: '#8b5cf6',\n\tmaskDash: '5 3'\n} as const;\n\n/** SVG path data of the diamond a width point is drawn with (screen pixels). */\nexport function vectorDiamondPath(x: number, y: number, r: number = VECTOR_OVERLAY_THEME.widthPointSize): string {\n\treturn `M${fmt(x)} ${fmt(y - r)}L${fmt(x + r)} ${fmt(y)}L${fmt(x)} ${fmt(y + r)}L${fmt(x - r)} ${fmt(y)}Z`;\n}\n\ninterface ModifierEventLike {\n\tshiftKey?: boolean;\n\taltKey?: boolean;\n\tmetaKey?: boolean;\n\tctrlKey?: boolean;\n}\n\n/** Modifiers of a mouse / pointer / key event (`mod` is Cmd or Ctrl). */\nexport function toVectorModifiers(event: ModifierEventLike): { shift: boolean; alt: boolean; mod: boolean } {\n\treturn { shift: !!event.shiftKey, alt: !!event.altKey, mod: !!(event.metaKey || event.ctrlKey) };\n}\n\n/**\n * A pointer event as `VectorEditorController.pointerDown/Move/Up` input: modifiers, button,\n * and the pressure and pointer type the pencil records as a width profile.\n */\nexport function toVectorPointerInput(event: ModifierEventLike & { button?: number; pressure?: number; pointerType?: string }) {\n\treturn {\n\t\t...toVectorModifiers(event),\n\t\tbutton: event.button,\n\t\t...(typeof event.pressure === 'number' ? { pressure: event.pressure } : {}),\n\t\t...(event.pointerType ? { pointerType: event.pointerType } : {})\n\t};\n}\n\n/** A key event as `VectorEditorController.keyDown/keyUp` input. */\nexport function toVectorKeyInput(event: ModifierEventLike & { key: string }) {\n\treturn { key: event.key, ...toVectorModifiers(event) };\n}\n\n/** True for targets that should keep their own keys (inputs, textareas, contenteditable). */\nexport function isVectorTextTarget(target: unknown): boolean {\n\tconst el = target as { tagName?: string; isContentEditable?: boolean; getAttribute?: (name: string) => string | null } | null;\n\tif (!el || typeof el !== 'object') return false;\n\tconst tag = (el.tagName ?? '').toUpperCase();\n\tif (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true;\n\treturn !!el.isContentEditable || el.getAttribute?.('contenteditable') === 'true';\n}\n\n// ---------------------------------------------------------------------------\n// Style panel helpers\n// ---------------------------------------------------------------------------\n\n/** Style shown in a style panel: the first selected path's, with `mixed` flags when paths differ. */\nexport interface VectorSelectionStyle {\n\tfill: VectorPaint;\n\tstroke: VectorPaint;\n\tstrokeWidth: number;\n\topacity: number;\n\tstrokeAlign: VectorStrokeAlign;\n\tstartArrow: VectorArrowhead | null;\n\tendArrow: VectorArrowhead | null;\n\tarrowAlign: VectorArrowAlign;\n\tstartArrowScale: number;\n\tendArrowScale: number;\n\t/** The first path's width profile (null when uniform) and the preset it matches. */\n\tstrokeWidthProfile: VectorWidthPoint[] | null;\n\twidthProfile: VectorWidthPreset | 'custom';\n\t/**\n\t * Live corners of the selected anchors (direct tool) or of every corner of the selected\n\t * paths: the first one's radius and kind, and how many corners there are (0: none to round).\n\t */\n\tcornerRadius: number;\n\tcornerKind: VectorCornerKind;\n\t/** Squircle smoothing (0–1) of the first corner. */\n\tcornerSmoothing: number;\n\tcorners: number;\n\t/** Some selected path is open, so arrowheads apply. */\n\thasOpenPath: boolean;\n\tmixed: {\n\t\tfill: boolean;\n\t\tstroke: boolean;\n\t\tstrokeWidth: boolean;\n\t\topacity: boolean;\n\t\tstrokeAlign: boolean;\n\t\tstartArrow: boolean;\n\t\tendArrow: boolean;\n\t\tarrowAlign: boolean;\n\t\tstartArrowScale: boolean;\n\t\tendArrowScale: boolean;\n\t\twidthProfile: boolean;\n\t\tcornerRadius: boolean;\n\t\tcornerKind: boolean;\n\t};\n\t/** Ids of the paths the style applies to. */\n\tpathIds: string[];\n}\n\nexport function getVectorSelectionStyle(\n\tstate: Pick<VectorEditorState, 'document' | 'selection'> & Partial<Pick<VectorEditorState, 'selectedAnchors' | 'tool'>>\n): VectorSelectionStyle | null {\n\tconst paths: VectorPathNode[] = [];\n\tfor (const id of state.selection) {\n\t\tconst node = findVectorNode(state.document.nodes, id);\n\t\tif (node) paths.push(...collect(node));\n\t}\n\tconst anchorMode = state.tool === 'direct' && !!state.selectedAnchors?.length;\n\tif (anchorMode) {\n\t\tfor (const r of state.selectedAnchors!) {\n\t\t\tif (paths.some((p) => p.id === r.nodeId)) continue;\n\t\t\tconst node = findVectorNode(state.document.nodes, r.nodeId);\n\t\t\tif (node?.type === 'path') paths.push(node);\n\t\t}\n\t}\n\tif (!paths.length) return null;\n\tconst first = paths[0];\n\tconst same = (get: (p: VectorPathNode) => unknown) => paths.every((p) => JSON.stringify(get(p) ?? null) === JSON.stringify(get(first) ?? null));\n\t// Corners: those of the selected anchors in the direct tool, else every corner of the paths.\n\tconst corners: { radius: number; kind: VectorCornerKind; smoothing: number }[] = [];\n\tfor (const p of paths) {\n\t\tfor (const c of vectorCorners(p.subpaths)) {\n\t\t\tif (anchorMode && !state.selectedAnchors!.some((r) => r.nodeId === p.id && r.subpath === c.subpath && r.index === c.index)) continue;\n\t\t\tconst corner = p.subpaths[c.subpath].anchors[c.index].corner;\n\t\t\tcorners.push({ radius: corner?.radius ?? 0, kind: corner?.kind ?? 'round', smoothing: corner?.smoothing ?? 0 });\n\t\t}\n\t}\n\tconst c0 = corners[0] ?? { radius: 0, kind: 'round' as const, smoothing: 0 };\n\treturn {\n\t\tfill: first.fill ?? null,\n\t\tstroke: first.stroke ?? null,\n\t\tstrokeWidth: first.strokeWidth ?? 1,\n\t\topacity: first.opacity ?? 1,\n\t\tstrokeAlign: first.strokeAlign ?? 'center',\n\t\tstartArrow: first.startArrow ?? null,\n\t\tendArrow: first.endArrow ?? null,\n\t\tarrowAlign: first.arrowAlign ?? 'extend',\n\t\tstartArrowScale: first.startArrowScale ?? 1,\n\t\tendArrowScale: first.endArrowScale ?? 1,\n\t\tstrokeWidthProfile: profileOf(first),\n\t\twidthProfile: widthProfilePresetId(profileOf(first)),\n\t\tcornerRadius: c0.radius,\n\t\tcornerKind: c0.kind,\n\t\tcornerSmoothing: c0.smoothing,\n\t\tcorners: corners.length,\n\t\thasOpenPath: paths.some((p) => p.subpaths.some((sp) => !sp.closed && sp.anchors.length > 1)),\n\t\tmixed: {\n\t\t\tfill: !same((p) => p.fill),\n\t\t\tstroke: !same((p) => p.stroke),\n\t\t\tstrokeWidth: !same((p) => p.strokeWidth ?? 1),\n\t\t\topacity: !same((p) => p.opacity ?? 1),\n\t\t\tstrokeAlign: !same((p) => p.strokeAlign ?? 'center'),\n\t\t\tstartArrow: !same((p) => p.startArrow ?? null),\n\t\t\tendArrow: !same((p) => p.endArrow ?? null),\n\t\t\tarrowAlign: !same((p) => p.arrowAlign ?? 'extend'),\n\t\t\tstartArrowScale: !same((p) => p.startArrowScale ?? 1),\n\t\t\tendArrowScale: !same((p) => p.endArrowScale ?? 1),\n\t\t\twidthProfile: !same(profileOf),\n\t\t\tcornerRadius: corners.some((c) => c.radius !== c0.radius),\n\t\t\tcornerKind: corners.some((c) => c.kind !== c0.kind)\n\t\t},\n\t\tpathIds: paths.map((p) => p.id)\n\t};\n}\n\nfunction profileOf(p: VectorPathNode): VectorWidthPoint[] | null {\n\tconst points = sanitizeWidthProfile(p.strokeWidthProfile);\n\treturn points.length && widthProfilePresetId(points) !== 'uniform' ? points : null;\n}\n\n/** Labels of the stroke alignments, corner kinds and arrowheads, for style panels. */\nexport const VECTOR_STROKE_ALIGN_LABELS: Readonly<Record<VectorStrokeAlign, string>> = { center: 'Center', inside: 'Inside', outside: 'Outside' };\nexport const VECTOR_CORNER_KIND_LABELS: Readonly<Record<VectorCornerKind, string>> = { round: 'Round', inverted: 'Inverted', chamfer: 'Chamfer' };\nexport const VECTOR_ARROWHEAD_LABELS: Readonly<Record<VectorArrowhead, string>> = {\n\ttriangle: 'Triangle',\n\tarrow: 'Arrow',\n\topen: 'Open arrow',\n\tcircle: 'Circle',\n\tsquare: 'Square',\n\tbar: 'Bar'\n};\n\n/** A paint as `#rrggbb` for `<input type=\"color\">`, or null for none, gradients and other CSS colours. */\nexport function vectorPaintToHex(paint: VectorPaint | undefined): string | null {\n\tif (typeof paint !== 'string') return null;\n\tconst m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(paint.trim());\n\tif (!m) return null;\n\tconst hex = m[1].length === 3 ? m[1].replace(/./g, (c) => c + c) : m[1];\n\treturn `#${hex.toLowerCase()}`;\n}\n\n// ---------------------------------------------------------------------------\n// Gradient editor\n// ---------------------------------------------------------------------------\n\nexport const VECTOR_GRADIENT_TYPE_LABELS: Readonly<Record<VectorGradientType, string>> = { linear: 'Linear', radial: 'Radial', conic: 'Conic', reflected: 'Reflected', diamond: 'Diamond' };\nexport const VECTOR_GRADIENT_SPREAD_LABELS: Readonly<Record<VectorGradientSpread, string>> = { pad: 'Pad', reflect: 'Reflect', repeat: 'Repeat' };\nexport const VECTOR_GRADIENT_INTERPOLATION_LABELS: Readonly<Record<VectorGradientInterpolation, string>> = { srgb: 'sRGB', linear: 'Linear light', oklab: 'Oklab' };\nexport const VECTOR_STROKE_GRADIENT_MODE_LABELS: Readonly<Record<VectorStrokeGradientMode, string>> = { within: 'Within stroke', along: 'Along stroke', across: 'Across stroke' };\n\n/** What a gradient editor shows for the selection's `target` paint (see `getVectorGradientEditor`). */\nexport interface VectorGradientEditorData {\n\ttarget: 'fill' | 'stroke';\n\t/** The layer of the `target` stack edited (null: the top one). */\n\tlayer: number | null;\n\t/** The first selected path's paint is a gradient. */\n\tgradient: VectorGradient | null;\n\t/** The paint as a `#rrggbb` colour when it is one (what a new gradient starts from). */\n\tcolor: string | null;\n\t/** Sorted stops; `index` is what `setGradientStop` takes. */\n\tstops: (VectorGradientStop & { index: number; selected: boolean })[];\n\tselected: (VectorGradientStop & { index: number; last: boolean }) | null;\n\t/** A CSS `linear-gradient(to right, …)` of the ramp (midpoints and colour space expanded). */\n\tpreview: string | null;\n\ttype: VectorGradientType | null;\n\tspread: VectorGradientSpread;\n\tinterpolation: VectorGradientInterpolation;\n\tsmoothness: number;\n\tstrokeMode: VectorStrokeGradientMode;\n\t/** The selected paths' gradients differ. */\n\tmixed: boolean;\n}\n\n/** The gradient editor of the selection's fill or stroke (`state.gradientEdit.target`), or null with nothing selected. */\nexport function getVectorGradientEditor(state: Pick<VectorEditorState, 'document' | 'selection' | 'gradientEdit'>): VectorGradientEditorData | null {\n\tconst paths: VectorPathNode[] = [];\n\tfor (const id of state.selection) {\n\t\tconst node = findVectorNode(state.document.nodes, id);\n\t\tif (node) paths.push(...collect(node));\n\t}\n\tif (!paths.length) return null;\n\tconst { target, layer } = state.gradientEdit;\n\tconst paint = vectorTargetPaint(paths[0], target, layer ?? null);\n\tconst gradient = isVectorGradient(paint) ? paint : null;\n\tconst sorted = gradient ? sortedVectorGradientStops(gradient) : [];\n\tconst stopIndex = state.gradientEdit.stop;\n\tconst stops = sorted.map((s, index) => ({ ...s, index, selected: index === stopIndex }));\n\tconst sel = stopIndex !== null && sorted[stopIndex] ? { ...sorted[stopIndex], index: stopIndex, last: stopIndex === sorted.length - 1 } : null;\n\tconst preview = gradient\n\t\t? `linear-gradient(to right, ${expandVectorGradientStops(gradient)\n\t\t\t\t.map((s) => {\n\t\t\t\t\tconst c = parseVectorColor(s.color);\n\t\t\t\t\tconst a = (c?.[3] ?? 1) * (s.opacity ?? 1);\n\t\t\t\t\tconst color = c ? `rgba(${Math.round(c[0] * 255)}, ${Math.round(c[1] * 255)}, ${Math.round(c[2] * 255)}, ${Math.round(a * 1000) / 1000})` : s.color;\n\t\t\t\t\treturn `${color} ${Math.round(s.offset * 10000) / 100}%`;\n\t\t\t\t})\n\t\t\t\t.join(', ')})`\n\t\t: null;\n\treturn {\n\t\ttarget,\n\t\tlayer: layer ?? null,\n\t\tgradient,\n\t\tcolor: vectorPaintToHex(paint),\n\t\tstops,\n\t\tselected: sel,\n\t\tpreview,\n\t\ttype: gradient?.type ?? null,\n\t\tspread: gradient?.spread ?? 'pad',\n\t\tinterpolation: gradient?.interpolation ?? 'srgb',\n\t\tsmoothness: gradient?.smoothness ?? 0,\n\t\tstrokeMode: gradient?.strokeMode ?? 'within',\n\t\tmixed: paths.some((p) => JSON.stringify(vectorTargetPaint(p, target, layer ?? null) ?? null) !== JSON.stringify(paint ?? null))\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Toolbar helpers\n// ---------------------------------------------------------------------------\n\n/** A path-editing button of the bundled toolbars. */\nexport interface VectorPathAction {\n\tid: 'join' | 'average' | 'cut' | 'clean' | 'smooth' | 'outline';\n\tlabel: string;\n\ttitle: string;\n\t/** Shortcut after Cmd / Ctrl, if any. */\n\tshortcut?: string;\n\t/** Whether it can run with `selected` nodes and `points` selected anchors (direct tool). */\n\tenabled: (selected: number, points: number) => boolean;\n\trun: (controller: VectorEditorController) => unknown;\n}\n\n/** Join, Average, Cut at Points, Remove Redundant Points, Smooth and Outline Stroke, as the toolbars show them. */\nexport const VECTOR_PATH_ACTIONS: readonly VectorPathAction[] = [\n\t{ id: 'join', label: '⋈', title: 'Join', shortcut: 'J', enabled: (s, p) => s + p > 0, run: (c) => c.joinPaths() },\n\t{ id: 'average', label: '⊹', title: 'Average Points', shortcut: 'Alt+J', enabled: (_s, p) => p > 1, run: (c) => c.averageAnchors('both') },\n\t{ id: 'cut', label: '✂·', title: 'Cut at Points', enabled: (_s, p) => p > 0, run: (c) => c.cutAtAnchors() },\n\t{ id: 'clean', label: '⌁', title: 'Remove Redundant Points', enabled: (s, p) => s + p > 0, run: (c) => c.removeRedundantPoints() },\n\t{ id: 'smooth', label: '∿', title: 'Smooth', enabled: (s, p) => s + p > 0, run: (c) => c.smoothPaths() },\n\t{ id: 'outline', label: '⊡', title: 'Outline Stroke', enabled: (s) => s > 0, run: (c) => c.outlineStroke() }\n];\n\n/** A Pathfinder button of the bundled toolbars. */\nexport interface VectorPathfinderAction {\n\tid: VectorPathfinderOperation;\n\tlabel: string;\n\ttitle: string;\n\t/** Paths (or groups) it needs selected. */\n\tmin: number;\n}\n\n/** Divide, Trim, Merge, Crop, Outline and Minus Back, as the toolbars show them. */\nexport const VECTOR_PATHFINDER_ACTIONS: readonly VectorPathfinderAction[] = [\n\t{ id: 'divide', label: '⊞', title: 'Divide', min: 1 },\n\t{ id: 'trim', label: '⧉', title: 'Trim', min: 2 },\n\t{ id: 'merge', label: '⧈', title: 'Merge', min: 2 },\n\t{ id: 'crop', label: '⌗', title: 'Crop', min: 2 },\n\t{ id: 'outline', label: '⬚', title: 'Outline', min: 1 },\n\t{ id: 'minusBack', label: '⊖', title: 'Minus Back', min: 2 }\n];\n\n/** The on / off path-editing settings the toolbars toggle. */\nexport const VECTOR_EDIT_TOGGLES: readonly { key: 'keepShapeOnDelete' | 'curvatureMoves' | 'recognizeShapes'; label: string; title: string }[] = [\n\t{ key: 'keepShapeOnDelete', label: 'Keep', title: 'Keep shape when deleting points' },\n\t{ key: 'curvatureMoves', label: 'Curv', title: 'Curvature point moves (Alt flips while dragging)' },\n\t{ key: 'recognizeShapes', label: 'Shapes', title: 'Turn pencil scribbles into shapes' }\n];\n\n\n// ---------------------------------------------------------------------------\n// Effects panel\n// ---------------------------------------------------------------------------\n\n/** One effect of the listed stack, with what a panel needs to edit it. */\nexport interface VectorEffectsPanelItem {\n\tid: string;\n\ttype: VectorEffectType;\n\tlabel: string;\n\tenabled: boolean;\n\tparams: Record<string, unknown>;\n\t/** The parameters to edit (none for an envelope: `getVectorEnvelopePanel` and the Envelope panels edit it). */\n\tspecs: readonly VectorEffectParamSpec[];\n}\n\n/** What an Effects panel shows for the selection. */\nexport interface VectorEffectsPanelData {\n\t/** The node whose stack is listed (when one node is selected), else null. */\n\tnodeId: string | null;\n\t/** Selected nodes an added effect goes to. */\n\tcount: number;\n\t/** The listed stack, applied top to bottom. */\n\teffects: VectorEffectsPanelItem[];\n\t/** Something selected is live (Expand Appearance applies). */\n\texpandable: boolean;\n}\n\n/** Effect types in menu order, with their names. */\nexport const VECTOR_EFFECT_MENU: readonly { type: VectorEffectType; label: string }[] = VECTOR_EFFECT_TYPES.map((type) => ({ type, label: VECTOR_EFFECT_LABELS[type] }));\n\n/** The Effects panel of the selection, or null when nothing that takes effects is selected. */\nexport function getVectorEffectsPanel(state: Pick<VectorEditorState, 'document' | 'selection'>): VectorEffectsPanelData | null {\n\tconst nodes = state.selection.map((id) => findVectorNode(state.document.nodes, id)).filter((n): n is VectorNode => !!n && !n.blendStep);\n\tif (!nodes.length) return null;\n\tconst one = nodes.length === 1 ? nodes[0] : null;\n\treturn {\n\t\tnodeId: one?.id ?? null,\n\t\tcount: nodes.length,\n\t\teffects: (one?.effects ?? []).map((e) => ({\n\t\t\tid: e.id,\n\t\t\ttype: e.type,\n\t\t\tlabel: VECTOR_EFFECT_LABELS[e.type] ?? e.type,\n\t\t\tenabled: e.enabled !== false,\n\t\t\tparams: e.params as unknown as Record<string, unknown>,\n\t\t\t// The Envelope panel (`getVectorEnvelopePanel`) edits an envelope's warp and mesh.\n\t\t\tspecs: e.type === 'envelope' ? [] : (VECTOR_EFFECT_PARAM_SPECS[e.type] ?? [])\n\t\t})),\n\t\texpandable: nodes.some(hasVectorAppearance)\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Appearance panel: fill and stroke stacks, blend mode, filters\n// ---------------------------------------------------------------------------\n\n/** One fill or stroke layer as an appearance panel lists it. */\nexport interface VectorAppearancePanelLayer {\n\tkind: 'fill' | 'stroke';\n\t/** Index in its stack (0 is the bottom); what `updatePaintLayer` and friends take. */\n\tindex: number;\n\tpaint: VectorPaint;\n\t/** The paint as `#rrggbb` when it is one (for a colour input). */\n\tcolor: string | null;\n\t/** A CSS `background` showing the paint (a colour, or the gradient's ramp). */\n\tswatch: string;\n\tgradient: boolean;\n\topacity: number;\n\tblendMode: VectorBlendMode;\n\tvisible: boolean;\n\t/** Stroke layers: width and alignment (for the compact row). */\n\tstrokeWidth?: number;\n\tstrokeAlign?: VectorStrokeAlign;\n\t/** A stroke of a kind this version cannot draw (kept, shown read-only). */\n\tunknownKind?: string;\n\t/** The gradient editor and tool work on this layer. */\n\tediting: boolean;\n}\n\n/** What an appearance panel shows for the selection. */\nexport interface VectorAppearancePanelData {\n\t/** Selected nodes (blend mode and filters apply to them). */\n\tnodeIds: string[];\n\t/** Paths the stacks belong to (groups give every path inside); the first one's stacks are listed. */\n\tpathIds: string[];\n\tfills: VectorAppearancePanelLayer[];\n\tstrokes: VectorAppearancePanelLayer[];\n\t/** The first selected node's blend mode. */\n\tblendMode: VectorBlendMode;\n\tmixed: { fills: boolean; strokes: boolean; blendMode: boolean };\n\t/** The filter stack, listed when one node is selected (`filterNodeId`). */\n\tfilterNodeId: string | null;\n\tfilters: VectorFiltersPanelItem[];\n}\n\n/** One filter of the listed stack, with what a panel needs to edit it. */\nexport interface VectorFiltersPanelItem {\n\tid: string;\n\ttype: VectorFilterType;\n\tlabel: string;\n\tenabled: boolean;\n\tparams: Record<string, unknown>;\n\tspecs: readonly VectorFilterParamSpec[];\n}\n\n/** A CSS background for a paint: the colour, or a left-to-right ramp of a gradient's stops. */\nexport function vectorPaintSwatch(paint: VectorPaint | undefined): string {\n\tif (typeof paint === 'string') return paint;\n\tif (isVectorGradient(paint)) {\n\t\treturn `linear-gradient(to right, ${expandVectorGradientStops(paint)\n\t\t\t.map((s) => {\n\t\t\t\tconst c = parseVectorColor(s.color);\n\t\t\t\tconst a = (c?.[3] ?? 1) * (s.opacity ?? 1);\n\t\t\t\tconst color = c ? `rgba(${Math.round(c[0] * 255)}, ${Math.round(c[1] * 255)}, ${Math.round(c[2] * 255)}, ${Math.round(a * 1000) / 1000})` : s.color;\n\t\t\t\treturn `${color} ${Math.round(s.offset * 10000) / 100}%`;\n\t\t\t})\n\t\t\t.join(', ')})`;\n\t}\n\treturn 'transparent';\n}\n\n/** The appearance panel of the selection, or null with nothing selected. */\nexport function getVectorAppearancePanel(state: Pick<VectorEditorState, 'document' | 'selection' | 'gradientEdit'>): VectorAppearancePanelData | null {\n\tconst nodes = state.selection.map((id) => findVectorNode(state.document.nodes, id)).filter((n): n is VectorNode => !!n && !n.blendStep);\n\tif (!nodes.length) return null;\n\tconst paths: VectorPathNode[] = [];\n\tfor (const n of nodes) for (const p of collect(n)) if (!p.blendStep && !paths.some((q) => q.id === p.id)) paths.push(p);\n\tconst first = paths[0];\n\tconst edit = state.gradientEdit;\n\tconst layer = (kind: 'fill' | 'stroke', l: VectorStrokeLayer, index: number, count: number): VectorAppearancePanelLayer => ({\n\t\tkind,\n\t\tindex,\n\t\tpaint: l.paint,\n\t\tcolor: vectorPaintToHex(l.paint),\n\t\tswatch: vectorPaintSwatch(l.paint),\n\t\tgradient: isVectorGradient(l.paint),\n\t\topacity: l.opacity ?? 1,\n\t\tblendMode: l.blendMode ?? 'normal',\n\t\tvisible: l.visible !== false,\n\t\t...(kind === 'stroke' ? { strokeWidth: l.strokeWidth ?? 1, strokeAlign: l.strokeAlign ?? 'center' } : {}),\n\t\t...((l.kind ?? 'paint') !== 'paint' ? { unknownKind: String(l.kind) } : {}),\n\t\tediting: edit.target === kind && (edit.layer === null ? index === count - 1 : edit.layer === index)\n\t});\n\tconst fills = first ? vectorFills(first) : [];\n\tconst strokes = first ? vectorStrokes(first) : [];\n\tconst same = (get: (p: VectorPathNode) => unknown) => paths.every((p) => JSON.stringify(get(p)) === JSON.stringify(get(first)));\n\tconst one = nodes.length === 1 ? nodes[0] : null;\n\treturn {\n\t\tnodeIds: nodes.map((n) => n.id),\n\t\tpathIds: paths.map((p) => p.id),\n\t\tfills: fills.map((l, i) => layer('fill', l, i, fills.length)),\n\t\tstrokes: strokes.map((l, i) => layer('stroke', l, i, strokes.length)),\n\t\tblendMode: nodes[0].blendMode ?? 'normal',\n\t\tmixed: {\n\t\t\tfills: !!first && !same(vectorFills),\n\t\t\tstrokes: !!first && !same(vectorStrokes),\n\t\t\tblendMode: nodes.some((n) => (n.blendMode ?? 'normal') !== (nodes[0].blendMode ?? 'normal'))\n\t\t},\n\t\tfilterNodeId: one?.id ?? null,\n\t\tfilters: (one?.filters ?? []).map((f) => ({\n\t\t\tid: f.id,\n\t\t\ttype: f.type,\n\t\t\tlabel: VECTOR_FILTER_LABELS[f.type] ?? f.type,\n\t\t\tenabled: f.enabled !== false,\n\t\t\tparams: f.params as unknown as Record<string, unknown>,\n\t\t\tspecs: VECTOR_FILTER_PARAM_SPECS[f.type] ?? []\n\t\t}))\n\t};\n}\n\n/** Blend modes in menu order, with their names. */\nexport const VECTOR_BLEND_MODE_MENU: readonly { mode: VectorBlendMode; label: string }[] = VECTOR_BLEND_MODES.map((mode) => ({ mode, label: VECTOR_BLEND_MODE_LABELS[mode] }));\n\n// ---------------------------------------------------------------------------\n// Masks panel\n// ---------------------------------------------------------------------------\n\nexport const VECTOR_MASK_TYPE_LABELS: Readonly<Record<VectorMaskType, string>> = { clip: 'Clip', alpha: 'Alpha', luminance: 'Luminance' };\n/** Mask types for a menu, with a hint each. */\nexport const VECTOR_MASK_TYPE_OPTIONS: readonly { value: VectorMaskType; label: string; title: string }[] = [\n\t{ value: 'clip', label: 'Clip', title: 'Clipping mask: the contents show inside the mask\\'s outline' },\n\t{ value: 'luminance', label: 'Luminance', title: 'Opacity mask by brightness: white shows, black hides' },\n\t{ value: 'alpha', label: 'Alpha', title: 'Opacity mask by opacity: opaque shows, transparent hides' }\n];\n\n/** What the mask panel shows for the selection. */\nexport interface VectorMaskPanelData {\n\t/** Make Clipping Mask / Make Opacity Mask would work: two or more unlocked objects sharing a parent. */\n\tcanMake: boolean;\n\t/** Why the make actions are off, for a tooltip (null when they are on). */\n\tmakeHint: string | null;\n\t/**\n\t * The mask group the selection is or is directly inside, with its settings and what of it is\n\t * selected (`group` itself, its `mask` or its `contents`); null otherwise.\n\t */\n\tgroup: (VectorMaskSummary & { id: string; name?: string; editing: 'group' | 'mask' | 'contents' }) | null;\n}\n\n/** The mask panel's data for the selection, or null with nothing selected. */\nexport function getVectorMaskPanel(state: Pick<VectorEditorState, 'document' | 'selection'>): VectorMaskPanelData | null {\n\tconst doc = state.document;\n\tif (!state.selection.length) return null;\n\tconst locs = state.selection.map((id) => locateVectorNode(doc.nodes, id));\n\tlet makeHint: string | null = null;\n\tif (state.selection.length < 2) makeHint = 'Select two or more objects: the topmost becomes the mask.';\n\telse if (locs.some((l) => !l)) makeHint = 'Select objects in the document.';\n\telse if (locs.some((l) => l!.list[l!.index].locked || l!.list[l!.index].blendStep)) makeHint = 'Unlock the selected objects first.';\n\telse if (locs.some((l) => (l!.parent?.id ?? null) !== (locs[0]!.parent?.id ?? null))) makeHint = 'Select objects at one level (sharing a parent).';\n\tlet group: VectorMaskPanelData['group'] = null;\n\tconst first = locs[0];\n\tconst node = first ? first.list[first.index] : null;\n\tconst owner = isVectorMaskGroup(node) ? node : isVectorMaskGroup(first?.parent) ? first!.parent : null;\n\tif (owner && state.selection.every((id) => id === owner.id || owner.children.some((c) => c.id === id))) {\n\t\tconst summary = vectorMaskSummary(owner)!;\n\t\tconst editing = state.selection.includes(owner.id) ? 'group' : state.selection.includes(summary.maskId) ? 'mask' : 'contents';\n\t\tgroup = { ...summary, id: owner.id, ...(owner.name ? { name: owner.name } : {}), editing };\n\t}\n\treturn { canMake: makeHint === null, makeHint, group };\n}\n","import type { CommandSpec } from '../commands/registry.js';\nimport type { VectorConstraintInput, VectorEditorController, VectorShapeInput } from './controller.js';\nimport { vectorNodeBounds, vectorNodeRotation, vectorParentMatrix, vectorPathData, IDENTITY_MATRIX } from './geometry.js';\nimport type { VectorConstraintKind, VectorMatrix, VectorTextNode } from './types.js';\nimport { VECTOR_CONSTRAINT_KINDS } from './types.js';\nimport { vectorConstraintEnv, vectorConstraintResiduals, vectorConstraintValue } from './constraints/solve.js';\nimport type { VectorAlign, VectorAnchorRef, VectorArrange, VectorAverageAxis, VectorBooleanOperation, VectorCornerKind, VectorNode, VectorPathfinderOperation, VectorStyle, VectorTool } from './types.js';\nimport { VECTOR_ARROWHEADS, VECTOR_CORNER_KINDS, VECTOR_EFFECT_TYPES, VECTOR_PATHFINDER_OPERATIONS, VECTOR_TOOLS, type VectorEffect, type VectorEffectType, type VectorWidthPoint } from './types.js';\nimport { VECTOR_WIDTH_PRESET_IDS, type VectorTaperOptions, type VectorWaveOptions, type VectorWidthPreset } from './width.js';\nimport { resolveVectorBlendOptions, vectorBlendKeys } from './blendGroup.js';\nimport { VECTOR_BLEND_SPACINGS, VECTOR_GRADIENT_INTERPOLATIONS, VECTOR_GRADIENT_SPREADS, VECTOR_GRADIENT_TYPES, VECTOR_STROKE_GRADIENT_MODES, type VectorBlendOptions } from './types.js';\nimport { sanitizeVectorGradient, type VectorGradientPatch } from './gradient.js';\nimport { isVectorBlendMode, vectorFills, vectorStrokes } from './paintLayers.js';\nimport { vectorFilterParamKeys } from './filters.js';\nimport { VECTOR_BLEND_MODES, VECTOR_FILTER_TYPES, type VectorBlendMode, type VectorFilter, type VectorFilterType, type VectorPaint, type VectorPaintLayer, type VectorStrokeLayer } from './types.js';\nimport { isValidVectorMesh, VECTOR_WARP_STYLES } from './warp.js';\nimport { isVectorMaskGroup, vectorMaskSummary, type VectorMaskSummary } from './mask.js';\nimport { isVectorFrame } from './frames.js';\nimport { vectorPolarAngles } from './snap.js';\nimport { VECTOR_CONNECTOR_ROUTES, VECTOR_CONNECTOR_SIDES, type VectorConnector, type VectorConnectorEnd, type VectorConnectorRoute } from './types.js';\nimport { VECTOR_HORIZONTAL_RESIZING, VECTOR_VERTICAL_RESIZING, type VectorFrame, type VectorHorizontalResizing, type VectorResizing, type VectorVerticalResizing } from './types.js';\nimport { layoutVectorText, vectorTextFamilies, vectorTextHasOutlines } from './text.js';\nimport type { VectorMaskType } from './types.js';\nimport type { VectorEnvelopeParams } from './appearance.js';\n\nexport interface VectorCommandOptions {\n\t/** Id prefix (default `vector.`); use one per editor when several are registered. */\n\tprefix?: string;\n\t/** Palette group (default `Vector`). */\n\tgroup?: string;\n\t/** Command scope. Omit for global commands. */\n\tscope?: string;\n}\n\n/** A node as listed for agents. */\nexport interface VectorNodeListing {\n\tid: string;\n\ttype: 'path' | 'group' | 'text' | 'instance';\n\tname?: string;\n\tdepth: number;\n\t/** Axis-aligned bounds in document units. */\n\tbounds: { x: number; y: number; width: number; height: number } | null;\n\t/** Rotation in degrees, clockwise (ancestors' rotations included). */\n\trotation: number;\n\tvisible: boolean;\n\tlocked: boolean;\n\tselected: boolean;\n\tfill?: unknown;\n\tstroke?: unknown;\n\tstrokeWidth?: number;\n\t/** SVG path data (paths only). */\n\td?: string;\n\t/** An instance's symbol, transform (it may scale and skew) and overrides. */\n\tsymbolId?: string;\n\ttransform?: number[];\n\toverrides?: unknown;\n\t/** A text node's text and frame kind (`point`, `area` or `path`); `vector.getText` has the rest. */\n\ttext?: string;\n\tframe?: 'point' | 'area' | 'path';\n\tchildren?: number;\n\t/** A blend group's options (its steps are listed as children with `blendStep`). */\n\tblend?: { spacing: string; steps: number; distance: number; orientToSpine: boolean; linear: boolean; spine: boolean; keys: string[] };\n\t/** An in-between generated by a blend (locked; changes come from its keys). */\n\tblendStep?: boolean;\n\t/** The node's live path effects, applied in order. */\n\teffects?: VectorEffect[];\n\t/** A live boolean group's operation (its children are the operands). */\n\tliveBoolean?: VectorBooleanOperation;\n\t/** An operand's own operation in a live boolean group. */\n\tbooleanOperation?: VectorBooleanOperation;\n\t/** The fill stack, bottom to top, when the path has more than its `fill` says. */\n\tfills?: VectorPaintLayer[];\n\t/** The stroke stack, bottom to top, when the path has more than its `stroke` says. */\n\tstrokes?: VectorStrokeLayer[];\n\t/** The node's blend mode, when not normal. */\n\tblendMode?: VectorBlendMode;\n\t/** The node's visual filters (shadows, glows, blurs, feather). */\n\tfilters?: VectorFilter[];\n\t/** A frame's rectangle (in its own space), fill and clip (see vector.listFrames). */\n\tframeRect?: VectorFrame;\n\t/** A frame child's resizing constraints, when set. */\n\tresizing?: VectorResizing;\n\t/** A connector's ends and route (see vector.listConnectors). */\n\tconnector?: VectorConnector;\n\t/** A mask group's mask: type, inversion, the mask's id (its first child) and the masked contents. */\n\tmask?: VectorMaskSummary;\n}\n\nconst EFFECT_PARAMS_DESCRIPTION =\n\t'Effect parameters (missing ones keep their value or default). trim: start, end (percent), offset (degrees), mode simultaneous|individual. roundCorners: radius. puckerBloat: amount (−100…100). twist: angle, center {x, y} (offset from the middle). zigzag: size, ridges, smooth. wiggle: size, detail, smooth, speed, correlation, phase, seed, time (seconds). offset: amount, join miter|round|bevel, miterLimit, copies, copyOffset. merge: mode merge|add|subtract|intersect|exclude. repeater: copies (fractions fade the last), offset, position {x, y}, anchor {x, y} (offset from the middle), rotation, scale {x, y} (percent), startOpacity, endOpacity (percent), composite below|above. envelope: style (a warp preset: arc|arcLower|arcUpper|arch|bulge|shellLower|shellUpper|flag|wave|fish|rise|fisheye|inflate|squeeze|twist, or mesh), bend, horizontal, vertical (percent, −100…100), orientation horizontal|vertical, rows, columns, mesh { us, vs, points } (node space; prefer vector.envelope).';\n\nconst fail = (message: string): never => {\n\tthrow new Error(message);\n};\n\nconst IDS = { type: 'array', items: { type: 'string' }, description: 'Node ids (default: the selection).' } as const;\n\nconst ANCHORS = {\n\ttype: 'array',\n\titems: {\n\t\ttype: 'object',\n\t\tproperties: { nodeId: { type: 'string' }, subpath: { type: 'integer', minimum: 0 }, index: { type: 'integer', minimum: 0 } },\n\t\trequired: ['nodeId', 'subpath', 'index']\n\t},\n\tdescription: 'Anchors as { nodeId, subpath, index } (default: the selected points).'\n} as const;\n\nconst POINTS = {\n\ttype: 'array',\n\titems: { type: 'array', items: { type: 'number' }, minItems: 2, maxItems: 2 },\n\tminItems: 1,\n\tdescription: 'Polyline [[x, y], …] in document units.'\n} as const;\n\nconst WIDTH_POINTS = {\n\ttype: ['array', 'null'],\n\titems: {\n\t\ttype: 'object',\n\t\tproperties: {\n\t\t\tt: { type: 'number', minimum: 0, maximum: 1, description: 'Position along each subpath by length (0 start, 1 end).' },\n\t\t\tleft: { type: 'number', minimum: 0, description: 'Left side as a multiple of half the stroke width (1 = uniform).' },\n\t\t\tright: { type: 'number', minimum: 0, description: 'Right side (looking along the path, y down).' }\n\t\t},\n\t\trequired: ['t', 'left', 'right']\n\t},\n\tdescription: 'Variable-width profile points, interpolated smoothly (monotone cubic); two points at one `t` make a step. `null` or `[]` strokes at a uniform width.'\n} as const;\n\nconst BLEND_PROPS = {\n\tspacing: { type: 'string', enum: ['steps', 'distance', 'smoothColor'], description: 'How in-betweens are spaced (default steps).' },\n\tsteps: { type: 'integer', minimum: 1, maximum: 1000, description: 'In-betweens between each pair of keys (steps spacing; default 5).' },\n\tdistance: { type: 'number', exclusiveMinimum: 0, description: 'Distance between in-betweens along the spine (distance spacing; default 20).' },\n\torientToSpine: { type: 'boolean', description: 'Turn the in-betweens with a replaced spine.' },\n\tlinear: { type: 'boolean', description: 'Move anchors in straight lines instead of the rotation-aware morph.' }\n} as const;\n\nconst checkBlendOptions = (params: Partial<VectorBlendOptions> | undefined) => {\n\tif (params?.spacing !== undefined && !VECTOR_BLEND_SPACINGS.includes(params.spacing)) fail(`Unknown blend spacing \"${params.spacing}\".`);\n\tif (params?.steps !== undefined && !(Number.isFinite(params.steps) && params.steps >= 1)) fail('steps must be at least 1.');\n\tif (params?.distance !== undefined && !(Number.isFinite(params.distance) && params.distance > 0)) fail('distance must be positive.');\n};\n\nconst POINT = { type: 'object', properties: { x: { type: 'number' }, y: { type: 'number' } }, required: ['x', 'y'] } as const;\n\n/** Gradient fields shared by paints and `setGradient`. */\nconst GRADIENT_PROPS = {\n\ttype: { type: 'string', enum: VECTOR_GRADIENT_TYPES, description: 'linear, radial, conic (an angle sweep), reflected (linear mirrored about the start) or diamond.' },\n\tstops: {\n\t\ttype: 'array',\n\t\tminItems: 1,\n\t\titems: {\n\t\t\ttype: 'object',\n\t\t\tproperties: {\n\t\t\t\toffset: { type: 'number', minimum: 0, maximum: 1 },\n\t\t\t\tcolor: { type: 'string', description: 'A CSS colour.' },\n\t\t\t\topacity: { type: 'number', minimum: 0, maximum: 1 },\n\t\t\t\tmidpoint: { type: 'number', minimum: 0.05, maximum: 0.95, description: 'Where between this stop and the next the colours are half-mixed (default 0.5).' }\n\t\t\t},\n\t\t\trequired: ['offset', 'color']\n\t\t}\n\t},\n\tangle: { type: 'number', description: 'Shorthand direction in degrees (0 up, clockwise): linear / reflected direction across the box, conic start angle, diamond turn.' },\n\tcx: { type: 'number', description: 'Shorthand centre x of radial / conic / diamond gradients, a fraction of the box (default 0.5).' },\n\tcy: { type: 'number' },\n\tr: { type: 'number', description: 'Shorthand radius, a fraction of the box (default 0.5).' },\n\tstart: { ...POINT, description: 'Explicit start (centre of radial / conic / diamond), in `units`. Wins over the shorthand.' },\n\tend: { ...POINT, description: 'Explicit end of the main axis (the radius of radial / conic / diamond).' },\n\twidth: { ...POINT, description: 'End of the secondary axis (aspect and turn of radial / conic / diamond); default square to the main axis, as long.' },\n\tfocal: { ...POINT, description: 'Radial focal point (where t = 0), in `units`.' },\n\tfocalRadius: { type: 'number', minimum: 0, maximum: 0.99, description: 'Radial focal circle radius as a fraction of the radius (SVG fr).' },\n\tunits: { type: 'string', enum: ['objectBoundingBox', 'userSpaceOnUse'], description: 'Space of start / end / width / focal: fractions of the shape box (default) or the node\\'s own coordinates.' },\n\tspread: { type: 'string', enum: VECTOR_GRADIENT_SPREADS, description: 'What paints past the ends: pad (default), reflect or repeat.' },\n\tinterpolation: { type: 'string', enum: VECTOR_GRADIENT_INTERPOLATIONS, description: 'Colour mixing space: srgb (default), linear (light) or oklab.' },\n\tsmoothness: { type: 'number', minimum: 0, maximum: 1, description: 'Bends the ramp along a smooth spline through the stops (0 = straight).' },\n\tstrokeMode: { type: 'string', enum: VECTOR_STROKE_GRADIENT_MODES, description: 'On strokes: within (default, like a fill), along the path or across it.' }\n} as const;\n\nconst PAINT = {\n\tdescription:\n\t\t'A CSS colour, `null` for none, or a gradient object: `{ type: \"linear\" | \"radial\" | \"conic\" | \"reflected\" | \"diamond\", stops: [{ offset, color, opacity?, midpoint? }] }` plus the shorthand `angle` / `cx, cy, r` or explicit `start`, `end`, `width`, `focal`, `focalRadius`, `units`, and `spread`, `interpolation`, `smoothness`, `strokeMode` (see vector.setGradient).',\n\tanyOf: [{ type: 'string' }, { type: 'null' }, { type: 'object', properties: GRADIENT_PROPS, required: ['type', 'stops'] }]\n} as const;\n\nconst BLEND_MODE = {\n\ttype: 'string',\n\tenum: VECTOR_BLEND_MODES,\n\tdescription: 'How it mixes with what is under it (CSS mix-blend-mode names; linear-dodge draws as plus-lighter). Default normal.'\n} as const;\n\n/** A fill layer's own fields. */\nconst FILL_LAYER_PROPS = {\n\tpaint: PAINT,\n\topacity: { type: 'number', minimum: 0, maximum: 1, description: 'Layer opacity 0 to 1 (default 1), multiplying the paint\\'s own alpha.' },\n\tblendMode: BLEND_MODE,\n\tvisible: { type: 'boolean', description: 'false hides the layer (kept, not drawn or hit). Default true.' }\n} as const;\n\n/** Stroke settings, each stroke layer's own (and the stroke shorthand's). */\nconst STROKE_SETTING_PROPS = {\n\tstrokeWidth: { type: 'number', minimum: 0, description: 'Stroke width in document units (default 1).' },\n\tstrokeLinecap: { type: 'string', enum: ['butt', 'round', 'square'], description: 'Line cap (default butt).' },\n\tstrokeLinejoin: { type: 'string', enum: ['miter', 'round', 'bevel'], description: 'Line join (default miter).' },\n\tstrokeDasharray: { type: ['array', 'null'], items: { type: 'number', minimum: 0 }, description: 'Dash and gap lengths in document units; null or [] for a solid line.' },\n\tstrokeAlign: { type: 'string', enum: ['center', 'inside', 'outside'], description: 'Where the stroke sits on the outline (closed shapes only; open paths stroke centred). Default center.' },\n\tstartArrow: { type: ['string', 'null'], enum: [...VECTOR_ARROWHEADS, null], description: 'Head at the start of an open path, or null for none.' },\n\tendArrow: { type: ['string', 'null'], enum: [...VECTOR_ARROWHEADS, null], description: 'Head at the end of an open path, or null for none.' },\n\tarrowAlign: { type: 'string', enum: ['extend', 'tip'], description: '`extend`: the tip sits past the end point; `tip`: on it, the stroke shortened under the head.' },\n\tstartArrowScale: { type: 'number', exclusiveMinimum: 0, description: 'Start head size as a multiple of the stroke width (default 1).' },\n\tendArrowScale: { type: 'number', exclusiveMinimum: 0, description: 'End head size as a multiple of the stroke width (default 1).' },\n\tstrokeWidthProfile: WIDTH_POINTS\n} as const;\n\nconst FILL_LAYER = { type: 'object', properties: FILL_LAYER_PROPS, required: ['paint'], additionalProperties: false } as const;\nconst STROKE_LAYER = { type: 'object', properties: { ...FILL_LAYER_PROPS, ...STROKE_SETTING_PROPS }, required: ['paint'], additionalProperties: false } as const;\n\nconst STYLE_PROPS = {\n\tfills: { type: 'array', items: FILL_LAYER, description: 'Replace the fill stack, bottom to top (fills draw under strokes). `fill` alone edits the top fill.' },\n\tstrokes: { type: 'array', items: STROKE_LAYER, description: 'Replace the stroke stack, bottom to top, each stroke with its own settings. `stroke` and the stroke settings alone edit the top stroke.' },\n\tfill: PAINT,\n\tstroke: PAINT,\n\t...STROKE_SETTING_PROPS,\n\tfillRule: { type: 'string', enum: ['nonzero', 'evenodd'], description: 'Which areas count as inside (default nonzero).' }\n} as const;\n\nconst checkPaint = (paint: unknown, where: string): VectorPaint => {\n\tif (paint && typeof paint === 'object') return sanitizeVectorGradient(paint) ?? fail(`${where} is not a valid gradient: give a type and at least one stop { offset, color }.`);\n\treturn paint as VectorPaint;\n};\n\nconst pickStyle = (params: Record<string, unknown>): VectorStyle => {\n\tconst out: Record<string, unknown> = {};\n\tfor (const key of Object.keys(STYLE_PROPS)) if (params[key] !== undefined) out[key] = params[key];\n\tfor (const key of ['fill', 'stroke'] as const) if (out[key] !== undefined) out[key] = checkPaint(out[key], key);\n\tfor (const key of ['fills', 'strokes'] as const) {\n\t\tconst stack = out[key];\n\t\tif (!Array.isArray(stack)) continue;\n\t\tstack.forEach((l, i) => {\n\t\t\tif (!l || typeof l !== 'object') fail(`${key}[${i}] must be a layer object { paint, … }.`);\n\t\t\tconst layer = l as Record<string, unknown>;\n\t\t\tcheckPaint(layer.paint, `${key}[${i}].paint`);\n\t\t\tif (layer.blendMode !== undefined && !isVectorBlendMode(layer.blendMode)) fail(`${key}[${i}].blendMode \"${String(layer.blendMode)}\" is not one of: ${VECTOR_BLEND_MODES.join(', ')}.`);\n\t\t});\n\t}\n\treturn out as VectorStyle;\n};\n\nconst check = (result: { ok: boolean; message?: string }) => {\n\tif (!result.ok) fail(result.message ?? 'The command failed.');\n};\n\n/**\n * Palette and agent commands for a vector editor: list, select, add shapes and paths, style,\n * transform, align, arrange, group, boolean operations and SVG import / export. Register them\n * in a `CommandRegistry` and agents get them through `createCommandToolProvider`. Failures\n * throw sentences, which the registry returns as `{ ok: false, code: 'failed', message }`.\n */\nexport function createVectorCommands(controller: VectorEditorController, options: VectorCommandOptions = {}): CommandSpec[] {\n\tconst prefix = options.prefix ?? 'vector.';\n\tconst group = options.group ?? 'Vector';\n\tconst base = { group, ...(options.scope ? { scope: options.scope } : {}) };\n\tconst hasSelection = () => (controller.getState().selection.length ? true : 'Nothing is selected.');\n\tconst ids = (params: { ids?: string[] } | undefined) => {\n\t\tconst list = params?.ids ?? controller.getState().selection;\n\t\tfor (const id of list) if (!controller.getNode(id)) fail(`There is no node \"${id}\".`);\n\t\tif (!list.length) fail('Nothing is selected; pass ids.');\n\t\treturn list;\n\t};\n\tconst enabledFor = (_: unknown, params: { ids?: string[] } | undefined) => (params?.ids?.length ? true : hasSelection());\n\tconst anchorsOf = (refs: VectorAnchorRef[] | undefined): VectorAnchorRef[] => {\n\t\tif (!refs) return [...controller.getState().selectedAnchors];\n\t\tfor (const r of refs) {\n\t\t\tconst node = controller.getNode(r.nodeId);\n\t\t\tif (node?.type !== 'path' || !node.subpaths[r.subpath]?.anchors[r.index]) fail(`There is no anchor ${r.index} on subpath ${r.subpath} of \"${r.nodeId}\".`);\n\t\t}\n\t\treturn refs;\n\t};\n\n\tconst list = (): { width: number; height: number; nodes: VectorNodeListing[]; selection: readonly string[]; tool: VectorTool } => {\n\t\tconst state = controller.getState();\n\t\tconst out: VectorNodeListing[] = [];\n\t\tconst visit = (nodes: readonly VectorNode[], depth: number) => {\n\t\t\tfor (const n of nodes) {\n\t\t\t\tconst parent = vectorParentMatrix(state.document.nodes, n.id) ?? IDENTITY_MATRIX;\n\t\t\t\tconst b = vectorNodeBounds(n, parent);\n\t\t\t\tout.push({\n\t\t\t\t\tid: n.id,\n\t\t\t\t\ttype: n.type,\n\t\t\t\t\t...(n.name ? { name: n.name } : {}),\n\t\t\t\t\tdepth,\n\t\t\t\t\trotation: Math.round(vectorNodeRotation(n, parent) * 100) / 100,\n\t\t\t\t\tbounds: b && { x: Math.round(b.x * 100) / 100, y: Math.round(b.y * 100) / 100, width: Math.round(b.width * 100) / 100, height: Math.round(b.height * 100) / 100 },\n\t\t\t\t\tvisible: n.visible !== false,\n\t\t\t\t\tlocked: !!n.locked,\n\t\t\t\t\tselected: state.selection.includes(n.id),\n\t\t\t\t\t...(n.type === 'path'\n\t\t\t\t\t\t? { fill: n.fill ?? null, stroke: n.stroke ?? null, strokeWidth: n.strokeWidth ?? 1, d: vectorPathData(n, 2) }\n\t\t\t\t\t\t: n.type === 'text'\n\t\t\t\t\t\t\t? { fill: n.fill ?? null, stroke: n.stroke ?? null, strokeWidth: n.strokeWidth ?? 1, text: n.text, frame: n.frame.kind }\n\t\t\t\t\t\t\t: n.type === 'instance'\n\t\t\t\t\t\t\t\t? { symbolId: n.symbolId, ...(n.transform ? { transform: [...n.transform] } : {}), ...(n.overrides ? { overrides: n.overrides } : {}) }\n\t\t\t\t\t\t\t\t: { children: n.children.length }),\n\t\t\t\t\t...(n.type === 'group' && n.blend\n\t\t\t\t\t\t? { blend: { ...resolveVectorBlendOptions(n.blend), spine: !!n.blend.spine, keys: vectorBlendKeys(n).map((k) => k.id) } }\n\t\t\t\t\t\t: {}),\n\t\t\t\t\t...(n.blendStep ? { blendStep: true } : {}),\n\t\t\t\t\t...(n.effects?.length ? { effects: n.effects } : {}),\n\t\t\t\t\t...(n.type === 'group' && n.liveBoolean ? { liveBoolean: n.liveBoolean } : {}),\n\t\t\t\t\t...(n.booleanOperation ? { booleanOperation: n.booleanOperation } : {}),\n\t\t\t\t\t...((n.type === 'path' || n.type === 'text') && Array.isArray(n.fills) ? { fills: n.fills } : {}),\n\t\t\t\t\t...((n.type === 'path' || n.type === 'text') && Array.isArray(n.strokes) ? { strokes: n.strokes } : {}),\n\t\t\t\t\t...(n.blendMode && n.blendMode !== 'normal' ? { blendMode: n.blendMode } : {}),\n\t\t\t\t\t...(n.filters?.length ? { filters: n.filters } : {}),\n\t\t\t\t\t...(isVectorMaskGroup(n) ? { mask: vectorMaskSummary(n)! } : {}),\n\t\t\t\t\t...(isVectorFrame(n) ? { frameRect: n.frame } : {}),\n\t\t\t\t\t...(n.resizing ? { resizing: n.resizing } : {}),\n\t\t\t\t\t...(n.type === 'path' && n.connector ? { connector: n.connector } : {})\n\t\t\t\t});\n\t\t\t\tif (n.type === 'group') visit(n.children, depth + 1);\n\t\t\t}\n\t\t};\n\t\tvisit(state.document.nodes, 0);\n\t\treturn { width: state.document.width, height: state.document.height, nodes: out, selection: state.selection, tool: state.tool };\n\t};\n\n\t/** A node's appearance as agents read it: stacks (paths), blend mode, opacity and filters. */\n\tconst appearanceOf = (id: string) => {\n\t\tconst n = controller.getNode(id) ?? fail(`There is no node \"${id}\".`);\n\t\treturn {\n\t\t\tid: n.id,\n\t\t\ttype: n.type,\n\t\t\t...(n.type === 'path' ? { fills: vectorFills(n), strokes: vectorStrokes(n), fillRule: n.fillRule ?? 'nonzero' } : {}),\n\t\t\tblendMode: n.blendMode ?? 'normal',\n\t\t\topacity: n.opacity ?? 1,\n\t\t\tfilters: n.filters ?? []\n\t\t};\n\t};\n\tconst pathOf = (id: string) => {\n\t\tconst n = controller.getNode(id) ?? fail(`There is no node \"${id}\". Call vector.list for ids.`);\n\t\tif (n.type !== 'path') fail(`\"${id}\" is a group; fills and strokes belong to paths (pass a path id, or use ids on vector.addFill / vector.addStroke for every path inside).`);\n\t\treturn n as Extract<VectorNode, { type: 'path' }>;\n\t};\n\tconst layerIndex = (id: string, kind: 'fill' | 'stroke', index: number, key = 'index') => {\n\t\tconst stack = kind === 'fill' ? vectorFills(pathOf(id)) : vectorStrokes(pathOf(id));\n\t\tif (!Number.isInteger(index) || !stack[index]) fail(`${key} ${index} is out of range: \"${id}\" has ${stack.length} ${kind}${stack.length === 1 ? '' : 's'} (0 is the bottom). Read them with vector.getAppearance.`);\n\t};\n\tconst stackResult = (id: string, kind: 'fill' | 'stroke') => (kind === 'fill' ? { id, fills: vectorFills(pathOf(id)) } : { id, strokes: vectorStrokes(pathOf(id)) });\n\tconst layerPatchOf = (params: Record<string, unknown>, keys: readonly string[]) => {\n\t\tconst out: Record<string, unknown> = {};\n\t\tfor (const key of keys) if (params[key] !== undefined) out[key] = key === 'paint' ? checkPaint(params[key], 'paint') : params[key];\n\t\treturn out;\n\t};\n\tconst pathsUnder = (list: readonly string[]) => {\n\t\tconst out: string[] = [];\n\t\tconst walk = (n: VectorNode) => (n.type === 'group' ? n.children.forEach(walk) : n.type === 'instance' ? undefined : out.push(n.id));\n\t\tfor (const id of list) walk(controller.getNode(id)!);\n\t\treturn out;\n\t};\n\tconst FILL_KEYS = Object.keys(FILL_LAYER_PROPS);\n\tconst STROKE_KEYS = [...FILL_KEYS, ...Object.keys(STROKE_SETTING_PROPS)];\n\tconst layerCommands = (kind: 'fill' | 'stroke'): CommandSpec[] => {\n\t\tconst Kind = kind === 'fill' ? 'Fill' : 'Stroke';\n\t\tconst props = kind === 'fill' ? FILL_LAYER_PROPS : { ...FILL_LAYER_PROPS, ...STROKE_SETTING_PROPS };\n\t\tconst keys = kind === 'fill' ? FILL_KEYS : STROKE_KEYS;\n\t\tconst NODE = { type: 'string', description: 'Id of the path (from vector.list).' } as const;\n\t\tconst INDEX = { type: 'integer', minimum: 0, description: `Position of the ${kind} in the path's ${kind} stack, 0 at the bottom (from vector.getAppearance).` } as const;\n\t\tconst listKey = `${kind}s`;\n\t\treturn [\n\t\t\t{\n\t\t\t\t...base,\n\t\t\t\tid: `${prefix}add${Kind}`,\n\t\t\t\tlabel: `Add ${Kind}`,\n\t\t\t\tkeywords: ['appearance', 'layer', kind, 'stack', 'multiple', 'paint'],\n\t\t\t\tdescription:\n\t\t\t\t\tkind === 'fill'\n\t\t\t\t\t\t? 'Adds a fill layer to paths (groups: every path inside), on top of the fill stack or at `index`. Fills draw bottom to top, all under the strokes. Returns each path\\'s new fill stack.'\n\t\t\t\t\t\t: 'Adds a stroke layer to paths (groups: every path inside), on top of the stroke stack or at `index`, with its own width, alignment, dashes, caps, joins, arrowheads and profile (unset settings come from the stack\\'s current top stroke). Strokes draw bottom to top, over every fill. Returns each path\\'s new stroke stack.',\n\t\t\t\tparams: {\n\t\t\t\t\ttype: 'object',\n\t\t\t\t\tproperties: {\n\t\t\t\t\t\tids: { ...IDS, description: 'Paths or groups to add it to (default: the selection; groups give every path inside).' },\n\t\t\t\t\t\t...props,\n\t\t\t\t\t\tpaint: { ...PAINT, description: `${PAINT.description} Default ${kind === 'fill' ? '#808080' : '#000000'}.` },\n\t\t\t\t\t\tindex: { type: 'integer', minimum: 0, description: `Where in the stack to put it, 0 at the bottom (default: on top).` }\n\t\t\t\t\t},\n\t\t\t\t\tadditionalProperties: false\n\t\t\t\t},\n\t\t\t\tundoable: true,\n\t\t\t\tenabled: enabledFor,\n\t\t\t\trun: (_ctx: unknown, params: ({ ids?: string[]; index?: number } & Record<string, unknown>) | undefined) => {\n\t\t\t\t\tconst targets = ids(params);\n\t\t\t\t\tcheck(controller.addPaintLayer(kind, layerPatchOf(params ?? {}, keys), { ids: targets, index: params?.index }));\n\t\t\t\t\treturn { nodes: pathsUnder(targets).map((id) => stackResult(id, kind)) };\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\t...base,\n\t\t\t\tid: `${prefix}update${Kind}`,\n\t\t\t\tlabel: `Edit ${Kind}`,\n\t\t\t\tkeywords: ['appearance', 'layer', kind, 'opacity', 'blend', 'hide', 'show'],\n\t\t\t\tdescription: `Changes ${kind} \\`index\\` of path \\`nodeId\\`: ${kind === 'fill' ? 'paint, opacity, blend mode, visibility' : 'paint, opacity, blend mode, visibility and any stroke setting (null removes an arrowhead, dashes or a profile)'}. Missing fields keep their value. Returns the path's ${kind} stack.`,\n\t\t\t\tparams: { type: 'object', properties: { nodeId: NODE, index: INDEX, ...props }, required: ['nodeId', 'index'], additionalProperties: false },\n\t\t\t\tundoable: true,\n\t\t\t\trun: (_ctx: unknown, params: { nodeId: string; index: number } & Record<string, unknown>) => {\n\t\t\t\t\tlayerIndex(params.nodeId, kind, params.index);\n\t\t\t\t\tconst patch = layerPatchOf(params, keys);\n\t\t\t\t\tif (!Object.keys(patch).length) fail(`Pass at least one of: ${keys.join(', ')}.`);\n\t\t\t\t\tcheck(controller.updatePaintLayer(kind, params.index, patch, { ids: [params.nodeId] }));\n\t\t\t\t\treturn stackResult(params.nodeId, kind);\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\t...base,\n\t\t\t\tid: `${prefix}remove${Kind}`,\n\t\t\t\tlabel: `Remove ${Kind}`,\n\t\t\t\tkeywords: ['appearance', 'layer', kind, 'delete'],\n\t\t\t\tdescription: `Removes ${kind} \\`index\\` from path \\`nodeId\\`. Returns the path's remaining ${kind} stack.`,\n\t\t\t\tparams: { type: 'object', properties: { nodeId: NODE, index: INDEX }, required: ['nodeId', 'index'], additionalProperties: false },\n\t\t\t\tundoable: true,\n\t\t\t\trun: (_ctx: unknown, params: { nodeId: string; index: number }) => {\n\t\t\t\t\tlayerIndex(params.nodeId, kind, params.index);\n\t\t\t\t\tcheck(controller.removePaintLayer(kind, params.index, { ids: [params.nodeId] }));\n\t\t\t\t\treturn stackResult(params.nodeId, kind);\n\t\t\t\t}\n\t\t\t},\n\t\t\t{\n\t\t\t\t...base,\n\t\t\t\tid: `${prefix}move${Kind}`,\n\t\t\t\tlabel: `Reorder ${Kind}s`,\n\t\t\t\tkeywords: ['appearance', 'layer', kind, 'order', 'reorder', 'stack'],\n\t\t\t\tdescription: `Moves ${kind} \\`index\\` of path \\`nodeId\\` to position \\`to\\` in its ${kind} stack (0 is the bottom). Returns the reordered ${listKey}.`,\n\t\t\t\tparams: {\n\t\t\t\t\ttype: 'object',\n\t\t\t\t\tproperties: { nodeId: NODE, index: INDEX, to: { type: 'integer', minimum: 0, description: `New position, 0 at the bottom (clamped to the stack).` } },\n\t\t\t\t\trequired: ['nodeId', 'index', 'to'],\n\t\t\t\t\tadditionalProperties: false\n\t\t\t\t},\n\t\t\t\tundoable: true,\n\t\t\t\trun: (_ctx: unknown, params: { nodeId: string; index: number; to: number }) => {\n\t\t\t\t\tlayerIndex(params.nodeId, kind, params.index);\n\t\t\t\t\tcheck(controller.movePaintLayer(kind, params.index, params.to, { ids: [params.nodeId] }));\n\t\t\t\t\treturn stackResult(params.nodeId, kind);\n\t\t\t\t}\n\t\t\t}\n\t\t];\n\t};\n\tconst FILTER_PARAMS = {\n\t\ttype: 'object',\n\t\tdescription:\n\t\t\t'Filter parameters (missing ones keep their value or default; lengths in document units). dropShadow / innerShadow: color (CSS), opacity (0 to 1), offsetX, offsetY, blur (radius; reaches about 1.5 times it), spread (inner: choke). outerGlow / innerGlow: color, opacity, blur, spread. layerBlur: radius. feather: radius (edge fade width).',\n\t\tproperties: {\n\t\t\tcolor: { type: 'string', description: 'Shadow or glow colour (CSS).' },\n\t\t\topacity: { type: 'number', minimum: 0, maximum: 1, description: 'Shadow or glow opacity, 0 to 1.' },\n\t\t\toffsetX: { type: 'number', description: 'Shadow offset to the right, document units.' },\n\t\t\toffsetY: { type: 'number', description: 'Shadow offset downwards, document units.' },\n\t\t\tblur: { type: 'number', minimum: 0, description: 'Shadow or glow blur radius, document units.' },\n\t\t\tspread: { type: 'number', minimum: 0, description: 'Grows the silhouette before blurring (inner shadow and glow: chokes inwards).' },\n\t\t\tradius: { type: 'number', minimum: 0, description: 'Layer blur radius, or feather width, document units.' }\n\t\t},\n\t\tadditionalProperties: false\n\t} as const;\n\tconst checkFilterParams = (type: VectorFilterType, params: Record<string, unknown> | undefined) => {\n\t\tconst allowed = vectorFilterParamKeys(type);\n\t\tfor (const key of Object.keys(params ?? {})) if (!allowed.includes(key)) fail(`${type} has no parameter \"${key}\"; it takes ${allowed.join(', ')}.`);\n\t};\n\tconst filtersResult = (id: string) => ({ id, filters: controller.getNode(id)?.filters ?? [] });\n\tconst filterOf = (nodeId: string, filterId: string) => {\n\t\tconst node = controller.getNode(nodeId) ?? fail(`There is no node \"${nodeId}\".`);\n\t\treturn node.filters?.find((f) => f.id === filterId) ?? fail(`Node \"${nodeId}\" has no filter \"${filterId}\"; it has ${(node.filters ?? []).map((f) => `\"${f.id}\" (${f.type})`).join(', ') || 'none'}.`);\n\t};\n\tconst maskResult = (id: string) => ({ id, mask: vectorMaskSummary(controller.getNode(id)) ?? fail(`\"${id}\" is not a mask group.`) });\n\t/** Checks the objects a mask is made of before anything changes. */\n\tconst maskInputs = (params: { ids?: string[]; maskId?: string } | undefined) => {\n\t\tconst list = [...new Set(ids(params))];\n\t\tif (list.length < 2) fail(`Masking needs at least two objects (the mask and what it masks); got ${list.length}. Pass ids, or select more.`);\n\t\tfor (const id of list) if (controller.getNode(id)!.locked) fail(`\"${id}\" is locked; ask the user to unlock it, or leave it out of ids.`);\n\t\tif (params?.maskId !== undefined && !list.includes(params.maskId)) fail(`maskId \"${params.maskId}\" must be one of ids (${list.map((id) => `\"${id}\"`).join(', ')}).`);\n\t\treturn list;\n\t};\n\tconst maskTargets = (params: { ids?: string[] } | undefined) => {\n\t\tconst list = ids(params);\n\t\tconst groups = list.map((id) => controller.getNode(id)).filter((n) => isVectorMaskGroup(n));\n\t\tif (!groups.length) fail(`None of ${list.map((id) => `\"${id}\"`).join(', ')} is a mask group. Mask groups have a \\`mask\\` in vector.list; make one with vector.makeClippingMask or vector.makeOpacityMask.`);\n\t\treturn groups.map((g) => g!.id);\n\t};\n\tconst MASK_INPUT = {\n\t\tids: { ...IDS, description: 'The objects to mask with, two or more sharing a parent (default: the selection). The topmost becomes the mask unless maskId says otherwise.' },\n\t\tmaskId: { type: 'string', description: 'Which of ids is the mask (default: the topmost).' },\n\t\tinvert: { type: 'boolean', description: 'Show the contents only where the mask hides them (default false).' }\n\t} as const;\n\tconst maskCommands = (): CommandSpec[] => [\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}makeClippingMask`,\n\t\t\tlabel: 'Make Clipping Mask',\n\t\t\tkeywords: ['mask', 'clip', 'clipping', 'clip path', 'crop', 'inside'],\n\t\t\tshortcut: 'Mod+7',\n\t\t\tdescription:\n\t\t\t\t'Groups objects into a clipping mask group: the mask (the topmost object, or maskId) moves to the bottom of the new group and the others show only inside its outline (its paint is not drawn; every path in it counts, by its fill rule). Hit-testing, bounds, snapping and booleans use the clipped result. One undo step. Returns { id, mask: { type, invert, maskId, contents, enabled } }.',\n\t\t\tparams: { type: 'object', properties: MASK_INPUT, additionalProperties: false },\n\t\t\tundoable: true,\n\t\t\tenabled: enabledFor,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[]; maskId?: string; invert?: boolean } | undefined) => {\n\t\t\t\tconst list = maskInputs(params);\n\t\t\t\tconst r = controller.makeMask('clip', { ids: list, ...(params?.maskId ? { maskId: params.maskId } : {}), invert: params?.invert });\n\t\t\t\tif (!r.ok) fail(r.message);\n\t\t\t\treturn maskResult((r as { id: string }).id);\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}makeOpacityMask`,\n\t\t\tlabel: 'Make Opacity Mask',\n\t\t\tkeywords: ['mask', 'opacity mask', 'alpha', 'luminance', 'fade', 'gradient mask', 'soft mask'],\n\t\t\tdescription:\n\t\t\t\t'Groups objects into an opacity mask group: the mask (the topmost object, or maskId) moves to the bottom of the new group and hides the others where it is dark (luminance: white shows, black hides) or transparent (alpha). A gradient mask fades the contents. One undo step. Returns { id, mask: { type, invert, maskId, contents, enabled } }.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: { ...MASK_INPUT, type: { type: 'string', enum: ['luminance', 'alpha'], description: 'luminance (by brightness, the default) or alpha (by opacity).' } },\n\t\t\t\tadditionalProperties: false\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\tenabled: enabledFor,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[]; maskId?: string; invert?: boolean; type?: 'alpha' | 'luminance' } | undefined) => {\n\t\t\t\tconst list = maskInputs(params);\n\t\t\t\tconst r = controller.makeMask(params?.type ?? 'luminance', { ids: list, ...(params?.maskId ? { maskId: params.maskId } : {}), invert: params?.invert });\n\t\t\t\tif (!r.ok) fail(r.message);\n\t\t\t\treturn maskResult((r as { id: string }).id);\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}setMaskOptions`,\n\t\t\tlabel: 'Mask Options…',\n\t\t\tkeywords: ['mask', 'clip', 'invert', 'alpha', 'luminance', 'opacity mask'],\n\t\t\tdescription:\n\t\t\t\t'Changes how mask groups mask: `type` clip (by outline), alpha or luminance, `invert`, and `enabled` (false hides the mask so the contents draw whole; true shows it again). Pass at least one. One undo step. Returns { groups: [{ id, mask }] }.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tids: { ...IDS, description: 'Mask group ids (default: the selection).' },\n\t\t\t\t\ttype: { type: 'string', enum: ['clip', 'alpha', 'luminance'], description: 'clip (the mask\\'s outline), alpha (its opacity) or luminance (its brightness).' },\n\t\t\t\t\tinvert: { type: 'boolean', description: 'true: show the contents where the mask hides them.' },\n\t\t\t\t\tenabled: { type: 'boolean', description: 'false: turn the mask off (it is hidden and the contents draw whole); true: on again.' }\n\t\t\t\t},\n\t\t\t\tadditionalProperties: false\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\tenabled: enabledFor,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[]; type?: VectorMaskType; invert?: boolean; enabled?: boolean } | undefined) => {\n\t\t\t\tif (params?.type === undefined && params?.invert === undefined && params?.enabled === undefined) fail('Pass at least one of type, invert, enabled.');\n\t\t\t\tconst groups = maskTargets(params);\n\t\t\t\tconst patch = { ...(params?.type ? { type: params.type } : {}), ...(params?.invert !== undefined ? { invert: params.invert } : {}), ...(params?.enabled !== undefined ? { enabled: params.enabled } : {}) };\n\t\t\t\tcheck(controller.setMaskOptions(patch, { ids: groups }));\n\t\t\t\treturn { groups: groups.map(maskResult) };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}releaseMask`,\n\t\t\tlabel: 'Release Mask',\n\t\t\tkeywords: ['mask', 'clip', 'release', 'unmask', 'remove mask'],\n\t\t\tshortcut: 'Mod+Alt+7',\n\t\t\tdescription:\n\t\t\t\t'Releases mask groups: the mask goes back on top of the contents and draws as itself again; the group is replaced by its children unless it has filters, a blend mode or path effects (then it stays a plain group). One undo step. Returns { ids } of the released objects (the mask last).',\n\t\t\tparams: { type: 'object', properties: { ids: { ...IDS, description: 'Mask group ids (default: the selection).' } }, additionalProperties: false },\n\t\t\tundoable: true,\n\t\t\tenabled: enabledFor,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[] } | undefined) => {\n\t\t\t\tconst groups = maskTargets(params);\n\t\t\t\treturn { ids: controller.releaseMask({ ids: groups }) };\n\t\t\t}\n\t\t}\n\t];\n\tconst appearanceCommands = (): CommandSpec[] => [\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}getAppearance`,\n\t\t\tlabel: 'Get Appearance',\n\t\t\treadOnly: true,\n\t\t\tkeywords: ['appearance', 'fills', 'strokes', 'blend mode', 'filters', 'shadow'],\n\t\t\tdescription:\n\t\t\t\t'Reads a node\\'s appearance: for paths the fill and stroke stacks (bottom to top, each with paint, opacity, blendMode, visible and, for strokes, their settings) and fill rule; for any node its blend mode, opacity and filters (with ids). Use the indexes and ids with the fill, stroke and filter commands.',\n\t\t\tparams: { type: 'object', properties: { id: { type: 'string', description: 'Node id (from vector.list).' } }, required: ['id'], additionalProperties: false },\n\t\t\trun: (_ctx: unknown, params: { id: string }) => appearanceOf(params.id)\n\t\t},\n\t\t...layerCommands('fill'),\n\t\t...layerCommands('stroke'),\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}setBlendMode`,\n\t\t\tlabel: 'Blend Mode…',\n\t\t\tkeywords: ['blend', 'multiply', 'screen', 'overlay', 'mix-blend-mode', 'composite'],\n\t\t\tdescription: 'Sets how whole nodes (paths or groups, with everything they draw) mix with what is under them. `normal` removes it. Returns each node\\'s blend mode.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: { ids: IDS, blendMode: { ...BLEND_MODE, description: 'The blend mode (CSS mix-blend-mode names; linear-dodge draws as plus-lighter).' } },\n\t\t\t\trequired: ['blendMode'],\n\t\t\t\tadditionalProperties: false\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\tenabled: enabledFor,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[]; blendMode: VectorBlendMode }) => {\n\t\t\t\tconst targets = ids(params);\n\t\t\t\tcheck(controller.setBlendMode(params.blendMode, { ids: targets }));\n\t\t\t\treturn { nodes: targets.map((id) => ({ id, blendMode: controller.getNode(id)?.blendMode ?? 'normal' })) };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}addFilter`,\n\t\t\tlabel: 'Add Filter…',\n\t\t\tkeywords: ['filter', 'drop shadow', 'inner shadow', 'glow', 'blur', 'feather', 'effect', 'appearance'],\n\t\t\tdescription:\n\t\t\t\t'Adds a visual filter `type` (dropShadow, innerShadow, outerGlow, innerGlow, layerBlur, feather) to nodes (paths or groups), at the end of each filter stack or at `index`. Filters never change geometry (bounds and hit-testing ignore them) and export as SVG filters. Returns each node\\'s new filter id and its filter stack.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\ttype: { type: 'string', enum: VECTOR_FILTER_TYPES, description: 'The filter type.' },\n\t\t\t\t\tparams: FILTER_PARAMS,\n\t\t\t\t\tindex: { type: 'integer', minimum: 0, description: 'Position in the stack (default: last).' },\n\t\t\t\t\tids: IDS\n\t\t\t\t},\n\t\t\t\trequired: ['type'],\n\t\t\t\tadditionalProperties: false\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\tenabled: enabledFor,\n\t\t\trun: (_ctx: unknown, params: { type: VectorFilterType; params?: Record<string, unknown>; index?: number; ids?: string[] }) => {\n\t\t\t\tif (!VECTOR_FILTER_TYPES.includes(params.type)) fail(`Unknown filter type \"${params.type}\"; use one of ${VECTOR_FILTER_TYPES.join(', ')}.`);\n\t\t\t\tcheckFilterParams(params.type, params.params);\n\t\t\t\tconst targets = ids(params);\n\t\t\t\tconst created = controller.addFilter(params.type, { ids: targets, params: params.params, index: params.index });\n\t\t\t\tif (!created.length) fail('Nothing to add the filter to (blend steps take no filters).');\n\t\t\t\treturn { nodes: targets.map((id, i) => ({ filterId: created[i], ...filtersResult(id) })) };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}updateFilter`,\n\t\t\tlabel: 'Edit Filter…',\n\t\t\tkeywords: ['filter', 'shadow', 'glow', 'blur', 'feather', 'enable', 'disable'],\n\t\t\tdescription: 'Changes filter `filterId` of node `nodeId`: its `params` (merged; only the keys its type takes) and whether it is `enabled`. Returns the node\\'s filter stack.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tnodeId: { type: 'string', description: 'Node id.' },\n\t\t\t\t\tfilterId: { type: 'string', description: 'Filter id (from vector.addFilter or vector.getAppearance).' },\n\t\t\t\t\tparams: FILTER_PARAMS,\n\t\t\t\t\tenabled: { type: 'boolean', description: 'false turns the filter off (kept in the stack).' }\n\t\t\t\t},\n\t\t\t\trequired: ['nodeId', 'filterId'],\n\t\t\t\tadditionalProperties: false\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { nodeId: string; filterId: string; params?: Record<string, unknown>; enabled?: boolean }) => {\n\t\t\t\tconst filter = filterOf(params.nodeId, params.filterId);\n\t\t\t\tcheckFilterParams(filter.type, params.params);\n\t\t\t\tif (params.params === undefined && params.enabled === undefined) fail('Pass params or enabled.');\n\t\t\t\tcheck(controller.updateFilter(params.nodeId, params.filterId, { params: params.params, enabled: params.enabled }));\n\t\t\t\treturn filtersResult(params.nodeId);\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}removeFilter`,\n\t\t\tlabel: 'Remove Filter',\n\t\t\tkeywords: ['filter', 'delete', 'shadow', 'blur'],\n\t\t\tdescription: 'Removes filter `filterId` from node `nodeId`. Returns the node\\'s remaining filters.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: { nodeId: { type: 'string', description: 'Node id.' }, filterId: { type: 'string', description: 'Filter id.' } },\n\t\t\t\trequired: ['nodeId', 'filterId'],\n\t\t\t\tadditionalProperties: false\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { nodeId: string; filterId: string }) => {\n\t\t\t\tfilterOf(params.nodeId, params.filterId);\n\t\t\t\tcheck(controller.removeFilter(params.nodeId, params.filterId));\n\t\t\t\treturn filtersResult(params.nodeId);\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}moveFilter`,\n\t\t\tlabel: 'Reorder Filters',\n\t\t\tkeywords: ['filter', 'order', 'reorder'],\n\t\t\tdescription: 'Moves filter `filterId` of node `nodeId` to position `index` of its stack. Order matters within a kind: later drop shadows draw over earlier ones, later inner effects over earlier ones. Returns the reordered filters.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tnodeId: { type: 'string', description: 'Node id.' },\n\t\t\t\t\tfilterId: { type: 'string', description: 'Filter id.' },\n\t\t\t\t\tindex: { type: 'integer', minimum: 0, description: 'New position, 0 first (clamped to the stack).' }\n\t\t\t\t},\n\t\t\t\trequired: ['nodeId', 'filterId', 'index'],\n\t\t\t\tadditionalProperties: false\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { nodeId: string; filterId: string; index: number }) => {\n\t\t\t\tfilterOf(params.nodeId, params.filterId);\n\t\t\t\tcheck(controller.moveFilter(params.nodeId, params.filterId, params.index));\n\t\t\t\treturn filtersResult(params.nodeId);\n\t\t\t}\n\t\t}\n\t];\n\n\n\t// --- Text ----------------------------------------------------------------\n\tconst round = (n: number) => Math.round(n * 100) / 100;\n\t/** A text node as agents read it: text, styles as ranges, frame, laid-out lines and bounds. */\n\tconst textSummary = (id: string) => {\n\t\tconst n = controller.getNode(id) ?? fail(`There is no node \"${id}\".`);\n\t\tif (n.type !== 'text') fail(`\"${id}\" is a ${n.type}, not text; vector.list shows each node's type.`);\n\t\tconst t = n as VectorTextNode;\n\t\tconst layout = layoutVectorText(t);\n\t\tconst parent = vectorParentMatrix(controller.getDocument().nodes, id) ?? IDENTITY_MATRIX;\n\t\tconst b = vectorNodeBounds(t, parent);\n\t\tlet at = 0;\n\t\tconst runs = (t.runs ?? []).flatMap((r) => {\n\t\t\tconst range = { start: at, end: at + r.length };\n\t\t\tat += r.length;\n\t\t\treturn Object.keys(r.style).length ? [{ ...range, style: r.style }] : [];\n\t\t});\n\t\treturn {\n\t\t\tid,\n\t\t\ttext: t.text,\n\t\t\tfont: t.font,\n\t\t\truns,\n\t\t\tparagraph: t.paragraph ?? {},\n\t\t\tframe: t.frame,\n\t\t\tfill: t.fill ?? null,\n\t\t\tstroke: t.stroke ?? null,\n\t\t\tlines: layout.lines.map((l) => ({ start: l.start, end: l.end, text: t.text.slice(l.start, l.end) })),\n\t\t\tbounds: b && { x: round(b.x), y: round(b.y), width: round(b.width), height: round(b.height) },\n\t\t\toverflow: layout.overflow,\n\t\t\toutlines: vectorTextHasOutlines(),\n\t\t\tshaper: layout.shaper,\n\t\t\tmissingFonts: layout.missingFonts\n\t\t};\n\t};\n\tconst FONT_PROPS = {\n\t\tfontFamily: { type: 'string', description: 'Font family name (\"Inter\", \"serif\"). A family the text shaper lacks draws in a fallback and is listed in missingFonts (vector.listFonts shows what it has).' },\n\t\tfontWeight: { type: 'integer', minimum: 1, maximum: 1000, description: 'CSS weight: 400 regular, 700 bold.' },\n\t\titalic: { type: 'boolean', description: 'Italic (default false).' },\n\t\tfontSize: { type: 'number', minimum: 0.5, maximum: 5000, description: 'Size in document units (default 24).' },\n\t\ttracking: { type: ['number', 'null'], minimum: -1000, maximum: 10000, description: 'Letter spacing in 1/1000 em (0 default; null clears).' },\n\t\tleading: { type: ['number', 'null'], minimum: 0, maximum: 10000, description: 'Baseline-to-baseline distance in document units; null for auto (autoLeading × size).' },\n\t\tbaselineShift: { type: ['number', 'null'], minimum: -5000, maximum: 5000, description: 'Raises (positive) or lowers the text, document units (0 default).' },\n\t\tunderline: { type: 'boolean', description: 'Underline.' },\n\t\tstrikethrough: { type: 'boolean', description: 'Strike through.' }\n\t} as const;\n\tconst PARAGRAPH_PROPS = {\n\t\talign: { type: 'string', enum: ['left', 'center', 'right', 'justify'], description: 'Alignment (default left). justify stretches area text lines but the last of a paragraph, and spreads path text over the path.' },\n\t\tautoLeading: { type: ['number', 'null'], minimum: 0.1, maximum: 10, description: 'Auto leading as a multiple of the font size (default 1.2).' },\n\t\tfirstLineIndent: { type: ['number', 'null'], description: 'Extra indent of each paragraph\\'s first line, document units.' },\n\t\tstartIndent: { type: ['number', 'null'], description: 'Indent from the start edge, document units.' },\n\t\tendIndent: { type: ['number', 'null'], description: 'Indent from the end edge, document units.' },\n\t\tspaceBefore: { type: ['number', 'null'], description: 'Space above each paragraph but the first, document units.' },\n\t\tspaceAfter: { type: ['number', 'null'], description: 'Space below each paragraph, document units.' }\n\t} as const;\n\tconst PATH_TEXT_PROPS = {\n\t\tstart: { type: 'number', minimum: 0, description: 'Margin from the path\\'s start where the text may begin (default 0).' },\n\t\tend: { type: 'number', minimum: 0, description: 'Margin from the path\\'s end (default 0).' },\n\t\tflip: { type: 'boolean', description: 'Run the text the other way along the path, on its other side.' },\n\t\torientation: { type: 'string', enum: ['rotate', 'upright'], description: 'rotate (default): characters turn with the path; upright: they stay level.' },\n\t\tbaseline: { type: 'string', enum: ['baseline', 'ascender', 'descender', 'center'], description: 'What sits on the path (default baseline; ascender hangs the text below it).' }\n\t} as const;\n\tconst RANGE_PROPS = {\n\t\tstart: { type: 'integer', minimum: 0, description: 'Start of the range in UTF-16 code units (default 0; vector.getText gives the text and its runs).' },\n\t\tend: { type: 'integer', minimum: 0, description: 'End of the range, exclusive (default the text\\'s length).' }\n\t} as const;\n\tconst pick = (params: Record<string, unknown>, keys: readonly string[]) => {\n\t\tconst out: Record<string, unknown> = {};\n\t\tfor (const k of keys) if (params[k] !== undefined) out[k] = params[k];\n\t\treturn out;\n\t};\n\tconst textCommands = (): CommandSpec[] => [\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}listFonts`,\n\t\t\tlabel: 'List Fonts',\n\t\t\treadOnly: true,\n\t\t\tkeywords: ['font', 'typeface', 'family', 'text'],\n\t\t\tdescription:\n\t\t\t\t'Lists the font families the text shaper can draw, which shaper it is, and whether it gives glyph outlines (needed for Create Outlines, and for path effects and envelopes on text). Returns { shaper, outlines, families }.',\n\t\t\tparams: { type: 'object', properties: {}, additionalProperties: false },\n\t\t\trun: () => ({ shaper: controller.getTextShaper().id, outlines: vectorTextHasOutlines(), families: vectorTextFamilies() })\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}addText`,\n\t\t\tlabel: 'Add Text…',\n\t\t\tkeywords: ['text', 'type', 'label', 'title', 'headline', 'caption', 'paragraph', 'words'],\n\t\t\tdescription:\n\t\t\t\t'Adds text: point text whose first baseline starts at (x, y), area text wrapped in a box (x, y, width, height), or text along `path` (SVG path data, document units). Paragraphs are separated by \"\\\\n\". Paint defaults to black with no stroke; style it with fill / stroke here or vector.setStyle. One undo step. Returns vector.getText\\'s summary of the new text (id, lines, bounds, overflow).',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\ttext: { type: 'string', description: 'The text; \"\\\\n\" starts a new paragraph.' },\n\t\t\t\t\tx: { type: 'number', description: 'Point text: x of the first baseline\\'s start; area text: the box\\'s left (default 0).' },\n\t\t\t\t\ty: { type: 'number', description: 'Point text: y of the first baseline; area text: the box\\'s top (default 0).' },\n\t\t\t\t\twidth: { type: 'number', minimum: 0, description: 'Area text: the box width (with height).' },\n\t\t\t\t\theight: { type: 'number', minimum: 0, description: 'Area text: the box height; lines below it are hidden (overflow).' },\n\t\t\t\t\tpath: { type: 'string', description: 'Text on a path: SVG path data of the line the text follows (its first subpath).' },\n\t\t\t\t\t...PATH_TEXT_PROPS,\n\t\t\t\t\tfont: { type: 'object', properties: FONT_PROPS, additionalProperties: false, description: 'Character style (default the editor\\'s text defaults: 24 sans-serif).' },\n\t\t\t\t\tparagraph: { type: 'object', properties: PARAGRAPH_PROPS, additionalProperties: false, description: 'Paragraph style.' },\n\t\t\t\t\tname: { type: 'string', description: 'Layer name (default the text\\'s start).' },\n\t\t\t\t\tfill: PAINT,\n\t\t\t\t\tstroke: PAINT,\n\t\t\t\t\tstrokeWidth: { type: 'number', minimum: 0, description: 'Stroke width (default 1).' }\n\t\t\t\t},\n\t\t\t\trequired: ['text'],\n\t\t\t\tadditionalProperties: false\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: Record<string, unknown> & { text: string }) => {\n\t\t\t\tconst pathOptions = pick(params, Object.keys(PATH_TEXT_PROPS));\n\t\t\t\tif (params.path === undefined && Object.keys(pathOptions).length) fail(`${Object.keys(pathOptions).join(', ')} only apply to text on a path; pass path too.`);\n\t\t\t\tif ((params.width === undefined) !== (params.height === undefined)) fail('Area text needs both width and height; leave both out for point text.');\n\t\t\t\tlet id = '';\n\t\t\t\ttry {\n\t\t\t\t\tid = controller.addText({\n\t\t\t\t\t\ttext: params.text,\n\t\t\t\t\t\t...pick(params, ['x', 'y', 'width', 'height', 'name', 'strokeWidth']),\n\t\t\t\t\t\t...(params.path !== undefined ? { path: params.path as string, pathOptions } : {}),\n\t\t\t\t\t\t...(params.font ? { font: params.font as Record<string, unknown> } : {}),\n\t\t\t\t\t\t...(params.paragraph ? { paragraph: params.paragraph as Record<string, unknown> } : {}),\n\t\t\t\t\t\t...(params.fill !== undefined ? { fill: checkPaint(params.fill, 'fill') } : {}),\n\t\t\t\t\t\t...(params.stroke !== undefined ? { stroke: checkPaint(params.stroke, 'stroke') } : {})\n\t\t\t\t\t});\n\t\t\t\t} catch (e) {\n\t\t\t\t\tfail(e instanceof Error ? e.message : String(e));\n\t\t\t\t}\n\t\t\t\treturn textSummary(id);\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}getText`,\n\t\t\tlabel: 'Get Text',\n\t\t\treadOnly: true,\n\t\t\tkeywords: ['text', 'type', 'read', 'font', 'runs'],\n\t\t\tdescription:\n\t\t\t\t'Reads a text node: its text, base font, runs (styled ranges as { start, end, style } in UTF-16 code units; style fields override the font, fill a range\\'s colour), paragraph style, frame (point / area / path), fill and stroke, the laid-out lines (start, end, text), document bounds, overflow (text that does not fit its box or path), whether it draws as outlines, the shaper and missing fonts. Use the ranges with vector.setText and vector.setTextStyle.',\n\t\t\tparams: { type: 'object', properties: { id: { type: 'string', description: 'Text node id (from vector.list).' } }, required: ['id'], additionalProperties: false },\n\t\t\trun: (_ctx: unknown, params: { id: string }) => textSummary(params.id)\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}setText`,\n\t\t\tlabel: 'Set Text…',\n\t\t\tkeywords: ['text', 'type', 'edit', 'replace', 'insert', 'delete'],\n\t\t\tdescription:\n\t\t\t\t'Replaces [start, end) of a text node\\'s text with `text` (default the whole text; start = end inserts). Inserted characters take the style of the character before them. One undo step. Returns the text summary (vector.getText).',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: { id: { type: 'string', description: 'Text node id.' }, text: { type: 'string', description: 'The new characters (\"\" deletes the range).' }, ...RANGE_PROPS },\n\t\t\t\trequired: ['id', 'text'],\n\t\t\t\tadditionalProperties: false\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { id: string; text: string; start?: number; end?: number }) => {\n\t\t\t\tcheck(controller.setText(params.id, params.text, { start: params.start, end: params.end }));\n\t\t\t\treturn textSummary(params.id);\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}setTextStyle`,\n\t\t\tlabel: 'Character Style…',\n\t\t\tkeywords: ['font', 'bold', 'italic', 'size', 'tracking', 'kerning', 'leading', 'underline', 'colour', 'color', 'text'],\n\t\t\tdescription:\n\t\t\t\t'Sets character style fields (and `fill`, a colour or gradient for those characters) over [start, end) of a text node; the whole text (the default) changes the node\\'s own font and fill. null clears tracking, leading and baseline shift. One undo step. Returns the text summary with its runs.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: { id: { type: 'string', description: 'Text node id.' }, ...RANGE_PROPS, ...FONT_PROPS, fill: { ...PAINT, description: 'Paint of these characters (the whole text: the node\\'s fill).' } },\n\t\t\t\trequired: ['id'],\n\t\t\t\tadditionalProperties: false\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: Record<string, unknown> & { id: string; start?: number; end?: number }) => {\n\t\t\t\tconst patch = pick(params, [...Object.keys(FONT_PROPS), 'fill']);\n\t\t\t\tif (!Object.keys(patch).length) fail(`Pass at least one of ${[...Object.keys(FONT_PROPS), 'fill'].join(', ')}.`);\n\t\t\t\tif (patch.fill !== undefined) patch.fill = checkPaint(patch.fill, 'fill');\n\t\t\t\tcheck(controller.setTextStyle(params.id, patch, { start: params.start, end: params.end }));\n\t\t\t\treturn textSummary(params.id);\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}setParagraphStyle`,\n\t\t\tlabel: 'Paragraph Style…',\n\t\t\tkeywords: ['align', 'centre', 'center', 'justify', 'indent', 'spacing', 'leading', 'paragraph'],\n\t\t\tdescription: 'Sets paragraph fields of a text node (all its paragraphs): align, autoLeading, indents and spacing; null clears one. One undo step. Returns the text summary.',\n\t\t\tparams: { type: 'object', properties: { id: { type: 'string', description: 'Text node id.' }, ...PARAGRAPH_PROPS }, required: ['id'], additionalProperties: false },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: Record<string, unknown> & { id: string }) => {\n\t\t\t\tconst patch = pick(params, Object.keys(PARAGRAPH_PROPS));\n\t\t\t\tif (!Object.keys(patch).length) fail(`Pass at least one of ${Object.keys(PARAGRAPH_PROPS).join(', ')}.`);\n\t\t\t\tcheck(controller.setParagraphStyle(params.id, patch));\n\t\t\t\treturn textSummary(params.id);\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}setTextFrame`,\n\t\t\tlabel: 'Text Frame…',\n\t\t\tkeywords: ['point text', 'area text', 'text box', 'wrap', 'resize text', 'path text', 'convert'],\n\t\t\tdescription:\n\t\t\t\t'Changes how a text node is set: kind point (no wrapping; x, y the first baseline), area (wrapped in a box; width / height default to its current size) or path (with `path` data in the node\\'s own space, or the current path), plus the path options. Omitting kind keeps the current one and edits its fields. One undo step. Returns the text summary.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tid: { type: 'string', description: 'Text node id.' },\n\t\t\t\t\tkind: { type: 'string', enum: ['point', 'area', 'path'], description: 'The frame to switch to (default: keep).' },\n\t\t\t\t\tx: { type: 'number', description: 'Point: the first baseline\\'s start; area: the box\\'s left.' },\n\t\t\t\t\ty: { type: 'number', description: 'Point: the first baseline; area: the box\\'s top.' },\n\t\t\t\t\twidth: { type: 'number', minimum: 0, description: 'Area: box width.' },\n\t\t\t\t\theight: { type: 'number', minimum: 0, description: 'Area: box height.' },\n\t\t\t\t\tpath: { type: 'string', description: 'Path: SVG path data of the line to follow, in the node\\'s own space.' },\n\t\t\t\t\t...PATH_TEXT_PROPS\n\t\t\t\t},\n\t\t\t\trequired: ['id'],\n\t\t\t\tadditionalProperties: false\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: Record<string, unknown> & { id: string }) => {\n\t\t\t\tconst { id, ...patch } = params;\n\t\t\t\tif (!Object.keys(patch).length) fail('Pass kind or a frame field to change.');\n\t\t\t\tcheck(controller.setTextFrame(id, patch));\n\t\t\t\treturn textSummary(id);\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}typeOnPath`,\n\t\t\tlabel: 'Type on a Path',\n\t\t\tkeywords: ['text on path', 'path text', 'curved text', 'circle text', 'text along'],\n\t\t\tdescription:\n\t\t\t\t'Turns a path node into text set along it (Type on a Path): the text takes the path\\'s place and its first subpath (the path\\'s paint is dropped; the text is black unless fill is given). One undo step. Returns the text summary (its id is new).',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tpathId: { type: 'string', description: 'The path node to set text along (from vector.list).' },\n\t\t\t\t\ttext: { type: 'string', description: 'The text (default empty).' },\n\t\t\t\t\tfont: { type: 'object', properties: FONT_PROPS, additionalProperties: false, description: 'Character style (default the text defaults).' },\n\t\t\t\t\tparagraph: { type: 'object', properties: PARAGRAPH_PROPS, additionalProperties: false, description: 'Paragraph style; align places the text along the path.' },\n\t\t\t\t\t...PATH_TEXT_PROPS,\n\t\t\t\t\tfill: PAINT\n\t\t\t\t},\n\t\t\t\trequired: ['pathId'],\n\t\t\t\tadditionalProperties: false\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: Record<string, unknown> & { pathId: string }) => {\n\t\t\t\tconst r = controller.typeOnPath(params.pathId, {\n\t\t\t\t\t...(params.text !== undefined ? { text: params.text as string } : {}),\n\t\t\t\t\t...(params.font ? { font: params.font as Record<string, unknown> } : {}),\n\t\t\t\t\t...(params.paragraph ? { paragraph: params.paragraph as Record<string, unknown> } : {}),\n\t\t\t\t\tpathOptions: pick(params, Object.keys(PATH_TEXT_PROPS)),\n\t\t\t\t\t...(params.fill !== undefined ? { fill: checkPaint(params.fill, 'fill') } : {})\n\t\t\t\t});\n\t\t\t\tcheck(r);\n\t\t\t\treturn textSummary(r.id!);\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}createOutlines`,\n\t\t\tlabel: 'Create Outlines',\n\t\t\tshortcut: 'Mod+Shift+O',\n\t\t\tkeywords: ['outline text', 'text to paths', 'convert text', 'expand text', 'glyphs'],\n\t\t\tdescription:\n\t\t\t\t'Turns text (the given nodes, or text inside given groups; default the selection) into paths of its glyph outlines, keeping paint, effects, filters, placement and id (runs with their own fill become a group of paths). The text can no longer be edited as text. Needs a text shaper with outlines (vector.listFonts says); fails otherwise. One undo step. Returns { ids }.',\n\t\t\tparams: { type: 'object', properties: { ids: { ...IDS, description: 'Text nodes or groups holding text (default: the selection).' } }, additionalProperties: false },\n\t\t\tundoable: true,\n\t\t\tenabled: enabledFor,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[] } | undefined) => {\n\t\t\t\tconst r = controller.createOutlines({ ids: ids(params) });\n\t\t\t\tcheck(r);\n\t\t\t\treturn { ids: r.ids };\n\t\t\t}\n\t\t}\n\t];\n\n\n\t// --- Symbols ---------------------------------------------------------------\n\tconst symbolSummary = (id: string) => {\n\t\tconst sym = controller.getSymbols().find((x) => x.id === id) ?? fail(`There is no symbol \"${id}\"; vector.listSymbols lists them.`);\n\t\tconst nodes: { id: string; type: string; name?: string }[] = [];\n\t\tconst walk = (list: readonly VectorNode[]) => {\n\t\t\tfor (const n of list) {\n\t\t\t\tnodes.push({ id: n.id, type: n.type, ...(n.name ? { name: n.name } : {}) });\n\t\t\t\tif (n.type === 'group') walk(n.children);\n\t\t\t}\n\t\t};\n\t\twalk(sym.nodes);\n\t\tconst b = unionBoundsOf(sym.nodes);\n\t\treturn { id: sym.id, name: sym.name, nodes, bounds: b, instances: controller.getSymbolInstances(sym.id) };\n\t};\n\tconst unionBoundsOf = (list: readonly VectorNode[]) => {\n\t\tlet x0 = Infinity;\n\t\tlet y0 = Infinity;\n\t\tlet x1 = -Infinity;\n\t\tlet y1 = -Infinity;\n\t\tfor (const n of list) {\n\t\t\tconst b = vectorNodeBounds(n, IDENTITY_MATRIX);\n\t\t\tif (!b) continue;\n\t\t\tx0 = Math.min(x0, b.x);\n\t\t\ty0 = Math.min(y0, b.y);\n\t\t\tx1 = Math.max(x1, b.x + b.width);\n\t\t\ty1 = Math.max(y1, b.y + b.height);\n\t\t}\n\t\treturn x0 < Infinity ? { x: round(x0), y: round(y0), width: round(x1 - x0), height: round(y1 - y0) } : null;\n\t};\n\tconst MATRIX = { type: 'array', items: { type: 'number' }, minItems: 6, maxItems: 6, description: 'An affine matrix [a, b, c, d, e, f] (SVG order): x\\' = a·x + c·y + e, y\\' = b·x + d·y + f. Instances may scale and skew.' } as const;\n\tconst symbolCommands = (): CommandSpec[] => [\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}listSymbols`,\n\t\t\tlabel: 'List Symbols',\n\t\t\treadOnly: true,\n\t\t\tkeywords: ['symbol', 'symbols', 'library', 'component', 'reuse', 'instance'],\n\t\t\tdescription:\n\t\t\t\t'Lists the document\\'s symbols: id, name, the art\\'s nodes (ids, types, names; use them for hidden overrides), its bounds in symbol space (around the registration point at 0, 0) and the ids of its instances. Also says which symbol is being edited in isolation, if any.',\n\t\t\tparams: { type: 'object', properties: {}, additionalProperties: false },\n\t\t\trun: () => ({ symbols: controller.getSymbols().map((x) => symbolSummary(x.id)), editing: controller.getState().symbolEdit?.symbolId ?? null })\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}makeSymbol`,\n\t\t\tlabel: 'Make Symbol',\n\t\t\tshortcut: 'F8',\n\t\t\tkeywords: ['symbol', 'make symbol', 'component', 'reuse', 'library'],\n\t\t\tdescription:\n\t\t\t\t'Turns objects that share a parent (`ids`, default the selection) into a new symbol, its registration point at their centre, and puts an instance of it in their place (selected). One undo step. Returns { symbolId, instanceId, symbol } (the symbol summary of vector.listSymbols).',\n\t\t\tparams: { type: 'object', properties: { ids: { ...IDS, description: 'Objects to make the symbol from (default: the selection).' }, name: { type: 'string', description: 'Symbol name (default \"Symbol n\").' } }, additionalProperties: false },\n\t\t\tundoable: true,\n\t\t\tenabled: enabledFor,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[]; name?: string } | undefined) => {\n\t\t\t\tconst r = controller.makeSymbol({ ids: ids(params), ...(params?.name ? { name: params.name } : {}) });\n\t\t\t\tcheck(r);\n\t\t\t\treturn { symbolId: r.symbolId, instanceId: r.instanceId, symbol: symbolSummary(r.symbolId!) };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}placeSymbol`,\n\t\t\tlabel: 'Place Symbol…',\n\t\t\tkeywords: ['symbol', 'instance', 'place', 'stamp', 'reuse'],\n\t\t\tdescription:\n\t\t\t\t'Places a new instance of symbol `symbolId` with its registration point at (x, y) (default the artboard centre), or through `transform` (which may scale, rotate and skew). Fails when the symbol is being edited and would contain itself. One undo step. Returns { id }.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tsymbolId: { type: 'string', description: 'The symbol (from vector.listSymbols).' },\n\t\t\t\t\tx: { type: 'number', description: 'Where the registration point goes (document units).' },\n\t\t\t\t\ty: { type: 'number', description: 'Where the registration point goes (document units).' },\n\t\t\t\t\ttransform: { ...MATRIX, description: 'Placement instead of x / y: an affine matrix [a, b, c, d, e, f] (scale and skew allowed).' },\n\t\t\t\t\tname: { type: 'string', description: 'Instance name (default the symbol\\'s).' }\n\t\t\t\t},\n\t\t\t\trequired: ['symbolId'],\n\t\t\t\tadditionalProperties: false\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { symbolId: string; x?: number; y?: number; transform?: number[]; name?: string }) => {\n\t\t\t\tif (params.transform && (params.x !== undefined || params.y !== undefined)) fail('Pass either x / y or transform, not both.');\n\t\t\t\tconst r = controller.placeSymbol(params.symbolId, {\n\t\t\t\t\t...(params.x !== undefined ? { x: params.x } : {}),\n\t\t\t\t\t...(params.y !== undefined ? { y: params.y } : {}),\n\t\t\t\t\t...(params.transform ? { transform: params.transform as unknown as VectorMatrix } : {}),\n\t\t\t\t\t...(params.name ? { name: params.name } : {})\n\t\t\t\t});\n\t\t\t\tcheck(r);\n\t\t\t\treturn { id: r.id };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}setInstanceOptions`,\n\t\t\tlabel: 'Instance Options…',\n\t\t\tkeywords: ['symbol', 'instance', 'override', 'tint', 'scale strokes', 'hide'],\n\t\t\tdescription:\n\t\t\t\t'Changes instances (`ids`, default the selection): `scaleStrokes` (strokes of the art scale with the instance, default true), and overrides — `fill` / `stroke` paint every fill or stroke of the art with one paint (null clears), `hidden` lists art node ids (from vector.listSymbols) not drawn ([] clears). Pass at least one. One undo step. Returns the instances as vector.list lists them.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tids: { ...IDS, description: 'Instance ids (default: the selection).' },\n\t\t\t\t\tscaleStrokes: { type: 'boolean', description: 'Strokes scale with the instance (true, default) or keep their width (false).' },\n\t\t\t\t\tfill: { ...PAINT, description: 'Paint for every fill of the art in these instances; null clears the override.' },\n\t\t\t\t\tstroke: { ...PAINT, description: 'Paint for every stroke of the art; null clears the override.' },\n\t\t\t\t\thidden: { type: ['array', 'null'], items: { type: 'string' }, description: 'Ids of art nodes to hide in these instances; [] or null shows them all.' }\n\t\t\t\t},\n\t\t\t\tadditionalProperties: false\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\tenabled: enabledFor,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[]; scaleStrokes?: boolean; fill?: unknown; stroke?: unknown; hidden?: string[] | null } | undefined) => {\n\t\t\t\tconst patch = {\n\t\t\t\t\t...(params?.scaleStrokes !== undefined ? { scaleStrokes: params.scaleStrokes } : {}),\n\t\t\t\t\t...(params?.fill !== undefined ? { fill: params.fill === null ? null : checkPaint(params.fill, 'fill') } : {}),\n\t\t\t\t\t...(params?.stroke !== undefined ? { stroke: params.stroke === null ? null : checkPaint(params.stroke, 'stroke') } : {}),\n\t\t\t\t\t...(params?.hidden !== undefined ? { hidden: params.hidden } : {})\n\t\t\t\t};\n\t\t\t\tif (!Object.keys(patch).length) fail('Pass at least one of scaleStrokes, fill, stroke, hidden.');\n\t\t\t\tconst targetIds = ids(params);\n\t\t\t\tcheck(controller.setInstanceOptions(patch, { ids: targetIds }));\n\t\t\t\tconst listed = list().nodes;\n\t\t\t\treturn { instances: targetIds.map((id) => listed.find((n) => n.id === id)) };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}replaceSymbol`,\n\t\t\tlabel: 'Replace Symbol…',\n\t\t\tkeywords: ['symbol', 'instance', 'swap', 'replace'],\n\t\t\tdescription: 'Makes instances (`ids`, default the selection) draw symbol `symbolId` instead, keeping their placement and colour overrides (hidden art ids are dropped). One undo step. Returns { ids }.',\n\t\t\tparams: { type: 'object', properties: { ids: { ...IDS, description: 'Instance ids (default: the selection).' }, symbolId: { type: 'string', description: 'The symbol to draw (from vector.listSymbols).' } }, required: ['symbolId'], additionalProperties: false },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[]; symbolId: string }) => {\n\t\t\t\tconst targetIds = ids(params);\n\t\t\t\tcheck(controller.replaceSymbol(params.symbolId, { ids: targetIds }));\n\t\t\t\treturn { ids: targetIds };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}detachInstance`,\n\t\t\tlabel: 'Break Link to Symbol',\n\t\t\tkeywords: ['symbol', 'instance', 'detach', 'break link', 'unlink', 'expand'],\n\t\t\tdescription: 'Replaces instances (`ids`, default the selection) by plain groups of their art as drawn (placement, scaled strokes and overrides baked in), no longer linked to the symbol. One undo step. Returns { ids } (the groups keep the instances\\' ids).',\n\t\t\tparams: { type: 'object', properties: { ids: { ...IDS, description: 'Instance ids (default: the selection).' } }, additionalProperties: false },\n\t\t\tundoable: true,\n\t\t\tenabled: enabledFor,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[] } | undefined) => {\n\t\t\t\tconst r = controller.detachInstances({ ids: ids(params) });\n\t\t\t\tcheck(r);\n\t\t\t\treturn { ids: r.ids };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}renameSymbol`,\n\t\t\tlabel: 'Rename Symbol…',\n\t\t\tkeywords: ['symbol', 'rename', 'name'],\n\t\t\tdescription: 'Renames symbol `symbolId`. One undo step. Returns the symbol summary.',\n\t\t\tparams: { type: 'object', properties: { symbolId: { type: 'string', description: 'The symbol (from vector.listSymbols).' }, name: { type: 'string', minLength: 1, description: 'Its new name.' } }, required: ['symbolId', 'name'], additionalProperties: false },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { symbolId: string; name: string }) => {\n\t\t\t\tcheck(controller.renameSymbol(params.symbolId, params.name));\n\t\t\t\treturn symbolSummary(params.symbolId);\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}deleteSymbol`,\n\t\t\tlabel: 'Delete Symbol…',\n\t\t\tkeywords: ['symbol', 'delete', 'remove'],\n\t\t\tdescription: 'Deletes symbol `symbolId`. Fails while instances use it unless `detach: true` turns them into plain groups first. One undo step. Returns { ok }.',\n\t\t\tparams: { type: 'object', properties: { symbolId: { type: 'string', description: 'The symbol (from vector.listSymbols).' }, detach: { type: 'boolean', description: 'Turn its instances into groups first (default false: fail when it is used).' } }, required: ['symbolId'], additionalProperties: false },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { symbolId: string; detach?: boolean }) => {\n\t\t\t\tcheck(controller.deleteSymbol(params.symbolId, { detach: params.detach }));\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}editSymbol`,\n\t\t\tlabel: 'Edit Symbol',\n\t\t\tkeywords: ['symbol', 'edit symbol', 'isolation', 'master'],\n\t\t\tdescription:\n\t\t\t\t'Enters isolation mode on a symbol (`id` of the symbol, or of one of its instances): the document then holds the symbol\\'s art in its own space (registration point at 0, 0), so every vector.* command edits the art and every instance follows. Call vector.exitSymbolEdit when done. Not an undo step itself. Returns { symbolId, nodes } (vector.list for the art).',\n\t\t\tparams: { type: 'object', properties: { id: { type: 'string', description: 'A symbol id (vector.listSymbols) or an instance id (vector.list).' } }, required: ['id'], additionalProperties: false },\n\t\t\trun: (_ctx: unknown, params: { id: string }) => {\n\t\t\t\tcheck(controller.editSymbol(params.id));\n\t\t\t\treturn { symbolId: controller.getState().symbolEdit!.symbolId, nodes: list().nodes };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}exitSymbolEdit`,\n\t\t\tlabel: 'Exit Symbol',\n\t\t\tkeywords: ['symbol', 'done', 'exit isolation'],\n\t\t\tdescription: 'Leaves isolation mode: back to the whole document, every instance of the edited symbol updated. Returns { symbolId } of the symbol that was edited (null when none was).',\n\t\t\tparams: { type: 'object', properties: {}, additionalProperties: false },\n\t\t\tenabled: () => (controller.getState().symbolEdit ? true : 'No symbol is being edited.'),\n\t\t\trun: () => {\n\t\t\t\tconst id = controller.getState().symbolEdit?.symbolId ?? null;\n\t\t\t\tcontroller.exitSymbolEdit();\n\t\t\t\treturn { symbolId: id };\n\t\t\t}\n\t\t}\n\t];\n\n\n\t// --- Constraints -------------------------------------------------------------\n\tconst CONSTRAINT_REF = {\n\t\ttype: 'object',\n\t\tproperties: {\n\t\t\tnodeId: { type: 'string', description: 'The path (from vector.list).' },\n\t\t\tsubpath: { type: 'integer', minimum: 0, description: 'Subpath of the anchor (with index).' },\n\t\t\tindex: { type: 'integer', minimum: 0, description: 'Anchor index in the subpath (with subpath).' },\n\t\t\tanchor: { type: 'string', description: 'The anchor\\'s id instead of subpath / index (vector.listConstraints shows ids in use).' },\n\t\t\thandle: { type: 'string', enum: ['in', 'out'], description: 'The anchor\\'s handle instead of the anchor point.' },\n\t\t\tsegment: { type: 'boolean', description: 'The segment from the anchor to the next one (a line between them) instead of the point.' },\n\t\t\tcircle: { type: 'boolean', description: 'The whole path as a circle (its centre and radius): for round closed paths, no anchor.' }\n\t\t},\n\t\trequired: ['nodeId'],\n\t\tadditionalProperties: false\n\t} as const;\n\tconst constraintsSummary = () => {\n\t\tconst state = controller.getState();\n\t\tconst residuals = vectorConstraintResiduals(controller.getDocument());\n\t\tconst env = vectorConstraintEnv(controller.getDocument());\n\t\treturn {\n\t\t\tconstraints: controller.getConstraints().map((c) => {\n\t\t\t\tlet current: number | null = null;\n\t\t\t\ttry {\n\t\t\t\t\tcurrent = c.kind === 'distance' || c.kind === 'angle' || c.kind === 'radius' || c.kind === 'diameter' ? round(vectorConstraintValue(env, c)) : null;\n\t\t\t\t} catch {\n\t\t\t\t\tcurrent = null;\n\t\t\t\t}\n\t\t\t\tconst r = residuals.get(c.id);\n\t\t\t\treturn { ...c, ...(current !== null ? { evaluated: current } : {}), satisfied: r !== null && r !== undefined && r <= 1e-6 * Math.max(state.document.width, state.document.height, 1) };\n\t\t\t}),\n\t\t\tparameters: controller.getParameters(),\n\t\t\tdof: controller.getConstraintDof(),\n\t\t\tissues: state.constraintIssues\n\t\t};\n\t};\n\tconst snapResult = () => {\n\t\tconst { grid, objects, guides, threshold, angles, tracking, trackPolar, polarRelative } = controller.getState().snap;\n\t\treturn { grid, objects, guides, threshold, angles: [...angles], tracking: tracking !== false, trackPolar: !!trackPolar, polarRelative: !!polarRelative };\n\t};\n\tconst trackingCommands = (): CommandSpec[] => [\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}getSnapOptions`,\n\t\t\tlabel: 'Snap Options',\n\t\t\treadOnly: true,\n\t\t\tkeywords: ['snap', 'grid', 'polar', 'tracking', 'angles', 'guides'],\n\t\t\tdescription: 'Returns the snap options: grid (0 = off), objects, guides, threshold (screen px), angles (polar / construction angles in degrees, counter-clockwise on the page), tracking, trackPolar, polarRelative.',\n\t\t\tparams: { type: 'object', properties: {}, additionalProperties: false },\n\t\t\trun: () => snapResult()\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}setSnapOptions`,\n\t\t\tlabel: 'Set Snap Options…',\n\t\t\tkeywords: ['snap', 'grid', 'polar', 'polar tracking', 'object snap tracking', 'otrack', 'angles', 'increment'],\n\t\t\tdescription:\n\t\t\t\t'Changes how drawing snaps for people at the pointer (it does not move anything already drawn). polarIncrement sets angles to every multiple of it (plus additionalAngles). Pass at least one. Returns the options as vector.getSnapOptions does.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tgrid: { type: 'number', minimum: 0, description: 'Grid size in document units; 0 turns the grid off.' },\n\t\t\t\t\tobjects: { type: 'boolean', description: 'Snap to other objects, construction angles and tracking.' },\n\t\t\t\t\tguides: { type: 'boolean', description: 'Show smart guide lines and labels.' },\n\t\t\t\t\tthreshold: { type: 'number', exclusiveMinimum: 0, description: 'How near a target pulls, in screen pixels (default 5).' },\n\t\t\t\t\tangles: { type: 'array', items: { type: 'number' }, description: 'Polar / construction angles in degrees (counter-clockwise on the page). Overrides polarIncrement.' },\n\t\t\t\t\tpolarIncrement: { type: 'number', exclusiveMinimum: 0, maximum: 360, description: 'Polar tracking increment in degrees (e.g. 15, 30, 45, 90): sets angles to its multiples.' },\n\t\t\t\t\tadditionalAngles: { type: 'array', items: { type: 'number' }, description: 'Extra polar angles in degrees, added to polarIncrement\\'s.' },\n\t\t\t\t\ttracking: { type: 'boolean', description: 'Object snap tracking: resting the pointer on an anchor acquires it (default true).' },\n\t\t\t\t\ttrackPolar: { type: 'boolean', description: 'Acquired points send paths at every polar angle, not only horizontal and vertical (default false).' },\n\t\t\t\t\tpolarRelative: { type: 'boolean', description: 'The pen measures polar angles from its last segment (default false).' }\n\t\t\t\t},\n\t\t\t\tadditionalProperties: false\n\t\t\t},\n\t\t\trun: (_ctx: unknown, params: { grid?: number; objects?: boolean; guides?: boolean; threshold?: number; angles?: number[]; polarIncrement?: number; additionalAngles?: number[]; tracking?: boolean; trackPolar?: boolean; polarRelative?: boolean } | undefined) => {\n\t\t\t\tconst { polarIncrement, additionalAngles, ...rest } = params ?? {};\n\t\t\t\tif (!Object.keys(params ?? {}).length) fail('Pass at least one option.');\n\t\t\t\tif (rest.angles?.some((a) => !Number.isFinite(a))) fail('angles must be numbers (degrees).');\n\t\t\t\tif (additionalAngles && polarIncrement === undefined) fail('additionalAngles go with polarIncrement.');\n\t\t\t\tconst angles = rest.angles ?? (polarIncrement !== undefined ? vectorPolarAngles(polarIncrement, additionalAngles) : undefined);\n\t\t\t\tcontroller.setSnap({ ...rest, ...(angles ? { angles } : {}) });\n\t\t\t\treturn snapResult();\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}addPathByDimensions`,\n\t\t\tlabel: 'Draw by Dimensions…',\n\t\t\tkeywords: ['dimensions', 'length', 'angle', 'polar', 'typed', 'direct distance', 'polyline', 'cad', 'turtle'],\n\t\t\tdescription:\n\t\t\t\t'Adds a path drawn by typed dimensions, as in CAD: from start, each step goes { length, angle } (degrees, counter-clockwise on the page: 0 right, 90 up), { dx, dy } (an offset) or { x, y } (a position), all in document units. closed joins the last point to the first. Default style. One undo step; selects it. Returns { id, anchors }.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tstart: { type: 'object', properties: { x: { type: 'number', description: 'Document units.' }, y: { type: 'number', description: 'Document units.' } }, required: ['x', 'y'], additionalProperties: false, description: 'The first point.' },\n\t\t\t\t\tsteps: {\n\t\t\t\t\t\ttype: 'array',\n\t\t\t\t\t\tminItems: 1,\n\t\t\t\t\t\tdescription: 'Each next point: { length, angle }, { dx, dy } or { x, y }.',\n\t\t\t\t\t\titems: {\n\t\t\t\t\t\t\tanyOf: [\n\t\t\t\t\t\t\t\t{ type: 'object', properties: { length: { type: 'number', description: 'Distance from the previous point.' }, angle: { type: 'number', description: 'Degrees counter-clockwise on the page (0 right, 90 up).' } }, required: ['length', 'angle'], additionalProperties: false },\n\t\t\t\t\t\t\t\t{ type: 'object', properties: { dx: { type: 'number', description: 'Offset right.' }, dy: { type: 'number', description: 'Offset down.' } }, required: ['dx', 'dy'], additionalProperties: false },\n\t\t\t\t\t\t\t\t{ type: 'object', properties: { x: { type: 'number', description: 'Position.' }, y: { type: 'number', description: 'Position.' } }, required: ['x', 'y'], additionalProperties: false }\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tclosed: { type: 'boolean', description: 'Close the path (default false).' },\n\t\t\t\t\tname: { type: 'string', description: 'Path name (default \"Path\").' }\n\t\t\t\t},\n\t\t\t\trequired: ['start', 'steps'],\n\t\t\t\tadditionalProperties: false\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: Parameters<VectorEditorController['addPathByDimensions']>[0]) => {\n\t\t\t\tconst r = controller.addPathByDimensions(params);\n\t\t\t\tif (!r.ok) fail(r.message);\n\t\t\t\treturn { id: (r as { id: string }).id, anchors: r.anchors };\n\t\t\t}\n\t\t}\n\t];\n\n\tconst connectorResult = (id: string) => controller.getConnectors().find((x) => x.id === id) ?? fail(`\"${id}\" is not a connector.`);\n\tconst SIDE = { type: 'string', enum: VECTOR_CONNECTOR_SIDES, description: 'Which site: the middle of the object\\'s top, right, bottom or left side (its own, unrotated bounds, turned with it), its centre, or auto (the side facing the other end, chosen again on every edit; the default).' } as const;\n\tconst GLUED_END = {\n\t\ttype: 'object',\n\t\tproperties: { nodeId: { type: 'string', description: 'The object to glue to (not a connector).' }, side: SIDE },\n\t\trequired: ['nodeId'],\n\t\tadditionalProperties: false\n\t} as const;\n\tconst CONNECTOR_END = {\n\t\tdescription: 'An end glued to an object, { nodeId, side? }, or a free point { x, y } in document units.',\n\t\tanyOf: [GLUED_END, { type: 'object', properties: { x: { type: 'number', description: 'Document units.' }, y: { type: 'number', description: 'Document units.' } }, required: ['x', 'y'], additionalProperties: false }]\n\t} as const;\n\tconst ROUTE_PROPS = {\n\t\troute: { type: 'string', enum: VECTOR_CONNECTOR_ROUTES, description: 'straight (default), elbow (right angles, leaving and entering along the sides) or curved (an S-curve with handles along the sides).' },\n\t\tradius: { type: 'number', minimum: 0, description: 'Corner radius of an elbow route\\'s bends, document units (default 0).' },\n\t\tgap: { type: 'number', minimum: 0, description: 'How far an elbow or curve runs straight out of a glued side before turning, document units (default 16).' }\n\t} as const;\n\tconst connectorCommands = (): CommandSpec[] => [\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}listConnectors`,\n\t\t\tlabel: 'List Connectors',\n\t\t\treadOnly: true,\n\t\t\tkeywords: ['connector', 'connectors', 'arrows', 'links', 'edges', 'diagram', 'flowchart'],\n\t\t\tdescription:\n\t\t\t\t'Lists every connector: id, name, route, radius, and each end: the glued nodeId, its side setting and the side in use (resolvedSide), and where the end is (x, y, document units). An end without nodeId is free.',\n\t\t\tparams: { type: 'object', properties: {}, additionalProperties: false },\n\t\t\trun: () => ({ connectors: controller.getConnectors() })\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}listConnectionSites`,\n\t\t\tlabel: 'List Connection Sites',\n\t\t\treadOnly: true,\n\t\t\tkeywords: ['connector', 'sites', 'anchors', 'ports', 'glue'],\n\t\t\tdescription: 'Returns the connection sites of an object: { sites: [{ side, x, y, nx, ny }] } in document units for top, right, bottom, left and centre (n is the outward normal). Empty for connectors, hidden objects or ones without extent.',\n\t\t\tparams: { type: 'object', properties: { id: { type: 'string', description: 'The object (from vector.list).' } }, required: ['id'], additionalProperties: false },\n\t\t\trun: (_ctx: unknown, params: { id: string }) => ({ sites: controller.getConnectionSites(params.id) ?? fail(`There is no node \"${params.id}\".`) })\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}connect`,\n\t\t\tlabel: 'Connect…',\n\t\t\tkeywords: ['connector', 'connect', 'arrow', 'link', 'edge', 'flowchart', 'diagram', 'line between'],\n\t\t\tdescription:\n\t\t\t\t'Adds a connector between two objects (or free points): each glued end follows its object whenever it moves, resizes or turns, through every later edit. Default: straight, the default stroke, a triangle arrowhead at the end. One undo step; selects it. Returns the connector as vector.listConnectors lists it.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tstart: CONNECTOR_END,\n\t\t\t\t\tend: CONNECTOR_END,\n\t\t\t\t\t...ROUTE_PROPS,\n\t\t\t\t\tname: { type: 'string', description: 'Connector name (default \"Connector\").' },\n\t\t\t\t\tstroke: PAINT,\n\t\t\t\t\tstrokeWidth: { type: 'number', minimum: 0, description: 'Stroke width in document units (default the editor default).' },\n\t\t\t\t\tstartArrow: { type: ['string', 'null'], enum: [...VECTOR_ARROWHEADS, null], description: 'Head at the start, or null for none (default none).' },\n\t\t\t\t\tendArrow: { type: ['string', 'null'], enum: [...VECTOR_ARROWHEADS, null], description: 'Head at the end, or null for none (default triangle).' }\n\t\t\t\t},\n\t\t\t\trequired: ['start', 'end'],\n\t\t\t\tadditionalProperties: false\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: Parameters<VectorEditorController['addConnector']>[0]) => {\n\t\t\t\tconst r = controller.addConnector(params);\n\t\t\t\tif (!r.ok) fail(r.message);\n\t\t\t\treturn connectorResult((r as { id: string }).id);\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}setConnector`,\n\t\t\tlabel: 'Connector Options…',\n\t\t\tkeywords: ['connector', 'glue', 'unglue', 'route', 'elbow', 'curved', 'straight', 'reconnect'],\n\t\t\tdescription:\n\t\t\t\t'Changes connectors (`ids`, default the selection): start / end glue an end to another object ({ nodeId, side? }) or unglue it (null; the end stays where it is); route, radius, gap set how they run. A plain open path given by id becomes a connector. Pass at least one. One undo step. Returns { connectors } as vector.listConnectors lists them.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tids: { ...IDS, description: 'Connector (or open path) ids (default: the selection).' },\n\t\t\t\t\tstart: { anyOf: [GLUED_END, { type: 'null' }], description: 'Glue the start to { nodeId, side? }, or null to unglue it.' },\n\t\t\t\t\tend: { anyOf: [GLUED_END, { type: 'null' }], description: 'Glue the end to { nodeId, side? }, or null to unglue it.' },\n\t\t\t\t\t...ROUTE_PROPS\n\t\t\t\t},\n\t\t\t\tadditionalProperties: false\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\tenabled: enabledFor,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[]; start?: VectorConnectorEnd | null; end?: VectorConnectorEnd | null; route?: VectorConnectorRoute; radius?: number; gap?: number } | undefined) => {\n\t\t\t\tconst { ids: list, ...patch } = params ?? {};\n\t\t\t\tif (!Object.keys(patch).length) fail('Pass at least one of start, end, route, radius, gap.');\n\t\t\t\tconst targets = ids({ ids: list });\n\t\t\t\tcheck(controller.setConnector(patch, { ids: targets }));\n\t\t\t\treturn { connectors: targets.map(connectorResult) };\n\t\t\t}\n\t\t}\n\t];\n\n\tconst frameResult = (id: string) => controller.getFrames().find((f) => f.id === id) ?? fail(`\"${id}\" is not a frame.`);\n\tconst FRAME_PAINT = { ...PAINT, description: `The frame's background, drawn below its children. ${PAINT.description}` };\n\tconst frameCommands = (): CommandSpec[] => [\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}listFrames`,\n\t\t\tlabel: 'List Frames',\n\t\t\treadOnly: true,\n\t\t\tkeywords: ['frame', 'frames', 'artboard', 'artboards', 'board', 'page', 'constraints'],\n\t\t\tdescription:\n\t\t\t\t'Lists every frame (top-level ones are artboards): id, name, parentId, artboard, frame { x, y, width, height, fill, clip } in its own space, bounds in document space, and its children with their resizing constraints (horizontal left|right|leftRight|center|scale, vertical top|bottom|topBottom|center|scale).',\n\t\t\tparams: { type: 'object', properties: {}, additionalProperties: false },\n\t\t\trun: () => ({ frames: controller.getFrames() })\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}addFrame`,\n\t\t\tlabel: 'Add Frame…',\n\t\t\tkeywords: ['frame', 'artboard', 'board', 'page', 'new artboard', 'canvas size'],\n\t\t\tdescription:\n\t\t\t\t'Adds an empty top-level frame (an artboard) at x, y with width × height (document units), white unless `fill` says otherwise, clipping its contents unless clip is false. With adopt: true the top-level objects it fully encloses move into it. Selects it. One undo step. Returns the frame as vector.listFrames lists it.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tx: { type: 'number', description: 'Left edge (document units).' },\n\t\t\t\t\ty: { type: 'number', description: 'Top edge (document units).' },\n\t\t\t\t\twidth: { type: 'number', exclusiveMinimum: 0, description: 'Width (document units).' },\n\t\t\t\t\theight: { type: 'number', exclusiveMinimum: 0, description: 'Height (document units).' },\n\t\t\t\t\tname: { type: 'string', description: 'Frame name (default \"Frame n\").' },\n\t\t\t\t\tfill: FRAME_PAINT,\n\t\t\t\t\tclip: { type: 'boolean', description: 'Clip the children to the frame (default true).' },\n\t\t\t\t\tadopt: { type: 'boolean', description: 'Move the top-level objects wholly inside the rectangle into the frame (default false).' }\n\t\t\t\t},\n\t\t\t\trequired: ['x', 'y', 'width', 'height'],\n\t\t\t\tadditionalProperties: false\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { x: number; y: number; width: number; height: number; name?: string; fill?: VectorPaint; clip?: boolean; adopt?: boolean }) => {\n\t\t\t\tconst r = controller.addFrame(params);\n\t\t\t\tif (!r.ok) fail(r.message);\n\t\t\t\treturn frameResult((r as { id: string }).id);\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}frameSelection`,\n\t\t\tlabel: 'Frame Selection',\n\t\t\tshortcut: 'Mod+Alt+G',\n\t\t\tkeywords: ['frame', 'artboard', 'wrap', 'group', 'board', 'container'],\n\t\t\tdescription:\n\t\t\t\t'Puts objects that share a parent (`ids`, default the selection) into a new frame fitted to their bounds plus padding, in their place in the stacking order. No fill unless given; clips unless clip is false. Selects the frame. One undo step. Returns the frame as vector.listFrames lists it.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tids: { ...IDS, description: 'Objects to frame, sharing a parent (default: the selection).' },\n\t\t\t\t\tpadding: { type: 'number', minimum: 0, description: 'Space around the objects inside the frame, document units (default 0).' },\n\t\t\t\t\tname: { type: 'string', description: 'Frame name (default \"Frame n\").' },\n\t\t\t\t\tfill: FRAME_PAINT,\n\t\t\t\t\tclip: { type: 'boolean', description: 'Clip the children to the frame (default true).' }\n\t\t\t\t},\n\t\t\t\tadditionalProperties: false\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\tenabled: enabledFor,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[]; padding?: number; name?: string; fill?: VectorPaint; clip?: boolean } | undefined) => {\n\t\t\t\tconst r = controller.frameSelection({ ...params, ids: ids(params) });\n\t\t\t\tif (!r.ok) fail(r.message);\n\t\t\t\treturn frameResult((r as { id: string }).id);\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}setFrame`,\n\t\t\tlabel: 'Frame Options…',\n\t\t\tkeywords: ['frame', 'artboard', 'resize', 'size', 'background', 'clip', 'clip content'],\n\t\t\tdescription:\n\t\t\t\t'Changes frames (`ids`, default the selected frame or the one holding the selection). x, y, width, height move and resize the rectangle (frame space); the children follow their resizing constraints instead of scaling (see vector.setResizing). fill sets the background (null for none); clip whether children outside are hidden. Pass at least one. One undo step. Returns { frames } as vector.listFrames lists them.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tids: { ...IDS, description: 'Frame ids (default: the selected frame, or the frame holding the selection).' },\n\t\t\t\t\tx: { type: 'number', description: 'New left edge (frame space, document units).' },\n\t\t\t\t\ty: { type: 'number', description: 'New top edge (frame space, document units).' },\n\t\t\t\t\twidth: { type: 'number', exclusiveMinimum: 0, description: 'New width (document units).' },\n\t\t\t\t\theight: { type: 'number', exclusiveMinimum: 0, description: 'New height (document units).' },\n\t\t\t\t\tfill: FRAME_PAINT,\n\t\t\t\t\tclip: { type: 'boolean', description: 'true: hide children outside the frame; false: let them show.' }\n\t\t\t\t},\n\t\t\t\tadditionalProperties: false\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[]; x?: number; y?: number; width?: number; height?: number; fill?: VectorPaint; clip?: boolean } | undefined) => {\n\t\t\t\tconst { ids: list, ...patch } = params ?? {};\n\t\t\t\tif (!Object.keys(patch).length) fail('Pass at least one of x, y, width, height, fill, clip.');\n\t\t\t\tconst before = new Set(controller.getFrames().map((f) => f.id));\n\t\t\t\tcheck(controller.setFrame(patch, list ? { ids: list } : {}));\n\t\t\t\tconst targets = list ?? controller.getFrames().filter((f) => before.has(f.id) && (controller.getState().selection.includes(f.id) || f.children.some((c) => controller.getState().selection.includes(c.id)))).map((f) => f.id);\n\t\t\t\treturn { frames: targets.map(frameResult) };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}setResizing`,\n\t\t\tlabel: 'Resizing Constraints…',\n\t\t\tkeywords: ['constraints', 'resizing', 'pin', 'anchor', 'responsive', 'stretch', 'frame', 'artboard'],\n\t\t\tdescription:\n\t\t\t\t'Sets how objects directly inside a frame (`ids`, default the selection) follow it when the frame is resized: horizontal left (keep the left margin, the default), right, leftRight (keep both margins: stretch), center or scale; vertical top (default), bottom, topBottom, center or scale. Text keeps its font size (area text takes the new width). A missing axis keeps its value. One undo step. Returns { nodes: [{ id, horizontal, vertical }] }.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tids: { ...IDS, description: 'Objects directly inside a frame (default: the selection).' },\n\t\t\t\t\thorizontal: { type: 'string', enum: VECTOR_HORIZONTAL_RESIZING, description: 'left, right, leftRight (stretch), center or scale.' },\n\t\t\t\t\tvertical: { type: 'string', enum: VECTOR_VERTICAL_RESIZING, description: 'top, bottom, topBottom (stretch), center or scale.' }\n\t\t\t\t},\n\t\t\t\tadditionalProperties: false\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\tenabled: enabledFor,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[]; horizontal?: VectorHorizontalResizing; vertical?: VectorVerticalResizing } | undefined) => {\n\t\t\t\tconst list = ids(params);\n\t\t\t\tcheck(controller.setResizing({ ...(params?.horizontal ? { horizontal: params.horizontal } : {}), ...(params?.vertical ? { vertical: params.vertical } : {}) }, { ids: list }));\n\t\t\t\treturn { nodes: list.map((id) => ({ id, horizontal: controller.getNode(id)?.resizing?.horizontal ?? 'left', vertical: controller.getNode(id)?.resizing?.vertical ?? 'top' })) };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}exportFrameSvg`,\n\t\t\tlabel: 'Export Frame as SVG',\n\t\t\treadOnly: true,\n\t\t\tkeywords: ['export', 'artboard', 'frame', 'svg', 'download', 'save'],\n\t\t\tdescription: 'Returns one frame (an artboard) as an SVG document of its own size, its top-left corner at the origin. Returns { svg, width, height }.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tid: { type: 'string', description: 'The frame (from vector.listFrames).' },\n\t\t\t\t\tprecision: { type: 'integer', minimum: 0, maximum: 8, description: 'Decimal places in coordinates (default 3).' }\n\t\t\t\t},\n\t\t\t\trequired: ['id'],\n\t\t\t\tadditionalProperties: false\n\t\t\t},\n\t\t\trun: (_ctx: unknown, params: { id: string; precision?: number }) => {\n\t\t\t\tconst f = frameResult(params.id);\n\t\t\t\tconst svg = controller.frameToSvg(params.id, params.precision !== undefined ? { precision: params.precision } : {})!;\n\t\t\t\treturn { svg, width: f.frame.width, height: f.frame.height };\n\t\t\t}\n\t\t}\n\t];\n\n\tconst constraintCommands = (): CommandSpec[] => [\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}listConstraints`,\n\t\t\tlabel: 'List Constraints',\n\t\t\treadOnly: true,\n\t\t\tkeywords: ['constraints', 'dimensions', 'parametric', 'dof', 'degrees of freedom', 'cad'],\n\t\t\tdescription:\n\t\t\t\t'Reads the parametric state: every constraint (id, kind, refs with anchor ids, value expression, name, evaluated value, satisfied), the named parameters, the degrees of freedom left (dof, status unconstrained / under / fully / over, redundant equations, per path) and any conflicts the last solve reported. Constraints are solved after every edit.',\n\t\t\tparams: { type: 'object', properties: {}, additionalProperties: false },\n\t\t\trun: () => constraintsSummary()\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}addConstraint`,\n\t\t\tlabel: 'Add Constraint…',\n\t\t\tkeywords: ['constraint', 'coincident', 'parallel', 'perpendicular', 'horizontal', 'vertical', 'tangent', 'equal', 'distance', 'angle', 'radius', 'dimension', 'fix', 'cad'],\n\t\t\tdescription:\n\t\t\t\t'Adds a constraint and solves (geometry moves as little as possible). Refs: anchors by { nodeId, subpath, index } (or anchor id), `handle`, `segment: true` (the line from the anchor to the next), or `circle: true` (a round path). Kinds: coincident (2 points, point + segment, point + circle), collinear (2 segments), concentric (2 circles or circle + point), fix (any), parallel / perpendicular (2 segments), horizontal / vertical (a segment or 2 points), tangent (segment + circle, 2 circles, or 1 anchor: G1), smooth (1 anchor: G2), symmetric (2 points + a segment axis), equal (2 segments or 2 circles), distance (a segment, 2 points or point + segment; `axis`), angle (2 segments, degrees), radius / diameter (a circle). Dimensional kinds take `value` (an expression; default the current measure) and `name`. Fails, changing nothing, on a conflict or a redundant constraint, naming the constraints involved. One undo step. Returns { id, constraint }.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tkind: { type: 'string', enum: VECTOR_CONSTRAINT_KINDS, description: 'The constraint kind.' },\n\t\t\t\t\trefs: { type: 'array', items: CONSTRAINT_REF, minItems: 1, maxItems: 3, description: 'What it constrains, in the order the kind lists.' },\n\t\t\t\t\tvalue: { type: ['string', 'number'], description: 'Dimensional kinds: the value (document units, degrees for angle) or an expression of parameters (\"w / 2\").' },\n\t\t\t\t\tname: { type: 'string', description: 'Dimensional kinds: a name expressions can use (default d1, ang1, rad1, dia1, …).' },\n\t\t\t\t\taxis: { type: 'string', enum: ['aligned', 'horizontal', 'vertical'], description: 'distance: measured along the line between (default), horizontally or vertically.' }\n\t\t\t\t},\n\t\t\t\trequired: ['kind', 'refs'],\n\t\t\t\tadditionalProperties: false\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: VectorConstraintInput) => {\n\t\t\t\tconst r = controller.addConstraint(params);\n\t\t\t\tcheck(r);\n\t\t\t\treturn { id: r.id, constraint: controller.getConstraints().find((c) => c.id === r.id) };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}removeConstraint`,\n\t\t\tlabel: 'Remove Constraint',\n\t\t\tkeywords: ['constraint', 'delete', 'remove', 'unconstrain'],\n\t\t\tdescription: 'Removes constraints by id (from vector.listConstraints); the geometry stays where it is. One undo step. Returns { ids }.',\n\t\t\tparams: { type: 'object', properties: { ids: { type: 'array', items: { type: 'string' }, minItems: 1, description: 'Constraint ids.' } }, required: ['ids'], additionalProperties: false },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { ids: string[] }) => {\n\t\t\t\tcheck(controller.removeConstraints(params.ids));\n\t\t\t\treturn { ids: params.ids };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}setConstraintValue`,\n\t\t\tlabel: 'Set Dimension…',\n\t\t\tkeywords: ['dimension', 'value', 'parameter', 'expression', 'driving'],\n\t\t\tdescription:\n\t\t\t\t'Sets the value of a dimensional constraint (by id or name) or of a named parameter (creating it; null removes a parameter) and solves. Values are expressions: numbers, names, + - * /, parentheses, pi. Fails, changing nothing, when it does not evaluate or the constraints cannot meet it. One undo step. Returns the parametric summary (vector.listConstraints).',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tkey: { type: 'string', description: 'A dimensional constraint\\'s id or name, or a parameter name.' },\n\t\t\t\t\tvalue: { type: ['string', 'number', 'null'], description: 'The new value or expression; null removes a parameter.' }\n\t\t\t\t},\n\t\t\t\trequired: ['key', 'value'],\n\t\t\t\tadditionalProperties: false\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { key: string; value: string | number | null }) => {\n\t\t\t\tcheck(controller.setConstraintValue(params.key, params.value));\n\t\t\t\treturn constraintsSummary();\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}autoConstrain`,\n\t\t\tlabel: 'Auto Constrain',\n\t\t\tkeywords: ['auto constrain', 'infer', 'constraints', 'parametric', 'sketch'],\n\t\t\tdescription:\n\t\t\t\t'Adds the constraints a sketch already nearly satisfies (within `distanceTolerance` document units, default 0.5, and `angleTolerance` degrees, default 1): coincident points of different paths, concentric / tangent circles, a segment tangent to a circle, horizontal and vertical segments, collinear, perpendicular (connected) and parallel segments, equal lengths and radii. Skips any that would conflict or repeat others. `ids` limits it to some paths (default the selection, or all). One undo step. Returns { ids } of the constraints added.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tids: { ...IDS, description: 'Paths or groups to constrain (default: the selection, or every path).' },\n\t\t\t\t\tdistanceTolerance: { type: 'number', minimum: 0, description: 'How close points must be to count as coincident (document units, default 0.5).' },\n\t\t\t\t\tangleTolerance: { type: 'number', minimum: 0, maximum: 45, description: 'How close to level, square or parallel lines must be (degrees, default 1).' },\n\t\t\t\t\tkinds: { type: 'array', items: { type: 'string', enum: VECTOR_CONSTRAINT_KINDS }, description: 'Kinds to infer (default all).' }\n\t\t\t\t},\n\t\t\t\tadditionalProperties: false\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[]; distanceTolerance?: number; angleTolerance?: number; kinds?: VectorConstraintKind[] } | undefined) => {\n\t\t\t\tconst r = controller.autoConstrain(params ?? {});\n\t\t\t\tcheck(r);\n\t\t\t\treturn { ids: r.ids };\n\t\t\t}\n\t\t}\n\t];\n\n\treturn [\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}list`,\n\t\t\tlabel: 'List Vector Nodes',\n\t\t\treadOnly: true,\n\t\t\tkeywords: ['layers', 'shapes', 'document', 'outline'],\n\t\t\tdescription: 'Lists every node (paths and groups, depth first) with bounds, style, path data and selection.',\n\t\t\trun: list\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}select`,\n\t\t\tlabel: 'Select Nodes',\n\t\t\tkeywords: ['pick', 'choose'],\n\t\t\tdescription: 'Selects nodes by id (empty list clears the selection).',\n\t\t\tparams: { type: 'object', properties: { ids: { type: 'array', items: { type: 'string' } }, add: { type: 'boolean' } }, required: ['ids'] },\n\t\t\trun: (_ctx: unknown, params: { ids: string[]; add?: boolean }) => {\n\t\t\t\tfor (const id of params.ids) if (!controller.getNode(id)) fail(`There is no node \"${id}\".`);\n\t\t\t\tif (!params.ids.length && !params.add) controller.clearSelection();\n\t\t\t\telse controller.select(params.ids, { add: params.add });\n\t\t\t\treturn { selection: controller.getState().selection };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}addShape`,\n\t\t\tlabel: 'Add Shape…',\n\t\t\tkeywords: ['rectangle', 'ellipse', 'circle', 'polygon', 'star', 'line', 'draw'],\n\t\t\tdescription: 'Adds a rectangle, ellipse, polygon, star or line in document units, using the current default style unless overridden.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tkind: { type: 'string', enum: ['rect', 'ellipse', 'polygon', 'star', 'line'] },\n\t\t\t\t\tx: { type: 'number' },\n\t\t\t\t\ty: { type: 'number' },\n\t\t\t\t\twidth: { type: 'number', description: 'For a line: the x offset of its end.' },\n\t\t\t\t\theight: { type: 'number', description: 'For a line: the y offset of its end.' },\n\t\t\t\t\tcornerRadius: { type: 'number', minimum: 0, description: 'Live corner radius of a rect, polygon or star.' },\n\t\t\t\t\tcornerKind: { type: 'string', enum: VECTOR_CORNER_KINDS },\n\t\t\t\t\tsides: { type: 'integer', minimum: 3 },\n\t\t\t\t\tpoints: { type: 'integer', minimum: 3 },\n\t\t\t\t\tinnerRatio: { type: 'number', minimum: 0, maximum: 1 },\n\t\t\t\t\tname: { type: 'string' },\n\t\t\t\t\t...STYLE_PROPS\n\t\t\t\t},\n\t\t\t\trequired: ['kind', 'x', 'y', 'width', 'height']\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: VectorShapeInput) => ({ id: controller.addShape(params) })\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}addPath`,\n\t\t\tlabel: 'Add Path…',\n\t\t\tkeywords: ['svg', 'path data', 'bezier', 'curve'],\n\t\t\tdescription: 'Adds a path from SVG path data (`d`), in document units.',\n\t\t\tparams: { type: 'object', properties: { d: { type: 'string' }, name: { type: 'string' }, ...STYLE_PROPS }, required: ['d'] },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { d: string; name?: string } & Record<string, unknown>) => {\n\t\t\t\tconst id = controller.addPath(params.d, { ...pickStyle(params), ...(params.name ? { name: params.name } : {}) });\n\t\t\t\tconst node = controller.getNode(id);\n\t\t\t\tif (node?.type === 'path' && !node.subpaths.length) {\n\t\t\t\t\tcontroller.undo();\n\t\t\t\t\tfail('The path data has no points.');\n\t\t\t\t}\n\t\t\t\treturn { id };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}setStyle`,\n\t\t\tlabel: 'Set Style…',\n\t\t\tkeywords: ['fill', 'stroke', 'colour', 'color', 'gradient', 'width'],\n\t\t\tdescription: 'Sets fill, stroke and stroke settings of nodes (groups apply to every path inside).',\n\t\t\tparams: { type: 'object', properties: { ids: IDS, ...STYLE_PROPS } },\n\t\t\tenabled: enabledFor,\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[] } & Record<string, unknown>) => {\n\t\t\t\tconst style = pickStyle(params);\n\t\t\t\tif (!Object.keys(style).length) fail('Pass at least one style property.');\n\t\t\t\tcheck(controller.setStyle(style, ids(params)));\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}setGradient`,\n\t\t\tlabel: 'Gradient…',\n\t\t\tkeywords: ['gradient', 'linear', 'radial', 'conic', 'angle', 'reflected', 'diamond', 'stops', 'midpoint', 'oklab', 'spread', 'focal', 'along stroke', 'across stroke', 'reverse'],\n\t\t\tdescription:\n\t\t\t\t'Edits the gradient of the fill (default) or stroke of paths (groups apply to every path inside): any gradient field (missing ones keep their value; a colour becomes a gradient from it to white), `aspect` (secondary axis over main axis), `reverse` (flip the stops and midpoints), `focal` / `width` null to remove them. Midpoints, smoothness and oklab / linear interpolation export to SVG as extra stops; conic and diamond gradients as patterns.',\n\t\t\tparams: { type: 'object', properties: { ids: IDS, target: { type: 'string', enum: ['fill', 'stroke'] }, ...GRADIENT_PROPS, aspect: { type: 'number' }, reverse: { type: 'boolean' } } },\n\t\t\tenabled: enabledFor,\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: ({ ids?: string[]; target?: 'fill' | 'stroke' } & VectorGradientPatch) | undefined) => {\n\t\t\t\tconst { ids: _i, target, ...patch } = params ?? {};\n\t\t\t\tif (patch.type !== undefined && !VECTOR_GRADIENT_TYPES.includes(patch.type)) fail(`Unknown gradient type \"${patch.type}\".`);\n\t\t\t\tif (patch.stops !== undefined && !(Array.isArray(patch.stops) && patch.stops.length)) fail('stops needs at least one { offset, color }.');\n\t\t\t\tcheck(controller.setGradient(patch, { ids: ids(params), target: target ?? 'fill' }));\n\t\t\t\tconst id = ids(params)[0];\n\t\t\t\tconst node = id ? controller.getNode(id) : undefined;\n\t\t\t\tconst path = node?.type === 'path' ? node : undefined;\n\t\t\t\treturn { ok: true, ...(path ? { gradient: path[target ?? 'fill'] } : {}) };\n\t\t\t}\n\t\t},\n\t\t...appearanceCommands(),\n\t\t...maskCommands(),\n\t\t...textCommands(),\n\t\t...symbolCommands(),\n\t\t...constraintCommands(),\n\t\t...frameCommands(),\n\t\t...connectorCommands(),\n\t\t...trackingCommands(),\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}setCorners`,\n\t\t\tlabel: 'Corner Radius…',\n\t\t\tkeywords: ['round', 'rounded', 'corners', 'radius', 'fillet', 'chamfer', 'live corners'],\n\t\t\tdescription:\n\t\t\t\t'Sets the live corner radius (and optionally `kind`: round, inverted or chamfer, and `smoothing`) of `anchors`, else the selected points (direct tool), else every corner of `ids` / the selection. Radius 0 makes corners sharp; radii shrink to fit short sides. `smoothing` (0–1) turns round corners between straight sides into squircles (Figma / iOS style continuous corners), easing into a shorter arc over (1 + smoothing) times the distance. The anchors stay editable.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tradius: { type: 'number', minimum: 0, description: 'Corner radius in document units (0: sharp).' },\n\t\t\t\t\tkind: { type: 'string', enum: VECTOR_CORNER_KINDS, description: 'round (default), inverted or chamfer.' },\n\t\t\t\t\tsmoothing: { type: 'number', minimum: 0, maximum: 1, description: 'Squircle smoothing of round corners between straight sides, 0 (a circular arc, default) to 1 (0.6 is iOS-like).' },\n\t\t\t\t\tids: IDS,\n\t\t\t\t\tanchors: ANCHORS\n\t\t\t\t}\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { radius?: number; kind?: VectorCornerKind; smoothing?: number; ids?: string[]; anchors?: VectorAnchorRef[] } | undefined) => {\n\t\t\t\tconst refs = params?.anchors ? anchorsOf(params.anchors) : undefined;\n\t\t\t\tconst target = { refs, ids: params?.ids ? ids(params) : undefined };\n\t\t\t\tif (params?.radius !== undefined) check(controller.setCornerRadius(params.radius, { ...target, kind: params.kind, ...(params.smoothing !== undefined ? { smoothing: params.smoothing } : {}) }));\n\t\t\t\telse if (params?.kind) check(controller.setCornerKind(params.kind, target));\n\t\t\t\tif (params?.radius === undefined && params?.smoothing !== undefined) check(controller.setCornerSmoothing(params.smoothing, target));\n\t\t\t\tif (params?.radius === undefined && !params?.kind && params?.smoothing === undefined) fail('Pass `radius`, `kind` and / or `smoothing`.');\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}setWidthProfile`,\n\t\t\tlabel: 'Width Profile…',\n\t\t\tkeywords: ['variable width', 'taper', 'tapered', 'pressure', 'profile', 'brush', 'calligraphy', 'bulge', 'thick', 'thin'],\n\t\t\tdescription:\n\t\t\t\t'Gives the stroke of paths a variable width: a `preset` (`uniform` removes the profile), explicit `points` ({ t, left, right }: side scales along each subpath), or a parametric `taper` (startLength / endLength as fractions of the path, startWidth / endWidth 0–1, startEase / endEase 0–1) and / or `wave` (amount 0–1, cycles, phase in degrees). Profiled strokes draw as filled outlines; Outline Stroke bakes them.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tids: IDS,\n\t\t\t\t\tpreset: { type: 'string', enum: VECTOR_WIDTH_PRESET_IDS },\n\t\t\t\t\tpoints: WIDTH_POINTS,\n\t\t\t\t\ttaper: {\n\t\t\t\t\t\ttype: 'object',\n\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\tstartLength: { type: 'number', minimum: 0, maximum: 1 },\n\t\t\t\t\t\t\tendLength: { type: 'number', minimum: 0, maximum: 1 },\n\t\t\t\t\t\t\tstartWidth: { type: 'number', minimum: 0, maximum: 1 },\n\t\t\t\t\t\t\tendWidth: { type: 'number', minimum: 0, maximum: 1 },\n\t\t\t\t\t\t\tstartEase: { type: 'number', minimum: 0, maximum: 1 },\n\t\t\t\t\t\t\tendEase: { type: 'number', minimum: 0, maximum: 1 }\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\twave: { type: 'object', properties: { amount: { type: 'number', minimum: 0, maximum: 1 }, cycles: { type: 'number', exclusiveMinimum: 0 }, phase: { type: 'number' } } }\n\t\t\t\t}\n\t\t\t},\n\t\t\tenabled: enabledFor,\n\t\t\tundoable: true,\n\t\t\trun: (\n\t\t\t\t_ctx: unknown,\n\t\t\t\tparams: { ids?: string[]; preset?: VectorWidthPreset; points?: VectorWidthPoint[] | null; taper?: VectorTaperOptions; wave?: VectorWaveOptions } | undefined\n\t\t\t) => {\n\t\t\t\tconst target = { ids: ids(params) };\n\t\t\t\tif (params?.preset) check(controller.setWidthProfile(params.preset, target));\n\t\t\t\telse if (params?.points !== undefined) check(controller.setWidthProfile(params.points ?? null, target));\n\t\t\t\telse if (params?.taper || params?.wave) check(controller.setWidthProfile({ taper: params.taper, wave: params.wave }, target));\n\t\t\t\telse fail('Pass `preset`, `points`, `taper` or `wave`.');\n\t\t\t\tconst node = controller.getNode(target.ids[0]);\n\t\t\t\treturn { ok: true, points: node?.type === 'path' ? (node.strokeWidthProfile ?? []) : [] };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}setWidthPoint`,\n\t\t\tlabel: 'Width Point…',\n\t\t\tkeywords: ['width point', 'variable width', 'stroke width at', 'taper', 'profile'],\n\t\t\tdescription:\n\t\t\t\t'Sets one width point of a path\\'s stroke profile numerically: `index` names an existing point (see `vector.setWidthProfile`\\'s result), leave it out to add one at `t`. `left` / `right` are side widths in document units (how far the stroke reaches on each side), `t` the position along each subpath (0–1); a point moved onto another\\'s `t` makes a step. Returns the profile.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tid: { type: 'string', description: 'The path.' },\n\t\t\t\t\tindex: { type: 'integer', minimum: 0 },\n\t\t\t\t\tt: { type: 'number', minimum: 0, maximum: 1 },\n\t\t\t\t\tleft: { type: 'number', minimum: 0 },\n\t\t\t\t\tright: { type: 'number', minimum: 0 }\n\t\t\t\t},\n\t\t\t\trequired: ['id']\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { id: string; index?: number; t?: number; left?: number; right?: number } | undefined) => {\n\t\t\t\tif (!params?.id) fail('Pass the path `id`.');\n\t\t\t\tcheck(controller.setWidthPoint({ nodeId: params!.id, index: params!.index ?? null }, { t: params!.t, left: params!.left, right: params!.right }));\n\t\t\t\tconst node = controller.getNode(params!.id);\n\t\t\t\treturn { ok: true, points: node?.type === 'path' ? (node.strokeWidthProfile ?? []) : [] };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}outlineStroke`,\n\t\t\tlabel: 'Outline Stroke',\n\t\t\tkeywords: ['expand', 'stroke to path', 'outline', 'convert stroke'],\n\t\t\tdescription: 'Turns the stroke of paths into filled shapes of the area it paints (alignment, caps, joins, dashes and arrowheads included). Returns the new ids.',\n\t\t\tparams: { type: 'object', properties: { ids: IDS } },\n\t\t\tenabled: enabledFor,\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[] } | undefined) => {\n\t\t\t\tconst created = controller.outlineStroke({ ids: params?.ids ? ids(params) : undefined });\n\t\t\t\tif (!created.length) fail('No selected path has a stroke.');\n\t\t\t\treturn { ids: created };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}distort`,\n\t\t\tlabel: 'Free Distort…',\n\t\t\tkeywords: ['distort', 'perspective', 'free distort', 'corners', 'projective', 'keystone'],\n\t\t\tdescription:\n\t\t\t\t'Free Distort: moves the four corners of the selection box (clockwise from the top left, in the box’s own rotation) to `corners` in document units, distorting the nodes in perspective (straight lines stay straight; curves are followed within `tolerance`). The corners must make a convex quadrilateral. One undo step.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tids: IDS,\n\t\t\t\t\tcorners: { type: 'array', items: POINT, minItems: 4, maxItems: 4, description: 'New top-left, top-right, bottom-right and bottom-left corners.' },\n\t\t\t\t\ttolerance: { type: 'number', exclusiveMinimum: 0, description: 'How closely curves follow the distortion (document units, default 0.05).' }\n\t\t\t\t},\n\t\t\t\trequired: ['corners']\n\t\t\t},\n\t\t\tenabled: enabledFor,\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[]; corners: { x: number; y: number }[]; tolerance?: number }) => {\n\t\t\t\tcheck(controller.distort(params.corners, { ids: ids(params), tolerance: params.tolerance }));\n\t\t\t\tconst frame = controller.getState().selectionFrame;\n\t\t\t\treturn { ok: true, bounds: frame ? { x: frame.x, y: frame.y, width: frame.width, height: frame.height } : null };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}skew`,\n\t\t\tlabel: 'Skew…',\n\t\t\tkeywords: ['skew', 'shear', 'slant', 'oblique'],\n\t\t\tdescription:\n\t\t\t\t'Skews nodes along the selection box’s axes: `ax` degrees horizontally (vertical lines lean right for positive angles) and `ay` degrees vertically, about the box centre or `origin`. One undo step.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tids: IDS,\n\t\t\t\t\tax: { type: 'number', minimum: -89.9, maximum: 89.9 },\n\t\t\t\t\tay: { type: 'number', minimum: -89.9, maximum: 89.9 },\n\t\t\t\t\torigin: POINT\n\t\t\t\t}\n\t\t\t},\n\t\t\tenabled: enabledFor,\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[]; ax?: number; ay?: number; origin?: { x: number; y: number } }) => {\n\t\t\t\tcheck(controller.skew(params.ax ?? 0, params.ay ?? 0, { ids: ids(params), origin: params.origin }));\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}envelope`,\n\t\t\tlabel: 'Envelope Distort…',\n\t\t\tkeywords: ['envelope', 'warp', 'mesh', 'arc', 'arch', 'bulge', 'flag', 'wave', 'fish', 'rise', 'fisheye', 'inflate', 'squeeze', 'twist', 'shell', 'bend', 'distort'],\n\t\t\tdescription:\n\t\t\t\t'Envelope distortion that stays editable (a live `envelope` effect). Give `preset` (with `bend`, `horizontal`, `vertical` in percent, −100…100, and `orientation`) to warp, or `mesh` ({ rows, columns } and optionally `points`, the control grid row by row in the node’s own space) for an editable mesh; a node that already has an envelope is changed. `topObject: true` uses the topmost selected closed path as the envelope of the others; `release: true` removes the envelope and gives back its shape as a path; `expand: true` bakes it (Expand Appearance). One undo step.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tids: IDS,\n\t\t\t\t\tpreset: { type: 'string', enum: VECTOR_WARP_STYLES },\n\t\t\t\t\tbend: { type: 'number', minimum: -100, maximum: 100 },\n\t\t\t\t\thorizontal: { type: 'number', minimum: -100, maximum: 100 },\n\t\t\t\t\tvertical: { type: 'number', minimum: -100, maximum: 100 },\n\t\t\t\t\torientation: { type: 'string', enum: ['horizontal', 'vertical'] },\n\t\t\t\t\tmesh: {\n\t\t\t\t\t\ttype: 'object',\n\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\trows: { type: 'integer', minimum: 1, maximum: 16 },\n\t\t\t\t\t\t\tcolumns: { type: 'integer', minimum: 1, maximum: 16 },\n\t\t\t\t\t\t\tpoints: { type: 'array', items: POINT, description: '(3·columns + 1) × (3·rows + 1) control points, row by row (node space).' }\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\ttopObject: { type: 'boolean' },\n\t\t\t\t\trelease: { type: 'boolean' },\n\t\t\t\t\texpand: { type: 'boolean' }\n\t\t\t\t}\n\t\t\t},\n\t\t\tenabled: enabledFor,\n\t\t\tundoable: true,\n\t\t\trun: (\n\t\t\t\t_ctx: unknown,\n\t\t\t\tparams: {\n\t\t\t\t\tids?: string[];\n\t\t\t\t\tpreset?: string;\n\t\t\t\t\tbend?: number;\n\t\t\t\t\thorizontal?: number;\n\t\t\t\t\tvertical?: number;\n\t\t\t\t\torientation?: 'horizontal' | 'vertical';\n\t\t\t\t\tmesh?: { rows?: number; columns?: number; points?: { x: number; y: number }[] };\n\t\t\t\t\ttopObject?: boolean;\n\t\t\t\t\trelease?: boolean;\n\t\t\t\t\texpand?: boolean;\n\t\t\t\t}\n\t\t\t) => {\n\t\t\t\tconst target = ids(params);\n\t\t\t\tif (params.release) return { released: target, shapes: controller.releaseEnvelope({ ids: target }) };\n\t\t\t\tif (params.expand) return { expanded: controller.expandAppearance({ ids: target }) };\n\t\t\t\tif (params.topObject) {\n\t\t\t\t\tconst r = controller.makeEnvelopeWithTopObject({ ids: target, rows: params.mesh?.rows, columns: params.mesh?.columns });\n\t\t\t\t\tcheck(r);\n\t\t\t\t\treturn { nodeId: r.nodeId };\n\t\t\t\t}\n\t\t\t\tif (params.preset !== undefined && !(VECTOR_WARP_STYLES as readonly string[]).includes(params.preset)) fail(`Unknown warp preset \"${params.preset}\".`);\n\t\t\t\tconst patch: Partial<VectorEnvelopeParams> = {};\n\t\t\t\tif (params.preset) patch.style = params.preset as VectorEnvelopeParams['style'];\n\t\t\t\tfor (const k of ['bend', 'horizontal', 'vertical'] as const) if (params[k] !== undefined) patch[k] = params[k];\n\t\t\t\tif (params.orientation) patch.orientation = params.orientation;\n\t\t\t\tif (params.mesh) {\n\t\t\t\t\tif (params.preset) fail('Give either `preset` or `mesh`, not both.');\n\t\t\t\t\tpatch.style = 'mesh';\n\t\t\t\t\tconst columns = params.mesh.columns ?? 2;\n\t\t\t\t\tconst rows = params.mesh.rows ?? 2;\n\t\t\t\t\tpatch.columns = columns;\n\t\t\t\t\tpatch.rows = rows;\n\t\t\t\t\tif (params.mesh.points) {\n\t\t\t\t\t\tconst even = (n: number) => Array.from({ length: n + 1 }, (_, i) => i / n);\n\t\t\t\t\t\tconst mesh = { us: even(columns), vs: even(rows), points: params.mesh.points.map((p) => ({ x: p.x, y: p.y })) };\n\t\t\t\t\t\tif (!isValidVectorMesh(mesh)) fail(`A ${columns} × ${rows} mesh needs ${(3 * columns + 1) * (3 * rows + 1)} finite points.`);\n\t\t\t\t\t\tpatch.mesh = mesh;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst made = controller.envelope(patch, { ids: target });\n\t\t\t\tif (!made.length) fail('Nothing to envelope.');\n\t\t\t\treturn { envelopes: made };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}transform`,\n\t\t\tlabel: 'Transform…',\n\t\t\tkeywords: ['move', 'translate', 'scale', 'resize', 'rotate', 'flip'],\n\t\t\tdescription:\n\t\t\t\t'Moves (dx, dy), scales (sx, sy, along the selection box axes) and/or rotates (`rotate`: by degrees clockwise around the combined centre; `angle`: to an absolute rotation, each node around its own centre), in that order.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tids: IDS,\n\t\t\t\t\tdx: { type: 'number' },\n\t\t\t\t\tdy: { type: 'number' },\n\t\t\t\t\tsx: { type: 'number' },\n\t\t\t\t\tsy: { type: 'number' },\n\t\t\t\t\trotate: { type: 'number' },\n\t\t\t\t\tangle: { type: 'number' }\n\t\t\t\t}\n\t\t\t},\n\t\t\tenabled: enabledFor,\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[]; dx?: number; dy?: number; sx?: number; sy?: number; rotate?: number; angle?: number }) => {\n\t\t\t\tconst target = ids(params);\n\t\t\t\tif (params.dx || params.dy) check(controller.translate(params.dx ?? 0, params.dy ?? 0, { ids: target }));\n\t\t\t\tif (params.sx !== undefined || params.sy !== undefined) check(controller.scale(params.sx ?? 1, params.sy ?? params.sx ?? 1, { ids: target }));\n\t\t\t\tif (params.rotate) check(controller.rotate(params.rotate, { ids: target }));\n\t\t\t\tif (params.angle !== undefined) check(controller.setRotation(params.angle, { ids: target }));\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}delete`,\n\t\t\tlabel: 'Delete',\n\t\t\tkeywords: ['remove'],\n\t\t\tdescription: 'Deletes nodes.',\n\t\t\tparams: { type: 'object', properties: { ids: IDS } },\n\t\t\tenabled: enabledFor,\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[] } | undefined) => {\n\t\t\t\tcheck(controller.removeNodes(ids(params)));\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}boolean`,\n\t\t\tlabel: 'Boolean Operation…',\n\t\t\tkeywords: ['union', 'unite', 'subtract', 'intersect', 'exclude', 'combine', 'pathfinder'],\n\t\t\tdescription:\n\t\t\t\t'Combines two or more paths with union, subtract (upper shapes from the lowest), intersect or exclude. With `live`, the shapes stay editable operands of a live boolean group instead (a lone live group gets the operation; its operands get their own).',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\toperation: { type: 'string', enum: ['union', 'subtract', 'intersect', 'exclude'] },\n\t\t\t\t\tids: IDS,\n\t\t\t\t\tlive: { type: 'boolean', description: 'Keep the operands editable in a live boolean group (default false: one new path).' }\n\t\t\t\t},\n\t\t\t\trequired: ['operation']\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { operation: VectorBooleanOperation; ids?: string[]; live?: boolean }) => {\n\t\t\t\tconst id = controller.booleanOp(params.operation, { ids: ids(params), live: params.live });\n\t\t\t\tif (!id) fail(params.live ? 'Select two or more nodes with one parent, a live boolean group or its operands.' : 'Select at least two overlapping paths (the result was empty).');\n\t\t\t\treturn { id };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}addEffect`,\n\t\t\tlabel: 'Add Path Effect…',\n\t\t\tkeywords: ['effect', 'trim', 'round corners', 'pucker', 'bloat', 'twist', 'zig zag', 'wiggle', 'offset', 'merge', 'repeater', 'appearance', 'live'],\n\t\t\tdescription: 'Adds a live path effect to the bottom of the effect stack of nodes (paths or groups). Effects apply top to bottom; effects after a repeater act on its copies. Returns the new effect ids.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\ttype: { type: 'string', enum: VECTOR_EFFECT_TYPES },\n\t\t\t\t\tparams: { type: 'object', description: EFFECT_PARAMS_DESCRIPTION },\n\t\t\t\t\tindex: { type: 'integer', minimum: 0, description: 'Position in the stack (default: last).' },\n\t\t\t\t\tids: IDS\n\t\t\t\t},\n\t\t\t\trequired: ['type']\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\tenabled: enabledFor,\n\t\t\trun: (_ctx: unknown, params: { type: VectorEffectType; params?: Record<string, unknown>; index?: number; ids?: string[] }) => {\n\t\t\t\tif (!VECTOR_EFFECT_TYPES.includes(params.type)) fail(`Unknown effect type \"${params.type}\".`);\n\t\t\t\tconst targets = ids(params);\n\t\t\t\tconst created = controller.addEffect(params.type, { ids: targets, params: params.params, index: params.index });\n\t\t\t\tif (!created.length) fail('Nothing to add the effect to.');\n\t\t\t\treturn { effects: targets.map((id, i) => ({ nodeId: id, effectId: created[i] })) };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}updateEffect`,\n\t\t\tlabel: 'Edit Path Effect…',\n\t\t\tkeywords: ['effect', 'parameters', 'enable', 'disable', 'reorder'],\n\t\t\tdescription: 'Changes a path effect: its parameters, whether it is enabled, and its position in the stack.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tnodeId: { type: 'string' },\n\t\t\t\t\teffectId: { type: 'string' },\n\t\t\t\t\tparams: { type: 'object', description: EFFECT_PARAMS_DESCRIPTION },\n\t\t\t\t\tenabled: { type: 'boolean' },\n\t\t\t\t\tindex: { type: 'integer', minimum: 0, description: 'Move it to this position in the stack (0 applies first).' }\n\t\t\t\t},\n\t\t\t\trequired: ['nodeId', 'effectId']\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { nodeId: string; effectId: string; params?: Record<string, unknown>; enabled?: boolean; index?: number }) => {\n\t\t\t\tif (params.params !== undefined || params.enabled !== undefined) check(controller.updateEffect(params.nodeId, params.effectId, { params: params.params, enabled: params.enabled }));\n\t\t\t\tif (params.index !== undefined) check(controller.moveEffect(params.nodeId, params.effectId, params.index));\n\t\t\t\treturn { effects: controller.getNode(params.nodeId)?.effects ?? [] };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}removeEffect`,\n\t\t\tlabel: 'Remove Path Effect',\n\t\t\tkeywords: ['effect', 'delete'],\n\t\t\tdescription: 'Removes a path effect from a node.',\n\t\t\tparams: { type: 'object', properties: { nodeId: { type: 'string' }, effectId: { type: 'string' } }, required: ['nodeId', 'effectId'] },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { nodeId: string; effectId: string }) => {\n\t\t\t\tcheck(controller.removeEffect(params.nodeId, params.effectId));\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}expandAppearance`,\n\t\t\tlabel: 'Expand Appearance',\n\t\t\tkeywords: ['effects', 'bake', 'flatten', 'live boolean', 'expand'],\n\t\t\tdescription: 'Bakes the path effects and live booleans of nodes (and everything inside them) into plain paths.',\n\t\t\tparams: { type: 'object', properties: { ids: IDS } },\n\t\t\tundoable: true,\n\t\t\tenabled: enabledFor,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[] } | undefined) => {\n\t\t\t\tconst expanded = controller.expandAppearance({ ids: ids(params) });\n\t\t\t\tif (!expanded.length) fail('Nothing selected has path effects or a live boolean.');\n\t\t\t\treturn { ids: expanded };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}blend`,\n\t\t\tlabel: 'Make Blend…',\n\t\t\tkeywords: ['blend', 'morph', 'tween', 'interpolate', 'in-between', 'steps', 'transition'],\n\t\t\tdescription:\n\t\t\t\t'Blends two or more paths or groups (sharing a parent; bottom to top, or in the order given with keepOrder) into a live blend group: the in-betweens morph shape (anchors matched by arc length, the start and winding that twist least, rotation-aware so turning shapes turn) and paint (OKLab colours, gradients, stroke width, opacity, width profiles), and regenerate whenever a key changes. Spacing: `steps` (count between each pair of keys), `distance` (along the spine) or `smoothColor`. `starts` maps key ids to the anchor of their first subpath the blend starts from. Returns the blend id.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tids: IDS,\n\t\t\t\t\t...BLEND_PROPS,\n\t\t\t\t\tkeepOrder: { type: 'boolean', description: 'Blend in the order of `ids` instead of stacking order.' },\n\t\t\t\t\tstarts: { type: 'object', additionalProperties: { type: 'integer', minimum: 0 }, description: 'Start anchor per key id.' }\n\t\t\t\t}\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: (VectorBlendOptions & { ids?: string[]; keepOrder?: boolean; starts?: Record<string, number> }) | undefined) => {\n\t\t\t\tcheckBlendOptions(params);\n\t\t\t\tconst id = controller.blend({ ...params, ids: ids(params) });\n\t\t\t\tif (!id) fail('Select two or more shapes that share a parent.');\n\t\t\t\treturn { id };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}blendOptions`,\n\t\t\tlabel: 'Blend Options…',\n\t\t\tkeywords: ['blend', 'steps', 'spacing', 'distance', 'smooth color', 'orientation', 'align to path'],\n\t\t\tdescription: 'Changes the spacing (steps, distance or smoothColor), step count, distance, orient-to-spine and linear morph of the given (or selected) blends; their steps regenerate.',\n\t\t\tparams: { type: 'object', properties: { ids: IDS, ...BLEND_PROPS } },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: (VectorBlendOptions & { ids?: string[] }) | undefined) => {\n\t\t\t\tcheckBlendOptions(params);\n\t\t\t\tconst r = controller.setBlendOptions(params ?? {}, { ids: params?.ids });\n\t\t\t\tif (!r.ok) fail(r.message);\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}expandBlend`,\n\t\t\tlabel: 'Expand Blend',\n\t\t\tkeywords: ['blend', 'expand', 'flatten', 'bake'],\n\t\t\tdescription: 'Expands the given (or selected) blends into plain groups of their keys and in-betweens (editable paths with fresh ids). Returns the group ids.',\n\t\t\tparams: { type: 'object', properties: { ids: IDS } },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[] } | undefined) => {\n\t\t\t\tconst groups = controller.expandBlend({ ids: params?.ids });\n\t\t\t\tif (!groups.length) fail('Select a blend to expand.');\n\t\t\t\treturn { ids: groups };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}releaseBlend`,\n\t\t\tlabel: 'Release Blend',\n\t\t\tkeywords: ['blend', 'release', 'undo blend'],\n\t\t\tdescription: 'Releases the given (or selected) blends: the in-betweens go and the keys (plus a replaced spine, as an unpainted path) take the blend\\'s place. Returns their ids.',\n\t\t\tparams: { type: 'object', properties: { ids: IDS } },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[] } | undefined) => {\n\t\t\t\tconst released = controller.releaseBlend({ ids: params?.ids });\n\t\t\t\tif (!released.length) fail('Select a blend to release.');\n\t\t\t\treturn { ids: released };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}replaceSpine`,\n\t\t\tlabel: 'Replace Spine',\n\t\t\tkeywords: ['blend', 'spine', 'path', 'follow', 'along path'],\n\t\t\tdescription: 'With one blend and one path given (or selected): the path becomes the blend\\'s spine (it is removed) and the keys move onto it, spread evenly by arc length; the in-betweens follow it.',\n\t\t\tparams: { type: 'object', properties: { ids: IDS } },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[] } | undefined) => {\n\t\t\t\tconst r = controller.replaceBlendSpine({ ids: params?.ids });\n\t\t\t\tif (!r.ok) fail(r.message);\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}pathfinder`,\n\t\t\tlabel: 'Pathfinder…',\n\t\t\tkeywords: ['divide', 'trim', 'merge', 'crop', 'outline', 'minus back', 'split', 'faces'],\n\t\t\tdescription:\n\t\t\t\t'Pathfinder over the given paths (bottom to top): divide (every face its own shape, painted like the top-most shape over it; open unfilled paths cut), trim (hidden parts removed), merge (trim, then touching parts of one fill joined), crop (the top shape crops the others), outline (outlines cut into open strokes where they meet) or minusBack (the top shape minus everything below). Results replace the paths, grouped when there are several.',\n\t\t\tparams: { type: 'object', properties: { operation: { type: 'string', enum: [...VECTOR_PATHFINDER_OPERATIONS] }, ids: IDS }, required: ['operation'] },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { operation: VectorPathfinderOperation; ids?: string[] }) => {\n\t\t\t\tif (!VECTOR_PATHFINDER_OPERATIONS.includes(params.operation)) fail(`Unknown Pathfinder operation \"${params.operation}\".`);\n\t\t\t\tconst created = controller.pathfinder(params.operation, { ids: ids(params) });\n\t\t\t\tif (!created.length) fail('Nothing came out: select paths that overlap (two or more, except for divide and outline).');\n\t\t\t\treturn { ids: created, selection: [...controller.getState().selection] };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}shapeBuilderFaces`,\n\t\t\tlabel: 'List Shape Builder Faces',\n\t\t\tkeywords: ['shape builder', 'regions', 'faces', 'planar'],\n\t\t\tdescription: 'Lists the faces the given paths divide the plane into: path data, area, a point inside and the ids of the paths filling each (empty when only outlines enclose it).',\n\t\t\tparams: { type: 'object', properties: { ids: IDS } },\n\t\t\trun: (_ctx: unknown, params: { ids?: string[] } | undefined) => ({ faces: controller.getShapeBuilderFaces({ ids: ids(params) }) })\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}shapeBuilder`,\n\t\t\tlabel: 'Shape Builder',\n\t\t\tkeywords: ['merge faces', 'build shape', 'combine regions', 'delete region'],\n\t\t\tdescription:\n\t\t\t\t'Shape Builder gesture over the faces of the given paths: `points` is the pointer path in document units (one point is a click; [[x, y], …]; use the points from shapeBuilderFaces). Merges the faces it touches into one shape, or with `erase: true` deletes them, the pieces of unfilled open paths and the edges of other outlines (cut where paths meet) within `tolerance`; a deleted edge opens its path there, and edges bounding a deleted face go with it. Shapes covering those faces lose that area. Returns the counts of faces, line pieces and edges.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: { points: POINTS, erase: { type: 'boolean' }, tolerance: { type: 'number', minimum: 0 }, ids: IDS },\n\t\t\t\trequired: ['points']\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { points: [number, number][]; erase?: boolean; tolerance?: number; ids?: string[] }) => {\n\t\t\t\tconst result = controller.shapeBuilder(params.points.map(([x, y]) => ({ x, y })), { ids: ids(params), erase: params.erase, tolerance: params.tolerance });\n\t\t\t\tcheck(result);\n\t\t\t\treturn { merged: result.merged, faces: result.faces, lines: result.lines, edges: result.edges };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}fillRegion`,\n\t\t\tlabel: 'Fill Region',\n\t\t\tkeywords: ['paint bucket', 'live paint', 'fill area', 'enclosed'],\n\t\t\tdescription:\n\t\t\t\t'Paint bucket: fills the area the visible outlines enclose around `point` (crossings split, nested shapes left as holes) with a new path in the current fill, below the selection or on top. `gap` closes gaps up to that size between loose ends.',\n\t\t\tparams: { type: 'object', properties: { point: { type: 'array', items: { type: 'number' }, minItems: 2, maxItems: 2, description: '[x, y] in document units.' }, gap: { type: 'number', minimum: 0 } }, required: ['point'] },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { point: [number, number]; gap?: number }) => {\n\t\t\t\tconst id = controller.fillRegion({ x: params.point[0], y: params.point[1] }, { gap: params.gap });\n\t\t\t\tif (!id) fail('No closed region surrounds that point.');\n\t\t\t\treturn { id };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}align`,\n\t\t\tlabel: 'Align…',\n\t\t\tkeywords: ['left', 'right', 'center', 'top', 'bottom', 'middle'],\n\t\t\tdescription: 'Aligns nodes to their combined bounds, or to the artboard (`to: \"artboard\"`, or when only one node is given).',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: { edge: { type: 'string', enum: ['left', 'center', 'right', 'top', 'middle', 'bottom'] }, to: { type: 'string', enum: ['selection', 'artboard'] }, ids: IDS },\n\t\t\t\trequired: ['edge']\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { edge: VectorAlign; to?: 'selection' | 'artboard'; ids?: string[] }) => {\n\t\t\t\tcheck(controller.align(params.edge, { ids: ids(params), to: params.to }));\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}distribute`,\n\t\t\tlabel: 'Distribute…',\n\t\t\tkeywords: ['space', 'evenly', 'gaps'],\n\t\t\tdescription: 'Spaces three or more nodes with equal gaps.',\n\t\t\tparams: { type: 'object', properties: { direction: { type: 'string', enum: ['horizontal', 'vertical'] }, ids: IDS }, required: ['direction'] },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { direction: 'horizontal' | 'vertical'; ids?: string[] }) => {\n\t\t\t\tcheck(controller.distribute(params.direction, { ids: ids(params) }));\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}arrange`,\n\t\t\tlabel: 'Arrange…',\n\t\t\tkeywords: ['order', 'front', 'back', 'forward', 'backward', 'z-index', 'stack'],\n\t\t\tdescription: 'Changes stacking order within each node’s parent.',\n\t\t\tparams: { type: 'object', properties: { order: { type: 'string', enum: ['front', 'forward', 'backward', 'back'] }, ids: IDS }, required: ['order'] },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { order: VectorArrange; ids?: string[] }) => {\n\t\t\t\tcheck(controller.arrange(params.order, { ids: ids(params) }));\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}group`,\n\t\t\tlabel: 'Group',\n\t\t\tshortcut: 'Mod+G',\n\t\t\tdescription: 'Groups nodes that share a parent.',\n\t\t\tparams: { type: 'object', properties: { ids: IDS, name: { type: 'string' } } },\n\t\t\tenabled: enabledFor,\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[]; name?: string } | undefined) => {\n\t\t\t\tconst id = controller.group({ ids: ids(params), name: params?.name });\n\t\t\t\tif (!id) fail('Only nodes that share a parent can be grouped.');\n\t\t\t\treturn { id };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}ungroup`,\n\t\t\tlabel: 'Ungroup',\n\t\t\tshortcut: 'Mod+Shift+G',\n\t\t\tdescription: 'Replaces groups with their children.',\n\t\t\tparams: { type: 'object', properties: { ids: IDS } },\n\t\t\tenabled: enabledFor,\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[] } | undefined) => {\n\t\t\t\tcheck(controller.ungroup({ ids: ids(params) }));\n\t\t\t\treturn { selection: controller.getState().selection };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}deleteAnchors`,\n\t\t\tlabel: 'Delete Points',\n\t\t\tkeywords: ['remove', 'anchor', 'point', 'node'],\n\t\t\tdescription:\n\t\t\t\t'Deletes anchors. With `keepShape` (default: the editor setting, on) the neighbouring handles are refitted so the outline keeps its shape; off, the neighbours are joined as they are.',\n\t\t\tparams: { type: 'object', properties: { anchors: ANCHORS, keepShape: { type: 'boolean' } } },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { anchors?: VectorAnchorRef[]; keepShape?: boolean } | undefined) => {\n\t\t\t\tcheck(controller.deleteAnchors(anchorsOf(params?.anchors), { keepShape: params?.keepShape }));\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}moveAnchor`,\n\t\t\tlabel: 'Move Point…',\n\t\t\tkeywords: ['anchor', 'drag', 'curvature'],\n\t\t\tdescription: 'Moves one anchor to (x, y) in document units. `curvature: true` bends its two segments like the Curvature tool instead of carrying its handles along.',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: { nodeId: { type: 'string' }, subpath: { type: 'integer', minimum: 0 }, index: { type: 'integer', minimum: 0 }, x: { type: 'number' }, y: { type: 'number' }, curvature: { type: 'boolean' } },\n\t\t\t\trequired: ['nodeId', 'subpath', 'index', 'x', 'y']\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: VectorAnchorRef & { x: number; y: number; curvature?: boolean }) => {\n\t\t\t\tcheck(controller.moveAnchor({ nodeId: params.nodeId, subpath: params.subpath, index: params.index }, { x: params.x, y: params.y }, { curvature: params.curvature }));\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}bendSegment`,\n\t\t\tlabel: 'Bend Segment…',\n\t\t\tkeywords: ['curve', 'segment', 'drag'],\n\t\t\tdescription: 'Bends segment `segment` (from anchor `segment` to the next) so the curve passes through (x, y) at parameter `t` (default 0.5).',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tnodeId: { type: 'string' },\n\t\t\t\t\tsubpath: { type: 'integer', minimum: 0 },\n\t\t\t\t\tsegment: { type: 'integer', minimum: 0 },\n\t\t\t\t\tx: { type: 'number' },\n\t\t\t\t\ty: { type: 'number' },\n\t\t\t\t\tt: { type: 'number', minimum: 0, maximum: 1 }\n\t\t\t\t},\n\t\t\t\trequired: ['nodeId', 'subpath', 'segment', 'x', 'y']\n\t\t\t},\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { nodeId: string; subpath: number; segment: number; x: number; y: number; t?: number }) => {\n\t\t\t\tcheck(controller.bendSegment({ nodeId: params.nodeId, subpath: params.subpath, segment: params.segment }, { x: params.x, y: params.y }, params.t));\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}join`,\n\t\t\tlabel: 'Join',\n\t\t\tshortcut: 'Mod+J',\n\t\t\tkeywords: ['connect', 'close', 'merge', 'endpoints'],\n\t\t\tdescription: 'Joins the open subpaths of paths nearest endpoints first (ends within `tolerance` merge); a single open path is closed.',\n\t\t\tparams: { type: 'object', properties: { ids: IDS, tolerance: { type: 'number', minimum: 0 } } },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[]; tolerance?: number } | undefined) => {\n\t\t\t\tcheck(controller.joinPaths({ ids: params?.ids ? ids(params) : undefined, tolerance: params?.tolerance }));\n\t\t\t\treturn { selection: controller.getState().selection };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}average`,\n\t\t\tlabel: 'Average Points…',\n\t\t\tshortcut: 'Mod+Alt+J',\n\t\t\tkeywords: ['align', 'points', 'anchors', 'collapse'],\n\t\t\tdescription: 'Moves anchors onto their average position: the same y (`horizontal`), the same x (`vertical`) or the same point (`both`, default).',\n\t\t\tparams: { type: 'object', properties: { axis: { type: 'string', enum: ['horizontal', 'vertical', 'both'] }, anchors: ANCHORS } },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { axis?: VectorAverageAxis; anchors?: VectorAnchorRef[] } | undefined) => {\n\t\t\t\tcheck(controller.averageAnchors(params?.axis ?? 'both', anchorsOf(params?.anchors)));\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}removeRedundantPoints`,\n\t\t\tlabel: 'Remove Redundant Points',\n\t\t\tkeywords: ['simplify', 'clean up', 'anchors', 'collinear'],\n\t\t\tdescription: 'Drops anchors that can go without moving the outline more than `tolerance` document units (default 0.5).',\n\t\t\tparams: { type: 'object', properties: { ids: IDS, tolerance: { type: 'number', minimum: 0 } } },\n\t\t\tenabled: enabledFor,\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[]; tolerance?: number } | undefined) => {\n\t\t\t\tcheck(controller.removeRedundantPoints({ ids: params?.ids ? ids(params) : undefined, tolerance: params?.tolerance }));\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}smooth`,\n\t\t\tlabel: 'Smooth Path',\n\t\t\tkeywords: ['round', 'soften', 'handles', 'curves'],\n\t\t\tdescription: 'Pulls the handles of paths (or of the selected points) towards smooth tangents by `amount` (0–1, default 1).',\n\t\t\tparams: { type: 'object', properties: { ids: IDS, amount: { type: 'number', minimum: 0, maximum: 1 }, anchors: ANCHORS } },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { ids?: string[]; amount?: number; anchors?: VectorAnchorRef[] } | undefined) => {\n\t\t\t\tcheck(controller.smoothPaths({ ids: params?.ids ? ids(params) : undefined, amount: params?.amount, refs: params?.anchors }));\n\t\t\t\treturn { ok: true };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}cut`,\n\t\t\tlabel: 'Cut Path (Scissors)…',\n\t\t\tkeywords: ['scissors', 'split', 'open', 'break'],\n\t\t\tdescription: 'Scissors: cuts path `nodeId` at the outline point nearest (x, y), or cuts at `anchors`. A closed path opens; an open one splits in two.',\n\t\t\tparams: { type: 'object', properties: { nodeId: { type: 'string' }, x: { type: 'number' }, y: { type: 'number' }, anchors: ANCHORS } },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { nodeId?: string; x?: number; y?: number; anchors?: VectorAnchorRef[] } | undefined) => {\n\t\t\t\tif (params?.nodeId !== undefined) {\n\t\t\t\t\tif (!controller.getNode(params.nodeId)) fail(`There is no node \"${params.nodeId}\".`);\n\t\t\t\t\tif (params.x === undefined || params.y === undefined) fail('Pass x and y with nodeId.');\n\t\t\t\t\tconst created = controller.cutPathAt(params.nodeId, { x: params.x!, y: params.y! });\n\t\t\t\t\tif (!created) fail('Nothing to cut there (the end of an open path, or a locked path).');\n\t\t\t\t\treturn { ids: created };\n\t\t\t\t}\n\t\t\t\tcheck(controller.cutAtAnchors(anchorsOf(params?.anchors)));\n\t\t\t\treturn { ids: controller.getState().selection };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}knife`,\n\t\t\tlabel: 'Knife…',\n\t\t\tkeywords: ['slice', 'cut', 'split', 'divide'],\n\t\t\tdescription: 'Cuts closed paths along a polyline wherever it crosses them edge to edge (no gap between pieces). Cuts `ids`, else the selection, else every path the line crosses.',\n\t\t\tparams: { type: 'object', properties: { points: { ...POINTS, minItems: 2 }, ids: IDS }, required: ['points'] },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { points: [number, number][]; ids?: string[] }) => {\n\t\t\t\tconst created = controller.knife(params.points.map(([x, y]) => ({ x, y })), { ids: params.ids ? ids(params) : undefined });\n\t\t\t\tif (!created.length) fail('The knife did not cross any closed path from edge to edge.');\n\t\t\t\treturn { ids: created };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}erase`,\n\t\t\tlabel: 'Erase…',\n\t\t\tkeywords: ['eraser', 'rub out', 'remove'],\n\t\t\tdescription:\n\t\t\t\t'Erases a round stroke of diameter `size` (default: the eraser size setting) along a polyline: filled shapes lose the area, outlines are cut under it. Erases from `ids`, else the selection, else every path it reaches.',\n\t\t\tparams: { type: 'object', properties: { points: POINTS, size: { type: 'number', exclusiveMinimum: 0 }, ids: IDS }, required: ['points'] },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { points: [number, number][]; size?: number; ids?: string[] }) => {\n\t\t\t\tconst changed = controller.erase(params.points.map(([x, y]) => ({ x, y })), { size: params.size, ids: params.ids ? ids(params) : undefined });\n\t\t\t\tif (!changed.length) fail('The eraser did not touch any path.');\n\t\t\t\treturn { ids: changed.filter((id) => controller.getNode(id)) };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}setEditing`,\n\t\t\tlabel: 'Path Editing Options…',\n\t\t\tkeywords: ['settings', 'curvature', 'pencil', 'smoothing', 'lazy mouse', 'shapes', 'eraser'],\n\t\t\tdescription:\n\t\t\t\t'Sets how editing behaves: `keepShapeOnDelete`, `curvatureMoves` (anchor drags bend like the Curvature tool), `pencilSmoothing` (lazy mouse, 0–1), `recognizeShapes` (pencil scribbles become lines, rectangles, ellipses, polygons), `eraserSize` and `pencilPressure` (`pen`: record pen pressure on pencil strokes as a width profile, `all`: any pointer reporting pressure, `off`).',\n\t\t\tparams: {\n\t\t\t\ttype: 'object',\n\t\t\t\tproperties: {\n\t\t\t\t\tkeepShapeOnDelete: { type: 'boolean' },\n\t\t\t\t\tcurvatureMoves: { type: 'boolean' },\n\t\t\t\t\tpencilSmoothing: { type: 'number', minimum: 0, maximum: 1 },\n\t\t\t\t\trecognizeShapes: { type: 'boolean' },\n\t\t\t\t\teraserSize: { type: 'number', exclusiveMinimum: 0 },\n\t\t\t\t\tpencilPressure: { type: 'string', enum: ['pen', 'all', 'off'] }\n\t\t\t\t}\n\t\t\t},\n\t\t\trun: (_ctx: unknown, params: Record<string, unknown> | undefined) => {\n\t\t\t\tconst patch: Record<string, unknown> = {};\n\t\t\t\tfor (const key of ['keepShapeOnDelete', 'curvatureMoves', 'pencilSmoothing', 'recognizeShapes', 'eraserSize', 'pencilPressure']) if (params?.[key] !== undefined) patch[key] = params[key];\n\t\t\t\tcontroller.setEditing(patch);\n\t\t\t\treturn { editing: controller.getState().editing };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}setTool`,\n\t\t\tlabel: 'Choose Tool…',\n\t\t\tkeywords: ['pen', 'pencil', 'select', 'rectangle', 'ellipse'],\n\t\t\tdescription: 'Switches the canvas tool.',\n\t\t\tparams: { type: 'object', properties: { tool: { type: 'string', enum: [...VECTOR_TOOLS] } }, required: ['tool'] },\n\t\t\trun: (_ctx: unknown, params: { tool: VectorTool }) => {\n\t\t\t\tcontroller.setTool(params.tool);\n\t\t\t\treturn { tool: params.tool };\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}exportSvg`,\n\t\t\tlabel: 'Export SVG',\n\t\t\treadOnly: true,\n\t\t\tkeywords: ['download', 'save', 'markup'],\n\t\t\tdescription: 'Returns the document as SVG markup.',\n\t\t\tparams: { type: 'object', properties: { precision: { type: 'integer', minimum: 0, maximum: 8 } } },\n\t\t\trun: (_ctx: unknown, params: { precision?: number } | undefined) => ({ svg: controller.toSvg({ precision: params?.precision }) })\n\t\t},\n\t\t{\n\t\t\t...base,\n\t\t\tid: `${prefix}importSvg`,\n\t\t\tlabel: 'Import SVG…',\n\t\t\tkeywords: ['open', 'load', 'paste', 'markup'],\n\t\t\tdescription: 'Adds the shapes of SVG markup on top of the document (or replaces it with `replace: true`).',\n\t\t\tparams: { type: 'object', properties: { svg: { type: 'string' }, replace: { type: 'boolean' } }, required: ['svg'] },\n\t\t\tundoable: true,\n\t\t\trun: (_ctx: unknown, params: { svg: string; replace?: boolean }) => {\n\t\t\t\tcheck(params.replace ? controller.loadSvg(params.svg) : controller.importSvg(params.svg));\n\t\t\t\treturn { nodes: controller.getState().document.nodes.length, selection: controller.getState().selection };\n\t\t\t}\n\t\t}\n\t];\n}\n","/**\n * Framework-neutral data for a text panel (React and Svelte render it the same way): what it edits\n * (the selected characters of the text being edited, the selected text nodes, or the defaults for\n * new text), the current values (`null` where they differ), and the apply helpers.\n */\nimport type { VectorEditorController, VectorEditorState } from './controller.js';\nimport { findVectorNode, walkVectorNodes } from './geometry.js';\nimport { vectorTextFamilies, vectorTextHasOutlines, vectorTextRuns } from './text.js';\nimport { VECTOR_TEXT_ALIGNS, type VectorCharacterStyle, type VectorTextAlign, type VectorTextNode } from './types.js';\n\nexport interface VectorTextPanelData {\n\t/** `range`: the selected characters of the text being edited; `nodes`: whole text nodes; `defaults`: new text. */\n\ttarget: 'range' | 'nodes' | 'defaults';\n\t/** The text nodes edited (empty for defaults). */\n\tids: string[];\n\t/** Character fields; `null` where the target mixes values (or, for leading, auto). */\n\tfont: { [K in keyof Required<VectorCharacterStyle>]: VectorCharacterStyle[K] | null };\n\talign: VectorTextAlign | null;\n\t/** The frame kind of a single text node. */\n\tframe: 'point' | 'area' | 'path' | null;\n\t/** Families the shaper can draw (always the generic ones too). */\n\tfamilies: string[];\n\t/** Whether the shaper gives outlines (Create Outlines works). */\n\toutlines: boolean;\n\t/** One path is selected: Type on a Path can turn it into path text. */\n\tpathId: string | null;\n}\n\nexport const VECTOR_TEXT_ALIGN_LABELS: Readonly<Record<VectorTextAlign, string>> = { left: 'Left', center: 'Centre', right: 'Right', justify: 'Justify' };\nexport const VECTOR_TEXT_ALIGN_OPTIONS: readonly { value: VectorTextAlign; label: string }[] = VECTOR_TEXT_ALIGNS.map((value) => ({ value, label: VECTOR_TEXT_ALIGN_LABELS[value] }));\n/** Weights a font menu offers. */\nexport const VECTOR_FONT_WEIGHTS: readonly { value: number; label: string }[] = [\n\t{ value: 100, label: 'Thin' },\n\t{ value: 300, label: 'Light' },\n\t{ value: 400, label: 'Regular' },\n\t{ value: 500, label: 'Medium' },\n\t{ value: 600, label: 'Semibold' },\n\t{ value: 700, label: 'Bold' },\n\t{ value: 900, label: 'Black' }\n];\n\nconst FIELDS = ['fontFamily', 'fontWeight', 'italic', 'fontSize', 'tracking', 'leading', 'baselineShift', 'underline', 'strikethrough'] as const;\n\n/**\n * The text panel for `state`: shown while a Type tool is active or text is selected; null\n * otherwise.\n */\nexport function getVectorTextPanel(state: Pick<VectorEditorState, 'document' | 'selection' | 'textEdit' | 'textDefaults' | 'tool'>): VectorTextPanelData | null {\n\tconst nodes: VectorTextNode[] = [];\n\tfor (const id of state.selection) {\n\t\tconst n = findVectorNode(state.document.nodes, id);\n\t\tif (n)\n\t\t\twalkVectorNodes([n], (c) => {\n\t\t\t\tif (c.type === 'text') nodes.push(c);\n\t\t\t});\n\t}\n\tconst typing = state.tool === 'type' || state.tool === 'typePath';\n\tconst one = state.selection.length === 1 ? findVectorNode(state.document.nodes, state.selection[0]) : undefined;\n\tconst pathId = one?.type === 'path' ? one.id : null;\n\tif (!nodes.length && !typing && !state.textEdit) return null;\n\tconst styles: VectorCharacterStyle[] = [];\n\tlet target: VectorTextPanelData['target'] = 'defaults';\n\tconst edit = state.textEdit;\n\tconst edited = edit ? (findVectorNode(state.document.nodes, edit.nodeId) as VectorTextNode | undefined) : undefined;\n\tif (edit && edited && edit.anchor !== edit.focus) {\n\t\ttarget = 'range';\n\t\tconst a = Math.min(edit.anchor, edit.focus);\n\t\tconst b = Math.max(edit.anchor, edit.focus);\n\t\tfor (const r of vectorTextRuns(edited)) if (r.end > a && r.start < b) styles.push(r.style);\n\t} else if (nodes.length) {\n\t\ttarget = 'nodes';\n\t\tfor (const n of nodes) for (const r of vectorTextRuns(n)) styles.push(r.style);\n\t} else {\n\t\tconst { align: _a, ...font } = state.textDefaults;\n\t\tstyles.push(font);\n\t}\n\tconst same = <K extends (typeof FIELDS)[number]>(k: K): VectorCharacterStyle[K] | null => {\n\t\tconst values = styles.map((s) => s[k] ?? (k === 'italic' || k === 'underline' || k === 'strikethrough' ? false : k === 'leading' ? null : k === 'tracking' || k === 'baselineShift' ? 0 : null));\n\t\treturn values.every((v) => v === values[0]) ? ((values[0] ?? null) as VectorCharacterStyle[K] | null) : null;\n\t};\n\tconst font = Object.fromEntries(FIELDS.map((k) => [k, same(k)])) as VectorTextPanelData['font'];\n\tconst alignSource = target === 'defaults' ? [state.textDefaults.align ?? 'left'] : (target === 'range' ? [edited!] : nodes).map((n) => (n as VectorTextNode).paragraph?.align ?? 'left');\n\tconst align = alignSource.every((a) => a === alignSource[0]) ? (alignSource[0] as VectorTextAlign) : null;\n\tconst ids = target === 'range' ? [edited!.id] : nodes.map((n) => n.id);\n\tconst frameNode = target === 'range' ? edited : nodes.length === 1 ? nodes[0] : undefined;\n\treturn { target, ids, font, align, frame: frameNode?.frame.kind ?? null, families: vectorTextFamilies(), outlines: vectorTextHasOutlines(), pathId };\n}\n\n/**\n * Applies character fields (and `fill`) from the panel: to the selected characters, the selected\n * text nodes (one undo step) or the defaults for new text. `merge` folds slider drags into one step.\n */\nexport function applyVectorTextStyle(controller: VectorEditorController, patch: Record<string, unknown>, options: { merge?: string } = {}): void {\n\tconst state = controller.getState();\n\tconst panel = getVectorTextPanel(state);\n\tif (!panel) return;\n\tif (panel.target === 'range' && state.textEdit) {\n\t\tconst a = Math.min(state.textEdit.anchor, state.textEdit.focus);\n\t\tconst b = Math.max(state.textEdit.anchor, state.textEdit.focus);\n\t\tcontroller.setTextStyle(state.textEdit.nodeId, patch, { start: a, end: b }, options);\n\t} else if (panel.target === 'nodes') controller.styleTexts({ style: patch }, { ids: panel.ids, ...options });\n\telse {\n\t\tconst { fill: _f, ...font } = patch;\n\t\tcontroller.setTextDefaults(font as Partial<VectorCharacterStyle>);\n\t}\n}\n\n/** Applies paragraph fields from the panel to the text being edited or selected, or to the defaults. */\nexport function applyVectorParagraphStyle(controller: VectorEditorController, patch: { align?: VectorTextAlign } & Record<string, unknown>, options: { merge?: string } = {}): void {\n\tconst state = controller.getState();\n\tconst panel = getVectorTextPanel(state);\n\tif (!panel) return;\n\tif (panel.target === 'defaults') {\n\t\tif (patch.align) controller.setTextDefaults({ align: patch.align });\n\t\treturn;\n\t}\n\tcontroller.styleTexts({ paragraph: patch }, { ids: panel.ids, ...options });\n}\n","/**\n * Framework-neutral data for symbol UI: the symbols panel (each symbol with a preview, the selected\n * instance's options) and the dimmed document behind a symbol edited in isolation.\n */\nimport type { VectorEditorState } from './controller.js';\nimport { findVectorNode, unionBounds, vectorNodeBounds, IDENTITY_MATRIX } from './geometry.js';\nimport { getVectorSceneRenderData, type VectorSceneRenderData } from './render.js';\nimport type { VectorBounds, VectorInstanceNode, VectorNode, VectorPaint } from './types.js';\n\nexport interface VectorSymbolsPanelItem {\n\tid: string;\n\tname: string;\n\tinstances: number;\n\t/** A preview: draw `scene.nodes` (with its defs and filters) in an `<svg viewBox={viewBox}>`. */\n\tviewBox: string;\n\tscene: VectorSceneRenderData;\n\t/** Being edited in isolation. */\n\tediting: boolean;\n}\n\nexport interface VectorSymbolsPanelData {\n\tsymbols: VectorSymbolsPanelItem[];\n\t/** The selection can become a symbol (objects selected, not one instance alone). */\n\tcanMake: boolean;\n\t/** The one selected instance, with its options. */\n\tinstance: { id: string; symbolId: string; scaleStrokes: boolean; fill: VectorPaint | null; stroke: VectorPaint | null; hidden: number } | null;\n\tediting: { symbolId: string; name: string } | null;\n}\n\nconst fmt = (n: number) => Math.round(n * 100) / 100;\n\n/** The symbols panel for `state`. */\nexport function getVectorSymbolsPanel(state: VectorEditorState, options: { idPrefix?: string } = {}): VectorSymbolsPanelData {\n\tconst host = state.symbolEdit?.host ?? state.document;\n\tconst counts = new Map<string, number>();\n\tconst count = (list: readonly VectorNode[]) => {\n\t\tfor (const n of list) {\n\t\t\tif (n.type === 'instance') counts.set(n.symbolId, (counts.get(n.symbolId) ?? 0) + 1);\n\t\t\tif (n.type === 'group') count(n.children);\n\t\t}\n\t};\n\tcount(host.nodes);\n\tfor (const s of host.symbols ?? []) count(s.nodes);\n\tconst prefix = options.idPrefix ?? 'vsym-';\n\tconst symbols = (host.symbols ?? []).map((s): VectorSymbolsPanelItem => {\n\t\tconst b: VectorBounds = unionBounds(s.nodes.map((n) => vectorNodeBounds(n, IDENTITY_MATRIX))) ?? { x: -10, y: -10, width: 20, height: 20 };\n\t\tconst pad = Math.max(b.width, b.height) * 0.08 + 1;\n\t\tconst scene = getVectorSceneRenderData({ document: { width: 0, height: 0, nodes: s.nodes }, viewport: { x: 0, y: 0, zoom: 1 } }, { idPrefix: `${prefix}${s.id}-` });\n\t\treturn {\n\t\t\tid: s.id,\n\t\t\tname: s.name,\n\t\t\tinstances: counts.get(s.id) ?? 0,\n\t\t\tviewBox: `${fmt(b.x - pad)} ${fmt(b.y - pad)} ${fmt(b.width + 2 * pad)} ${fmt(b.height + 2 * pad)}`,\n\t\t\tscene,\n\t\t\tediting: state.symbolEdit?.symbolId === s.id\n\t\t};\n\t});\n\tconst one = state.selection.length === 1 ? findVectorNode(state.document.nodes, state.selection[0]) : undefined;\n\tconst inst = one?.type === 'instance' ? (one as VectorInstanceNode) : null;\n\treturn {\n\t\tsymbols,\n\t\tcanMake: state.selection.length > 0 && !(state.selection.length === 1 && inst),\n\t\tinstance: inst\n\t\t\t? { id: inst.id, symbolId: inst.symbolId, scaleStrokes: inst.scaleStrokes !== false, fill: inst.overrides?.fill ?? null, stroke: inst.overrides?.stroke ?? null, hidden: inst.overrides?.hidden?.length ?? 0 }\n\t\t\t: null,\n\t\tediting: state.symbolEdit ? { symbolId: state.symbolEdit.symbolId, name: state.symbolEdit.name } : null\n\t};\n}\n\n/**\n * The document behind a symbol edited in isolation, to draw dimmed under the art: its scene, placed\n * so the instance it was entered from lines up with the art (that instance left out). Null outside\n * isolation mode, or when the symbol was entered without an instance.\n */\nexport function getVectorSymbolEditBackground(state: VectorEditorState, options: { idPrefix?: string } = {}): (VectorSceneRenderData & { name: string }) | null {\n\tconst e = state.symbolEdit;\n\tif (!e || !e.hostTransform) return null;\n\tconst drop = (list: VectorNode[]): VectorNode[] => list.filter((n) => n.id !== e.instanceId).map((n) => (n.type === 'group' ? { ...n, children: drop(n.children) } : n));\n\tconst scene = getVectorSceneRenderData({ document: { ...e.host, nodes: drop(e.host.nodes) }, viewport: state.viewport }, { idPrefix: options.idPrefix ?? 've-host-' });\n\tconst m = e.hostTransform;\n\treturn { ...scene, transform: `${scene.transform} matrix(${m.map((v) => Math.round(v * 1e6) / 1e6).join(' ')})`, name: e.name };\n}\n"]}
|