@pixerate/editor 0.135.0 → 0.137.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.
Files changed (164) hide show
  1. package/dist/animation-editor/index.cjs +63 -55
  2. package/dist/animation-editor/index.cjs.map +1 -1
  3. package/dist/animation-editor/index.d.cts +8 -5
  4. package/dist/animation-editor/index.d.ts +8 -5
  5. package/dist/animation-editor/index.js +10 -2
  6. package/dist/animation-editor/index.js.map +1 -1
  7. package/dist/{bezier-J8i4uqSH.d.cts → bezier-CZ2AhjID.d.cts} +1 -1
  8. package/dist/{bezier-lNEOp0Jx.d.ts → bezier-Dywv3Bw8.d.ts} +1 -1
  9. package/dist/blending-D7jCmjO6.d.cts +1026 -0
  10. package/dist/blending-NgF3qo3d.d.ts +1026 -0
  11. package/dist/brushes-CnvZSYYU.d.cts +744 -0
  12. package/dist/brushes-DJmUbIgz.d.ts +744 -0
  13. package/dist/{brushstrokeRender-SII6MG4R.js → brushstrokeRender-2FAXA3XC.js} +13 -16
  14. package/dist/{brushstrokeRender-SII6MG4R.js.map → brushstrokeRender-2FAXA3XC.js.map} +1 -1
  15. package/dist/{brushstrokeRender-JNHVEUFG.cjs → brushstrokeRender-IMFY3FF6.cjs} +16 -19
  16. package/dist/brushstrokeRender-IMFY3FF6.cjs.map +1 -0
  17. package/dist/canvas/index.cjs +10 -10
  18. package/dist/canvas/index.cjs.map +1 -1
  19. package/dist/canvas/index.d.cts +10 -7
  20. package/dist/canvas/index.d.ts +10 -7
  21. package/dist/canvas/index.js +4 -4
  22. package/dist/{chunk-W4LNKTHT.cjs → chunk-2XS6LTUP.cjs} +63 -63
  23. package/dist/{chunk-W4LNKTHT.cjs.map → chunk-2XS6LTUP.cjs.map} +1 -1
  24. package/dist/{chunk-HHTCDFZC.js → chunk-3D77LOBM.js} +234 -136
  25. package/dist/chunk-3D77LOBM.js.map +1 -0
  26. package/dist/{chunk-BR7EKQUP.js → chunk-3G6J7UFV.js} +322 -2
  27. package/dist/chunk-3G6J7UFV.js.map +1 -0
  28. package/dist/{chunk-62XUO5LB.js → chunk-3X6IAJUQ.js} +2 -2
  29. package/dist/{chunk-EM3FMJWE.js → chunk-5FSRTP6E.js} +320 -3
  30. package/dist/chunk-5FSRTP6E.js.map +1 -0
  31. package/dist/{chunk-Z2H7KECK.cjs → chunk-5OOIUXNJ.cjs} +345 -247
  32. package/dist/chunk-5OOIUXNJ.cjs.map +1 -0
  33. package/dist/{chunk-7RJ6P7MI.js → chunk-6S6362D2.js} +2 -2
  34. package/dist/chunk-A3BVTI3G.js +150 -0
  35. package/dist/chunk-A3BVTI3G.js.map +1 -0
  36. package/dist/{chunk-24ATT2PE.cjs → chunk-AEAAN4AD.cjs} +120 -120
  37. package/dist/{chunk-24ATT2PE.cjs.map → chunk-AEAAN4AD.cjs.map} +1 -1
  38. package/dist/chunk-ECCXGELC.cjs +727 -0
  39. package/dist/chunk-ECCXGELC.cjs.map +1 -0
  40. package/dist/{chunk-SVYE5HIM.js → chunk-EQQMGO42.js} +2 -2
  41. package/dist/{chunk-FHPVDTI2.cjs → chunk-F2Y4E4GU.cjs} +22 -22
  42. package/dist/{chunk-FHPVDTI2.cjs.map → chunk-F2Y4E4GU.cjs.map} +1 -1
  43. package/dist/{chunk-OXSV64JX.js → chunk-G6IKY6PL.js} +4 -4
  44. package/dist/{chunk-UYQUPUR6.js → chunk-HUO3TI4C.js} +3 -3
  45. package/dist/chunk-I52DRGGJ.js +727 -0
  46. package/dist/chunk-I52DRGGJ.js.map +1 -0
  47. package/dist/{chunk-DDSO4HS7.cjs → chunk-KEENUAFN.cjs} +2 -2
  48. package/dist/{chunk-DDSO4HS7.cjs.map → chunk-KEENUAFN.cjs.map} +1 -1
  49. package/dist/{chunk-2HC4DRF4.js → chunk-LLFHCSU2.js} +2 -2
  50. package/dist/{chunk-J5BA3BQ6.cjs → chunk-O7LUD7MH.cjs} +323 -3
  51. package/dist/chunk-O7LUD7MH.cjs.map +1 -0
  52. package/dist/{chunk-ILYFOJUN.js → chunk-OIBIRXRG.js} +3 -3
  53. package/dist/{chunk-FWEO3HSL.js → chunk-PQQF5KOW.js} +8 -8
  54. package/dist/{chunk-T6TV6HY6.cjs → chunk-PRSRYHQD.cjs} +36 -36
  55. package/dist/{chunk-T6TV6HY6.cjs.map → chunk-PRSRYHQD.cjs.map} +1 -1
  56. package/dist/chunk-Q6OUXEK3.cjs +150 -0
  57. package/dist/chunk-Q6OUXEK3.cjs.map +1 -0
  58. package/dist/{chunk-6GFRKLLS.cjs → chunk-QBWEH3IM.cjs} +429 -112
  59. package/dist/chunk-QBWEH3IM.cjs.map +1 -0
  60. package/dist/{chunk-ESIWZMMK.js → chunk-SYT2TLQF.js} +35 -37
  61. package/dist/{chunk-ESIWZMMK.js.map → chunk-SYT2TLQF.js.map} +1 -1
  62. package/dist/{chunk-YT3JL7YM.cjs → chunk-TLCTTQFF.cjs} +53 -53
  63. package/dist/{chunk-YT3JL7YM.cjs.map → chunk-TLCTTQFF.cjs.map} +1 -1
  64. package/dist/{chunk-P3RQ5UJE.cjs → chunk-UTQKC66X.cjs} +4 -4
  65. package/dist/{chunk-P3RQ5UJE.cjs.map → chunk-UTQKC66X.cjs.map} +1 -1
  66. package/dist/{chunk-J3B45HTA.cjs → chunk-XB25WOC2.cjs} +6 -6
  67. package/dist/{chunk-J3B45HTA.cjs.map → chunk-XB25WOC2.cjs.map} +1 -1
  68. package/dist/{chunk-YAFS2QFZ.cjs → chunk-XN2NBLV5.cjs} +313 -315
  69. package/dist/chunk-XN2NBLV5.cjs.map +1 -0
  70. package/dist/effects/index.d.cts +7 -4
  71. package/dist/effects/index.d.ts +7 -4
  72. package/dist/{export-DMeG6E06.d.ts → export-41rL6Cne.d.ts} +1 -1
  73. package/dist/{export-AZSwX087.d.cts → export-CyJdfP_Z.d.cts} +5 -4
  74. package/dist/{export-KzubBKo7.d.cts → export-D9mXp05Q.d.cts} +1 -1
  75. package/dist/{export-BDkyDNM8.d.ts → export-DwnZ_hvQ.d.ts} +5 -4
  76. package/dist/extensions/index.cjs +3 -3
  77. package/dist/extensions/index.cjs.map +1 -1
  78. package/dist/extensions/index.js +2 -2
  79. package/dist/generation/index.cjs +150 -945
  80. package/dist/generation/index.cjs.map +1 -1
  81. package/dist/generation/index.d.cts +3 -423
  82. package/dist/generation/index.d.ts +3 -423
  83. package/dist/generation/index.js +59 -854
  84. package/dist/generation/index.js.map +1 -1
  85. package/dist/grammar/index.cjs +3 -3
  86. package/dist/grammar/index.cjs.map +1 -1
  87. package/dist/grammar/index.js +2 -2
  88. package/dist/{helpers-Coa4e2j_.d.ts → helpers-C-RgeyvH.d.ts} +25 -8
  89. package/dist/{helpers-DHl28yLO.d.cts → helpers-v6GMazzg.d.cts} +25 -8
  90. package/dist/image-editor/index.cjs +986 -986
  91. package/dist/image-editor/index.cjs.map +1 -1
  92. package/dist/image-editor/index.d.cts +15 -11
  93. package/dist/image-editor/index.d.ts +15 -11
  94. package/dist/image-editor/index.js +94 -94
  95. package/dist/index.cjs +28 -28
  96. package/dist/index.cjs.map +1 -1
  97. package/dist/index.js +36 -36
  98. package/dist/{opentype-BWR7Qv8W.d.cts → opentype-KR_jj6b_.d.cts} +1 -1
  99. package/dist/{opentype-Dfcx6wFx.d.ts → opentype-a1KAW-qs.d.ts} +1 -1
  100. package/dist/paint/index.cjs +5 -8
  101. package/dist/paint/index.cjs.map +1 -1
  102. package/dist/paint/index.d.cts +4 -2
  103. package/dist/paint/index.d.ts +4 -2
  104. package/dist/paint/index.js +15 -18
  105. package/dist/paint-editor/index.cjs +405 -403
  106. package/dist/paint-editor/index.cjs.map +1 -1
  107. package/dist/paint-editor/index.d.cts +15 -748
  108. package/dist/paint-editor/index.d.ts +15 -748
  109. package/dist/paint-editor/index.js +79 -77
  110. package/dist/paint-editor/index.js.map +1 -1
  111. package/dist/{pathfinder-D8KQ6blQ.d.cts → pathfinder-BHS4vQNm.d.cts} +1 -1
  112. package/dist/{pathfinder-Celbi3e6.d.ts → pathfinder-BVvTIBMo.d.ts} +1 -1
  113. package/dist/{pool-8JZGWHvR.d.ts → pool-CZQOrSXZ.d.ts} +1 -1
  114. package/dist/{pool-BfuxBKTd.d.cts → pool-FjZZmZ80.d.cts} +1 -1
  115. package/dist/provenance-DmlcgdzT.d.cts +426 -0
  116. package/dist/provenance-uh5r2TQ8.d.ts +426 -0
  117. package/dist/rand-8xbUb6ur.d.ts +1118 -0
  118. package/dist/rand-Cw9VLrVC.d.cts +1118 -0
  119. package/dist/raster/index.cjs +17 -19
  120. package/dist/raster/index.cjs.map +1 -1
  121. package/dist/raster/index.js +9 -11
  122. package/dist/raster/index.js.map +1 -1
  123. package/dist/{svg-CxxqG3_W.d.ts → svg-CSz3mzkw.d.ts} +2 -2
  124. package/dist/{svg-pwXWq6s2.d.cts → svg-DTb40ICU.d.cts} +2 -2
  125. package/dist/{tools-y1M6O3UT.d.ts → tools-B0wXP5cF.d.ts} +1 -1
  126. package/dist/{tools-3EBjR91b.d.cts → tools-Ca0kI1qo.d.cts} +1 -1
  127. package/dist/vector/index.cjs +2698 -1674
  128. package/dist/vector/index.cjs.map +1 -1
  129. package/dist/vector/index.d.cts +390 -72
  130. package/dist/vector/index.d.ts +390 -72
  131. package/dist/vector/index.js +1665 -641
  132. package/dist/vector/index.js.map +1 -1
  133. package/dist/video-editor/index.d.cts +7 -4
  134. package/dist/video-editor/index.d.ts +7 -4
  135. package/dist/{warp-Ceqdz7kj.d.cts → warp-DTOp0lON.d.cts} +1 -1
  136. package/dist/{warp-C1gwu2G_.d.ts → warp-pd7hrH4T.d.ts} +1 -1
  137. package/package.json +1 -1
  138. package/skills/pixerate-vector/SKILL.md +22 -0
  139. package/dist/brushstrokeRender-JNHVEUFG.cjs.map +0 -1
  140. package/dist/chunk-64PYNEHJ.js +0 -340
  141. package/dist/chunk-64PYNEHJ.js.map +0 -1
  142. package/dist/chunk-6GFRKLLS.cjs.map +0 -1
  143. package/dist/chunk-7QN76ESJ.js +0 -323
  144. package/dist/chunk-7QN76ESJ.js.map +0 -1
  145. package/dist/chunk-BR7EKQUP.js.map +0 -1
  146. package/dist/chunk-DCLJNVZS.cjs +0 -323
  147. package/dist/chunk-DCLJNVZS.cjs.map +0 -1
  148. package/dist/chunk-EM3FMJWE.js.map +0 -1
  149. package/dist/chunk-HHTCDFZC.js.map +0 -1
  150. package/dist/chunk-J5BA3BQ6.cjs.map +0 -1
  151. package/dist/chunk-YAFS2QFZ.cjs.map +0 -1
  152. package/dist/chunk-Z2H7KECK.cjs.map +0 -1
  153. package/dist/chunk-Z6OUCS2K.cjs +0 -340
  154. package/dist/chunk-Z6OUCS2K.cjs.map +0 -1
  155. package/dist/rand-BszBST4s.d.ts +0 -2140
  156. package/dist/rand-DOH_aMvm.d.cts +0 -2140
  157. /package/dist/{chunk-62XUO5LB.js.map → chunk-3X6IAJUQ.js.map} +0 -0
  158. /package/dist/{chunk-7RJ6P7MI.js.map → chunk-6S6362D2.js.map} +0 -0
  159. /package/dist/{chunk-SVYE5HIM.js.map → chunk-EQQMGO42.js.map} +0 -0
  160. /package/dist/{chunk-OXSV64JX.js.map → chunk-G6IKY6PL.js.map} +0 -0
  161. /package/dist/{chunk-UYQUPUR6.js.map → chunk-HUO3TI4C.js.map} +0 -0
  162. /package/dist/{chunk-2HC4DRF4.js.map → chunk-LLFHCSU2.js.map} +0 -0
  163. /package/dist/{chunk-ILYFOJUN.js.map → chunk-OIBIRXRG.js.map} +0 -0
  164. /package/dist/{chunk-FWEO3HSL.js.map → chunk-PQQF5KOW.js.map} +0 -0
@@ -1,3 +1,7 @@
1
+ import {
2
+ planGenerationRequest,
3
+ stableStringify
4
+ } from "../chunk-I52DRGGJ.js";
1
5
  import {
2
6
  PlanarMap,
3
7
  cutOutPiece,
@@ -16,15 +20,6 @@ import {
16
20
  orderQuad,
17
21
  solve8
18
22
  } from "../chunk-GX332W2C.js";
19
- import {
20
- bytesToBase64,
21
- decodePng,
22
- isPng,
23
- parseDataUrl
24
- } from "../chunk-7QN76ESJ.js";
25
- import {
26
- deflate
27
- } from "../chunk-6FCKOS2B.js";
28
23
  import {
29
24
  correspondMorphContours,
30
25
  createVectorMorph,
@@ -43,7 +38,7 @@ import {
43
38
  reverseMorphContour,
44
39
  subpathToMorphContour,
45
40
  turnMorphContour
46
- } from "../chunk-7RJ6P7MI.js";
41
+ } from "../chunk-6S6362D2.js";
47
42
  import {
48
43
  DEFAULT_VECTOR_FONT,
49
44
  IDENTITY_MATRIX,
@@ -52,6 +47,7 @@ import {
52
47
  VECTOR_BLEND_MODES,
53
48
  VECTOR_BLEND_MODE_LABELS,
54
49
  VECTOR_BLEND_SPACINGS,
50
+ VECTOR_BRUSHSTROKE_SCALE,
55
51
  VECTOR_CHARACTER_STYLE_KEYS,
56
52
  VECTOR_CONNECTOR_ROUTES,
57
53
  VECTOR_CONNECTOR_ROUTE_LABELS,
@@ -198,6 +194,7 @@ import {
198
194
  isUniformWidthProfile,
199
195
  isValidWarpMesh,
200
196
  isVectorBlendMode,
197
+ isVectorBrushstrokeLayer,
201
198
  isVectorConnector,
202
199
  isVectorFilterType,
203
200
  isVectorFrame,
@@ -288,6 +285,7 @@ import {
288
285
  sampleVectorGradient,
289
286
  sampleVectorGradientAt,
290
287
  sampleVectorPaintDefAt,
288
+ sanitizeVectorBrushstroke,
291
289
  sanitizeVectorConnector,
292
290
  sanitizeVectorEffects,
293
291
  sanitizeVectorFills,
@@ -356,6 +354,10 @@ import {
356
354
  vectorBlendModeFromCss,
357
355
  vectorBoundsInside,
358
356
  vectorBoxMatrix,
357
+ vectorBrushstrokeBrush,
358
+ vectorBrushstrokeBrushes,
359
+ vectorBrushstrokeImages,
360
+ vectorBrushstrokeReach,
359
361
  vectorChildAppearanceShapes,
360
362
  vectorConnectionSites,
361
363
  vectorConnectorTargetAt,
@@ -501,7 +503,11 @@ import {
501
503
  withVectorStrokes,
502
504
  withVectorTargetPaint,
503
505
  zigzagPaths
504
- } from "../chunk-HHTCDFZC.js";
506
+ } from "../chunk-3D77LOBM.js";
507
+ import "../chunk-HUO3TI4C.js";
508
+ import {
509
+ createProvenance
510
+ } from "../chunk-A3BVTI3G.js";
505
511
  import {
506
512
  bezierPathBounds,
507
513
  bezierPathToSvg,
@@ -509,6 +515,7 @@ import {
509
515
  contourSignedArea,
510
516
  contoursToPath,
511
517
  distanceToBezierPath,
518
+ flattenChain,
512
519
  pathBoolean,
513
520
  pathToContours,
514
521
  pointInBezierPath,
@@ -532,6 +539,19 @@ import {
532
539
  resolveEffectValues
533
540
  } from "../chunk-QFSTQHZY.js";
534
541
  import "../chunk-4FX5ACVG.js";
542
+ import "../chunk-5FSRTP6E.js";
543
+ import "../chunk-63AZAUSN.js";
544
+ import {
545
+ bytesToBase64,
546
+ decodePng,
547
+ isPng,
548
+ parseDataUrl
549
+ } from "../chunk-3G6J7UFV.js";
550
+ import {
551
+ deflate
552
+ } from "../chunk-6FCKOS2B.js";
553
+ import "../chunk-FPENXAEJ.js";
554
+ import "../chunk-P6ODPZ4W.js";
535
555
  import {
536
556
  HistoryManager
537
557
  } from "../chunk-QVZZGT4X.js";
@@ -541,225 +561,831 @@ import {
541
561
  hexToRgba
542
562
  } from "../chunk-TGD52FEZ.js";
543
563
 
544
- // src/vector/gradientTool.ts
545
- var VECTOR_GRADIENT_STOP_GAP = 12;
546
- var VECTOR_GRADIENT_MIDPOINT_GAP = 9;
547
- var VECTOR_GRADIENT_OFF_BAR = 24;
548
- var STOP_HIT = 6;
549
- var MIDPOINT_HIT = 5;
550
- var HANDLE_HIT = 6;
551
- var BAR_HIT = 5;
552
- var fmt = (n2) => Math.round(n2 * 100) / 100;
553
- var apply = (m, p) => ({ x: m[0] * p.x + m[2] * p.y + m[4], y: m[1] * p.x + m[3] * p.y + m[5] });
554
- function vectorGradientTargetPath(doc, selection, target, layer = null) {
555
- for (const id of selection) {
556
- const node = findVectorNode(doc.nodes, id);
557
- if (!node) continue;
558
- const path = collectPathNodes(node).find((p) => isVectorGradient(vectorTargetPaint(p, target, layer)));
559
- if (path) return path;
564
+ // src/vector/trace.ts
565
+ var VECTOR_TRACE_MAX_PIXELS = 4e6;
566
+ function signedArea(p) {
567
+ let a = 0;
568
+ for (let i = 0; i < p.length; i++) {
569
+ const [x0, y0] = p[i];
570
+ const [x1, y1] = p[(i + 1) % p.length];
571
+ a += x0 * y1 - x1 * y0;
560
572
  }
561
- return null;
573
+ return a / 2;
562
574
  }
563
- function getVectorGradientAnnotator(doc, selection, viewport, target, layer = null) {
564
- const path = vectorGradientTargetPath(doc, selection, target, layer);
565
- if (!path) return null;
566
- const gradient = vectorTargetPaint(path, target, layer);
567
- const world = vectorWorldMatrix(doc.nodes, path.id) ?? IDENTITY_MATRIX;
568
- const screen = multiplyMatrices(world, [viewport.zoom, 0, 0, viewport.zoom, viewport.x, viewport.y]);
569
- const bounds = vectorPathLocalBounds(path);
570
- const h = vectorGradientHandles(gradient, bounds, screen);
571
- const v = { x: h.end.x - h.start.x, y: h.end.y - h.start.y };
572
- const len2 = Math.hypot(v.x, v.y);
573
- const normal = len2 > 1e-9 ? { x: -v.y / len2, y: v.x / len2 } : { x: 0, y: 1 };
574
- const at = (t, gap) => ({ x: h.start.x + v.x * t + normal.x * gap, y: h.start.y + v.y * t + normal.y * gap });
575
- const sorted = sortedVectorGradientStops(gradient);
576
- const stops = sorted.map((s, index) => ({ ...s, index, ...at(s.offset, VECTOR_GRADIENT_STOP_GAP) }));
577
- const midpoints = sorted.slice(0, -1).flatMap((s, index) => {
578
- const pos = vectorGradientMidpointPosition(sorted, index);
579
- return pos === null || sorted[index + 1].offset - s.offset < 1e-6 ? [] : [{ index, midpoint: s.midpoint ?? 0.5, ...at(pos, -VECTOR_GRADIENT_MIDPOINT_GAP) }];
580
- });
581
- let extent = null;
582
- if (isCentredVectorGradient(gradient.type)) {
583
- const frame = multiplyMatrices(vectorGradientMatrix(gradient, bounds), screen);
584
- const pts = gradient.type === "diamond" ? [{ x: 1, y: 0 }, { x: 0, y: 1 }, { x: -1, y: 0 }, { x: 0, y: -1 }] : Array.from({ length: 48 }, (_, k) => ({ x: Math.cos(k * Math.PI / 24), y: Math.sin(k * Math.PI / 24) }));
585
- extent = `M${pts.map((p) => apply(frame, p)).map((p) => `${fmt(p.x)} ${fmt(p.y)}`).join("L")}Z`;
575
+ function pointInPoly(p, poly) {
576
+ let inside = false;
577
+ for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
578
+ const [xi, yi] = poly[i];
579
+ const [xj, yj] = poly[j];
580
+ if (yi > p[1] !== yj > p[1] && p[0] < (xj - xi) * (p[1] - yi) / (yj - yi) + xi) inside = !inside;
586
581
  }
587
- return { nodeId: path.id, target, gradient, bounds, world, start: h.start, end: h.end, aspect: h.aspect, focal: h.focal, normal, stops, midpoints, extent };
588
- }
589
- function vectorGradientBarPosition(a, p) {
590
- const v = { x: a.end.x - a.start.x, y: a.end.y - a.start.y };
591
- const l2 = v.x * v.x + v.y * v.y;
592
- const d = { x: p.x - a.start.x, y: p.y - a.start.y };
593
- if (l2 < 1e-12) return { offset: 0, across: Math.hypot(d.x, d.y) };
594
- return { offset: (d.x * v.x + d.y * v.y) / l2, across: d.x * a.normal.x + d.y * a.normal.y };
595
- }
596
- function hitVectorGradientAnnotator(a, p) {
597
- const near = (q, r) => !!q && Math.hypot(q.x - p.x, q.y - p.y) <= r;
598
- for (let i = a.stops.length - 1; i >= 0; i--) if (near(a.stops[i], STOP_HIT)) return { kind: "stop", index: a.stops[i].index };
599
- for (const m of a.midpoints) if (near(m, MIDPOINT_HIT)) return { kind: "midpoint", index: m.index };
600
- const centred = isCentredVectorGradient(a.gradient.type);
601
- if (near(a.end, HANDLE_HIT)) return { kind: "handle", handle: centred ? "radius" : "end" };
602
- if (a.focal && near(a.focal, HANDLE_HIT) && !near(a.start, 1)) return { kind: "handle", handle: "focal" };
603
- if (near(a.start, HANDLE_HIT)) return { kind: "handle", handle: "start" };
604
- if (near(a.aspect, HANDLE_HIT)) return { kind: "handle", handle: "aspect" };
605
- const { offset, across } = vectorGradientBarPosition(a, p);
606
- if (offset >= 0 && offset <= 1 && across >= -BAR_HIT && across <= VECTOR_GRADIENT_STOP_GAP + STOP_HIT) return { kind: "bar", offset };
607
- return null;
608
- }
609
-
610
- // src/vector/blendGroup.ts
611
- var DEFAULT_VECTOR_BLEND_STEPS = 5;
612
- var DEFAULT_VECTOR_BLEND_DISTANCE = 20;
613
- var VECTOR_BLEND_MAX_STEPS = 1e3;
614
- var VECTOR_BLEND_MAX_SMOOTH_STEPS = 256;
615
- function isVectorBlendGroup(node) {
616
- return !!node && node.type === "group" && !!node.blend;
617
- }
618
- function vectorBlendKeys(group) {
619
- return group.children.filter((c) => !c.blendStep);
582
+ return inside;
620
583
  }
621
- function resolveVectorBlendOptions(options) {
622
- return {
623
- spacing: options?.spacing ?? "steps",
624
- steps: options?.steps ?? DEFAULT_VECTOR_BLEND_STEPS,
625
- distance: options?.distance ?? DEFAULT_VECTOR_BLEND_DISTANCE,
626
- orientToSpine: options?.orientToSpine ?? false,
627
- linear: options?.linear ?? false
584
+ function vectorTraceContours(field, w, h, threshold) {
585
+ const gw = w + 2;
586
+ const gh = h + 2;
587
+ const val = (i, j) => i === 0 || j === 0 || i > w || j > h ? -Infinity : field[(j - 1) * w + (i - 1)];
588
+ const inside = (i, j) => val(i, j) >= threshold;
589
+ const pos = (i, j) => [i - 0.5, j - 0.5];
590
+ const cross2 = (a, b) => {
591
+ const va = val(a[0], a[1]);
592
+ const vb = val(b[0], b[1]);
593
+ let t;
594
+ if (Number.isFinite(va) && Number.isFinite(vb) && Math.abs(vb - va) > 1e-12) t = Math.min(1, Math.max(0, (threshold - va) / (vb - va)));
595
+ else if (Number.isFinite(va)) t = va >= threshold ? 0.5 : 0;
596
+ else t = vb >= threshold ? 0.5 : 1;
597
+ const pa = pos(a[0], a[1]);
598
+ const pb = pos(b[0], b[1]);
599
+ return [pa[0] + (pb[0] - pa[0]) * t, pa[1] + (pb[1] - pa[1]) * t];
628
600
  };
629
- }
630
- function paintColors(p) {
631
- if (typeof p === "string") return [p];
632
- if (p && typeof p === "object") {
633
- if (p.type === "mesh") return p.colors;
634
- if (p.type === "freeform") return p.points.map((q) => q.color);
635
- return p.stops.map((s) => s.color);
601
+ const hid = (i, j) => 2 * (j * gw + i);
602
+ const vid = (i, j) => 2 * (j * gw + i) + 1;
603
+ const next = /* @__PURE__ */ new Map();
604
+ const point = /* @__PURE__ */ new Map();
605
+ for (let j = 0; j < gh - 1; j++) {
606
+ for (let i = 0; i < gw - 1; i++) {
607
+ const tl = inside(i, j);
608
+ const tr = inside(i + 1, j);
609
+ const br = inside(i + 1, j + 1);
610
+ const bl = inside(i, j + 1);
611
+ const code = +tl << 3 | +tr << 2 | +br << 1 | +bl;
612
+ if (code === 0 || code === 15) continue;
613
+ const top = hid(i, j);
614
+ const bottom = hid(i, j + 1);
615
+ const left = vid(i, j);
616
+ const right = vid(i + 1, j);
617
+ const seg = (a, b) => next.set(a, b);
618
+ const mean = () => (val(i, j) + val(i + 1, j) + val(i + 1, j + 1) + val(i, j + 1)) / 4;
619
+ switch (code) {
620
+ case 1:
621
+ seg(left, bottom);
622
+ break;
623
+ case 2:
624
+ seg(bottom, right);
625
+ break;
626
+ case 3:
627
+ seg(left, right);
628
+ break;
629
+ case 4:
630
+ seg(right, top);
631
+ break;
632
+ case 5:
633
+ if (mean() >= threshold) {
634
+ seg(left, top);
635
+ seg(right, bottom);
636
+ } else {
637
+ seg(left, bottom);
638
+ seg(right, top);
639
+ }
640
+ break;
641
+ case 6:
642
+ seg(bottom, top);
643
+ break;
644
+ case 7:
645
+ seg(left, top);
646
+ break;
647
+ case 8:
648
+ seg(top, left);
649
+ break;
650
+ case 9:
651
+ seg(top, bottom);
652
+ break;
653
+ case 10:
654
+ if (mean() >= threshold) {
655
+ seg(top, right);
656
+ seg(bottom, left);
657
+ } else {
658
+ seg(top, left);
659
+ seg(bottom, right);
660
+ }
661
+ break;
662
+ case 11:
663
+ seg(top, right);
664
+ break;
665
+ case 12:
666
+ seg(right, left);
667
+ break;
668
+ case 13:
669
+ seg(right, bottom);
670
+ break;
671
+ case 14:
672
+ seg(bottom, left);
673
+ break;
674
+ }
675
+ const edges = [
676
+ [top, [i, j], [i + 1, j]],
677
+ [bottom, [i, j + 1], [i + 1, j + 1]],
678
+ [left, [i, j], [i, j + 1]],
679
+ [right, [i + 1, j], [i + 1, j + 1]]
680
+ ];
681
+ for (const [e, a, b] of edges) if (inside(a[0], a[1]) !== inside(b[0], b[1]) && !point.has(e)) point.set(e, cross2(a, b));
682
+ }
636
683
  }
637
- return [];
638
- }
639
- function vectorBlendColorDistance(a, b) {
640
- const pa = collectPathNodes(a)[0];
641
- const pb = collectPathNodes(b)[0];
642
- if (!pa || !pb) return 0;
643
- let d = 0;
644
- for (const key of ["fill", "stroke"]) {
645
- const ca = paintColors(pa[key]);
646
- const cb = paintColors(pb[key]);
647
- ca.forEach((c, i) => {
648
- const other = cb[i] ?? cb[0];
649
- if (other === void 0) return;
650
- const x = hexToRgba(c);
651
- const y = hexToRgba(other);
652
- if (!x || !y) return;
653
- d = 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));
654
- });
684
+ const out = [];
685
+ for (const start of [...next.keys()].sort((a, b) => a - b)) {
686
+ if (!next.has(start)) continue;
687
+ const poly = [];
688
+ let e = start;
689
+ for (; ; ) {
690
+ const n2 = next.get(e);
691
+ if (n2 === void 0) break;
692
+ next.delete(e);
693
+ const p = point.get(e);
694
+ if (p) poly.push(p);
695
+ e = n2;
696
+ if (e === start) break;
697
+ }
698
+ if (poly.length >= 3) out.push(poly);
655
699
  }
656
- return d;
700
+ return out;
657
701
  }
658
- var centreOf = (n2) => {
659
- const b = vectorNodeBounds(n2);
660
- return b ? [b.x + b.width / 2, b.y + b.height / 2] : [0, 0];
661
- };
662
- function vectorBlendStepCount(a, b, options, length) {
663
- const o = resolveVectorBlendOptions(options);
664
- if (o.spacing === "distance") {
665
- const d = Math.max(0.01, o.distance);
666
- return Math.min(VECTOR_BLEND_MAX_STEPS, Math.max(0, Math.round(length / d) - 1));
667
- }
668
- if (o.spacing === "smoothColor") {
669
- const cd = vectorBlendColorDistance(a, b);
670
- if (cd > 1 / 255) return Math.min(VECTOR_BLEND_MAX_SMOOTH_STEPS, Math.max(1, Math.ceil(cd * 255 / 2)));
671
- const ba = vectorNodeBounds(a);
672
- const bb = vectorNodeBounds(b);
673
- const dist4 = ba && bb ? 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)) : length;
674
- return Math.min(VECTOR_BLEND_MAX_SMOOTH_STEPS, Math.max(1, Math.ceil(dist4 / 2)));
702
+ function loopsOf(field, w, h, threshold, tolerance, minArea) {
703
+ const loops = [];
704
+ for (const raw of vectorTraceContours(field, w, h, threshold)) {
705
+ const area = signedArea(raw);
706
+ if (Math.abs(area) < Math.max(0, minArea)) continue;
707
+ const simple = simplifyRing(raw, Math.max(0.01, tolerance));
708
+ if (simple.length < 3) continue;
709
+ loops.push({ raw, simple, area, depth: 0 });
675
710
  }
676
- return Math.min(VECTOR_BLEND_MAX_STEPS, Math.max(1, Math.round(Number.isFinite(o.steps) ? o.steps : DEFAULT_VECTOR_BLEND_STEPS)));
711
+ for (const l of loops) l.depth = loops.filter((o) => o !== l && Math.abs(o.area) > Math.abs(l.area) && pointInPoly(l.simple[0], o.simple)).length;
712
+ return loops.sort((a, b) => a.depth - b.depth || Math.abs(b.area) - Math.abs(a.area));
677
713
  }
678
- function vectorBlendRail(sp) {
679
- const pts = [];
680
- const angles = [];
681
- const push = (p, tangent) => {
682
- const last = pts[pts.length - 1];
683
- if (last && Math.hypot(p[0] - last[0], p[1] - last[1]) < 1e-9) return;
684
- pts.push(p);
685
- angles.push(Math.atan2(tangent[1], tangent[0]));
714
+ function loopSubpath(l, tolerance, roundness) {
715
+ if (roundness <= 0) return { closed: true, anchors: l.simple.map(([x, y]) => ({ x, y })) };
716
+ const cornerAngleDeg = 25 + roundness * 125;
717
+ const cubics = fitCubics(l.raw, Math.max(0.05, tolerance), {
718
+ closed: true,
719
+ cornerAngleDeg
720
+ });
721
+ if (!cubics.length) return { closed: true, anchors: l.simple.map(([x, y]) => ({ x, y })) };
722
+ const anchors = cubics.map((c, k) => {
723
+ const prev = cubics[(k - 1 + cubics.length) % cubics.length];
724
+ return {
725
+ x: c.p0[0],
726
+ y: c.p0[1],
727
+ in: { x: prev.p2[0], y: prev.p2[1] },
728
+ out: { x: c.p1[0], y: c.p1[1] }
729
+ };
730
+ });
731
+ return { closed: true, anchors };
732
+ }
733
+ function traceField(field, w, h, threshold, o) {
734
+ const tolerance = o.tolerance ?? 1;
735
+ const roundness = Math.min(1, Math.max(0, o.cornerRoundness ?? 0.5));
736
+ return loopsOf(field, w, h, threshold, tolerance, o.minArea ?? 10).map((l) => loopSubpath(l, tolerance, roundness));
737
+ }
738
+ function quantizeVectorTraceColors(pixels, count) {
739
+ const n2 = pixels.width * pixels.height;
740
+ const d = pixels.data;
741
+ const opaque = [];
742
+ for (let i = 0; i < n2; i++) if (d[i * 4 + 3] >= 128) opaque.push(i);
743
+ const boxes = opaque.length ? [opaque] : [];
744
+ const range = (box, c) => {
745
+ let lo = 255;
746
+ let hi = 0;
747
+ for (const i of box) {
748
+ const v = d[i * 4 + c];
749
+ if (v < lo) lo = v;
750
+ if (v > hi) hi = v;
751
+ }
752
+ return hi - lo;
686
753
  };
687
- const count = subpathSegmentCount(sp);
688
- for (let i = 0; i < count; i++) {
689
- const seg = subpathSegment(sp, i);
690
- const samples = seg.length === 2 ? 1 : 32;
691
- if (i === 0) push(seg[0], segmentTangent(seg, 0));
692
- for (let k = 1; k <= samples; k++) push(segmentPoint(seg, k / samples), segmentTangent(seg, k / samples));
754
+ while (boxes.length < count) {
755
+ let best = -1;
756
+ let bestRange = 0;
757
+ let bestC = 0;
758
+ boxes.forEach((b, k) => {
759
+ if (b.length < 2) return;
760
+ for (let c = 0; c < 3; c++) {
761
+ const r = range(b, c);
762
+ if (r > bestRange) {
763
+ bestRange = r;
764
+ best = k;
765
+ bestC = c;
766
+ }
767
+ }
768
+ });
769
+ if (best < 0) break;
770
+ const box = boxes[best].slice().sort((a, b) => d[a * 4 + bestC] - d[b * 4 + bestC]);
771
+ const key = (k) => d[box[k] * 4 + bestC];
772
+ const v = key(box.length >> 1);
773
+ let mid = box.findIndex((_, k) => key(k) >= v);
774
+ if (mid <= 0) mid = box.findIndex((_, k) => key(k) > v);
775
+ boxes.splice(best, 1, box.slice(0, mid), box.slice(mid));
693
776
  }
694
- if (pts.length < 2) return null;
695
- const cum = [0];
696
- for (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]));
697
- return { pts, cum, angles, total: cum[cum.length - 1], closed: sp.closed };
777
+ const palette = boxes.map((b) => {
778
+ const s = [0, 0, 0];
779
+ for (const i of b) for (let c = 0; c < 3; c++) s[c] += d[i * 4 + c];
780
+ return [Math.round(s[0] / b.length), Math.round(s[1] / b.length), Math.round(s[2] / b.length)];
781
+ });
782
+ const index = new Int16Array(n2).fill(-1);
783
+ const counts = palette.map(() => 0);
784
+ for (const i of opaque) {
785
+ let best = 0;
786
+ let bestD = Infinity;
787
+ palette.forEach((p, k) => {
788
+ const dd = (d[i * 4] - p[0]) ** 2 + (d[i * 4 + 1] - p[1]) ** 2 + (d[i * 4 + 2] - p[2]) ** 2;
789
+ if (dd < bestD) {
790
+ bestD = dd;
791
+ best = k;
792
+ }
793
+ });
794
+ index[i] = best;
795
+ counts[best]++;
796
+ }
797
+ return { palette, index, counts };
698
798
  }
699
- function vectorBlendRailAt(rail, s) {
700
- let len2 = Number.isFinite(s) ? s : 0;
701
- if (rail.closed && rail.total > 0) len2 = (len2 % rail.total + rail.total) % rail.total;
702
- len2 = Math.min(rail.total, Math.max(0, len2));
703
- let lo = 0;
704
- let hi = rail.cum.length - 1;
705
- while (hi - lo > 1) {
706
- const mid = lo + hi >> 1;
707
- if (rail.cum[mid] <= len2) lo = mid;
708
- else hi = mid;
799
+ var hex = (c) => `#${c.map((v) => Math.round(v).toString(16).padStart(2, "0")).join("")}`;
800
+ function traceVectorImage(pixels, options = {}) {
801
+ const { width: w, height: h, data } = pixels;
802
+ if (!(w > 0 && h > 0) || data.length < w * h * 4) throw new Error("The image has no pixels (or fewer than width \xD7 height \xD7 4 values).");
803
+ if (w * h > VECTOR_TRACE_MAX_PIXELS) throw new Error(`The image has ${w * h} pixels; scale it down to at most ${VECTOR_TRACE_MAX_PIXELS} to trace it.`);
804
+ const n2 = w * h;
805
+ if ((options.mode ?? "threshold") === "threshold") {
806
+ const field = new Float32Array(n2);
807
+ const byAlpha = options.channel === "alpha";
808
+ for (let i = 0; i < n2; i++) {
809
+ const a = data[i * 4 + 3] / 255;
810
+ field[i] = byAlpha ? a : a * (1 - (0.2126 * data[i * 4] + 0.7152 * data[i * 4 + 1] + 0.0722 * data[i * 4 + 2]) / 255);
811
+ }
812
+ const subpaths = traceField(field, w, h, Math.min(1, Math.max(0, options.threshold ?? 0.5)), options);
813
+ return subpaths.length ? [
814
+ {
815
+ id: "trace-1",
816
+ type: "path",
817
+ name: "Trace",
818
+ fill: options.color ?? "#000000",
819
+ stroke: null,
820
+ fillRule: "evenodd",
821
+ subpaths
822
+ }
823
+ ] : [];
709
824
  }
710
- const span = rail.cum[hi] - rail.cum[lo];
711
- const u = span > 1e-12 ? (len2 - rail.cum[lo]) / span : 0;
712
- const a = rail.pts[lo];
713
- const b = rail.pts[hi];
714
- return { 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]) };
825
+ const count = Math.min(32, Math.max(2, Math.round(options.colors ?? 6)));
826
+ const q = quantizeVectorTraceColors(pixels, count);
827
+ const order = q.palette.map((_, k) => k).filter((k) => q.counts[k] > 0).sort((a, b) => q.counts[b] - q.counts[a]);
828
+ const rank = new Map(order.map((k, r) => [k, r]));
829
+ const out = [];
830
+ order.forEach((k, r) => {
831
+ const field = new Uint8Array(n2);
832
+ for (let i = 0; i < n2; i++) {
833
+ const ix = q.index[i];
834
+ field[i] = ix >= 0 && rank.get(ix) >= r ? 1 : 0;
835
+ }
836
+ const subpaths = traceField(field, w, h, 0.5, options);
837
+ if (subpaths.length)
838
+ out.push({
839
+ id: `trace-${out.length + 1}`,
840
+ type: "path",
841
+ name: hex(q.palette[k]),
842
+ fill: hex(q.palette[k]),
843
+ stroke: null,
844
+ fillRule: "evenodd",
845
+ subpaths
846
+ });
847
+ });
848
+ return out;
715
849
  }
716
- function vectorBlendSpine(group) {
717
- const keys = vectorBlendKeys(group);
718
- if (keys.length < 2) return null;
719
- const stored = group.blend?.spine;
720
- if (stored && stored.anchors.length >= 2) {
721
- const rail2 = vectorBlendRail(stored);
722
- if (rail2) {
723
- const end = rail2.closed ? rail2.total * (1 - 1 / keys.length) : rail2.total;
724
- return { spine: stored, rail: rail2, stops: keys.map((_, i) => end * i / (keys.length - 1)), replaced: true };
850
+
851
+ // src/vector/stylize.ts
852
+ var VECTOR_STYLIZE_STYLES = ["mosaic", "halftone", "lowPoly", "hatch"];
853
+ var VECTOR_STYLIZE_LABELS = { mosaic: "Mosaic", halftone: "Halftone", lowPoly: "Low Poly", hatch: "Engraving" };
854
+ var VECTOR_STYLIZE_MAX_SHAPES = 6e4;
855
+ function sampleBox(px, cx, cy, size) {
856
+ const { width: w, height: h, data: d } = px;
857
+ const n2 = Math.max(1, Math.min(5, Math.round(size)));
858
+ const s = [0, 0, 0, 0];
859
+ let count = 0;
860
+ for (let j = 0; j < n2; j++) {
861
+ for (let i = 0; i < n2; i++) {
862
+ const x = Math.floor(cx - size / 2 + (i + 0.5) * size / n2);
863
+ const y = Math.floor(cy - size / 2 + (j + 0.5) * size / n2);
864
+ if (x < 0 || y < 0 || x >= w || y >= h) continue;
865
+ const k = (y * w + x) * 4;
866
+ const a = d[k + 3];
867
+ s[0] += d[k] * a;
868
+ s[1] += d[k + 1] * a;
869
+ s[2] += d[k + 2] * a;
870
+ s[3] += a;
871
+ count++;
872
+ }
873
+ }
874
+ if (!count || s[3] <= 0) return [0, 0, 0, 0];
875
+ return [s[0] / s[3], s[1] / s[3], s[2] / s[3], s[3] / count];
876
+ }
877
+ var darkness = (c) => (1 - (0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]) / 255) * (c[3] / 255);
878
+ var hex2 = (c) => `#${c.slice(0, 3).map((v) => Math.round(Math.min(255, Math.max(0, v))).toString(16).padStart(2, "0")).join("")}`;
879
+ function paletteOf(colors, count) {
880
+ const data = new Uint8ClampedArray(colors.length * 4);
881
+ colors.forEach((c, i) => {
882
+ data[i * 4] = c[0];
883
+ data[i * 4 + 1] = c[1];
884
+ data[i * 4 + 2] = c[2];
885
+ data[i * 4 + 3] = c[3];
886
+ });
887
+ const q = quantizeVectorTraceColors({ width: colors.length, height: 1, data }, Math.min(32, Math.max(2, Math.round(count))));
888
+ return { palette: q.palette.map(hex2), index: q.index };
889
+ }
890
+ function byColour(shapes, palette, index, name, extra = {}) {
891
+ const groups = palette.map(() => []);
892
+ shapes.forEach((s, i) => {
893
+ if (index[i] >= 0) groups[index[i]].push(...s);
894
+ });
895
+ return groups.map((subpaths, k) => ({ subpaths, k })).filter((g) => g.subpaths.length).sort((a, b) => b.subpaths.length - a.subpaths.length).map((g, n2) => ({ id: `stylize-${n2}`, type: "path", name: `${name} ${palette[g.k]}`, fill: palette[g.k], stroke: null, subpaths: g.subpaths, ...extra }));
896
+ }
897
+ function check(px, cell) {
898
+ const { width: w, height: h, data } = px;
899
+ if (!(w > 0 && h > 0) || data.length < w * h * 4) throw new Error("The image has no pixels (or fewer than width \xD7 height \xD7 4 values).");
900
+ if (w * h > VECTOR_TRACE_MAX_PIXELS) throw new Error(`The image has ${w * h} pixels; scale it down to at most ${VECTOR_TRACE_MAX_PIXELS}.`);
901
+ if (!(cell >= 1)) throw new Error("cell must be at least 1 pixel.");
902
+ }
903
+ function tooMany(count) {
904
+ if (count > VECTOR_STYLIZE_MAX_SHAPES) throw new Error(`That would draw about ${count} shapes (at most ${VECTOR_STYLIZE_MAX_SHAPES}): use a larger cell.`);
905
+ }
906
+ function turnedGrid(w, h, cell, angle) {
907
+ const a = angle * Math.PI / 180;
908
+ const c = Math.cos(a);
909
+ const s = Math.sin(a);
910
+ const cx = w / 2;
911
+ const cy = h / 2;
912
+ const reach = Math.ceil(Math.hypot(w, h) / 2 / cell) + 1;
913
+ tooMany((2 * reach + 1) ** 2 * 0.7);
914
+ const out = [];
915
+ for (let j = -reach; j <= reach; j++) {
916
+ for (let i = -reach; i <= reach; i++) {
917
+ const x = cx + (i * c - j * s) * cell;
918
+ const y = cy + (i * s + j * c) * cell;
919
+ if (x < -cell || y < -cell || x > w + cell || y > h + cell) continue;
920
+ out.push({ x, y, row: j, col: i });
725
921
  }
726
922
  }
727
- const centres = keys.map(centreOf);
728
- const spine = { closed: false, anchors: centres.map((c) => ({ x: c[0], y: c[1] })) };
729
- const stops = [0];
730
- for (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]));
731
- const rail = vectorBlendRail(spine) ?? { pts: centres, cum: stops, angles: centres.map(() => 0), total: stops[stops.length - 1], closed: false };
732
- return { spine, rail, stops, replaced: false };
923
+ return out;
733
924
  }
734
- function vectorBlendKeyTargets(group) {
735
- const s = vectorBlendSpine(group);
736
- return s ? s.stops.map((stop) => vectorBlendRailAt(s.rail, stop).point) : [];
925
+ function vectorMosaic(px, o = {}) {
926
+ const cell = o.cell ?? 12;
927
+ check(px, cell);
928
+ const cols = Math.ceil(px.width / cell);
929
+ const rows = Math.ceil(px.height / cell);
930
+ tooMany(cols * rows);
931
+ const gap = Math.min(0.9, Math.max(0, o.gap ?? 0.1)) * cell;
932
+ const colors = [];
933
+ const shapes = [];
934
+ for (let r = 0; r < rows; r++) {
935
+ for (let c = 0; c < cols; c++) {
936
+ const color = sampleBox(px, (c + 0.5) * cell, (r + 0.5) * cell, cell);
937
+ if (color[3] < 128) continue;
938
+ const box = { x: c * cell + gap / 2, y: r * cell + gap / 2, width: cell - gap, height: cell - gap };
939
+ colors.push(color);
940
+ shapes.push([o.shape === "circle" ? ellipseSubpath(box) : rectSubpath(box)]);
941
+ }
942
+ }
943
+ if (!shapes.length) return [];
944
+ const { palette, index } = paletteOf(colors, o.colors ?? 12);
945
+ return byColour(shapes, palette, index, "Mosaic");
946
+ }
947
+ function vectorHalftone(px, o = {}) {
948
+ const cell = o.cell ?? 8;
949
+ check(px, cell);
950
+ const colors = [];
951
+ const shapes = [];
952
+ for (const p of turnedGrid(px.width, px.height, cell, o.angle ?? 45)) {
953
+ if (p.x < 0 || p.y < 0 || p.x >= px.width || p.y >= px.height) continue;
954
+ const color = sampleBox(px, p.x, p.y, cell);
955
+ const r = cell / Math.SQRT2 * Math.sqrt(darkness(color));
956
+ if (r < cell * 0.04) continue;
957
+ colors.push(color);
958
+ shapes.push([ellipseSubpath({ x: p.x - r, y: p.y - r, width: 2 * r, height: 2 * r })]);
959
+ }
960
+ if (!shapes.length) return [];
961
+ if (o.color === null) {
962
+ const { palette, index } = paletteOf(colors, o.colors ?? 12);
963
+ return byColour(shapes, palette, index, "Halftone");
964
+ }
965
+ return [{ id: "stylize-0", type: "path", name: "Halftone", fill: o.color ?? "#000000", stroke: null, subpaths: shapes.flat() }];
966
+ }
967
+ function hash01(a, b, seed) {
968
+ let h = Math.imul(a ^ 2654435769, 2246822507) ^ Math.imul(b ^ 3266489909, 668265263) ^ Math.imul(seed, 374761393);
969
+ h ^= h >>> 15;
970
+ h = Math.imul(h, 739982445);
971
+ h ^= h >>> 12;
972
+ return (h >>> 0) / 4294967295;
973
+ }
974
+ function vectorLowPoly(px, o = {}) {
975
+ const cell = o.cell ?? 24;
976
+ check(px, cell);
977
+ const cols = Math.max(1, Math.round(px.width / cell));
978
+ const rows = Math.max(1, Math.round(px.height / cell));
979
+ tooMany(cols * rows * 2);
980
+ const sx = px.width / cols;
981
+ const sy = px.height / rows;
982
+ const jitter = Math.min(0.5, Math.max(0, o.jitter ?? 0.35));
983
+ const seed = Math.round(o.seed ?? 1);
984
+ const pt2 = (c, r) => {
985
+ const jx = c === 0 || c === cols ? 0 : (hash01(c, r, seed) * 2 - 1) * jitter * sx;
986
+ const jy = r === 0 || r === rows ? 0 : (hash01(c, r, seed + 7919) * 2 - 1) * jitter * sy;
987
+ return [c * sx + jx, r * sy + jy];
988
+ };
989
+ const colors = [];
990
+ const shapes = [];
991
+ const tri = (a, b, c) => {
992
+ const cx = (a[0] + b[0] + c[0]) / 3;
993
+ const cy = (a[1] + b[1] + c[1]) / 3;
994
+ const color = sampleBox(px, cx, cy, Math.min(sx, sy) * 0.6);
995
+ if (color[3] < 128) return;
996
+ colors.push(color);
997
+ shapes.push([{ closed: true, anchors: [a, b, c].map(([x, y]) => ({ x, y })) }]);
998
+ };
999
+ for (let r = 0; r < rows; r++) {
1000
+ for (let c = 0; c < cols; c++) {
1001
+ const p00 = pt2(c, r);
1002
+ const p10 = pt2(c + 1, r);
1003
+ const p01 = pt2(c, r + 1);
1004
+ const p11 = pt2(c + 1, r + 1);
1005
+ if ((r + c) % 2) {
1006
+ tri(p00, p10, p11);
1007
+ tri(p00, p11, p01);
1008
+ } else {
1009
+ tri(p00, p10, p01);
1010
+ tri(p10, p11, p01);
1011
+ }
1012
+ }
1013
+ }
1014
+ if (!shapes.length) return [];
1015
+ const { palette, index } = paletteOf(colors, o.colors ?? 16);
1016
+ return byColour(shapes, palette, index, "Low Poly").map((n2) => ({ ...n2, stroke: n2.fill, strokeWidth: 0.5, strokeLinejoin: "round" }));
1017
+ }
1018
+ function vectorHatch(px, o = {}) {
1019
+ const cell = o.cell ?? 6;
1020
+ check(px, cell);
1021
+ const a = (o.angle ?? 45) * Math.PI / 180;
1022
+ const dir = { x: Math.cos(a), y: Math.sin(a) };
1023
+ const nrm = { x: -dir.y, y: dir.x };
1024
+ const pts = turnedGrid(px.width, px.height, cell, o.angle ?? 45);
1025
+ const rows = /* @__PURE__ */ new Map();
1026
+ for (const p of pts) {
1027
+ const list = rows.get(p.row);
1028
+ if (list) list.push(p);
1029
+ else rows.set(p.row, [p]);
1030
+ }
1031
+ const subpaths = [];
1032
+ const min = cell * 0.04;
1033
+ for (const list of rows.values()) {
1034
+ list.sort((p, q) => p.col - q.col);
1035
+ let run = [];
1036
+ const flush = () => {
1037
+ if (run.length >= 2) {
1038
+ const top = run.map((p) => ({ x: p.x + nrm.x * p.t, y: p.y + nrm.y * p.t }));
1039
+ const bottom = run.map((p) => ({ x: p.x - nrm.x * p.t, y: p.y - nrm.y * p.t })).reverse();
1040
+ subpaths.push({ closed: true, anchors: [...top, ...bottom] });
1041
+ }
1042
+ run = [];
1043
+ };
1044
+ for (const p of list) {
1045
+ const inside = p.x >= 0 && p.y >= 0 && p.x < px.width && p.y < px.height;
1046
+ const t = inside ? cell * 0.45 * darkness(sampleBox(px, p.x, p.y, cell)) : 0;
1047
+ if (t < min) {
1048
+ if (run.length) run.push({ x: p.x, y: p.y, t: 0 });
1049
+ flush();
1050
+ continue;
1051
+ }
1052
+ if (!run.length) run.push({ x: p.x - dir.x * cell, y: p.y - dir.y * cell, t: 0 });
1053
+ run.push({ x: p.x, y: p.y, t });
1054
+ }
1055
+ flush();
1056
+ }
1057
+ if (!subpaths.length) return [];
1058
+ tooMany(subpaths.length);
1059
+ return [{ id: "stylize-0", type: "path", name: "Hatch", fill: o.color ?? "#000000", stroke: null, subpaths }];
737
1060
  }
738
- var morphCache = /* @__PURE__ */ new WeakMap();
739
- function morphFor(a, b, blend) {
740
- const starts = [blend.starts?.[a.id] ?? null, blend.starts?.[b.id] ?? null];
741
- const key = JSON.stringify([!!blend.linear, starts]);
742
- let inner = morphCache.get(a);
743
- if (!inner) morphCache.set(a, inner = /* @__PURE__ */ new WeakMap());
744
- let byKey = inner.get(b);
745
- if (!byKey) inner.set(b, byKey = /* @__PURE__ */ new Map());
746
- let morph = byKey.get(key);
747
- if (!morph) byKey.set(key, morph = createVectorMorph(a, b, { linear: blend.linear, starts }));
748
- return morph;
1061
+ function stylizeVectorImage(px, style, options = {}) {
1062
+ switch (style) {
1063
+ case "mosaic":
1064
+ return vectorMosaic(px, options);
1065
+ case "halftone":
1066
+ return vectorHalftone(px, options);
1067
+ case "lowPoly":
1068
+ return vectorLowPoly(px, options);
1069
+ case "hatch":
1070
+ return vectorHatch(px, options);
1071
+ }
749
1072
  }
750
- var wrapAngle = (a) => Math.atan2(Math.sin(a), Math.cos(a));
751
- function asStep(node, id) {
752
- const rename = (n2, nid) => n2.type === "group" ? { ...n2, id: nid, children: n2.children.map((c, i) => rename(c, `${nid}.${i}`)) } : { ...n2, id: nid };
753
- return { ...rename(node, id), blendStep: true, locked: true };
1073
+
1074
+ // src/vector/distortBrush.ts
1075
+ var VECTOR_DISTORT_BRUSH_MODES = ["smudge", "twirl", "attract", "repel", "roughen"];
1076
+ function vectorDistortFalloff(d, r) {
1077
+ if (!(r > 0) || d >= r) return 0;
1078
+ const u = d / r;
1079
+ const k = 1 - u * u;
1080
+ return k * k;
1081
+ }
1082
+ function vectorDistortDabMap(mode, dab) {
1083
+ const s = Math.min(1, Math.max(0, dab.strength));
1084
+ return (p) => {
1085
+ const dx = p.x - dab.x;
1086
+ const dy = p.y - dab.y;
1087
+ const f = vectorDistortFalloff(Math.hypot(dx, dy), dab.radius) * s;
1088
+ if (f <= 0) return p;
1089
+ switch (mode) {
1090
+ case "smudge":
1091
+ return { x: p.x + (dab.dx ?? 0) * f, y: p.y + (dab.dy ?? 0) * f };
1092
+ case "twirl": {
1093
+ const a = (dab.angle ?? 15) * Math.PI / 180 * f;
1094
+ const c = Math.cos(a);
1095
+ const sn = Math.sin(a);
1096
+ return { x: dab.x + dx * c - dy * sn, y: dab.y + dx * sn + dy * c };
1097
+ }
1098
+ case "attract":
1099
+ return { x: p.x - dx * f * 0.5, y: p.y - dy * f * 0.5 };
1100
+ case "repel":
1101
+ return { x: p.x + dx * f * 0.5, y: p.y + dy * f * 0.5 };
1102
+ }
1103
+ };
754
1104
  }
755
- function generateVectorBlendChildren(group) {
756
- const keys = vectorBlendKeys(group);
757
- const blend = group.blend;
758
- if (!blend || keys.length < 2) return keys;
759
- const spine = vectorBlendSpine(group);
760
- if (!spine) return keys;
761
- const out = [];
762
- let counter = 0;
1105
+ function noise(x, y, seed) {
1106
+ let h = Math.imul(Math.round(x * 16) ^ 2654435769, 2246822507) ^ Math.imul(Math.round(y * 16) ^ 3266489909, 668265263) ^ Math.imul(seed | 0, 374761393);
1107
+ h ^= h >>> 15;
1108
+ h = Math.imul(h, 739982445);
1109
+ h ^= h >>> 12;
1110
+ return (h >>> 0) / 4294967295 * 2 - 1;
1111
+ }
1112
+ var reaches = (b, dab) => dab.x + dab.radius >= b.minX && dab.x - dab.radius <= b.maxX && dab.y + dab.radius >= b.minY && dab.y - dab.radius <= b.maxY;
1113
+ function roughen(path, dabs) {
1114
+ const step = Math.max(0.5, Math.min(...dabs.map((d) => d.radius)) / 6);
1115
+ const contours = pathToContours(path).map((c) => {
1116
+ const b = bezierPathBounds(contoursToPath([c]));
1117
+ if (!b || !dabs.some((d) => reaches(b, d))) return c;
1118
+ const ring = flattenChain(closeContour(c, c.closed), Math.min(0.25, step / 8));
1119
+ const pts = [];
1120
+ for (let i = 0; i < ring.length; i++) {
1121
+ const a = ring[i];
1122
+ const nxt = ring[i + 1] ?? (c.closed ? ring[0] : null);
1123
+ pts.push(a);
1124
+ if (!nxt) continue;
1125
+ const n2 = Math.floor(Math.hypot(nxt[0] - a[0], nxt[1] - a[1]) / step);
1126
+ for (let k = 1; k < n2; k++) pts.push([a[0] + (nxt[0] - a[0]) * k / n2, a[1] + (nxt[1] - a[1]) * k / n2]);
1127
+ }
1128
+ const out = pts.map((p, i) => {
1129
+ const prev = pts[i - 1] ?? (c.closed ? pts[pts.length - 1] : p);
1130
+ const next = pts[i + 1] ?? (c.closed ? pts[0] : p);
1131
+ const tx = next[0] - prev[0];
1132
+ const ty = next[1] - prev[1];
1133
+ const tl = Math.hypot(tx, ty) || 1;
1134
+ let off = 0;
1135
+ for (const dab of dabs) {
1136
+ const f = vectorDistortFalloff(Math.hypot(p[0] - dab.x, p[1] - dab.y), dab.radius) * Math.min(1, Math.max(0, dab.strength));
1137
+ if (f > 0) off += noise(p[0], p[1], dab.seed ?? 1) * f * dab.radius * 0.25 * (i % 2 ? 1 : -1);
1138
+ }
1139
+ return [p[0] - ty / tl * off, p[1] + tx / tl * off];
1140
+ });
1141
+ const segments = [];
1142
+ for (let i = 0; i + 1 < out.length; i++) segments.push([out[i], out[i + 1]]);
1143
+ if (c.closed && out.length > 1) segments.push([out[out.length - 1], out[0]]);
1144
+ return { segments, closed: c.closed };
1145
+ });
1146
+ return contoursToPath(contours);
1147
+ }
1148
+ function distortVectorPath(path, mode, dabs) {
1149
+ const b0 = bezierPathBounds(path);
1150
+ const live = dabs.filter((d) => d.radius > 0 && d.strength > 0);
1151
+ if (!b0 || !live.length) return path;
1152
+ if (mode === "roughen") {
1153
+ const reaching2 = live.filter((d) => reaches(b0, d));
1154
+ return reaching2.length ? roughen(path, reaching2) : path;
1155
+ }
1156
+ let grow = 0;
1157
+ const reaching = [];
1158
+ for (const d of live) {
1159
+ const b = { minX: b0.minX - grow, minY: b0.minY - grow, maxX: b0.maxX + grow, maxY: b0.maxY + grow };
1160
+ if (reaches(b, d)) {
1161
+ reaching.push(d);
1162
+ grow += Math.hypot(d.dx ?? 0, d.dy ?? 0) + d.radius * 0.5;
1163
+ }
1164
+ }
1165
+ if (!reaching.length) return path;
1166
+ const maps = reaching.map((d) => vectorDistortDabMap(mode, d));
1167
+ return mapBezierPath(path, (p) => maps.reduce((q, m) => m(q), p), 0.05);
1168
+ }
1169
+
1170
+ // src/vector/gradientTool.ts
1171
+ var VECTOR_GRADIENT_STOP_GAP = 12;
1172
+ var VECTOR_GRADIENT_MIDPOINT_GAP = 9;
1173
+ var VECTOR_GRADIENT_OFF_BAR = 24;
1174
+ var STOP_HIT = 6;
1175
+ var MIDPOINT_HIT = 5;
1176
+ var HANDLE_HIT = 6;
1177
+ var BAR_HIT = 5;
1178
+ var fmt = (n2) => Math.round(n2 * 100) / 100;
1179
+ var apply = (m, p) => ({ x: m[0] * p.x + m[2] * p.y + m[4], y: m[1] * p.x + m[3] * p.y + m[5] });
1180
+ function vectorGradientTargetPath(doc, selection, target, layer = null) {
1181
+ for (const id of selection) {
1182
+ const node = findVectorNode(doc.nodes, id);
1183
+ if (!node) continue;
1184
+ const path = collectPathNodes(node).find((p) => isVectorGradient(vectorTargetPaint(p, target, layer)));
1185
+ if (path) return path;
1186
+ }
1187
+ return null;
1188
+ }
1189
+ function getVectorGradientAnnotator(doc, selection, viewport, target, layer = null) {
1190
+ const path = vectorGradientTargetPath(doc, selection, target, layer);
1191
+ if (!path) return null;
1192
+ const gradient = vectorTargetPaint(path, target, layer);
1193
+ const world = vectorWorldMatrix(doc.nodes, path.id) ?? IDENTITY_MATRIX;
1194
+ const screen = multiplyMatrices(world, [viewport.zoom, 0, 0, viewport.zoom, viewport.x, viewport.y]);
1195
+ const bounds = vectorPathLocalBounds(path);
1196
+ const h = vectorGradientHandles(gradient, bounds, screen);
1197
+ const v = { x: h.end.x - h.start.x, y: h.end.y - h.start.y };
1198
+ const len2 = Math.hypot(v.x, v.y);
1199
+ const normal = len2 > 1e-9 ? { x: -v.y / len2, y: v.x / len2 } : { x: 0, y: 1 };
1200
+ const at = (t, gap) => ({ x: h.start.x + v.x * t + normal.x * gap, y: h.start.y + v.y * t + normal.y * gap });
1201
+ const sorted = sortedVectorGradientStops(gradient);
1202
+ const stops = sorted.map((s, index) => ({ ...s, index, ...at(s.offset, VECTOR_GRADIENT_STOP_GAP) }));
1203
+ const midpoints = sorted.slice(0, -1).flatMap((s, index) => {
1204
+ const pos = vectorGradientMidpointPosition(sorted, index);
1205
+ return pos === null || sorted[index + 1].offset - s.offset < 1e-6 ? [] : [{ index, midpoint: s.midpoint ?? 0.5, ...at(pos, -VECTOR_GRADIENT_MIDPOINT_GAP) }];
1206
+ });
1207
+ let extent = null;
1208
+ if (isCentredVectorGradient(gradient.type)) {
1209
+ const frame = multiplyMatrices(vectorGradientMatrix(gradient, bounds), screen);
1210
+ const pts = gradient.type === "diamond" ? [{ x: 1, y: 0 }, { x: 0, y: 1 }, { x: -1, y: 0 }, { x: 0, y: -1 }] : Array.from({ length: 48 }, (_, k) => ({ x: Math.cos(k * Math.PI / 24), y: Math.sin(k * Math.PI / 24) }));
1211
+ extent = `M${pts.map((p) => apply(frame, p)).map((p) => `${fmt(p.x)} ${fmt(p.y)}`).join("L")}Z`;
1212
+ }
1213
+ return { nodeId: path.id, target, gradient, bounds, world, start: h.start, end: h.end, aspect: h.aspect, focal: h.focal, normal, stops, midpoints, extent };
1214
+ }
1215
+ function vectorGradientBarPosition(a, p) {
1216
+ const v = { x: a.end.x - a.start.x, y: a.end.y - a.start.y };
1217
+ const l2 = v.x * v.x + v.y * v.y;
1218
+ const d = { x: p.x - a.start.x, y: p.y - a.start.y };
1219
+ if (l2 < 1e-12) return { offset: 0, across: Math.hypot(d.x, d.y) };
1220
+ return { offset: (d.x * v.x + d.y * v.y) / l2, across: d.x * a.normal.x + d.y * a.normal.y };
1221
+ }
1222
+ function hitVectorGradientAnnotator(a, p) {
1223
+ const near = (q, r) => !!q && Math.hypot(q.x - p.x, q.y - p.y) <= r;
1224
+ for (let i = a.stops.length - 1; i >= 0; i--) if (near(a.stops[i], STOP_HIT)) return { kind: "stop", index: a.stops[i].index };
1225
+ for (const m of a.midpoints) if (near(m, MIDPOINT_HIT)) return { kind: "midpoint", index: m.index };
1226
+ const centred = isCentredVectorGradient(a.gradient.type);
1227
+ if (near(a.end, HANDLE_HIT)) return { kind: "handle", handle: centred ? "radius" : "end" };
1228
+ if (a.focal && near(a.focal, HANDLE_HIT) && !near(a.start, 1)) return { kind: "handle", handle: "focal" };
1229
+ if (near(a.start, HANDLE_HIT)) return { kind: "handle", handle: "start" };
1230
+ if (near(a.aspect, HANDLE_HIT)) return { kind: "handle", handle: "aspect" };
1231
+ const { offset, across } = vectorGradientBarPosition(a, p);
1232
+ if (offset >= 0 && offset <= 1 && across >= -BAR_HIT && across <= VECTOR_GRADIENT_STOP_GAP + STOP_HIT) return { kind: "bar", offset };
1233
+ return null;
1234
+ }
1235
+
1236
+ // src/vector/blendGroup.ts
1237
+ var DEFAULT_VECTOR_BLEND_STEPS = 5;
1238
+ var DEFAULT_VECTOR_BLEND_DISTANCE = 20;
1239
+ var VECTOR_BLEND_MAX_STEPS = 1e3;
1240
+ var VECTOR_BLEND_MAX_SMOOTH_STEPS = 256;
1241
+ function isVectorBlendGroup(node) {
1242
+ return !!node && node.type === "group" && !!node.blend;
1243
+ }
1244
+ function vectorBlendKeys(group) {
1245
+ return group.children.filter((c) => !c.blendStep);
1246
+ }
1247
+ function resolveVectorBlendOptions(options) {
1248
+ return {
1249
+ spacing: options?.spacing ?? "steps",
1250
+ steps: options?.steps ?? DEFAULT_VECTOR_BLEND_STEPS,
1251
+ distance: options?.distance ?? DEFAULT_VECTOR_BLEND_DISTANCE,
1252
+ orientToSpine: options?.orientToSpine ?? false,
1253
+ linear: options?.linear ?? false
1254
+ };
1255
+ }
1256
+ function paintColors(p) {
1257
+ if (typeof p === "string") return [p];
1258
+ if (p && typeof p === "object") {
1259
+ if (p.type === "mesh") return p.colors;
1260
+ if (p.type === "freeform") return p.points.map((q) => q.color);
1261
+ return p.stops.map((s) => s.color);
1262
+ }
1263
+ return [];
1264
+ }
1265
+ function vectorBlendColorDistance(a, b) {
1266
+ const pa = collectPathNodes(a)[0];
1267
+ const pb = collectPathNodes(b)[0];
1268
+ if (!pa || !pb) return 0;
1269
+ let d = 0;
1270
+ for (const key of ["fill", "stroke"]) {
1271
+ const ca = paintColors(pa[key]);
1272
+ const cb = paintColors(pb[key]);
1273
+ ca.forEach((c, i) => {
1274
+ const other = cb[i] ?? cb[0];
1275
+ if (other === void 0) return;
1276
+ const x = hexToRgba(c);
1277
+ const y = hexToRgba(other);
1278
+ if (!x || !y) return;
1279
+ d = 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));
1280
+ });
1281
+ }
1282
+ return d;
1283
+ }
1284
+ var centreOf = (n2) => {
1285
+ const b = vectorNodeBounds(n2);
1286
+ return b ? [b.x + b.width / 2, b.y + b.height / 2] : [0, 0];
1287
+ };
1288
+ function vectorBlendStepCount(a, b, options, length) {
1289
+ const o = resolveVectorBlendOptions(options);
1290
+ if (o.spacing === "distance") {
1291
+ const d = Math.max(0.01, o.distance);
1292
+ return Math.min(VECTOR_BLEND_MAX_STEPS, Math.max(0, Math.round(length / d) - 1));
1293
+ }
1294
+ if (o.spacing === "smoothColor") {
1295
+ const cd = vectorBlendColorDistance(a, b);
1296
+ if (cd > 1 / 255) return Math.min(VECTOR_BLEND_MAX_SMOOTH_STEPS, Math.max(1, Math.ceil(cd * 255 / 2)));
1297
+ const ba = vectorNodeBounds(a);
1298
+ const bb = vectorNodeBounds(b);
1299
+ const dist4 = ba && bb ? 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)) : length;
1300
+ return Math.min(VECTOR_BLEND_MAX_SMOOTH_STEPS, Math.max(1, Math.ceil(dist4 / 2)));
1301
+ }
1302
+ return Math.min(VECTOR_BLEND_MAX_STEPS, Math.max(1, Math.round(Number.isFinite(o.steps) ? o.steps : DEFAULT_VECTOR_BLEND_STEPS)));
1303
+ }
1304
+ function vectorBlendRail(sp) {
1305
+ const pts = [];
1306
+ const angles = [];
1307
+ const push = (p, tangent) => {
1308
+ const last = pts[pts.length - 1];
1309
+ if (last && Math.hypot(p[0] - last[0], p[1] - last[1]) < 1e-9) return;
1310
+ pts.push(p);
1311
+ angles.push(Math.atan2(tangent[1], tangent[0]));
1312
+ };
1313
+ const count = subpathSegmentCount(sp);
1314
+ for (let i = 0; i < count; i++) {
1315
+ const seg = subpathSegment(sp, i);
1316
+ const samples = seg.length === 2 ? 1 : 32;
1317
+ if (i === 0) push(seg[0], segmentTangent(seg, 0));
1318
+ for (let k = 1; k <= samples; k++) push(segmentPoint(seg, k / samples), segmentTangent(seg, k / samples));
1319
+ }
1320
+ if (pts.length < 2) return null;
1321
+ const cum = [0];
1322
+ for (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]));
1323
+ return { pts, cum, angles, total: cum[cum.length - 1], closed: sp.closed };
1324
+ }
1325
+ function vectorBlendRailAt(rail, s) {
1326
+ let len2 = Number.isFinite(s) ? s : 0;
1327
+ if (rail.closed && rail.total > 0) len2 = (len2 % rail.total + rail.total) % rail.total;
1328
+ len2 = Math.min(rail.total, Math.max(0, len2));
1329
+ let lo = 0;
1330
+ let hi = rail.cum.length - 1;
1331
+ while (hi - lo > 1) {
1332
+ const mid = lo + hi >> 1;
1333
+ if (rail.cum[mid] <= len2) lo = mid;
1334
+ else hi = mid;
1335
+ }
1336
+ const span = rail.cum[hi] - rail.cum[lo];
1337
+ const u = span > 1e-12 ? (len2 - rail.cum[lo]) / span : 0;
1338
+ const a = rail.pts[lo];
1339
+ const b = rail.pts[hi];
1340
+ return { 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]) };
1341
+ }
1342
+ function vectorBlendSpine(group) {
1343
+ const keys = vectorBlendKeys(group);
1344
+ if (keys.length < 2) return null;
1345
+ const stored = group.blend?.spine;
1346
+ if (stored && stored.anchors.length >= 2) {
1347
+ const rail2 = vectorBlendRail(stored);
1348
+ if (rail2) {
1349
+ const end = rail2.closed ? rail2.total * (1 - 1 / keys.length) : rail2.total;
1350
+ return { spine: stored, rail: rail2, stops: keys.map((_, i) => end * i / (keys.length - 1)), replaced: true };
1351
+ }
1352
+ }
1353
+ const centres = keys.map(centreOf);
1354
+ const spine = { closed: false, anchors: centres.map((c) => ({ x: c[0], y: c[1] })) };
1355
+ const stops = [0];
1356
+ for (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]));
1357
+ const rail = vectorBlendRail(spine) ?? { pts: centres, cum: stops, angles: centres.map(() => 0), total: stops[stops.length - 1], closed: false };
1358
+ return { spine, rail, stops, replaced: false };
1359
+ }
1360
+ function vectorBlendKeyTargets(group) {
1361
+ const s = vectorBlendSpine(group);
1362
+ return s ? s.stops.map((stop) => vectorBlendRailAt(s.rail, stop).point) : [];
1363
+ }
1364
+ var morphCache = /* @__PURE__ */ new WeakMap();
1365
+ function morphFor(a, b, blend) {
1366
+ const starts = [blend.starts?.[a.id] ?? null, blend.starts?.[b.id] ?? null];
1367
+ const key = JSON.stringify([!!blend.linear, starts]);
1368
+ let inner = morphCache.get(a);
1369
+ if (!inner) morphCache.set(a, inner = /* @__PURE__ */ new WeakMap());
1370
+ let byKey = inner.get(b);
1371
+ if (!byKey) inner.set(b, byKey = /* @__PURE__ */ new Map());
1372
+ let morph = byKey.get(key);
1373
+ if (!morph) byKey.set(key, morph = createVectorMorph(a, b, { linear: blend.linear, starts }));
1374
+ return morph;
1375
+ }
1376
+ var wrapAngle = (a) => Math.atan2(Math.sin(a), Math.cos(a));
1377
+ function asStep(node, id) {
1378
+ const rename = (n2, nid) => n2.type === "group" ? { ...n2, id: nid, children: n2.children.map((c, i) => rename(c, `${nid}.${i}`)) } : { ...n2, id: nid };
1379
+ return { ...rename(node, id), blendStep: true, locked: true };
1380
+ }
1381
+ function generateVectorBlendChildren(group) {
1382
+ const keys = vectorBlendKeys(group);
1383
+ const blend = group.blend;
1384
+ if (!blend || keys.length < 2) return keys;
1385
+ const spine = vectorBlendSpine(group);
1386
+ if (!spine) return keys;
1387
+ const out = [];
1388
+ let counter = 0;
763
1389
  keys.forEach((a, i) => {
764
1390
  out.push(a);
765
1391
  const b = keys[i + 1];
@@ -2279,6 +2905,12 @@ function lineStyle(node) {
2279
2905
  function renderLayers(node, defs, idPrefix) {
2280
2906
  const out = [];
2281
2907
  for (const l of vectorPaintLayerNodes(node)) {
2908
+ if (l.kind === "stroke" && isVectorBrushstrokeLayer(l.layer) && l.layer.visible !== false && (l.layer.opacity ?? 1) > 0) {
2909
+ const images = vectorBrushstrokeImages(node, l.layer);
2910
+ const blend2 = vectorBlendModeCss(l.layer.blendMode);
2911
+ if (images.length) out.push({ kind: "brushstroke", index: l.index, fill: "none", stroke: "none", strokeWidth: 0, opacity: l.layer.opacity ?? 1, ...blend2 ? { blendMode: blend2 } : {}, images });
2912
+ continue;
2913
+ }
2282
2914
  if (!l.drawn) continue;
2283
2915
  const role = l.kind;
2284
2916
  const paint = vectorPaintToSvg(l.layer.paint, `${idPrefix}${role}-${node.id}-${l.index}`, { node: l.node, ...role === "stroke" ? { role } : {} });
@@ -2777,6 +3409,11 @@ function getVectorOverlayRenderData(state) {
2777
3409
  const d = pts.map((q, i) => `${i ? "L" : "M"}${fmt2(q.x)} ${fmt2(q.y)}`).join("") + (pts.length === 1 ? `L${fmt2(pts[0].x)} ${fmt2(pts[0].y)}` : "");
2778
3410
  toolStroke = { tool: it.tool, d, width: fmt2(it.width * z) };
2779
3411
  }
3412
+ let brush = null;
3413
+ if (it?.type === "brush") {
3414
+ const c = toScreen(state, it);
3415
+ brush = { x: fmt2(c.x), y: fmt2(c.y), r: fmt2(it.radius * z) };
3416
+ }
2780
3417
  let pencilString = null;
2781
3418
  if (it?.type === "draw" && it.string) {
2782
3419
  const a = toScreen(state, it.string[0]);
@@ -2858,6 +3495,7 @@ function getVectorOverlayRenderData(state) {
2858
3495
  guides,
2859
3496
  status,
2860
3497
  toolStroke,
3498
+ brush,
2861
3499
  pencilString,
2862
3500
  cutPoint,
2863
3501
  cornerWidgets,
@@ -3183,8 +3821,8 @@ function vectorPaintToHex(paint) {
3183
3821
  if (typeof paint !== "string") return null;
3184
3822
  const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(paint.trim());
3185
3823
  if (!m) return null;
3186
- const hex2 = m[1].length === 3 ? m[1].replace(/./g, (c) => c + c) : m[1];
3187
- return `#${hex2.toLowerCase()}`;
3824
+ const hex3 = m[1].length === 3 ? m[1].replace(/./g, (c) => c + c) : m[1];
3825
+ return `#${hex3.toLowerCase()}`;
3188
3826
  }
3189
3827
  var VECTOR_GRADIENT_TYPE_LABELS = { linear: "Linear", radial: "Radial", conic: "Conic", reflected: "Reflected", diamond: "Diamond" };
3190
3828
  var VECTOR_GRADIENT_SPREAD_LABELS = { pad: "Pad", reflect: "Reflect", repeat: "Repeat" };
@@ -3297,7 +3935,11 @@ function getVectorAppearancePanel(state) {
3297
3935
  blendMode: l.blendMode ?? "normal",
3298
3936
  visible: l.visible !== false,
3299
3937
  ...kind === "stroke" ? { strokeWidth: l.strokeWidth ?? 1, strokeAlign: l.strokeAlign ?? "center" } : {},
3300
- ...(l.kind ?? "paint") !== "paint" ? { unknownKind: String(l.kind) } : {},
3938
+ ...isVectorBrushstrokeLayer(l) ? (() => {
3939
+ const b = l.brush;
3940
+ const preset = vectorBrushstrokeBrush(b.brushId);
3941
+ return { brushstroke: { brushId: b.brushId, name: preset.name, size: b.size ?? preset.tip.size } };
3942
+ })() : (l.kind ?? "paint") !== "paint" ? { unknownKind: String(l.kind) } : {},
3301
3943
  editing: edit.target === kind && (edit.layer === null ? index === count - 1 : edit.layer === index)
3302
3944
  });
3303
3945
  const fills = first ? vectorFills(first) : [];
@@ -3450,6 +4092,7 @@ var Page = class {
3450
4092
  content = [];
3451
4093
  states = /* @__PURE__ */ new Map();
3452
4094
  shadings = /* @__PURE__ */ new Map();
4095
+ images = /* @__PURE__ */ new Map();
3453
4096
  /** A graphics state with this fill / stroke alpha and blend mode, by name. */
3454
4097
  gs(alpha, strokeAlpha, blend) {
3455
4098
  const bm = blend ? BLEND[blend] : void 0;
@@ -3463,6 +4106,36 @@ var Page = class {
3463
4106
  }
3464
4107
  return name;
3465
4108
  }
4109
+ /** An image (a PNG data URL) as an XObject with its alpha as a soft mask, by name; null when unreadable. */
4110
+ image(href) {
4111
+ const hit = this.images.get(href);
4112
+ if (hit) return hit;
4113
+ const parsed = parseDataUrl(href);
4114
+ if (!parsed || !isPng(parsed.bytes)) return null;
4115
+ let img;
4116
+ try {
4117
+ img = decodePng(parsed.bytes);
4118
+ } catch {
4119
+ return null;
4120
+ }
4121
+ const px = img.width * img.height;
4122
+ const rgb = new Uint8Array(px * 3);
4123
+ const alpha = new Uint8Array(px);
4124
+ for (let i = 0; i < px; i++) {
4125
+ rgb[i * 3] = img.data[i * 4];
4126
+ rgb[i * 3 + 1] = img.data[i * 4 + 1];
4127
+ rgb[i * 3 + 2] = img.data[i * 4 + 2];
4128
+ alpha[i] = img.data[i * 4 + 3];
4129
+ }
4130
+ const w = this.writer;
4131
+ const dims = `/Type /XObject /Subtype /Image /Width ${img.width} /Height ${img.height} /BitsPerComponent 8`;
4132
+ const mask = w.add(w.stream(`${dims} /ColorSpace /DeviceGray`, alpha, true));
4133
+ const id = w.add(w.stream(`${dims} /ColorSpace /DeviceRGB /SMask ${mask} 0 R`, rgb, true));
4134
+ const name = `Im${this.images.size + 1}`;
4135
+ this.images.set(href, name);
4136
+ w.imageIds.set(`${w.pageIndex}:${name}`, id);
4137
+ return name;
4138
+ }
3466
4139
  /** Sets the fill colour (or prepares a paint server) for `paint`: returns its alpha, or a def to fill with by clipping. */
3467
4140
  paint(paint, stroking) {
3468
4141
  if (!paint || paint === "none") return null;
@@ -3608,6 +4281,14 @@ var Page = class {
3608
4281
  for (const l of layers) {
3609
4282
  const a = alpha * l.opacity;
3610
4283
  const blend = l.blendMode ?? p.blendMode;
4284
+ if (l.kind === "brushstroke") {
4285
+ for (const im of l.images ?? []) {
4286
+ const name = this.image(im.href);
4287
+ if (!name) continue;
4288
+ this.content.push("q", `/${this.gs(a, a, blend)} gs`, `${n(im.width)} 0 0 ${n(-im.height)} ${n(im.x)} ${n(im.y + im.height)} cm`, `/${name} Do`, "Q");
4289
+ }
4290
+ continue;
4291
+ }
3611
4292
  if (l.kind === "fill") {
3612
4293
  if (l.fill !== "none") this.fill(p.d, l.fill, a, evenodd, blend);
3613
4294
  } else if (l.stroke !== "none" && l.strokeWidth > 0) {
@@ -3625,6 +4306,7 @@ var PdfWriter = class {
3625
4306
  objects = [];
3626
4307
  gsDicts = /* @__PURE__ */ new Map();
3627
4308
  shadingDicts = /* @__PURE__ */ new Map();
4309
+ imageIds = /* @__PURE__ */ new Map();
3628
4310
  pageIndex = 0;
3629
4311
  add(body) {
3630
4312
  this.objects.push(body);
@@ -3710,7 +4392,8 @@ function vectorDocumentToPdf(doc, options = {}) {
3710
4392
  const content = w.add(w.stream("", new TextEncoder().encode(page.content.join("\n")), compress));
3711
4393
  const gsEntries = [...page.states.values()].map((name) => `/${name} ${w.add(w.gsDicts.get(`${k}:${name}`))} 0 R`);
3712
4394
  const shEntries = [...page.shadings.values()].map((name) => `/${name} ${w.add(w.shadingDicts.get(`${k}:${name}`))} 0 R`);
3713
- const resources = `<< ${gsEntries.length ? `/ExtGState << ${gsEntries.join(" ")} >> ` : ""}${shEntries.length ? `/Shading << ${shEntries.join(" ")} >>` : ""} >>`;
4395
+ const imEntries = [...page.images.values()].map((name) => `/${name} ${w.imageIds.get(`${k}:${name}`)} 0 R`);
4396
+ const resources = `<< ${gsEntries.length ? `/ExtGState << ${gsEntries.join(" ")} >> ` : ""}${shEntries.length ? `/Shading << ${shEntries.join(" ")} >> ` : ""}${imEntries.length ? `/XObject << ${imEntries.join(" ")} >>` : ""} >>`;
3714
4397
  pageIds.push(w.add(`<< /Type /Page /Parent ${pagesId} 0 R /MediaBox [0 0 ${n(W)} ${n(H)}] /Resources ${resources} /Contents ${content} 0 R >>`));
3715
4398
  });
3716
4399
  w.set(catalog, `<< /Type /Catalog /Pages ${pagesId} 0 R >>`);
@@ -3895,6 +4578,7 @@ function pathItems(node, notes) {
3895
4578
  function nodeGroup(node, notes) {
3896
4579
  if (node.visible === false) return null;
3897
4580
  if (node.filters?.length) notes.push(`${label(node)}: filters (shadows, glows, blurs) are not part of Lottie (left out).`);
4581
+ if (node.type === "path" && node.strokes?.some((l) => l.kind === "brushstroke" && l.visible !== false)) notes.push(`${label(node)}: brushstrokes are raster images, not part of Lottie (left out).`);
3898
4582
  const m = nodeMatrix(node);
3899
4583
  let items = [];
3900
4584
  const live = (node.effects ?? []).some((e) => e.enabled !== false && (e.type === "wiggle" || e.type === "envelope" || e.type === "contour" || e.type === "blockShadow" || e.type === "extrude" || e.type === "bevel"));
@@ -4030,8 +4714,8 @@ function subpathOf(prop, frame) {
4030
4714
  }
4031
4715
  function cssColor(c) {
4032
4716
  const k = c.slice(0, 3).some((v) => v > 1) ? 255 : 1;
4033
- const hex2 = (v) => Math.round(Math.min(1, Math.max(0, v / k)) * 255).toString(16).padStart(2, "0");
4034
- return `#${hex2(c[0] ?? 0)}${hex2(c[1] ?? 0)}${hex2(c[2] ?? 0)}`;
4717
+ const hex3 = (v) => Math.round(Math.min(1, Math.max(0, v / k)) * 255).toString(16).padStart(2, "0");
4718
+ return `#${hex3(c[0] ?? 0)}${hex3(c[1] ?? 0)}${hex3(c[2] ?? 0)}`;
4035
4719
  }
4036
4720
  function geometryOf(it, frame, notes) {
4037
4721
  switch (it.ty) {
@@ -4271,338 +4955,51 @@ function parseVectorLottie(input, options = {}) {
4271
4955
  if (!parent || seen.has(parent)) return own;
4272
4956
  seen.add(l);
4273
4957
  return multiplyMatrices(own, worldOf2(parent, seen));
4274
- };
4275
- const out = [];
4276
- for (const l of layers) {
4277
- const ty = Number(l.ty);
4278
- if (l.hd === true || frame < Number(l.ip ?? -Infinity) || frame >= Number(l.op ?? Infinity)) continue;
4279
- if (l.td === 1 || l.tt) notes.push(`Layer "${l.nm ?? ""}": track mattes are not imported.`);
4280
- if (l.ef) notes.push(`Layer "${l.nm ?? ""}": layer effects are not imported.`);
4281
- if (l.ks && JSON.stringify(l.ks).includes('"x":"')) notes.push("Expressions are not evaluated (their stored values are used).");
4282
- let children = [];
4283
- if (ty === 4) children = itemsToNodes(Array.isArray(l.shapes) ? l.shapes : [], { paints: [], modifiers: [] }, s);
4284
- else if (ty === 1) {
4285
- const w = Number(l.sw ?? 0);
4286
- const h = Number(l.sh ?? 0);
4287
- const c = typeof l.sc === "string" ? l.sc : "#000000";
4288
- children = [{ id: s.next(), type: "path", name: "Solid", subpaths: [rectSubpath({ x: 0, y: 0, width: w, height: h })], fill: c, stroke: null }];
4289
- } else if (ty === 0 && typeof l.refId === "string" && depth < 16) {
4290
- const asset = assets.get(l.refId);
4291
- const local = frame - Number(l.st ?? 0);
4292
- if (asset && Array.isArray(asset.layers)) children = layersToNodes(asset.layers, local, depth + 1);
4293
- } else if (ty !== 3) {
4294
- notes.push(`Layer "${l.nm ?? ""}" (type ${ty}) is not supported (skipped).`);
4295
- continue;
4296
- }
4297
- if (!children.length) continue;
4298
- const m = worldOf2(l);
4299
- const opacity = transformOf(l.ks, frame, notes).opacity;
4300
- let group = { id: s.next(), type: "group", ...typeof l.nm === "string" ? { name: l.nm } : {}, children };
4301
- group = transformVectorNode(group, m);
4302
- if (opacity < 1) group = { ...group, opacity };
4303
- const masks = (Array.isArray(l.masksProperties) ? l.masksProperties : []).filter((mk) => (mk.mode ?? "a") === "a" && !mk.inv);
4304
- if (Array.isArray(l.masksProperties) && masks.length !== l.masksProperties.length) notes.push(`Layer "${l.nm ?? ""}": only additive masks are imported.`);
4305
- if (masks.length) {
4306
- const subpaths = masks.map((mk) => subpathOf(mk.pt, frame)).filter((x) => !!x);
4307
- const clip = transformVectorNode({ id: s.next(), type: "path", name: "Mask", subpaths, fill: "#000000", stroke: null }, m);
4308
- const { transform: _t, opacity: _o, ...inner } = group;
4309
- group = { id: s.next(), type: "group", name: `${group.name ?? "Layer"} mask`, mask: { type: "clip" }, children: [clip, { ...inner, id: s.next() }], ...group.opacity !== void 0 ? { opacity: group.opacity } : {} };
4310
- }
4311
- out.push(group);
4312
- }
4313
- return out.reverse();
4314
- };
4315
- const nodes = layersToNodes(json.layers, s.frame, 0);
4316
- const width = Number(json.w ?? 512);
4317
- const height = Number(json.h ?? 512);
4318
- return { document: { width, height, nodes }, notes: [...new Set(notes)] };
4319
- }
4320
-
4321
- // src/vector/trace.ts
4322
- var VECTOR_TRACE_MAX_PIXELS = 4e6;
4323
- function signedArea(p) {
4324
- let a = 0;
4325
- for (let i = 0; i < p.length; i++) {
4326
- const [x0, y0] = p[i];
4327
- const [x1, y1] = p[(i + 1) % p.length];
4328
- a += x0 * y1 - x1 * y0;
4329
- }
4330
- return a / 2;
4331
- }
4332
- function pointInPoly(p, poly) {
4333
- let inside = false;
4334
- for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
4335
- const [xi, yi] = poly[i];
4336
- const [xj, yj] = poly[j];
4337
- if (yi > p[1] !== yj > p[1] && p[0] < (xj - xi) * (p[1] - yi) / (yj - yi) + xi) inside = !inside;
4338
- }
4339
- return inside;
4340
- }
4341
- function vectorTraceContours(field, w, h, threshold) {
4342
- const gw = w + 2;
4343
- const gh = h + 2;
4344
- const val = (i, j) => i === 0 || j === 0 || i > w || j > h ? -Infinity : field[(j - 1) * w + (i - 1)];
4345
- const inside = (i, j) => val(i, j) >= threshold;
4346
- const pos = (i, j) => [i - 0.5, j - 0.5];
4347
- const cross2 = (a, b) => {
4348
- const va = val(a[0], a[1]);
4349
- const vb = val(b[0], b[1]);
4350
- let t;
4351
- if (Number.isFinite(va) && Number.isFinite(vb) && Math.abs(vb - va) > 1e-12) t = Math.min(1, Math.max(0, (threshold - va) / (vb - va)));
4352
- else if (Number.isFinite(va)) t = va >= threshold ? 0.5 : 0;
4353
- else t = vb >= threshold ? 0.5 : 1;
4354
- const pa = pos(a[0], a[1]);
4355
- const pb = pos(b[0], b[1]);
4356
- return [pa[0] + (pb[0] - pa[0]) * t, pa[1] + (pb[1] - pa[1]) * t];
4357
- };
4358
- const hid = (i, j) => 2 * (j * gw + i);
4359
- const vid = (i, j) => 2 * (j * gw + i) + 1;
4360
- const next = /* @__PURE__ */ new Map();
4361
- const point = /* @__PURE__ */ new Map();
4362
- for (let j = 0; j < gh - 1; j++) {
4363
- for (let i = 0; i < gw - 1; i++) {
4364
- const tl = inside(i, j);
4365
- const tr = inside(i + 1, j);
4366
- const br = inside(i + 1, j + 1);
4367
- const bl = inside(i, j + 1);
4368
- const code = +tl << 3 | +tr << 2 | +br << 1 | +bl;
4369
- if (code === 0 || code === 15) continue;
4370
- const top = hid(i, j);
4371
- const bottom = hid(i, j + 1);
4372
- const left = vid(i, j);
4373
- const right = vid(i + 1, j);
4374
- const seg = (a, b) => next.set(a, b);
4375
- const mean = () => (val(i, j) + val(i + 1, j) + val(i + 1, j + 1) + val(i, j + 1)) / 4;
4376
- switch (code) {
4377
- case 1:
4378
- seg(left, bottom);
4379
- break;
4380
- case 2:
4381
- seg(bottom, right);
4382
- break;
4383
- case 3:
4384
- seg(left, right);
4385
- break;
4386
- case 4:
4387
- seg(right, top);
4388
- break;
4389
- case 5:
4390
- if (mean() >= threshold) {
4391
- seg(left, top);
4392
- seg(right, bottom);
4393
- } else {
4394
- seg(left, bottom);
4395
- seg(right, top);
4396
- }
4397
- break;
4398
- case 6:
4399
- seg(bottom, top);
4400
- break;
4401
- case 7:
4402
- seg(left, top);
4403
- break;
4404
- case 8:
4405
- seg(top, left);
4406
- break;
4407
- case 9:
4408
- seg(top, bottom);
4409
- break;
4410
- case 10:
4411
- if (mean() >= threshold) {
4412
- seg(top, right);
4413
- seg(bottom, left);
4414
- } else {
4415
- seg(top, left);
4416
- seg(bottom, right);
4417
- }
4418
- break;
4419
- case 11:
4420
- seg(top, right);
4421
- break;
4422
- case 12:
4423
- seg(right, left);
4424
- break;
4425
- case 13:
4426
- seg(right, bottom);
4427
- break;
4428
- case 14:
4429
- seg(bottom, left);
4430
- break;
4431
- }
4432
- const edges = [
4433
- [top, [i, j], [i + 1, j]],
4434
- [bottom, [i, j + 1], [i + 1, j + 1]],
4435
- [left, [i, j], [i, j + 1]],
4436
- [right, [i + 1, j], [i + 1, j + 1]]
4437
- ];
4438
- for (const [e, a, b] of edges) if (inside(a[0], a[1]) !== inside(b[0], b[1]) && !point.has(e)) point.set(e, cross2(a, b));
4439
- }
4440
- }
4441
- const out = [];
4442
- for (const start of [...next.keys()].sort((a, b) => a - b)) {
4443
- if (!next.has(start)) continue;
4444
- const poly = [];
4445
- let e = start;
4446
- for (; ; ) {
4447
- const n2 = next.get(e);
4448
- if (n2 === void 0) break;
4449
- next.delete(e);
4450
- const p = point.get(e);
4451
- if (p) poly.push(p);
4452
- e = n2;
4453
- if (e === start) break;
4454
- }
4455
- if (poly.length >= 3) out.push(poly);
4456
- }
4457
- return out;
4458
- }
4459
- function loopsOf(field, w, h, threshold, tolerance, minArea) {
4460
- const loops = [];
4461
- for (const raw of vectorTraceContours(field, w, h, threshold)) {
4462
- const area = signedArea(raw);
4463
- if (Math.abs(area) < Math.max(0, minArea)) continue;
4464
- const simple = simplifyRing(raw, Math.max(0.01, tolerance));
4465
- if (simple.length < 3) continue;
4466
- loops.push({ raw, simple, area, depth: 0 });
4467
- }
4468
- for (const l of loops) l.depth = loops.filter((o) => o !== l && Math.abs(o.area) > Math.abs(l.area) && pointInPoly(l.simple[0], o.simple)).length;
4469
- return loops.sort((a, b) => a.depth - b.depth || Math.abs(b.area) - Math.abs(a.area));
4470
- }
4471
- function loopSubpath(l, tolerance, roundness) {
4472
- if (roundness <= 0) return { closed: true, anchors: l.simple.map(([x, y]) => ({ x, y })) };
4473
- const cornerAngleDeg = 25 + roundness * 125;
4474
- const cubics = fitCubics(l.raw, Math.max(0.05, tolerance), {
4475
- closed: true,
4476
- cornerAngleDeg
4477
- });
4478
- if (!cubics.length) return { closed: true, anchors: l.simple.map(([x, y]) => ({ x, y })) };
4479
- const anchors = cubics.map((c, k) => {
4480
- const prev = cubics[(k - 1 + cubics.length) % cubics.length];
4481
- return {
4482
- x: c.p0[0],
4483
- y: c.p0[1],
4484
- in: { x: prev.p2[0], y: prev.p2[1] },
4485
- out: { x: c.p1[0], y: c.p1[1] }
4486
- };
4487
- });
4488
- return { closed: true, anchors };
4489
- }
4490
- function traceField(field, w, h, threshold, o) {
4491
- const tolerance = o.tolerance ?? 1;
4492
- const roundness = Math.min(1, Math.max(0, o.cornerRoundness ?? 0.5));
4493
- return loopsOf(field, w, h, threshold, tolerance, o.minArea ?? 10).map((l) => loopSubpath(l, tolerance, roundness));
4494
- }
4495
- function quantizeVectorTraceColors(pixels, count) {
4496
- const n2 = pixels.width * pixels.height;
4497
- const d = pixels.data;
4498
- const opaque = [];
4499
- for (let i = 0; i < n2; i++) if (d[i * 4 + 3] >= 128) opaque.push(i);
4500
- const boxes = opaque.length ? [opaque] : [];
4501
- const range = (box, c) => {
4502
- let lo = 255;
4503
- let hi = 0;
4504
- for (const i of box) {
4505
- const v = d[i * 4 + c];
4506
- if (v < lo) lo = v;
4507
- if (v > hi) hi = v;
4508
- }
4509
- return hi - lo;
4510
- };
4511
- while (boxes.length < count) {
4512
- let best = -1;
4513
- let bestRange = 0;
4514
- let bestC = 0;
4515
- boxes.forEach((b, k) => {
4516
- if (b.length < 2) return;
4517
- for (let c = 0; c < 3; c++) {
4518
- const r = range(b, c);
4519
- if (r > bestRange) {
4520
- bestRange = r;
4521
- best = k;
4522
- bestC = c;
4523
- }
4524
- }
4525
- });
4526
- if (best < 0) break;
4527
- const box = boxes[best].slice().sort((a, b) => d[a * 4 + bestC] - d[b * 4 + bestC]);
4528
- const key = (k) => d[box[k] * 4 + bestC];
4529
- const v = key(box.length >> 1);
4530
- let mid = box.findIndex((_, k) => key(k) >= v);
4531
- if (mid <= 0) mid = box.findIndex((_, k) => key(k) > v);
4532
- boxes.splice(best, 1, box.slice(0, mid), box.slice(mid));
4533
- }
4534
- const palette = boxes.map((b) => {
4535
- const s = [0, 0, 0];
4536
- for (const i of b) for (let c = 0; c < 3; c++) s[c] += d[i * 4 + c];
4537
- return [Math.round(s[0] / b.length), Math.round(s[1] / b.length), Math.round(s[2] / b.length)];
4538
- });
4539
- const index = new Int16Array(n2).fill(-1);
4540
- const counts = palette.map(() => 0);
4541
- for (const i of opaque) {
4542
- let best = 0;
4543
- let bestD = Infinity;
4544
- palette.forEach((p, k) => {
4545
- const dd = (d[i * 4] - p[0]) ** 2 + (d[i * 4 + 1] - p[1]) ** 2 + (d[i * 4 + 2] - p[2]) ** 2;
4546
- if (dd < bestD) {
4547
- bestD = dd;
4548
- best = k;
4958
+ };
4959
+ const out = [];
4960
+ for (const l of layers) {
4961
+ const ty = Number(l.ty);
4962
+ if (l.hd === true || frame < Number(l.ip ?? -Infinity) || frame >= Number(l.op ?? Infinity)) continue;
4963
+ if (l.td === 1 || l.tt) notes.push(`Layer "${l.nm ?? ""}": track mattes are not imported.`);
4964
+ if (l.ef) notes.push(`Layer "${l.nm ?? ""}": layer effects are not imported.`);
4965
+ if (l.ks && JSON.stringify(l.ks).includes('"x":"')) notes.push("Expressions are not evaluated (their stored values are used).");
4966
+ let children = [];
4967
+ if (ty === 4) children = itemsToNodes(Array.isArray(l.shapes) ? l.shapes : [], { paints: [], modifiers: [] }, s);
4968
+ else if (ty === 1) {
4969
+ const w = Number(l.sw ?? 0);
4970
+ const h = Number(l.sh ?? 0);
4971
+ const c = typeof l.sc === "string" ? l.sc : "#000000";
4972
+ children = [{ id: s.next(), type: "path", name: "Solid", subpaths: [rectSubpath({ x: 0, y: 0, width: w, height: h })], fill: c, stroke: null }];
4973
+ } else if (ty === 0 && typeof l.refId === "string" && depth < 16) {
4974
+ const asset = assets.get(l.refId);
4975
+ const local = frame - Number(l.st ?? 0);
4976
+ if (asset && Array.isArray(asset.layers)) children = layersToNodes(asset.layers, local, depth + 1);
4977
+ } else if (ty !== 3) {
4978
+ notes.push(`Layer "${l.nm ?? ""}" (type ${ty}) is not supported (skipped).`);
4979
+ continue;
4549
4980
  }
4550
- });
4551
- index[i] = best;
4552
- counts[best]++;
4553
- }
4554
- return { palette, index, counts };
4555
- }
4556
- var hex = (c) => `#${c.map((v) => Math.round(v).toString(16).padStart(2, "0")).join("")}`;
4557
- function traceVectorImage(pixels, options = {}) {
4558
- const { width: w, height: h, data } = pixels;
4559
- if (!(w > 0 && h > 0) || data.length < w * h * 4) throw new Error("The image has no pixels (or fewer than width \xD7 height \xD7 4 values).");
4560
- if (w * h > VECTOR_TRACE_MAX_PIXELS) throw new Error(`The image has ${w * h} pixels; scale it down to at most ${VECTOR_TRACE_MAX_PIXELS} to trace it.`);
4561
- const n2 = w * h;
4562
- if ((options.mode ?? "threshold") === "threshold") {
4563
- const field = new Float32Array(n2);
4564
- const byAlpha = options.channel === "alpha";
4565
- for (let i = 0; i < n2; i++) {
4566
- const a = data[i * 4 + 3] / 255;
4567
- field[i] = byAlpha ? a : a * (1 - (0.2126 * data[i * 4] + 0.7152 * data[i * 4 + 1] + 0.0722 * data[i * 4 + 2]) / 255);
4568
- }
4569
- const subpaths = traceField(field, w, h, Math.min(1, Math.max(0, options.threshold ?? 0.5)), options);
4570
- return subpaths.length ? [
4571
- {
4572
- id: "trace-1",
4573
- type: "path",
4574
- name: "Trace",
4575
- fill: options.color ?? "#000000",
4576
- stroke: null,
4577
- fillRule: "evenodd",
4578
- subpaths
4981
+ if (!children.length) continue;
4982
+ const m = worldOf2(l);
4983
+ const opacity = transformOf(l.ks, frame, notes).opacity;
4984
+ let group = { id: s.next(), type: "group", ...typeof l.nm === "string" ? { name: l.nm } : {}, children };
4985
+ group = transformVectorNode(group, m);
4986
+ if (opacity < 1) group = { ...group, opacity };
4987
+ const masks = (Array.isArray(l.masksProperties) ? l.masksProperties : []).filter((mk) => (mk.mode ?? "a") === "a" && !mk.inv);
4988
+ if (Array.isArray(l.masksProperties) && masks.length !== l.masksProperties.length) notes.push(`Layer "${l.nm ?? ""}": only additive masks are imported.`);
4989
+ if (masks.length) {
4990
+ const subpaths = masks.map((mk) => subpathOf(mk.pt, frame)).filter((x) => !!x);
4991
+ const clip = transformVectorNode({ id: s.next(), type: "path", name: "Mask", subpaths, fill: "#000000", stroke: null }, m);
4992
+ const { transform: _t, opacity: _o, ...inner } = group;
4993
+ group = { id: s.next(), type: "group", name: `${group.name ?? "Layer"} mask`, mask: { type: "clip" }, children: [clip, { ...inner, id: s.next() }], ...group.opacity !== void 0 ? { opacity: group.opacity } : {} };
4579
4994
  }
4580
- ] : [];
4581
- }
4582
- const count = Math.min(32, Math.max(2, Math.round(options.colors ?? 6)));
4583
- const q = quantizeVectorTraceColors(pixels, count);
4584
- const order = q.palette.map((_, k) => k).filter((k) => q.counts[k] > 0).sort((a, b) => q.counts[b] - q.counts[a]);
4585
- const rank = new Map(order.map((k, r) => [k, r]));
4586
- const out = [];
4587
- order.forEach((k, r) => {
4588
- const field = new Uint8Array(n2);
4589
- for (let i = 0; i < n2; i++) {
4590
- const ix = q.index[i];
4591
- field[i] = ix >= 0 && rank.get(ix) >= r ? 1 : 0;
4995
+ out.push(group);
4592
4996
  }
4593
- const subpaths = traceField(field, w, h, 0.5, options);
4594
- if (subpaths.length)
4595
- out.push({
4596
- id: `trace-${out.length + 1}`,
4597
- type: "path",
4598
- name: hex(q.palette[k]),
4599
- fill: hex(q.palette[k]),
4600
- stroke: null,
4601
- fillRule: "evenodd",
4602
- subpaths
4603
- });
4604
- });
4605
- return out;
4997
+ return out.reverse();
4998
+ };
4999
+ const nodes = layersToNodes(json.layers, s.frame, 0);
5000
+ const width = Number(json.w ?? 512);
5001
+ const height = Number(json.h ?? 512);
5002
+ return { document: { width, height, nodes }, notes: [...new Set(notes)] };
4606
5003
  }
4607
5004
 
4608
5005
  // src/vector/constraints/auto.ts
@@ -6555,6 +6952,9 @@ var DEFAULT_VECTOR_PATH_EDIT = {
6555
6952
  pencilSmoothing: 0,
6556
6953
  recognizeShapes: false,
6557
6954
  eraserSize: 10,
6955
+ distortMode: "smudge",
6956
+ distortSize: 20,
6957
+ distortStrength: 0.5,
6558
6958
  pencilPressure: "pen",
6559
6959
  fillGap: 0
6560
6960
  };
@@ -6827,8 +7227,10 @@ var TOOL_LABELS = {
6827
7227
  type: "Type",
6828
7228
  typePath: "Type on a Path",
6829
7229
  frame: "Frame",
6830
- connector: "Connector"
7230
+ connector: "Connector",
7231
+ distort: "Distort Brush"
6831
7232
  };
7233
+ var DISTORT_LABELS = { smudge: "Smudge", twirl: "Twirl", attract: "Attract", repel: "Repel", roughen: "Roughen" };
6832
7234
  var TOOL_KEYS = {
6833
7235
  v: "select",
6834
7236
  a: "direct",
@@ -6851,7 +7253,8 @@ var TOOL_KEYS = {
6851
7253
  t: "type",
6852
7254
  q: "typePath",
6853
7255
  f: "frame",
6854
- x: "connector"
7256
+ x: "connector",
7257
+ d: "distort"
6855
7258
  };
6856
7259
  function vectorToolLabel(tool) {
6857
7260
  return TOOL_LABELS[tool];
@@ -6878,7 +7281,8 @@ var VECTOR_TOOL_SHORTCUTS = {
6878
7281
  type: "T",
6879
7282
  typePath: "Q",
6880
7283
  frame: "F",
6881
- connector: "X"
7284
+ connector: "X",
7285
+ distort: "D"
6882
7286
  };
6883
7287
  var VECTOR_TOOL_GLYPHS = {
6884
7288
  select: "\u2196",
@@ -6902,7 +7306,8 @@ var VECTOR_TOOL_GLYPHS = {
6902
7306
  type: "T",
6903
7307
  typePath: "\u{1D4AF}",
6904
7308
  frame: "\u2B1A",
6905
- connector: "\u2933"
7309
+ connector: "\u2933",
7310
+ distort: "\u2248"
6906
7311
  };
6907
7312
  var SHAPE_TOOLS = /* @__PURE__ */ new Set(["rect", "ellipse", "polygon", "star", "line", "frame"]);
6908
7313
  var BOOLEAN_OPS = {
@@ -7433,6 +7838,9 @@ var VectorEditorController = class _VectorEditorController {
7433
7838
  const next = { ...this.editing, ...editing };
7434
7839
  next.pencilSmoothing = Math.min(1, Math.max(0, Number.isFinite(next.pencilSmoothing) ? next.pencilSmoothing : 0));
7435
7840
  if (!(next.eraserSize > 0)) next.eraserSize = this.editing.eraserSize;
7841
+ if (!VECTOR_DISTORT_BRUSH_MODES.includes(next.distortMode)) next.distortMode = this.editing.distortMode;
7842
+ if (!(next.distortSize > 0)) next.distortSize = this.editing.distortSize;
7843
+ next.distortStrength = Math.min(1, Math.max(0, Number.isFinite(next.distortStrength) ? next.distortStrength : this.editing.distortStrength));
7436
7844
  this.editing = next;
7437
7845
  this.notify();
7438
7846
  }
@@ -7675,6 +8083,22 @@ var VectorEditorController = class _VectorEditorController {
7675
8083
  });
7676
8084
  return next.id;
7677
8085
  }
8086
+ /**
8087
+ * Replaces a generated group's art (its children) and provenance, in one undo step; used by
8088
+ * `regenerateVectorArt`. The group keeps its id, name, transform and place.
8089
+ */
8090
+ replaceGeneratedArt(id, children, generation) {
8091
+ const node = this.getNode(id);
8092
+ if (!node || node.type !== "group" || !node.generation) return { ok: false, message: `"${id}" is not generated art.` };
8093
+ if (node.locked) return { ok: false, message: `"${id}" is locked; unlock it first.` };
8094
+ const inv = invertMatrix(this.worldOf(id));
8095
+ const placed = children.map((c) => this.freshIds(isIdentityMatrix(inv) ? c : transformVectorNode(c, inv)));
8096
+ this.change("Regenerate Art", () => {
8097
+ this.replaceNode(id, { ...node, generation, children: placed });
8098
+ this.anchors = [];
8099
+ });
8100
+ return { ok: true };
8101
+ }
7678
8102
  /** Builds a path node for a shape with the current defaults (not added). */
7679
8103
  createShapeNode(input) {
7680
8104
  const b = { x: input.x, y: input.y, width: input.width, height: input.height };
@@ -8364,6 +8788,57 @@ var VectorEditorController = class _VectorEditorController {
8364
8788
  merge
8365
8789
  );
8366
8790
  }
8791
+ /**
8792
+ * Adds a brushstroke to the selected (or given) paths: a stroke layer that paints the path with
8793
+ * brush `brushId` from the paint editor's library (`vectorBrushstrokeBrushes`), `size` document
8794
+ * units across (default the brush's own), an optional `pressure` profile along the length, in
8795
+ * `color` (default the top stroke's paint, else black) at `opacity`. At the top of the stroke
8796
+ * stack or at `index`. One undo step.
8797
+ */
8798
+ addBrushstroke(brush, options = {}) {
8799
+ const clean = sanitizeVectorBrushstroke(brush);
8800
+ if (!clean) return { ok: false, message: `Unknown brush "${brush?.brushId}": use one of ${vectorBrushstrokeBrushes().map((b) => b.id).join(", ")}.` };
8801
+ if (brush.size !== void 0 && !(brush.size > 0 && brush.size <= 500)) return { ok: false, message: "size must be above 0 and at most 500 (document units)." };
8802
+ if (options.color !== void 0 && !parseVectorColor(options.color)) return { ok: false, message: `"${options.color}" is not a colour.` };
8803
+ if (options.opacity !== void 0 && !(options.opacity >= 0 && options.opacity <= 1)) return { ok: false, message: "opacity must be from 0 to 1." };
8804
+ return this.editStacks("stroke", options.ids, "Add Brushstroke", (stack, path) => {
8805
+ const top = [...stack].reverse().find((l) => (l.kind ?? "paint") === "paint");
8806
+ const paint = options.color ?? (typeof top?.paint === "string" && top.paint !== "none" ? top.paint : typeof path.stroke === "string" && path.stroke !== "none" ? path.stroke : "#000000");
8807
+ const layer = { kind: "brushstroke", paint, brush: clean, ...options.opacity !== void 0 && options.opacity < 1 ? { opacity: options.opacity } : {} };
8808
+ const at = options.index === void 0 ? stack.length : Math.min(stack.length, Math.max(0, Math.round(options.index)));
8809
+ return [...stack.slice(0, at), layer, ...stack.slice(at)];
8810
+ });
8811
+ }
8812
+ /**
8813
+ * Changes brushstroke layer `index` (in the stroke stack, 0 is the bottom) of the selected (or
8814
+ * given) paths: another brush, size, pressure profile (null removes it), colour or opacity.
8815
+ * Fails, changing nothing, when that layer is not a brushstroke. With `merge`, consecutive edits
8816
+ * fold into one undo step.
8817
+ */
8818
+ setBrushstroke(index, patch, options = {}) {
8819
+ if (patch.brushId !== void 0 && !vectorBrushstrokeBrush(patch.brushId)) return { ok: false, message: `Unknown brush "${patch.brushId}": use one of ${vectorBrushstrokeBrushes().map((b) => b.id).join(", ")}.` };
8820
+ if (patch.size !== void 0 && patch.size !== null && !(patch.size > 0 && patch.size <= 500)) return { ok: false, message: "size must be above 0 and at most 500 (document units)." };
8821
+ if (patch.color !== void 0 && !parseVectorColor(patch.color)) return { ok: false, message: `"${patch.color}" is not a colour.` };
8822
+ if (patch.opacity !== void 0 && !(patch.opacity >= 0 && patch.opacity <= 1)) return { ok: false, message: "opacity must be from 0 to 1." };
8823
+ const merge = options.merge === true ? `brushstroke:${index}` : options.merge || void 0;
8824
+ return this.editStacks(
8825
+ "stroke",
8826
+ options.ids,
8827
+ "Edit Brushstroke",
8828
+ (stack, path) => {
8829
+ const layer = stack[index];
8830
+ if (!layer || layer.kind !== "brushstroke" || !layer.brush) return `Path "${path.id}" has no brushstroke at stroke ${index} (vector.getAppearance lists its strokes; 0 is the bottom).`;
8831
+ const brush = { ...layer.brush, ...patch.brushId ? { brushId: patch.brushId } : {} };
8832
+ if (patch.size === null) delete brush.size;
8833
+ else if (patch.size !== void 0) brush.size = patch.size;
8834
+ if (patch.pressure === null) delete brush.pressure;
8835
+ else if (patch.pressure !== void 0) brush.pressure = patch.pressure;
8836
+ const next = { ...layer, brush: sanitizeVectorBrushstroke(brush) ?? layer.brush, ...patch.color ? { paint: patch.color } : {}, ...patch.opacity !== void 0 ? { opacity: patch.opacity } : {} };
8837
+ return stack.map((l, i) => i === index ? next : l);
8838
+ },
8839
+ merge
8840
+ );
8841
+ }
8367
8842
  /** Removes layer `index` of the fill or stroke stack of the selected (or given) paths. */
8368
8843
  removePaintLayer(kind, index, options = {}) {
8369
8844
  return this.editStacks(
@@ -9844,6 +10319,38 @@ var VectorEditorController = class _VectorEditorController {
9844
10319
  });
9845
10320
  return { ok: true, id, ids: children.map((c) => c.id) };
9846
10321
  }
10322
+ /**
10323
+ * Bitmap-to-vector stylise: turns RGBA `pixels` into stylised vector art (see
10324
+ * `stylizeVectorImage`: mosaic tiles, halftone dots, low-poly triangles or engraving lines) and
10325
+ * adds it as one group, placed like Image Trace (top-left at `x, y`, `width` wide, default one
10326
+ * unit per pixel). One undo step; selects the group. Returns its id and its paths' ids.
10327
+ */
10328
+ stylizeImage(pixels, style, options = {}) {
10329
+ if (!VECTOR_STYLIZE_STYLES.includes(style)) return { ok: false, message: `Unknown style "${style}": use ${VECTOR_STYLIZE_STYLES.join(", ")}.` };
10330
+ if (options.cell !== void 0 && !(options.cell >= 1)) return { ok: false, message: "cell must be at least 1 pixel." };
10331
+ if (options.colors !== void 0 && !(Number.isInteger(options.colors) && options.colors >= 2 && options.colors <= 32)) return { ok: false, message: "colors must be a whole number from 2 to 32." };
10332
+ if (options.width !== void 0 && !(options.width > 0)) return { ok: false, message: "width must be above 0." };
10333
+ if (typeof options.color === "string" && !parseVectorColor(options.color)) return { ok: false, message: `"${options.color}" is not a colour.` };
10334
+ let made;
10335
+ try {
10336
+ made = stylizeVectorImage(pixels, style, options);
10337
+ } catch (e) {
10338
+ return { ok: false, message: e.message };
10339
+ }
10340
+ if (!made.length) return { ok: false, message: "Nothing to draw: the picture is empty or too light for this style (try a smaller cell, or mosaic)." };
10341
+ const k = (options.width ?? pixels.width) / pixels.width;
10342
+ const m = [k, 0, 0, k, options.x ?? 0, options.y ?? 0];
10343
+ const children = made.map((n2) => ({ ...transformVectorNode(n2, m), id: this.newId() }));
10344
+ const id = this.newId();
10345
+ const label2 = VECTOR_STYLIZE_LABELS[style];
10346
+ this.change(label2, () => {
10347
+ this.setDoc([...this.doc.nodes, { id, type: "group", name: options.name ?? label2, children }]);
10348
+ this.selection = [id];
10349
+ this.anchors = [];
10350
+ this.editingId = null;
10351
+ });
10352
+ return { ok: true, id, ids: children.map((c) => c.id) };
10353
+ }
9847
10354
  // -----------------------------------------------------------------------------------------
9848
10355
  // Gradient meshes and freeform gradients
9849
10356
  /** The selected point of a mesh or freeform fill (Gradient tool), or null. */
@@ -11646,6 +12153,81 @@ var VectorEditorController = class _VectorEditorController {
11646
12153
  });
11647
12154
  return out;
11648
12155
  }
12156
+ /** Starts a Distort Brush stroke at `p` (document space). */
12157
+ downDistort(p) {
12158
+ const before = this.snapshot();
12159
+ const r = this.editing.distortSize;
12160
+ const drag = { kind: "distort", mode: this.editing.distortMode, base: this.doc, before, targets: /* @__PURE__ */ new Set(), dabs: [], last: p };
12161
+ this.addDistortDab(drag, p, true);
12162
+ this.drag = drag;
12163
+ this.interaction = { type: "brush", x: p.x, y: p.y, radius: r };
12164
+ this.notify();
12165
+ }
12166
+ /** Adds the paths the brush reaches at `p` to the stroke's targets, and (unless `targetsOnly`) a dab there. */
12167
+ addDistortDab(drag, p, targetsOnly = false) {
12168
+ const radius = this.editing.distortSize;
12169
+ const strength = this.editing.distortStrength;
12170
+ const base = this.doc;
12171
+ this.doc = drag.base;
12172
+ for (const n2 of this.strokeTargets([p], void 0, radius)) drag.targets.add(n2.id);
12173
+ this.doc = base;
12174
+ if (targetsOnly) return;
12175
+ if (drag.mode === "smudge") drag.dabs.push({ x: drag.last.x, y: drag.last.y, radius, strength, dx: p.x - drag.last.x, dy: p.y - drag.last.y });
12176
+ else drag.dabs.push({ x: p.x, y: p.y, radius, strength, angle: 15, seed: drag.dabs.length + 1 });
12177
+ }
12178
+ /** `base` with the target paths distorted by the dabs (document space dabs, mapped into each path's own space). */
12179
+ distortedDoc(base, targets, mode, dabs) {
12180
+ if (!dabs.length || !targets.size) return base;
12181
+ const nodes = mapTree(base.nodes, (n2) => {
12182
+ if (!targets.has(n2.id) || n2.type !== "path" || n2.locked) return n2;
12183
+ const world = vectorWorldMatrix(base.nodes, n2.id) ?? IDENTITY_MATRIX;
12184
+ const inv = invertMatrix(world);
12185
+ const k = 1 / Math.sqrt(Math.abs(world[0] * world[3] - world[1] * world[2]) || 1);
12186
+ const local = dabs.map((d) => {
12187
+ const c = applyMatrix(inv, d);
12188
+ const moved = d.dx !== void 0 || d.dy !== void 0 ? applyMatrix(inv, { x: d.x + (d.dx ?? 0), y: d.y + (d.dy ?? 0) }) : null;
12189
+ return { ...d, x: c.x, y: c.y, radius: d.radius * k, ...moved ? { dx: moved.x - c.x, dy: moved.y - c.y } : {} };
12190
+ });
12191
+ const path = subpathsToBezier(n2.subpaths);
12192
+ const out = distortVectorPath(path, mode, local);
12193
+ return out === path ? n2 : { ...n2, subpaths: bezierToSubpaths(out) };
12194
+ });
12195
+ return nodes === base.nodes ? base : { ...base, nodes };
12196
+ }
12197
+ /**
12198
+ * Distortion brush (CorelDRAW's Smudge, Twirl, Attract, Repel and Roughen tools): runs a round
12199
+ * brush of `radius` (default: the `distortSize` setting) and `strength` 0–1 along `points`
12200
+ * (document space). Smudge pushes the outlines along the stroke (needs two or more points),
12201
+ * twirl turns them about each point by `angle` degrees (default 15) at full strength, attract
12202
+ * pulls them toward each point, repel pushes them away, roughen spikes them. It acts on `ids`,
12203
+ * the selection, or every path the brush reaches. One undo step. Returns the changed ids.
12204
+ */
12205
+ distortBrush(mode, points, options = {}) {
12206
+ if (!VECTOR_DISTORT_BRUSH_MODES.includes(mode)) return { ok: false, message: `Unknown brush "${mode}": use ${VECTOR_DISTORT_BRUSH_MODES.join(", ")}.` };
12207
+ if (!points.length || points.some((q) => !Number.isFinite(q?.x) || !Number.isFinite(q?.y))) return { ok: false, message: "points must be one or more { x, y } numbers." };
12208
+ if (mode === "smudge" && points.length < 2) return { ok: false, message: "Smudge needs two or more points: the stroke pushes the outlines along it." };
12209
+ const radius = options.radius ?? this.editing.distortSize;
12210
+ const strength = options.strength ?? this.editing.distortStrength;
12211
+ if (!(radius > 0)) return { ok: false, message: "radius must be more than 0." };
12212
+ if (!(strength >= 0 && strength <= 1)) return { ok: false, message: "strength must be from 0 to 1." };
12213
+ if (options.ids) {
12214
+ for (const id of options.ids) {
12215
+ const n2 = this.getNode(id);
12216
+ if (!n2) return { ok: false, message: `There is no node "${id}".` };
12217
+ if (n2.locked) return { ok: false, message: `"${id}" is locked; unlock it first.` };
12218
+ }
12219
+ }
12220
+ const targets = new Set(this.strokeTargets(points, options.ids, radius).map((n2) => n2.id));
12221
+ const dabs = mode === "smudge" ? points.slice(1).map((q, i) => ({ x: points[i].x, y: points[i].y, radius, strength, dx: q.x - points[i].x, dy: q.y - points[i].y })) : points.map((q, i) => ({ x: q.x, y: q.y, radius, strength, angle: options.angle ?? 15, seed: (options.seed ?? 1) + i }));
12222
+ const next = this.distortedDoc(this.doc, targets, mode, dabs);
12223
+ const changed = [...targets].filter((id) => findVectorNode(next.nodes, id) !== this.getNode(id));
12224
+ if (!changed.length) return { ok: false, message: "The brush reached no outline: put the points within radius of a path (or pass its id)." };
12225
+ this.change(DISTORT_LABELS[mode], () => {
12226
+ this.doc = next;
12227
+ this.anchors = [];
12228
+ });
12229
+ return { ok: true, ids: changed };
12230
+ }
11649
12231
  /** A stroke's points in node `id`'s own space. */
11650
12232
  localPoints(id, points) {
11651
12233
  const inv = invertMatrix(this.worldOf(id));
@@ -12212,6 +12794,8 @@ var VectorEditorController = class _VectorEditorController {
12212
12794
  return;
12213
12795
  case "connector":
12214
12796
  return this.downConnector(p);
12797
+ case "distort":
12798
+ return this.downDistort(p);
12215
12799
  default:
12216
12800
  return this.downShape(p, input);
12217
12801
  }
@@ -12634,6 +13218,16 @@ var VectorEditorController = class _VectorEditorController {
12634
13218
  }
12635
13219
  case "builder":
12636
13220
  return this.moveBuilder(drag, p);
13221
+ case "distort": {
13222
+ const r = this.editing.distortSize;
13223
+ if (Math.hypot(p.x - drag.last.x, p.y - drag.last.y) < r * 0.1) return;
13224
+ this.addDistortDab(drag, p);
13225
+ drag.last = p;
13226
+ this.doc = this.distortedDoc(drag.base, drag.targets, drag.mode, drag.dabs);
13227
+ this.interaction = { type: "brush", x: p.x, y: p.y, radius: r };
13228
+ this.notify(true);
13229
+ return;
13230
+ }
12637
13231
  case "stroke": {
12638
13232
  const last = drag.points[drag.points.length - 1];
12639
13233
  if (Math.hypot(p.x - last.x, p.y - last.y) * this.viewport.zoom < 1.5) return;
@@ -12927,6 +13521,17 @@ var VectorEditorController = class _VectorEditorController {
12927
13521
  else this.erase(drag.points);
12928
13522
  return;
12929
13523
  }
13524
+ case "distort": {
13525
+ if (!drag.dabs.length && drag.mode !== "smudge") {
13526
+ this.addDistortDab(drag, drag.last);
13527
+ this.doc = this.distortedDoc(drag.base, drag.targets, drag.mode, drag.dabs);
13528
+ }
13529
+ if (this.doc === drag.base) {
13530
+ this.notify(true);
13531
+ return;
13532
+ }
13533
+ return this.commit(DISTORT_LABELS[drag.mode], drag.before);
13534
+ }
12930
13535
  case "pull":
12931
13536
  if (drag.closing) this.pen = null;
12932
13537
  return this.commit(drag.label, drag.before);
@@ -12982,9 +13587,9 @@ var VectorEditorController = class _VectorEditorController {
12982
13587
  this.selection = [drag.nodeId];
12983
13588
  return this.commit(`Draw ${inkShapeLabel(shape)}`, drag.before);
12984
13589
  }
12985
- const fitted = fitCubics(drag.points, 1.5 / this.viewport.zoom, { cornerAngleDeg: 60 });
13590
+ const fitted2 = fitCubics(drag.points, 1.5 / this.viewport.zoom, { cornerAngleDeg: 60 });
12986
13591
  const anchors = [];
12987
- for (const c of fitted) {
13592
+ for (const c of fitted2) {
12988
13593
  if (!anchors.length) anchors.push({ x: c.p0[0], y: c.p0[1] });
12989
13594
  anchors[anchors.length - 1].out = { x: c.p1[0], y: c.p1[1] };
12990
13595
  anchors.push({ x: c.p3[0], y: c.p3[1], in: { x: c.p2[0], y: c.p2[1] } });
@@ -14493,7 +15098,7 @@ var pickStyle = (params) => {
14493
15098
  }
14494
15099
  return out;
14495
15100
  };
14496
- var check = (result) => {
15101
+ var check2 = (result) => {
14497
15102
  if (!result.ok) fail(result.message ?? "The command failed.");
14498
15103
  };
14499
15104
  function createVectorCommands(controller, options = {}) {
@@ -14623,7 +15228,7 @@ function createVectorCommands(controller, options = {}) {
14623
15228
  enabled: enabledFor,
14624
15229
  run: (_ctx, params) => {
14625
15230
  const targets = ids(params);
14626
- check(controller.addPaintLayer(kind, layerPatchOf(params ?? {}, keys), { ids: targets, index: params?.index }));
15231
+ check2(controller.addPaintLayer(kind, layerPatchOf(params ?? {}, keys), { ids: targets, index: params?.index }));
14627
15232
  return { nodes: pathsUnder(targets).map((id) => stackResult(id, kind)) };
14628
15233
  }
14629
15234
  },
@@ -14639,7 +15244,7 @@ function createVectorCommands(controller, options = {}) {
14639
15244
  layerIndex(params.nodeId, kind, params.index);
14640
15245
  const patch = layerPatchOf(params, keys);
14641
15246
  if (!Object.keys(patch).length) fail(`Pass at least one of: ${keys.join(", ")}.`);
14642
- check(controller.updatePaintLayer(kind, params.index, patch, { ids: [params.nodeId] }));
15247
+ check2(controller.updatePaintLayer(kind, params.index, patch, { ids: [params.nodeId] }));
14643
15248
  return stackResult(params.nodeId, kind);
14644
15249
  }
14645
15250
  },
@@ -14653,7 +15258,7 @@ function createVectorCommands(controller, options = {}) {
14653
15258
  undoable: true,
14654
15259
  run: (_ctx, params) => {
14655
15260
  layerIndex(params.nodeId, kind, params.index);
14656
- check(controller.removePaintLayer(kind, params.index, { ids: [params.nodeId] }));
15261
+ check2(controller.removePaintLayer(kind, params.index, { ids: [params.nodeId] }));
14657
15262
  return stackResult(params.nodeId, kind);
14658
15263
  }
14659
15264
  },
@@ -14672,7 +15277,7 @@ function createVectorCommands(controller, options = {}) {
14672
15277
  undoable: true,
14673
15278
  run: (_ctx, params) => {
14674
15279
  layerIndex(params.nodeId, kind, params.index);
14675
- check(controller.movePaintLayer(kind, params.index, params.to, { ids: [params.nodeId] }));
15280
+ check2(controller.movePaintLayer(kind, params.index, params.to, { ids: [params.nodeId] }));
14676
15281
  return stackResult(params.nodeId, kind);
14677
15282
  }
14678
15283
  }
@@ -14780,7 +15385,7 @@ function createVectorCommands(controller, options = {}) {
14780
15385
  if (params?.type === void 0 && params?.invert === void 0 && params?.enabled === void 0) fail("Pass at least one of type, invert, enabled.");
14781
15386
  const groups = maskTargets(params);
14782
15387
  const patch = { ...params?.type ? { type: params.type } : {}, ...params?.invert !== void 0 ? { invert: params.invert } : {}, ...params?.enabled !== void 0 ? { enabled: params.enabled } : {} };
14783
- check(controller.setMaskOptions(patch, { ids: groups }));
15388
+ check2(controller.setMaskOptions(patch, { ids: groups }));
14784
15389
  return { groups: groups.map(maskResult) };
14785
15390
  }
14786
15391
  },
@@ -14829,7 +15434,7 @@ function createVectorCommands(controller, options = {}) {
14829
15434
  enabled: enabledFor,
14830
15435
  run: (_ctx, params) => {
14831
15436
  const targets = ids(params);
14832
- check(controller.setBlendMode(params.blendMode, { ids: targets }));
15437
+ check2(controller.setBlendMode(params.blendMode, { ids: targets }));
14833
15438
  return { nodes: targets.map((id) => ({ id, blendMode: controller.getNode(id)?.blendMode ?? "normal" })) };
14834
15439
  }
14835
15440
  },
@@ -14883,7 +15488,7 @@ function createVectorCommands(controller, options = {}) {
14883
15488
  const filter = filterOf(params.nodeId, params.filterId);
14884
15489
  checkFilterParams(filter.type, params.params);
14885
15490
  if (params.params === void 0 && params.enabled === void 0) fail("Pass params or enabled.");
14886
- check(controller.updateFilter(params.nodeId, params.filterId, { params: params.params, enabled: params.enabled }));
15491
+ check2(controller.updateFilter(params.nodeId, params.filterId, { params: params.params, enabled: params.enabled }));
14887
15492
  return filtersResult(params.nodeId);
14888
15493
  }
14889
15494
  },
@@ -14902,7 +15507,7 @@ function createVectorCommands(controller, options = {}) {
14902
15507
  undoable: true,
14903
15508
  run: (_ctx, params) => {
14904
15509
  filterOf(params.nodeId, params.filterId);
14905
- check(controller.removeFilter(params.nodeId, params.filterId));
15510
+ check2(controller.removeFilter(params.nodeId, params.filterId));
14906
15511
  return filtersResult(params.nodeId);
14907
15512
  }
14908
15513
  },
@@ -14925,7 +15530,7 @@ function createVectorCommands(controller, options = {}) {
14925
15530
  undoable: true,
14926
15531
  run: (_ctx, params) => {
14927
15532
  filterOf(params.nodeId, params.filterId);
14928
- check(controller.moveFilter(params.nodeId, params.filterId, params.index));
15533
+ check2(controller.moveFilter(params.nodeId, params.filterId, params.index));
14929
15534
  return filtersResult(params.nodeId);
14930
15535
  }
14931
15536
  }
@@ -15081,7 +15686,7 @@ function createVectorCommands(controller, options = {}) {
15081
15686
  },
15082
15687
  undoable: true,
15083
15688
  run: (_ctx, params) => {
15084
- check(controller.setText(params.id, params.text, { start: params.start, end: params.end }));
15689
+ check2(controller.setText(params.id, params.text, { start: params.start, end: params.end }));
15085
15690
  return textSummary(params.id);
15086
15691
  }
15087
15692
  },
@@ -15102,7 +15707,7 @@ function createVectorCommands(controller, options = {}) {
15102
15707
  const patch = pick(params, [...Object.keys(FONT_PROPS), "fill"]);
15103
15708
  if (!Object.keys(patch).length) fail(`Pass at least one of ${[...Object.keys(FONT_PROPS), "fill"].join(", ")}.`);
15104
15709
  if (patch.fill !== void 0) patch.fill = checkPaint(patch.fill, "fill");
15105
- check(controller.setTextStyle(params.id, patch, { start: params.start, end: params.end }));
15710
+ check2(controller.setTextStyle(params.id, patch, { start: params.start, end: params.end }));
15106
15711
  return textSummary(params.id);
15107
15712
  }
15108
15713
  },
@@ -15117,7 +15722,7 @@ function createVectorCommands(controller, options = {}) {
15117
15722
  run: (_ctx, params) => {
15118
15723
  const patch = pick(params, Object.keys(PARAGRAPH_PROPS));
15119
15724
  if (!Object.keys(patch).length) fail(`Pass at least one of ${Object.keys(PARAGRAPH_PROPS).join(", ")}.`);
15120
- check(controller.setParagraphStyle(params.id, patch));
15725
+ check2(controller.setParagraphStyle(params.id, patch));
15121
15726
  return textSummary(params.id);
15122
15727
  }
15123
15728
  },
@@ -15146,7 +15751,7 @@ function createVectorCommands(controller, options = {}) {
15146
15751
  run: (_ctx, params) => {
15147
15752
  const { id, ...patch } = params;
15148
15753
  if (!Object.keys(patch).length) fail("Pass kind or a frame field to change.");
15149
- check(controller.setTextFrame(id, patch));
15754
+ check2(controller.setTextFrame(id, patch));
15150
15755
  return textSummary(id);
15151
15756
  }
15152
15757
  },
@@ -15178,7 +15783,7 @@ function createVectorCommands(controller, options = {}) {
15178
15783
  pathOptions: pick(params, Object.keys(PATH_TEXT_PROPS)),
15179
15784
  ...params.fill !== void 0 ? { fill: checkPaint(params.fill, "fill") } : {}
15180
15785
  });
15181
- check(r);
15786
+ check2(r);
15182
15787
  return textSummary(r.id);
15183
15788
  }
15184
15789
  },
@@ -15194,7 +15799,7 @@ function createVectorCommands(controller, options = {}) {
15194
15799
  enabled: enabledFor,
15195
15800
  run: (_ctx, params) => {
15196
15801
  const r = controller.createOutlines({ ids: ids(params) });
15197
- check(r);
15802
+ check2(r);
15198
15803
  return { ids: r.ids };
15199
15804
  }
15200
15805
  }
@@ -15251,7 +15856,7 @@ function createVectorCommands(controller, options = {}) {
15251
15856
  enabled: enabledFor,
15252
15857
  run: (_ctx, params) => {
15253
15858
  const r = controller.makeSymbol({ ids: ids(params), ...params?.name ? { name: params.name } : {} });
15254
- check(r);
15859
+ check2(r);
15255
15860
  return { symbolId: r.symbolId, instanceId: r.instanceId, symbol: symbolSummary(r.symbolId) };
15256
15861
  }
15257
15862
  },
@@ -15282,7 +15887,7 @@ function createVectorCommands(controller, options = {}) {
15282
15887
  ...params.transform ? { transform: params.transform } : {},
15283
15888
  ...params.name ? { name: params.name } : {}
15284
15889
  });
15285
- check(r);
15890
+ check2(r);
15286
15891
  return { id: r.id };
15287
15892
  }
15288
15893
  },
@@ -15314,7 +15919,7 @@ function createVectorCommands(controller, options = {}) {
15314
15919
  };
15315
15920
  if (!Object.keys(patch).length) fail("Pass at least one of scaleStrokes, fill, stroke, hidden.");
15316
15921
  const targetIds = ids(params);
15317
- check(controller.setInstanceOptions(patch, { ids: targetIds }));
15922
+ check2(controller.setInstanceOptions(patch, { ids: targetIds }));
15318
15923
  const listed = list().nodes;
15319
15924
  return { instances: targetIds.map((id) => listed.find((n2) => n2.id === id)) };
15320
15925
  }
@@ -15329,7 +15934,7 @@ function createVectorCommands(controller, options = {}) {
15329
15934
  undoable: true,
15330
15935
  run: (_ctx, params) => {
15331
15936
  const targetIds = ids(params);
15332
- check(controller.replaceSymbol(params.symbolId, { ids: targetIds }));
15937
+ check2(controller.replaceSymbol(params.symbolId, { ids: targetIds }));
15333
15938
  return { ids: targetIds };
15334
15939
  }
15335
15940
  },
@@ -15344,7 +15949,7 @@ function createVectorCommands(controller, options = {}) {
15344
15949
  enabled: enabledFor,
15345
15950
  run: (_ctx, params) => {
15346
15951
  const r = controller.detachInstances({ ids: ids(params) });
15347
- check(r);
15952
+ check2(r);
15348
15953
  return { ids: r.ids };
15349
15954
  }
15350
15955
  },
@@ -15357,7 +15962,7 @@ function createVectorCommands(controller, options = {}) {
15357
15962
  params: { 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 },
15358
15963
  undoable: true,
15359
15964
  run: (_ctx, params) => {
15360
- check(controller.renameSymbol(params.symbolId, params.name));
15965
+ check2(controller.renameSymbol(params.symbolId, params.name));
15361
15966
  return symbolSummary(params.symbolId);
15362
15967
  }
15363
15968
  },
@@ -15370,7 +15975,7 @@ function createVectorCommands(controller, options = {}) {
15370
15975
  params: { 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 },
15371
15976
  undoable: true,
15372
15977
  run: (_ctx, params) => {
15373
- check(controller.deleteSymbol(params.symbolId, { detach: params.detach }));
15978
+ check2(controller.deleteSymbol(params.symbolId, { detach: params.detach }));
15374
15979
  return { ok: true };
15375
15980
  }
15376
15981
  },
@@ -15382,7 +15987,7 @@ function createVectorCommands(controller, options = {}) {
15382
15987
  description: "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).",
15383
15988
  params: { type: "object", properties: { id: { type: "string", description: "A symbol id (vector.listSymbols) or an instance id (vector.list)." } }, required: ["id"], additionalProperties: false },
15384
15989
  run: (_ctx, params) => {
15385
- check(controller.editSymbol(params.id));
15990
+ check2(controller.editSymbol(params.id));
15386
15991
  return { symbolId: controller.getState().symbolEdit.symbolId, nodes: list().nodes };
15387
15992
  }
15388
15993
  },
@@ -15473,7 +16078,7 @@ function createVectorCommands(controller, options = {}) {
15473
16078
  run: (_ctx, params) => {
15474
16079
  const { ids: list2, ...patch } = params ?? {};
15475
16080
  if (!Object.keys(patch).length) fail("Pass at least one of mode, center, rotation, copies, mirror.");
15476
- check(controller.setSymmetry(patch, list2 ? { ids: list2 } : {}));
16081
+ check2(controller.setSymmetry(patch, list2 ? { ids: list2 } : {}));
15477
16082
  const ids2 = list2 ?? controller.getState().selection.filter((id) => controller.getNode(id)?.type === "group");
15478
16083
  return { groups: ids2.filter((id) => controller.getNode(id)?.symmetry).map(symmetryResult) };
15479
16084
  }
@@ -15489,7 +16094,7 @@ function createVectorCommands(controller, options = {}) {
15489
16094
  enabled: enabledFor,
15490
16095
  run: (_ctx, params) => {
15491
16096
  const r = controller.releaseSymmetry({ ...params?.ids ? { ids: params.ids } : {}, ...params?.expand ? { expand: true } : {} });
15492
- check(r);
16097
+ check2(r);
15493
16098
  return { ids: r.ids ?? [] };
15494
16099
  }
15495
16100
  },
@@ -15501,7 +16106,7 @@ function createVectorCommands(controller, options = {}) {
15501
16106
  description: "Hands the canvas to the user drawing inside a top-level symmetry group: new pen, pencil, shape and text art goes into it, mirrored as it is drawn, until vector.exitSymmetry or Escape. Returns { id }.",
15502
16107
  params: { type: "object", properties: { id: { type: "string", description: "The symmetry group." } }, required: ["id"], additionalProperties: false },
15503
16108
  run: (_ctx, params) => {
15504
- check(controller.enterSymmetry(params.id));
16109
+ check2(controller.enterSymmetry(params.id));
15505
16110
  return { id: params.id };
15506
16111
  }
15507
16112
  },
@@ -15542,7 +16147,7 @@ function createVectorCommands(controller, options = {}) {
15542
16147
  const { ids: list2, ...patch } = params ?? {};
15543
16148
  if (!Object.keys(patch).length) fail("Pass at least one of type, amount, color.");
15544
16149
  const r = controller.setLens(patch, list2 ? { ids: list2 } : {});
15545
- check(r);
16150
+ check2(r);
15546
16151
  return { lenses: r.lenses ?? [] };
15547
16152
  }
15548
16153
  },
@@ -15557,11 +16162,43 @@ function createVectorCommands(controller, options = {}) {
15557
16162
  enabled: enabledFor,
15558
16163
  run: (_ctx, params) => {
15559
16164
  const r = controller.setLens(null, params?.ids ? { ids: params.ids } : {});
15560
- check(r);
16165
+ check2(r);
15561
16166
  return { ids: (r.lenses ?? []).map((l) => l.id) };
15562
16167
  }
15563
16168
  }
15564
16169
  ];
16170
+ const BRUSH_PROPS = {
16171
+ ids: { ...IDS, description: "Paths to distort (default: the selection, else every path the brush reaches)." },
16172
+ radius: { type: "number", exclusiveMinimum: 0, description: "Brush radius, document units (default: the distortSize setting, 20)." },
16173
+ strength: { type: "number", minimum: 0, maximum: 1, description: "Strength at the brush centre, 0\u20131, fading to 0 at the rim (default: the distortStrength setting, 0.5)." }
16174
+ };
16175
+ const brushRun = (mode) => (_ctx, params) => {
16176
+ const { points, ids: list2, ...rest } = params;
16177
+ const r = controller.distortBrush(
16178
+ mode,
16179
+ points.map(([x, y]) => ({ x, y })),
16180
+ { ...rest, ...list2 ? { ids: list2 } : {} }
16181
+ );
16182
+ check2(r);
16183
+ return { ids: r.ids ?? [] };
16184
+ };
16185
+ const brush = (mode, label2, keywords, description, extra = {}, minPoints = 1) => ({
16186
+ ...base,
16187
+ id: `${prefix}${mode}Brush`,
16188
+ label: label2,
16189
+ keywords: ["distort", "brush", "corel", ...keywords],
16190
+ description: `${description} One undo step. Returns { ids } of the changed paths; fails, changing nothing, when the brush reaches no outline.`,
16191
+ params: { type: "object", properties: { points: { ...POINTS, minItems: minPoints, description: minPoints > 1 ? "The brush stroke [[x, y], \u2026] in document units: at least two points." : "Where the brush dabs [[x, y], \u2026], document units: one dab per point." }, ...BRUSH_PROPS, ...extra }, required: ["points"], additionalProperties: false },
16192
+ undoable: true,
16193
+ run: brushRun(mode)
16194
+ });
16195
+ const distortCommands = () => [
16196
+ brush("smudge", "Smudge Brush\u2026", ["smudge", "push", "drag", "liquify"], "Smudges outlines along a stroke: what is under the brush is pushed the way it moves, most at its centre (CorelDRAW's Smudge).", {}, 2),
16197
+ brush("twirl", "Twirl Brush\u2026", ["twirl", "swirl", "spiral", "twist"], "Twirls the outlines under the brush about each point (CorelDRAW's Twirl).", { angle: { type: "number", description: "Degrees turned at the brush centre per point, clockwise on screen; negative turns the other way (default 15)." } }),
16198
+ brush("attract", "Attract Brush\u2026", ["attract", "pinch", "pull", "pucker"], "Pulls the outlines under the brush toward each point (CorelDRAW's Attract)."),
16199
+ brush("repel", "Repel Brush\u2026", ["repel", "bloat", "push out", "inflate"], "Pushes the outlines under the brush away from each point (CorelDRAW's Repel)."),
16200
+ brush("roughen", "Roughen Brush\u2026", ["roughen", "jagged", "spiky", "fur", "grass"], "Breaks the outlines under the brush into spikes (CorelDRAW's Roughen): use for fur, grass and torn edges.", { seed: { type: "integer", description: "Seed of the spikes (default 1): the same seed gives the same spikes." } })
16201
+ ];
15565
16202
  const PLANE = { type: "string", enum: [...VECTOR_PERSPECTIVE_PLANES], description: "A plane of the grid: floor, left and right (2 or 3 vanishing points), or floor, side and front (1 point)." };
15566
16203
  const perspectiveResult = () => {
15567
16204
  const grid = controller.getPerspective();
@@ -15597,7 +16234,7 @@ function createVectorCommands(controller, options = {}) {
15597
16234
  },
15598
16235
  undoable: true,
15599
16236
  run: (_ctx, params) => {
15600
- check(controller.setPerspective(params));
16237
+ check2(controller.setPerspective(params));
15601
16238
  return perspectiveResult();
15602
16239
  }
15603
16240
  },
@@ -15610,7 +16247,7 @@ function createVectorCommands(controller, options = {}) {
15610
16247
  params: { type: "object", properties: {}, additionalProperties: false },
15611
16248
  undoable: true,
15612
16249
  run: () => {
15613
- check(controller.setPerspective(null));
16250
+ check2(controller.setPerspective(null));
15614
16251
  return { perspective: null };
15615
16252
  }
15616
16253
  },
@@ -15622,7 +16259,7 @@ function createVectorCommands(controller, options = {}) {
15622
16259
  description: "Makes the Rectangle, Ellipse, Polygon and Star tools draw on a plane of the grid (their drag box is taken on the plane, so shapes recede with it), or flat again with plane null. Not an undo step. Returns the grid as vector.getPerspective does.",
15623
16260
  params: { type: "object", properties: { plane: { ...PLANE, type: ["string", "null"], enum: [...VECTOR_PERSPECTIVE_PLANES, null], description: `${PLANE.description} null draws flat.` } }, required: ["plane"], additionalProperties: false },
15624
16261
  run: (_ctx, params) => {
15625
- check(controller.setPerspectivePlane(params.plane));
16262
+ check2(controller.setPerspectivePlane(params.plane));
15626
16263
  return perspectiveResult();
15627
16264
  }
15628
16265
  },
@@ -15637,11 +16274,90 @@ function createVectorCommands(controller, options = {}) {
15637
16274
  enabled: enabledFor,
15638
16275
  run: (_ctx, params) => {
15639
16276
  const r = controller.attachToPerspective(params.plane, params.ids ? { ids: params.ids } : {});
15640
- check(r);
16277
+ check2(r);
15641
16278
  return { ids: r.ids ?? [] };
15642
16279
  }
15643
16280
  }
15644
16281
  ];
16282
+ const brushstrokeCommands = () => [
16283
+ {
16284
+ ...base,
16285
+ id: `${prefix}listBrushes`,
16286
+ label: "Brushstroke Brushes",
16287
+ readOnly: true,
16288
+ keywords: ["brush", "brushstroke", "pencil", "pastel", "charcoal", "ink", "marker", "natural media"],
16289
+ description: "Lists the brushes a brushstroke can use (the paint editor's dry brushes): { brushes: [{ id, name, category, size }] } (size: the brush's own diameter in document units).",
16290
+ params: { type: "object", properties: {}, additionalProperties: false },
16291
+ run: () => ({ brushes: vectorBrushstrokeBrushes().map((b) => ({ id: b.id, name: b.name, category: b.category, size: b.tip.size })) })
16292
+ },
16293
+ {
16294
+ ...base,
16295
+ id: `${prefix}addBrushstroke`,
16296
+ label: "Add Brushstroke\u2026",
16297
+ keywords: ["brushstroke", "brush", "natural media", "pencil line", "chalk", "sketchy", "hand-drawn", "ink stroke"],
16298
+ description: "Paints paths (`ids`, default the selection; groups: every path inside) with a natural-media brush along their outline, as a stroke layer of kind brushstroke: `brushId` from vector.listBrushes, `size` (document units), an optional `pressure` profile (0\u20131 values spread from start to end, e.g. [0.2, 1, 0.2] for a tapered stroke), `color` and `opacity`. The path stays editable; the strokes follow it. One undo step. Returns { paths: [{ id, strokes }] } (the brushstroke is a stroke layer; change it with vector.setBrushstroke, remove it with vector.removeStroke).",
16299
+ params: {
16300
+ type: "object",
16301
+ properties: {
16302
+ ids: { ...IDS, description: "Paths or groups (default: the selection)." },
16303
+ brushId: { type: "string", description: 'Brush id from vector.listBrushes, e.g. "pencil-hb".' },
16304
+ size: { type: "number", exclusiveMinimum: 0, maximum: 500, description: "Brush diameter in document units (default: the brush's own)." },
16305
+ pressure: { type: "array", items: { type: "number", minimum: 0, maximum: 1 }, minItems: 1, maxItems: 64, description: "Pressure 0\u20131 along the length, spread evenly from start to end (default: full pressure)." },
16306
+ color: { type: "string", description: "CSS colour (default: the path's stroke colour, else black)." },
16307
+ opacity: { type: "number", minimum: 0, maximum: 1, description: "Layer opacity 0\u20131 (default 1)." },
16308
+ index: { type: "integer", minimum: 0, description: "Position in the stroke stack, 0 the bottom (default: on top)." }
16309
+ },
16310
+ required: ["brushId"],
16311
+ additionalProperties: false
16312
+ },
16313
+ undoable: true,
16314
+ enabled: enabledFor,
16315
+ run: (_ctx, params) => {
16316
+ const targets = params.ids ?? controller.getState().selection;
16317
+ check2(
16318
+ controller.addBrushstroke(
16319
+ { brushId: params.brushId, ...params.size !== void 0 ? { size: params.size } : {}, ...params.pressure ? { pressure: params.pressure } : {} },
16320
+ { ...params.ids ? { ids: params.ids } : {}, ...params.index !== void 0 ? { index: params.index } : {}, ...params.color ? { color: params.color } : {}, ...params.opacity !== void 0 ? { opacity: params.opacity } : {} }
16321
+ )
16322
+ );
16323
+ const paths = [];
16324
+ for (const id of targets) {
16325
+ const n2 = controller.getNode(id);
16326
+ if (n2?.type === "path") paths.push(stackResult(id, "stroke"));
16327
+ }
16328
+ return { paths };
16329
+ }
16330
+ },
16331
+ {
16332
+ ...base,
16333
+ id: `${prefix}setBrushstroke`,
16334
+ label: "Edit Brushstroke\u2026",
16335
+ keywords: ["brushstroke", "brush", "size", "pressure", "taper"],
16336
+ description: "Changes the brushstroke at stroke layer `index` of path `id` (vector.getAppearance lists strokes; 0 is the bottom): `brushId`, `size` (null: the brush's own), `pressure` (null: full pressure), `color`, `opacity`. Pass at least one. One undo step. Returns { id, strokes }.",
16337
+ params: {
16338
+ type: "object",
16339
+ properties: {
16340
+ id: { type: "string", description: "The path." },
16341
+ index: { type: "integer", minimum: 0, description: "Stroke layer index of the brushstroke." },
16342
+ brushId: { type: "string", description: "Another brush from vector.listBrushes." },
16343
+ size: { type: ["number", "null"], description: "Diameter in document units, or null for the brush's own." },
16344
+ pressure: { type: ["array", "null"], items: { type: "number", minimum: 0, maximum: 1 }, description: "Pressure profile 0\u20131 from start to end, or null for full pressure." },
16345
+ color: { type: "string", description: "CSS colour." },
16346
+ opacity: { type: "number", minimum: 0, maximum: 1, description: "Layer opacity 0\u20131." }
16347
+ },
16348
+ required: ["id", "index"],
16349
+ additionalProperties: false
16350
+ },
16351
+ undoable: true,
16352
+ run: (_ctx, params) => {
16353
+ const { id, index, ...patch } = params;
16354
+ if (!Object.keys(patch).length) fail("Pass at least one of brushId, size, pressure, color, opacity.");
16355
+ layerIndex(id, "stroke", index);
16356
+ check2(controller.setBrushstroke(index, patch, { ids: [id] }));
16357
+ return stackResult(id, "stroke");
16358
+ }
16359
+ }
16360
+ ];
15645
16361
  const pdfCommands = () => [
15646
16362
  {
15647
16363
  ...base,
@@ -15710,7 +16426,7 @@ function createVectorCommands(controller, options = {}) {
15710
16426
  undoable: true,
15711
16427
  run: (_ctx, params) => {
15712
16428
  const r = controller.importLottie(params.lottie, { ...params.frame !== void 0 ? { frame: params.frame } : {}, ...params.replace ? { replace: true } : {} });
15713
- check(r);
16429
+ check2(r);
15714
16430
  return { ids: r.ids, notes: r.notes };
15715
16431
  }
15716
16432
  }
@@ -15754,7 +16470,7 @@ function createVectorCommands(controller, options = {}) {
15754
16470
  return fail(`The PNG could not be read: ${e.message}`);
15755
16471
  }
15756
16472
  const r = controller.traceImage(decoded, options2);
15757
- check(r);
16473
+ check2(r);
15758
16474
  return {
15759
16475
  id: r.id,
15760
16476
  paths: r.ids.map((id) => {
@@ -15765,6 +16481,70 @@ function createVectorCommands(controller, options = {}) {
15765
16481
  }
15766
16482
  }
15767
16483
  ];
16484
+ const IMAGE_PLACE = {
16485
+ image: { type: "string", description: "The picture as a data:image/png;base64,\u2026 URL (up to 4 million pixels)." },
16486
+ x: { type: "number", description: "Left edge in document units (default 0)." },
16487
+ y: { type: "number", description: "Top edge in document units (default 0)." },
16488
+ width: { type: "number", exclusiveMinimum: 0, description: "Width in document units (default: the image width in pixels)." },
16489
+ name: { type: "string", description: "Group name (default: the style's name)." }
16490
+ };
16491
+ const decodeImage = (image) => {
16492
+ const parsed = parseDataUrl(image);
16493
+ if (!parsed || !isPng(parsed.bytes)) fail("image must be a PNG data URL (data:image/png;base64,\u2026).");
16494
+ try {
16495
+ return decodePng(parsed.bytes, { maxPixels: VECTOR_TRACE_MAX_PIXELS });
16496
+ } catch (e) {
16497
+ return fail(`The PNG could not be read: ${e.message}`);
16498
+ }
16499
+ };
16500
+ const stylize = (style, label2, keywords, description, props) => ({
16501
+ ...base,
16502
+ id: `${prefix}${style}Image`,
16503
+ label: label2,
16504
+ keywords: ["stylize", "stylise", "bitmap to vector", "artistic", "png to svg", ...keywords],
16505
+ description: `${description} Added as one group placed with its top-left at x, y and width wide; colours of one kind share a compound path. One undo step. Returns { id, paths: [{ id, name, fill, subpaths }] }.`,
16506
+ params: { type: "object", properties: { ...IMAGE_PLACE, ...props }, required: ["image"], additionalProperties: false },
16507
+ undoable: true,
16508
+ run: (_ctx, params) => {
16509
+ const { image, ...options2 } = params;
16510
+ const r = controller.stylizeImage(decodeImage(image), style, options2);
16511
+ check2(r);
16512
+ return {
16513
+ id: r.id,
16514
+ paths: r.ids.map((id) => {
16515
+ const n2 = controller.getNode(id);
16516
+ return { id, name: n2.name, fill: n2.fill, subpaths: n2.subpaths.length };
16517
+ })
16518
+ };
16519
+ }
16520
+ });
16521
+ const CELL = (dflt, what) => ({ type: "number", minimum: 1, description: `${what} in image pixels (default ${dflt}); larger draws fewer, bigger shapes.` });
16522
+ const PALETTE = (dflt) => ({ type: "integer", minimum: 2, maximum: 32, description: `Palette size (default ${dflt}).` });
16523
+ const stylizeCommands = () => [
16524
+ stylize("mosaic", "Mosaic from Image\u2026", ["mosaic", "tiles", "pixelate", "pixel art"], "Turns a PNG into mosaic tiles: a square or round tile per cell in the colour under it.", {
16525
+ cell: CELL(12, "Tile size"),
16526
+ shape: { type: "string", enum: ["square", "circle"], description: "Tile shape (default square)." },
16527
+ gap: { type: "number", minimum: 0, maximum: 0.9, description: "Grout between tiles as a fraction of the tile (default 0.1)." },
16528
+ colors: PALETTE(12)
16529
+ }),
16530
+ stylize("halftone", "Halftone from Image\u2026", ["halftone", "dots", "comic", "pop art", "screen print"], "Turns a PNG into halftone dots on a turned grid, each dot's area following how dark the picture is there.", {
16531
+ cell: CELL(8, "Dot spacing"),
16532
+ angle: { type: "number", description: "Screen angle in degrees (default 45)." },
16533
+ color: { type: ["string", "null"], description: "Ink colour (default black); null keeps the picture's colours, reduced to `colors`." },
16534
+ colors: PALETTE(12)
16535
+ }),
16536
+ stylize("lowPoly", "Low Poly from Image\u2026", ["low poly", "triangles", "polygon art", "geometric", "faceted"], "Turns a PNG into low-poly art: a jittered grid of triangles, each in the colour under it.", {
16537
+ cell: CELL(24, "Triangle size"),
16538
+ jitter: { type: "number", minimum: 0, maximum: 0.5, description: "How far the grid points wander, as a fraction of a cell (default 0.35)." },
16539
+ seed: { type: "integer", description: "Seed of the wandering (default 1): the same seed gives the same triangles." },
16540
+ colors: PALETTE(16)
16541
+ }),
16542
+ stylize("hatch", "Engraving from Image\u2026", ["engraving", "hatch", "hatching", "line art", "woodcut", "banknote"], "Turns a PNG into an engraving: parallel lines on a turned grid that swell where the picture is dark and break where it is light.", {
16543
+ cell: CELL(6, "Line spacing"),
16544
+ angle: { type: "number", description: "Line angle in degrees (default 45)." },
16545
+ color: { type: "string", description: "Ink colour (default black)." }
16546
+ })
16547
+ ];
15768
16548
  const shadingResult = (id) => ({ id, shading: controller.getShading(id) ?? fail(`"${id}" has no mesh or freeform fill.`) });
15769
16549
  const COLOR = { type: "string", description: "A CSS colour." };
15770
16550
  const FREEFORM_POINT = {
@@ -15813,7 +16593,7 @@ function createVectorCommands(controller, options = {}) {
15813
16593
  enabled: enabledFor,
15814
16594
  run: (_ctx, params) => {
15815
16595
  const list2 = ids(params);
15816
- check(controller.setMeshGradient({ rows: params.rows, columns: params.columns, ...params.colors ? { colors: params.colors } : {}, ...params.corners ? { corners: params.corners } : {} }, { ids: list2 }));
16596
+ check2(controller.setMeshGradient({ rows: params.rows, columns: params.columns, ...params.colors ? { colors: params.colors } : {}, ...params.corners ? { corners: params.corners } : {} }, { ids: list2 }));
15817
16597
  return { meshes: list2.map(shadingResult) };
15818
16598
  }
15819
16599
  },
@@ -15840,7 +16620,7 @@ function createVectorCommands(controller, options = {}) {
15840
16620
  run: (_ctx, params) => {
15841
16621
  const { id, index, ...patch } = params;
15842
16622
  if (!Object.keys(patch).length) fail("Pass at least one of x / y, color, opacity.");
15843
- check(controller.setMeshPoint(id, index, patch));
16623
+ check2(controller.setMeshPoint(id, index, patch));
15844
16624
  return shadingResult(id);
15845
16625
  }
15846
16626
  },
@@ -15865,7 +16645,7 @@ function createVectorCommands(controller, options = {}) {
15865
16645
  run: (_ctx, params) => {
15866
16646
  const { ids: _i, ...input } = params ?? {};
15867
16647
  const list2 = ids(params);
15868
- check(controller.setFreeformGradient(input, { ids: list2 }));
16648
+ check2(controller.setFreeformGradient(input, { ids: list2 }));
15869
16649
  return { gradients: list2.map(shadingResult) };
15870
16650
  }
15871
16651
  },
@@ -15885,7 +16665,7 @@ function createVectorCommands(controller, options = {}) {
15885
16665
  run: (_ctx, params) => {
15886
16666
  const { id, ...point } = params;
15887
16667
  const r = controller.addFreeformPoint(id, point);
15888
- check(r);
16668
+ check2(r);
15889
16669
  return { ...shadingResult(id), index: r.index };
15890
16670
  }
15891
16671
  },
@@ -15913,7 +16693,7 @@ function createVectorCommands(controller, options = {}) {
15913
16693
  run: (_ctx, params) => {
15914
16694
  const { id, index, ...patch } = params;
15915
16695
  if (!Object.keys(patch).length) fail("Pass at least one of x / y, color, opacity, spread.");
15916
- check(controller.setFreeformPoint(id, index, patch));
16696
+ check2(controller.setFreeformPoint(id, index, patch));
15917
16697
  return shadingResult(id);
15918
16698
  }
15919
16699
  },
@@ -15926,7 +16706,7 @@ function createVectorCommands(controller, options = {}) {
15926
16706
  params: { type: "object", properties: { id: { type: "string", description: "The path." }, index: { type: "integer", minimum: 0, description: "Point index." } }, required: ["id", "index"], additionalProperties: false },
15927
16707
  undoable: true,
15928
16708
  run: (_ctx, params) => {
15929
- check(controller.removeFreeformPoint(params.id, params.index));
16709
+ check2(controller.removeFreeformPoint(params.id, params.index));
15930
16710
  return shadingResult(params.id);
15931
16711
  }
15932
16712
  },
@@ -15939,7 +16719,7 @@ function createVectorCommands(controller, options = {}) {
15939
16719
  params: { type: "object", properties: { id: { type: "string", description: "The path." }, lines: LINES }, required: ["id", "lines"], additionalProperties: false },
15940
16720
  undoable: true,
15941
16721
  run: (_ctx, params) => {
15942
- check(controller.setFreeformLines(params.id, params.lines));
16722
+ check2(controller.setFreeformLines(params.id, params.lines));
15943
16723
  return shadingResult(params.id);
15944
16724
  }
15945
16725
  }
@@ -16115,7 +16895,7 @@ function createVectorCommands(controller, options = {}) {
16115
16895
  const { ids: list2, ...patch } = params ?? {};
16116
16896
  if (!Object.keys(patch).length) fail("Pass at least one of start, end, route, radius, gap.");
16117
16897
  const targets = ids({ ids: list2 });
16118
- check(controller.setConnector(patch, { ids: targets }));
16898
+ check2(controller.setConnector(patch, { ids: targets }));
16119
16899
  return { connectors: targets.map(connectorResult) };
16120
16900
  }
16121
16901
  }
@@ -16151,7 +16931,7 @@ function createVectorCommands(controller, options = {}) {
16151
16931
  undoable: true,
16152
16932
  run: (_ctx, params) => {
16153
16933
  const r = controller.addPage(params ?? {});
16154
- check(r);
16934
+ check2(r);
16155
16935
  return { id: r.id, ...pagesResult() };
16156
16936
  }
16157
16937
  },
@@ -16172,7 +16952,7 @@ function createVectorCommands(controller, options = {}) {
16172
16952
  undoable: true,
16173
16953
  run: (_ctx, params) => {
16174
16954
  const r = controller.addMasterPage(params ?? {});
16175
- check(r);
16955
+ check2(r);
16176
16956
  return { id: r.id, ...pagesResult() };
16177
16957
  }
16178
16958
  },
@@ -16185,7 +16965,7 @@ function createVectorCommands(controller, options = {}) {
16185
16965
  params: { type: "object", properties: { ids: { ...IDS, description: "Page ids (default: the selected pages)." }, masterId: { type: ["string", "null"], description: "Master page id, or null for none." } }, required: ["masterId"], additionalProperties: false },
16186
16966
  undoable: true,
16187
16967
  run: (_ctx, params) => {
16188
- check(controller.setPageMaster(params.masterId, params.ids ? { ids: params.ids } : {}));
16968
+ check2(controller.setPageMaster(params.masterId, params.ids ? { ids: params.ids } : {}));
16189
16969
  return pagesResult();
16190
16970
  }
16191
16971
  },
@@ -16198,7 +16978,7 @@ function createVectorCommands(controller, options = {}) {
16198
16978
  params: { type: "object", properties: { id: { type: "string", description: "The page." }, index: { type: "integer", minimum: 0, description: "New position, 0 for first." } }, required: ["id", "index"], additionalProperties: false },
16199
16979
  undoable: true,
16200
16980
  run: (_ctx, params) => {
16201
- check(controller.movePage(params.id, params.index));
16981
+ check2(controller.movePage(params.id, params.index));
16202
16982
  return pagesResult();
16203
16983
  }
16204
16984
  },
@@ -16220,7 +17000,7 @@ function createVectorCommands(controller, options = {}) {
16220
17000
  },
16221
17001
  run: (_ctx, params) => {
16222
17002
  const size = params.canvasWidth && params.canvasHeight ? { width: params.canvasWidth, height: params.canvasHeight } : void 0;
16223
- check(controller.goToPage(params.id, size ? { size } : {}));
17003
+ check2(controller.goToPage(params.id, size ? { size } : {}));
16224
17004
  return { id: params.id };
16225
17005
  }
16226
17006
  }
@@ -16314,7 +17094,7 @@ function createVectorCommands(controller, options = {}) {
16314
17094
  const { ids: list2, ...patch } = params ?? {};
16315
17095
  if (!Object.keys(patch).length) fail("Pass at least one of x, y, width, height, fill, clip.");
16316
17096
  const before = new Set(controller.getFrames().map((f) => f.id));
16317
- check(controller.setFrame(patch, list2 ? { ids: list2 } : {}));
17097
+ check2(controller.setFrame(patch, list2 ? { ids: list2 } : {}));
16318
17098
  const targets = list2 ?? 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);
16319
17099
  return { frames: targets.map(frameResult) };
16320
17100
  }
@@ -16338,7 +17118,7 @@ function createVectorCommands(controller, options = {}) {
16338
17118
  enabled: enabledFor,
16339
17119
  run: (_ctx, params) => {
16340
17120
  const list2 = ids(params);
16341
- check(controller.setResizing({ ...params?.horizontal ? { horizontal: params.horizontal } : {}, ...params?.vertical ? { vertical: params.vertical } : {} }, { ids: list2 }));
17121
+ check2(controller.setResizing({ ...params?.horizontal ? { horizontal: params.horizontal } : {}, ...params?.vertical ? { vertical: params.vertical } : {} }, { ids: list2 }));
16342
17122
  return { nodes: list2.map((id) => ({ id, horizontal: controller.getNode(id)?.resizing?.horizontal ?? "left", vertical: controller.getNode(id)?.resizing?.vertical ?? "top" })) };
16343
17123
  }
16344
17124
  },
@@ -16397,7 +17177,7 @@ function createVectorCommands(controller, options = {}) {
16397
17177
  undoable: true,
16398
17178
  run: (_ctx, params) => {
16399
17179
  const r = controller.addConstraint(params);
16400
- check(r);
17180
+ check2(r);
16401
17181
  return { id: r.id, constraint: controller.getConstraints().find((c) => c.id === r.id) };
16402
17182
  }
16403
17183
  },
@@ -16410,7 +17190,7 @@ function createVectorCommands(controller, options = {}) {
16410
17190
  params: { type: "object", properties: { ids: { type: "array", items: { type: "string" }, minItems: 1, description: "Constraint ids." } }, required: ["ids"], additionalProperties: false },
16411
17191
  undoable: true,
16412
17192
  run: (_ctx, params) => {
16413
- check(controller.removeConstraints(params.ids));
17193
+ check2(controller.removeConstraints(params.ids));
16414
17194
  return { ids: params.ids };
16415
17195
  }
16416
17196
  },
@@ -16431,7 +17211,7 @@ function createVectorCommands(controller, options = {}) {
16431
17211
  },
16432
17212
  undoable: true,
16433
17213
  run: (_ctx, params) => {
16434
- check(controller.setConstraintValue(params.key, params.value));
17214
+ check2(controller.setConstraintValue(params.key, params.value));
16435
17215
  return constraintsSummary();
16436
17216
  }
16437
17217
  },
@@ -16454,7 +17234,7 @@ function createVectorCommands(controller, options = {}) {
16454
17234
  undoable: true,
16455
17235
  run: (_ctx, params) => {
16456
17236
  const r = controller.autoConstrain(params ?? {});
16457
- check(r);
17237
+ check2(r);
16458
17238
  return { ids: r.ids };
16459
17239
  }
16460
17240
  }
@@ -16544,7 +17324,7 @@ function createVectorCommands(controller, options = {}) {
16544
17324
  run: (_ctx, params) => {
16545
17325
  const style = pickStyle(params);
16546
17326
  if (!Object.keys(style).length) fail("Pass at least one style property.");
16547
- check(controller.setStyle(style, ids(params)));
17327
+ check2(controller.setStyle(style, ids(params)));
16548
17328
  return { ok: true };
16549
17329
  }
16550
17330
  },
@@ -16561,7 +17341,7 @@ function createVectorCommands(controller, options = {}) {
16561
17341
  const { ids: _i, target, ...patch } = params ?? {};
16562
17342
  if (patch.type !== void 0 && !VECTOR_GRADIENT_TYPES.includes(patch.type)) fail(`Unknown gradient type "${patch.type}".`);
16563
17343
  if (patch.stops !== void 0 && !(Array.isArray(patch.stops) && patch.stops.length)) fail("stops needs at least one { offset, color }.");
16564
- check(controller.setGradient(patch, { ids: ids(params), target: target ?? "fill" }));
17344
+ check2(controller.setGradient(patch, { ids: ids(params), target: target ?? "fill" }));
16565
17345
  const id = ids(params)[0];
16566
17346
  const node = id ? controller.getNode(id) : void 0;
16567
17347
  const path = node?.type === "path" ? node : void 0;
@@ -16582,6 +17362,8 @@ function createVectorCommands(controller, options = {}) {
16582
17362
  ...lottieCommands(),
16583
17363
  ...pdfCommands(),
16584
17364
  ...symmetryCommands(),
17365
+ ...brushstrokeCommands(),
17366
+ ...stylizeCommands(),
16585
17367
  ...perspectiveCommands(),
16586
17368
  ...lensCommands(),
16587
17369
  {
@@ -16604,9 +17386,9 @@ function createVectorCommands(controller, options = {}) {
16604
17386
  run: (_ctx, params) => {
16605
17387
  const refs = params?.anchors ? anchorsOf(params.anchors) : void 0;
16606
17388
  const target = { refs, ids: params?.ids ? ids(params) : void 0 };
16607
- if (params?.radius !== void 0) check(controller.setCornerRadius(params.radius, { ...target, kind: params.kind, ...params.smoothing !== void 0 ? { smoothing: params.smoothing } : {} }));
16608
- else if (params?.kind) check(controller.setCornerKind(params.kind, target));
16609
- if (params?.radius === void 0 && params?.smoothing !== void 0) check(controller.setCornerSmoothing(params.smoothing, target));
17389
+ if (params?.radius !== void 0) check2(controller.setCornerRadius(params.radius, { ...target, kind: params.kind, ...params.smoothing !== void 0 ? { smoothing: params.smoothing } : {} }));
17390
+ else if (params?.kind) check2(controller.setCornerKind(params.kind, target));
17391
+ if (params?.radius === void 0 && params?.smoothing !== void 0) check2(controller.setCornerSmoothing(params.smoothing, target));
16610
17392
  if (params?.radius === void 0 && !params?.kind && params?.smoothing === void 0) fail("Pass `radius`, `kind` and / or `smoothing`.");
16611
17393
  return { ok: true };
16612
17394
  }
@@ -16641,9 +17423,9 @@ function createVectorCommands(controller, options = {}) {
16641
17423
  undoable: true,
16642
17424
  run: (_ctx, params) => {
16643
17425
  const target = { ids: ids(params) };
16644
- if (params?.preset) check(controller.setWidthProfile(params.preset, target));
16645
- else if (params?.points !== void 0) check(controller.setWidthProfile(params.points ?? null, target));
16646
- else if (params?.taper || params?.wave) check(controller.setWidthProfile({ taper: params.taper, wave: params.wave }, target));
17426
+ if (params?.preset) check2(controller.setWidthProfile(params.preset, target));
17427
+ else if (params?.points !== void 0) check2(controller.setWidthProfile(params.points ?? null, target));
17428
+ else if (params?.taper || params?.wave) check2(controller.setWidthProfile({ taper: params.taper, wave: params.wave }, target));
16647
17429
  else fail("Pass `preset`, `points`, `taper` or `wave`.");
16648
17430
  const node = controller.getNode(target.ids[0]);
16649
17431
  return { ok: true, points: node?.type === "path" ? node.strokeWidthProfile ?? [] : [] };
@@ -16669,7 +17451,7 @@ function createVectorCommands(controller, options = {}) {
16669
17451
  undoable: true,
16670
17452
  run: (_ctx, params) => {
16671
17453
  if (!params?.id) fail("Pass the path `id`.");
16672
- check(controller.setWidthPoint({ nodeId: params.id, index: params.index ?? null }, { t: params.t, left: params.left, right: params.right }));
17454
+ check2(controller.setWidthPoint({ nodeId: params.id, index: params.index ?? null }, { t: params.t, left: params.left, right: params.right }));
16673
17455
  const node = controller.getNode(params.id);
16674
17456
  return { ok: true, points: node?.type === "path" ? node.strokeWidthProfile ?? [] : [] };
16675
17457
  }
@@ -16707,7 +17489,7 @@ function createVectorCommands(controller, options = {}) {
16707
17489
  enabled: enabledFor,
16708
17490
  undoable: true,
16709
17491
  run: (_ctx, params) => {
16710
- check(controller.distort(params.corners, { ids: ids(params), tolerance: params.tolerance }));
17492
+ check2(controller.distort(params.corners, { ids: ids(params), tolerance: params.tolerance }));
16711
17493
  const frame = controller.getState().selectionFrame;
16712
17494
  return { ok: true, bounds: frame ? { x: frame.x, y: frame.y, width: frame.width, height: frame.height } : null };
16713
17495
  }
@@ -16730,7 +17512,7 @@ function createVectorCommands(controller, options = {}) {
16730
17512
  enabled: enabledFor,
16731
17513
  undoable: true,
16732
17514
  run: (_ctx, params) => {
16733
- check(controller.skew(params.ax ?? 0, params.ay ?? 0, { ids: ids(params), origin: params.origin }));
17515
+ check2(controller.skew(params.ax ?? 0, params.ay ?? 0, { ids: ids(params), origin: params.origin }));
16734
17516
  return { ok: true };
16735
17517
  }
16736
17518
  },
@@ -16770,7 +17552,7 @@ function createVectorCommands(controller, options = {}) {
16770
17552
  if (params.expand) return { expanded: controller.expandAppearance({ ids: target }) };
16771
17553
  if (params.topObject) {
16772
17554
  const r = controller.makeEnvelopeWithTopObject({ ids: target, rows: params.mesh?.rows, columns: params.mesh?.columns });
16773
- check(r);
17555
+ check2(r);
16774
17556
  return { nodeId: r.nodeId };
16775
17557
  }
16776
17558
  if (params.preset !== void 0 && !WARP_STYLES.includes(params.preset)) fail(`Unknown warp preset "${params.preset}".`);
@@ -16819,10 +17601,10 @@ function createVectorCommands(controller, options = {}) {
16819
17601
  undoable: true,
16820
17602
  run: (_ctx, params) => {
16821
17603
  const target = ids(params);
16822
- if (params.dx || params.dy) check(controller.translate(params.dx ?? 0, params.dy ?? 0, { ids: target }));
16823
- if (params.sx !== void 0 || params.sy !== void 0) check(controller.scale(params.sx ?? 1, params.sy ?? params.sx ?? 1, { ids: target }));
16824
- if (params.rotate) check(controller.rotate(params.rotate, { ids: target }));
16825
- if (params.angle !== void 0) check(controller.setRotation(params.angle, { ids: target }));
17604
+ if (params.dx || params.dy) check2(controller.translate(params.dx ?? 0, params.dy ?? 0, { ids: target }));
17605
+ if (params.sx !== void 0 || params.sy !== void 0) check2(controller.scale(params.sx ?? 1, params.sy ?? params.sx ?? 1, { ids: target }));
17606
+ if (params.rotate) check2(controller.rotate(params.rotate, { ids: target }));
17607
+ if (params.angle !== void 0) check2(controller.setRotation(params.angle, { ids: target }));
16826
17608
  return { ok: true };
16827
17609
  }
16828
17610
  },
@@ -16836,7 +17618,7 @@ function createVectorCommands(controller, options = {}) {
16836
17618
  enabled: enabledFor,
16837
17619
  undoable: true,
16838
17620
  run: (_ctx, params) => {
16839
- check(controller.removeNodes(ids(params)));
17621
+ check2(controller.removeNodes(ids(params)));
16840
17622
  return { ok: true };
16841
17623
  }
16842
17624
  },
@@ -16907,8 +17689,8 @@ function createVectorCommands(controller, options = {}) {
16907
17689
  },
16908
17690
  undoable: true,
16909
17691
  run: (_ctx, params) => {
16910
- if (params.params !== void 0 || params.enabled !== void 0) check(controller.updateEffect(params.nodeId, params.effectId, { params: params.params, enabled: params.enabled }));
16911
- if (params.index !== void 0) check(controller.moveEffect(params.nodeId, params.effectId, params.index));
17692
+ if (params.params !== void 0 || params.enabled !== void 0) check2(controller.updateEffect(params.nodeId, params.effectId, { params: params.params, enabled: params.enabled }));
17693
+ if (params.index !== void 0) check2(controller.moveEffect(params.nodeId, params.effectId, params.index));
16912
17694
  return { effects: controller.getNode(params.nodeId)?.effects ?? [] };
16913
17695
  }
16914
17696
  },
@@ -16921,7 +17703,7 @@ function createVectorCommands(controller, options = {}) {
16921
17703
  params: { type: "object", properties: { nodeId: { type: "string" }, effectId: { type: "string" } }, required: ["nodeId", "effectId"] },
16922
17704
  undoable: true,
16923
17705
  run: (_ctx, params) => {
16924
- check(controller.removeEffect(params.nodeId, params.effectId));
17706
+ check2(controller.removeEffect(params.nodeId, params.effectId));
16925
17707
  return { ok: true };
16926
17708
  }
16927
17709
  },
@@ -17058,7 +17840,7 @@ function createVectorCommands(controller, options = {}) {
17058
17840
  undoable: true,
17059
17841
  run: (_ctx, params) => {
17060
17842
  const result = controller.shapeBuilder(params.points.map(([x, y]) => ({ x, y })), { ids: ids(params), erase: params.erase, tolerance: params.tolerance });
17061
- check(result);
17843
+ check2(result);
17062
17844
  return { merged: result.merged, faces: result.faces, lines: result.lines, edges: result.edges };
17063
17845
  }
17064
17846
  },
@@ -17089,7 +17871,7 @@ function createVectorCommands(controller, options = {}) {
17089
17871
  },
17090
17872
  undoable: true,
17091
17873
  run: (_ctx, params) => {
17092
- check(controller.align(params.edge, { ids: ids(params), to: params.to }));
17874
+ check2(controller.align(params.edge, { ids: ids(params), to: params.to }));
17093
17875
  return { ok: true };
17094
17876
  }
17095
17877
  },
@@ -17102,7 +17884,7 @@ function createVectorCommands(controller, options = {}) {
17102
17884
  params: { type: "object", properties: { direction: { type: "string", enum: ["horizontal", "vertical"] }, ids: IDS }, required: ["direction"] },
17103
17885
  undoable: true,
17104
17886
  run: (_ctx, params) => {
17105
- check(controller.distribute(params.direction, { ids: ids(params) }));
17887
+ check2(controller.distribute(params.direction, { ids: ids(params) }));
17106
17888
  return { ok: true };
17107
17889
  }
17108
17890
  },
@@ -17115,7 +17897,7 @@ function createVectorCommands(controller, options = {}) {
17115
17897
  params: { type: "object", properties: { order: { type: "string", enum: ["front", "forward", "backward", "back"] }, ids: IDS }, required: ["order"] },
17116
17898
  undoable: true,
17117
17899
  run: (_ctx, params) => {
17118
- check(controller.arrange(params.order, { ids: ids(params) }));
17900
+ check2(controller.arrange(params.order, { ids: ids(params) }));
17119
17901
  return { ok: true };
17120
17902
  }
17121
17903
  },
@@ -17144,7 +17926,7 @@ function createVectorCommands(controller, options = {}) {
17144
17926
  enabled: enabledFor,
17145
17927
  undoable: true,
17146
17928
  run: (_ctx, params) => {
17147
- check(controller.ungroup({ ids: ids(params) }));
17929
+ check2(controller.ungroup({ ids: ids(params) }));
17148
17930
  return { selection: controller.getState().selection };
17149
17931
  }
17150
17932
  },
@@ -17157,7 +17939,7 @@ function createVectorCommands(controller, options = {}) {
17157
17939
  params: { type: "object", properties: { anchors: ANCHORS, keepShape: { type: "boolean" } } },
17158
17940
  undoable: true,
17159
17941
  run: (_ctx, params) => {
17160
- check(controller.deleteAnchors(anchorsOf(params?.anchors), { keepShape: params?.keepShape }));
17942
+ check2(controller.deleteAnchors(anchorsOf(params?.anchors), { keepShape: params?.keepShape }));
17161
17943
  return { ok: true };
17162
17944
  }
17163
17945
  },
@@ -17174,7 +17956,7 @@ function createVectorCommands(controller, options = {}) {
17174
17956
  },
17175
17957
  undoable: true,
17176
17958
  run: (_ctx, params) => {
17177
- check(controller.moveAnchor({ nodeId: params.nodeId, subpath: params.subpath, index: params.index }, { x: params.x, y: params.y }, { curvature: params.curvature }));
17959
+ check2(controller.moveAnchor({ nodeId: params.nodeId, subpath: params.subpath, index: params.index }, { x: params.x, y: params.y }, { curvature: params.curvature }));
17178
17960
  return { ok: true };
17179
17961
  }
17180
17962
  },
@@ -17198,7 +17980,7 @@ function createVectorCommands(controller, options = {}) {
17198
17980
  },
17199
17981
  undoable: true,
17200
17982
  run: (_ctx, params) => {
17201
- check(controller.bendSegment({ nodeId: params.nodeId, subpath: params.subpath, segment: params.segment }, { x: params.x, y: params.y }, params.t));
17983
+ check2(controller.bendSegment({ nodeId: params.nodeId, subpath: params.subpath, segment: params.segment }, { x: params.x, y: params.y }, params.t));
17202
17984
  return { ok: true };
17203
17985
  }
17204
17986
  },
@@ -17212,7 +17994,7 @@ function createVectorCommands(controller, options = {}) {
17212
17994
  params: { type: "object", properties: { ids: IDS, tolerance: { type: "number", minimum: 0 } } },
17213
17995
  undoable: true,
17214
17996
  run: (_ctx, params) => {
17215
- check(controller.joinPaths({ ids: params?.ids ? ids(params) : void 0, tolerance: params?.tolerance }));
17997
+ check2(controller.joinPaths({ ids: params?.ids ? ids(params) : void 0, tolerance: params?.tolerance }));
17216
17998
  return { selection: controller.getState().selection };
17217
17999
  }
17218
18000
  },
@@ -17226,7 +18008,7 @@ function createVectorCommands(controller, options = {}) {
17226
18008
  params: { type: "object", properties: { axis: { type: "string", enum: ["horizontal", "vertical", "both"] }, anchors: ANCHORS } },
17227
18009
  undoable: true,
17228
18010
  run: (_ctx, params) => {
17229
- check(controller.averageAnchors(params?.axis ?? "both", anchorsOf(params?.anchors)));
18011
+ check2(controller.averageAnchors(params?.axis ?? "both", anchorsOf(params?.anchors)));
17230
18012
  return { ok: true };
17231
18013
  }
17232
18014
  },
@@ -17240,7 +18022,7 @@ function createVectorCommands(controller, options = {}) {
17240
18022
  enabled: enabledFor,
17241
18023
  undoable: true,
17242
18024
  run: (_ctx, params) => {
17243
- check(controller.removeRedundantPoints({ ids: params?.ids ? ids(params) : void 0, tolerance: params?.tolerance }));
18025
+ check2(controller.removeRedundantPoints({ ids: params?.ids ? ids(params) : void 0, tolerance: params?.tolerance }));
17244
18026
  return { ok: true };
17245
18027
  }
17246
18028
  },
@@ -17253,7 +18035,7 @@ function createVectorCommands(controller, options = {}) {
17253
18035
  params: { type: "object", properties: { ids: IDS, amount: { type: "number", minimum: 0, maximum: 1 }, anchors: ANCHORS } },
17254
18036
  undoable: true,
17255
18037
  run: (_ctx, params) => {
17256
- check(controller.smoothPaths({ ids: params?.ids ? ids(params) : void 0, amount: params?.amount, refs: params?.anchors }));
18038
+ check2(controller.smoothPaths({ ids: params?.ids ? ids(params) : void 0, amount: params?.amount, refs: params?.anchors }));
17257
18039
  return { ok: true };
17258
18040
  }
17259
18041
  },
@@ -17273,7 +18055,7 @@ function createVectorCommands(controller, options = {}) {
17273
18055
  if (!created) fail("Nothing to cut there (the end of an open path, or a locked path).");
17274
18056
  return { ids: created };
17275
18057
  }
17276
- check(controller.cutAtAnchors(anchorsOf(params?.anchors)));
18058
+ check2(controller.cutAtAnchors(anchorsOf(params?.anchors)));
17277
18059
  return { ids: controller.getState().selection };
17278
18060
  }
17279
18061
  },
@@ -17305,12 +18087,13 @@ function createVectorCommands(controller, options = {}) {
17305
18087
  return { ids: changed.filter((id) => controller.getNode(id)) };
17306
18088
  }
17307
18089
  },
18090
+ ...distortCommands(),
17308
18091
  {
17309
18092
  ...base,
17310
18093
  id: `${prefix}setEditing`,
17311
18094
  label: "Path Editing Options\u2026",
17312
18095
  keywords: ["settings", "curvature", "pencil", "smoothing", "lazy mouse", "shapes", "eraser"],
17313
- description: "Sets how editing behaves: `keepShapeOnDelete`, `curvatureMoves` (anchor drags bend like the Curvature tool), `pencilSmoothing` (lazy mouse, 0\u20131), `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`).",
18096
+ description: "Sets how editing behaves: `keepShapeOnDelete`, `curvatureMoves` (anchor drags bend like the Curvature tool), `pencilSmoothing` (lazy mouse, 0\u20131), `recognizeShapes` (pencil scribbles become lines, rectangles, ellipses, polygons), `eraserSize`, the Distort Brush settings `distortMode` (smudge, twirl, attract, repel, roughen), `distortSize` (radius) and `distortStrength` (0\u20131), and `pencilPressure` (`pen`: record pen pressure on pencil strokes as a width profile, `all`: any pointer reporting pressure, `off`).",
17314
18097
  params: {
17315
18098
  type: "object",
17316
18099
  properties: {
@@ -17319,12 +18102,15 @@ function createVectorCommands(controller, options = {}) {
17319
18102
  pencilSmoothing: { type: "number", minimum: 0, maximum: 1 },
17320
18103
  recognizeShapes: { type: "boolean" },
17321
18104
  eraserSize: { type: "number", exclusiveMinimum: 0 },
17322
- pencilPressure: { type: "string", enum: ["pen", "all", "off"] }
18105
+ pencilPressure: { type: "string", enum: ["pen", "all", "off"] },
18106
+ distortMode: { type: "string", enum: [...VECTOR_DISTORT_BRUSH_MODES], description: "What the Distort Brush tool does when the user drags it." },
18107
+ distortSize: { type: "number", exclusiveMinimum: 0, description: "Distort Brush radius, document units." },
18108
+ distortStrength: { type: "number", minimum: 0, maximum: 1, description: "Distort Brush strength, 0\u20131." }
17323
18109
  }
17324
18110
  },
17325
18111
  run: (_ctx, params) => {
17326
18112
  const patch = {};
17327
- for (const key of ["keepShapeOnDelete", "curvatureMoves", "pencilSmoothing", "recognizeShapes", "eraserSize", "pencilPressure"]) if (params?.[key] !== void 0) patch[key] = params[key];
18113
+ for (const key of ["keepShapeOnDelete", "curvatureMoves", "pencilSmoothing", "recognizeShapes", "eraserSize", "pencilPressure", "distortMode", "distortSize", "distortStrength"]) if (params?.[key] !== void 0) patch[key] = params[key];
17328
18114
  controller.setEditing(patch);
17329
18115
  return { editing: controller.getState().editing };
17330
18116
  }
@@ -17360,13 +18146,227 @@ function createVectorCommands(controller, options = {}) {
17360
18146
  params: { type: "object", properties: { svg: { type: "string" }, replace: { type: "boolean" } }, required: ["svg"] },
17361
18147
  undoable: true,
17362
18148
  run: (_ctx, params) => {
17363
- check(params.replace ? controller.loadSvg(params.svg) : controller.importSvg(params.svg));
18149
+ check2(params.replace ? controller.loadSvg(params.svg) : controller.importSvg(params.svg));
17364
18150
  return { nodes: controller.getState().document.nodes.length, selection: controller.getState().selection };
17365
18151
  }
17366
18152
  }
17367
18153
  ];
17368
18154
  }
17369
18155
 
18156
+ // src/vector/generate.ts
18157
+ var VECTOR_GENERATED_IMAGE_AS = ["color", "threshold", ...VECTOR_STYLIZE_STYLES];
18158
+ function vectorArtModels(models) {
18159
+ return models.filter((m) => m.media === "image" || m.media === "text");
18160
+ }
18161
+ var listModels = (o) => vectorArtModels(typeof o.models === "function" ? o.models() : o.models);
18162
+ function artNodes(art, as, colors) {
18163
+ if (!art || typeof art !== "object") return "The generator returned nothing usable (expected { svg }, { image } or { png }).";
18164
+ if ("svg" in art && typeof art.svg === "string") {
18165
+ let doc;
18166
+ try {
18167
+ doc = parseVectorSvg(art.svg);
18168
+ } catch (e) {
18169
+ return `The generated SVG could not be read: ${e.message}`;
18170
+ }
18171
+ if (!doc.nodes.length) return "The generated SVG has no shapes the editor can read.";
18172
+ const drawn = unionBounds(doc.nodes.map((n2) => vectorNodeBounds(n2)));
18173
+ const box = doc.width > 0 && doc.height > 0 ? { x: 0, y: 0, width: doc.width, height: doc.height } : drawn;
18174
+ return box ? { nodes: doc.nodes, box } : "The generated SVG is empty.";
18175
+ }
18176
+ let pixels = null;
18177
+ if ("image" in art) pixels = art.image;
18178
+ else if ("png" in art && typeof art.png === "string") {
18179
+ const parsed = parseDataUrl(art.png);
18180
+ if (!parsed || !isPng(parsed.bytes)) return "The generated image is not a PNG data URL.";
18181
+ try {
18182
+ pixels = decodePng(parsed.bytes, { maxPixels: VECTOR_TRACE_MAX_PIXELS });
18183
+ } catch (e) {
18184
+ return `The generated PNG could not be read: ${e.message}`;
18185
+ }
18186
+ }
18187
+ if (!pixels) return "The generator returned nothing usable (expected { svg }, { image } or { png }).";
18188
+ let nodes;
18189
+ try {
18190
+ nodes = as === "color" || as === "threshold" ? traceVectorImage(pixels, { mode: as, colors }) : stylizeVectorImage(pixels, as, { colors });
18191
+ } catch (e) {
18192
+ return e.message;
18193
+ }
18194
+ if (!nodes.length) return "The generated image had nothing to trace (try another `as`).";
18195
+ return { nodes, box: { x: 0, y: 0, width: pixels.width, height: pixels.height } };
18196
+ }
18197
+ function fitted(nodes, box, target) {
18198
+ const k = Math.min(target.width / (box.width || 1), target.height / (box.height || 1));
18199
+ const dx = target.x + (target.width - box.width * k) / 2 - box.x * k;
18200
+ const dy = target.y + (target.height - box.height * k) / 2 - box.y * k;
18201
+ return nodes.map((n2) => transformVectorNode(n2, [k, 0, 0, k, dx, dy]));
18202
+ }
18203
+ function ratioOf(w, h) {
18204
+ const g = (a2, b2) => b2 < 1 ? a2 : g(b2, a2 % b2);
18205
+ const a = Math.max(1, Math.round(w));
18206
+ const b = Math.max(1, Math.round(h));
18207
+ const d = g(a, b);
18208
+ return `${a / d}:${b / d}`;
18209
+ }
18210
+ async function produce(request, model, target, as, colors, options, idempotencyKey, signal) {
18211
+ const plan = planGenerationRequest(request, model, { strategy: "nearest" });
18212
+ if (!plan.ok) return `The request cannot be sent to "${model.id}": ${plan.errors.map((e) => `${e.field}: ${e.message}`).join("; ")}`;
18213
+ let art;
18214
+ try {
18215
+ art = await options.generate(plan.request, { model, plan, idempotencyKey, signal });
18216
+ } catch (e) {
18217
+ return `The generator failed: ${e.message}`;
18218
+ }
18219
+ const made = artNodes(art, as, colors);
18220
+ if (typeof made === "string") return made;
18221
+ return { nodes: fitted(made.nodes, made.box, target), plan };
18222
+ }
18223
+ var inFlight = /* @__PURE__ */ new WeakMap();
18224
+ function once(controller, key, args, fn) {
18225
+ let map = inFlight.get(controller);
18226
+ if (!map) inFlight.set(controller, map = /* @__PURE__ */ new Map());
18227
+ const sig = stableStringify(args);
18228
+ const hit = map.get(key);
18229
+ if (hit) return hit.args === sig ? hit.result : Promise.resolve({ ok: false, message: `idempotencyKey "${key}" was already used with different input: pass a new key for a new request.` });
18230
+ const result = fn().then((r) => {
18231
+ if (!r.ok) map.delete(key);
18232
+ return r;
18233
+ });
18234
+ map.set(key, { args: sig, result });
18235
+ if (map.size > 500) map.delete(map.keys().next().value);
18236
+ return result;
18237
+ }
18238
+ function generateVectorArt(controller, input, options) {
18239
+ const { signal, ...args } = input;
18240
+ return once(controller, input.idempotencyKey, args, async () => {
18241
+ if (typeof input.prompt !== "string" || !input.prompt.trim()) return { ok: false, message: "prompt must be some text describing the art." };
18242
+ if (!input.idempotencyKey) return { ok: false, message: "idempotencyKey is required (any unique string per request)." };
18243
+ const models = listModels(options);
18244
+ if (!models.length) return { ok: false, message: "No model can make vector art (an image model, or a text model that writes SVG)." };
18245
+ const model = input.model ? models.find((m) => m.id === input.model) : models[0];
18246
+ if (!model) return { ok: false, message: `Unknown model "${input.model}": use one of ${models.map((m) => m.id).join(", ")}.` };
18247
+ const as = input.as ?? "color";
18248
+ if (!VECTOR_GENERATED_IMAGE_AS.includes(as)) return { ok: false, message: `as must be one of ${VECTOR_GENERATED_IMAGE_AS.join(", ")}.` };
18249
+ const doc = controller.getState().document;
18250
+ const side = Math.min(doc.width, doc.height) * 0.6;
18251
+ const target = {
18252
+ x: input.x ?? (doc.width - (input.width ?? side)) / 2,
18253
+ y: input.y ?? (doc.height - (input.height ?? side)) / 2,
18254
+ width: input.width ?? side,
18255
+ height: input.height ?? input.width ?? side
18256
+ };
18257
+ if (!(target.width > 0 && target.height > 0)) return { ok: false, message: "width and height must be above 0." };
18258
+ const request = {
18259
+ prompt: input.prompt,
18260
+ mode: "generate",
18261
+ aspectRatio: input.aspectRatio ?? ratioOf(target.width, target.height),
18262
+ ...input.negativePrompt ? { negativePrompt: input.negativePrompt } : {},
18263
+ ...input.seed !== void 0 ? { seed: input.seed } : {}
18264
+ };
18265
+ const made = await produce(request, model, target, as, input.colors ?? 8, options, input.idempotencyKey, signal);
18266
+ if (typeof made === "string") return { ok: false, message: made };
18267
+ const provenance = createProvenance({ rawPrompt: input.prompt, model, plan: made.plan, meta: { as, colors: input.colors ?? 8, box: target } });
18268
+ const group = { id: "generated", type: "group", name: input.name ?? input.prompt.slice(0, 40), generation: provenance, children: made.nodes };
18269
+ const id = controller.addNode(group, { label: "Generate Art" });
18270
+ const added = controller.getNode(id);
18271
+ return { ok: true, id, ids: added.children.map((c) => c.id), adjustments: made.plan.adjustments, model: model.id };
18272
+ });
18273
+ }
18274
+ function regenerateVectorArt(controller, input, options) {
18275
+ const { signal, ...args } = input;
18276
+ return once(controller, input.idempotencyKey, args, async () => {
18277
+ const node = controller.getNode(input.id);
18278
+ const prov = node?.type === "group" ? node.generation : void 0;
18279
+ if (!node || node.type !== "group" || !prov) return { ok: false, message: `"${input.id}" is not generated art (generate some with generate_vector_art).` };
18280
+ if (node.locked) return { ok: false, message: `"${input.id}" is locked; unlock it first.` };
18281
+ const models = listModels(options);
18282
+ const model = models.find((m) => m.id === prov.modelId);
18283
+ if (!model) return { ok: false, message: `The model "${prov.modelId}" it was made with is not available.` };
18284
+ const meta = prov.meta ?? {};
18285
+ const box = vectorNodeBounds(node) ?? meta.box ?? { x: 0, y: 0, width: 100, height: 100 };
18286
+ const prompt = input.prompt ?? prov.rawPrompt;
18287
+ const request = { ...prov.request, prompt, seed: input.seed ?? Math.floor(Math.random() * 2 ** 31) };
18288
+ const made = await produce(request, model, box, meta.as ?? "color", meta.colors ?? 8, options, input.idempotencyKey, signal);
18289
+ if (typeof made === "string") return { ok: false, message: made };
18290
+ const provenance = createProvenance({ rawPrompt: prompt, model, plan: made.plan, meta: { ...meta, box } });
18291
+ const r = controller.replaceGeneratedArt(input.id, made.nodes, provenance);
18292
+ if (!r.ok) return { ok: false, message: r.message };
18293
+ const now = controller.getNode(input.id);
18294
+ return { ok: true, id: input.id, ids: now.children.map((c) => c.id), adjustments: made.plan.adjustments, model: model.id };
18295
+ });
18296
+ }
18297
+ var IMAGE_AS_DESCRIPTION = "How an image result becomes vectors: color (traced, a path per colour; default), threshold (one colour), or stylised as mosaic, halftone, lowPoly or hatch. Ignored for SVG results.";
18298
+ function createVectorGenerationToolProvider(controller, options) {
18299
+ const readOnly = () => {
18300
+ const r = typeof options.readOnly === "function" ? options.readOnly() : options.readOnly;
18301
+ return r ?? false;
18302
+ };
18303
+ const result = (r) => {
18304
+ if (!r.ok) return { isError: true, content: [{ type: "text", text: r.message ?? "Generation failed." }] };
18305
+ return { id: r.id, ids: r.ids, model: r.model, adjustments: r.adjustments };
18306
+ };
18307
+ const list = {
18308
+ name: "list_vector_art_models",
18309
+ title: "List vector art models",
18310
+ description: "Lists the models generate_vector_art can use: { models: [{ id, name, media, aspectRatios, seed }] }. Image models give pictures that are traced or stylised; text models write SVG.",
18311
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
18312
+ annotations: { title: "List vector art models", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
18313
+ handler: () => ({
18314
+ models: listModels(options).map((m) => ({ id: m.id, name: m.name ?? m.id, media: m.media, ...m.aspectRatios ? { aspectRatios: m.aspectRatios } : {}, seed: !!m.seed }))
18315
+ })
18316
+ };
18317
+ const generate = {
18318
+ name: "generate_vector_art",
18319
+ title: "Generate vector art",
18320
+ description: "Generates vector art from a text prompt with a model (list_vector_art_models) and adds it as one editable group fitted into a box on the artboard (one undo step). The request is adjusted to what the model supports. Costs money: pass a unique idempotencyKey; a retry with the same key and arguments returns the first result. Returns { id (the group), ids (its paths), model, adjustments }.",
18321
+ inputSchema: {
18322
+ type: "object",
18323
+ properties: {
18324
+ prompt: { type: "string", minLength: 1, description: 'What to draw, e.g. "a flat-style rocket, three colours".' },
18325
+ model: { type: "string", description: "Model id from list_vector_art_models (default: the first)." },
18326
+ negativePrompt: { type: "string", description: "What to avoid, for models that support it." },
18327
+ seed: { type: "integer", description: "Seed for repeatable results, for models that support it." },
18328
+ x: { type: "number", description: "Left of the box, document units (default: centred)." },
18329
+ y: { type: "number", description: "Top of the box, document units (default: centred)." },
18330
+ width: { type: "number", exclusiveMinimum: 0, description: "Box width, document units (default 60% of the artboard's smaller side)." },
18331
+ height: { type: "number", exclusiveMinimum: 0, description: "Box height, document units (default: the width). The art keeps its proportions inside." },
18332
+ as: { type: "string", enum: [...VECTOR_GENERATED_IMAGE_AS], description: IMAGE_AS_DESCRIPTION },
18333
+ colors: { type: "integer", minimum: 2, maximum: 32, description: "Palette size when tracing or stylising in colour (default 8)." },
18334
+ name: { type: "string", description: "Group name (default: the start of the prompt)." },
18335
+ idempotencyKey: { type: "string", minLength: 1, description: "A unique string per request; reuse it only to retry the same request." }
18336
+ },
18337
+ required: ["prompt", "idempotencyKey"],
18338
+ additionalProperties: false
18339
+ },
18340
+ annotations: { title: "Generate vector art", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
18341
+ handler: async (args, ctx) => result(await generateVectorArt(controller, { ...args, signal: ctx.signal }, options))
18342
+ };
18343
+ const regenerate = {
18344
+ name: "regenerate_vector_art",
18345
+ title: "Regenerate vector art",
18346
+ description: "Generates a generated group's art again (same model and settings, from its provenance) with a new seed (default random) and optionally a new prompt, replacing its paths in place (one undo step). Costs money: pass a unique idempotencyKey. Returns { id, ids, model, adjustments }.",
18347
+ inputSchema: {
18348
+ type: "object",
18349
+ properties: {
18350
+ id: { type: "string", description: "A group generate_vector_art made." },
18351
+ prompt: { type: "string", minLength: 1, description: "A new prompt (default: the one it was made with)." },
18352
+ seed: { type: "integer", description: "Seed (default: a random one)." },
18353
+ idempotencyKey: { type: "string", minLength: 1, description: "A unique string per request; reuse it only to retry the same request." }
18354
+ },
18355
+ required: ["id", "idempotencyKey"],
18356
+ additionalProperties: false
18357
+ },
18358
+ annotations: { title: "Regenerate vector art", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
18359
+ handler: async (args, ctx) => result(await regenerateVectorArt(controller, { ...args, signal: ctx.signal }, options))
18360
+ };
18361
+ return {
18362
+ id: options.id ?? "vector-generation",
18363
+ ...options.namespace ? { namespace: options.namespace } : {},
18364
+ tools: () => readOnly() ? [list] : [list, generate, regenerate],
18365
+ // The write tools come and go with a read-only state that can change.
18366
+ ...typeof options.readOnly === "function" ? { subscribe: (notify) => controller.subscribe(() => notify()) } : {}
18367
+ };
18368
+ }
18369
+
17370
18370
  // src/vector/textPanel.ts
17371
18371
  var VECTOR_TEXT_ALIGN_LABELS = { left: "Left", center: "Centre", right: "Right", justify: "Justify" };
17372
18372
  var VECTOR_TEXT_ALIGN_OPTIONS = VECTOR_TEXT_ALIGNS.map((value) => ({ value, label: VECTOR_TEXT_ALIGN_LABELS[value] }));
@@ -17571,6 +18571,7 @@ export {
17571
18571
  VECTOR_BLEND_MODE_MENU,
17572
18572
  VECTOR_BLEND_SPACINGS,
17573
18573
  VECTOR_BLEND_SPACING_LABELS,
18574
+ VECTOR_BRUSHSTROKE_SCALE,
17574
18575
  VECTOR_BUILDER_HATCH_GAP,
17575
18576
  VECTOR_CHARACTER_STYLE_KEYS,
17576
18577
  VECTOR_CLIPBOARD_TYPE,
@@ -17586,6 +18587,7 @@ export {
17586
18587
  VECTOR_CORNER_KIND_LABELS,
17587
18588
  VECTOR_CORNER_WIDGET_INSET,
17588
18589
  VECTOR_DIMENSIONAL_KINDS,
18590
+ VECTOR_DISTORT_BRUSH_MODES,
17589
18591
  VECTOR_DISTORT_TOLERANCE,
17590
18592
  VECTOR_EDIT_TOGGLES,
17591
18593
  VECTOR_EFFECT_DEFAULTS,
@@ -17601,6 +18603,7 @@ export {
17601
18603
  VECTOR_FILTER_PARAM_SPECS,
17602
18604
  VECTOR_FILTER_TYPES,
17603
18605
  VECTOR_FONT_WEIGHTS,
18606
+ VECTOR_GENERATED_IMAGE_AS,
17604
18607
  VECTOR_GRADIENT_INTERPOLATIONS,
17605
18608
  VECTOR_GRADIENT_INTERPOLATION_LABELS,
17606
18609
  VECTOR_GRADIENT_MAX_PERIODS,
@@ -17642,6 +18645,9 @@ export {
17642
18645
  VECTOR_STROKE_GRADIENT_MODE_LABELS,
17643
18646
  VECTOR_STROKE_SETTING_KEYS,
17644
18647
  VECTOR_STYLE_KEYS,
18648
+ VECTOR_STYLIZE_LABELS,
18649
+ VECTOR_STYLIZE_MAX_SHAPES,
18650
+ VECTOR_STYLIZE_STYLES,
17645
18651
  VECTOR_SYMBOL_MAX_DEPTH,
17646
18652
  VECTOR_SYMMETRY_MODES,
17647
18653
  VECTOR_SYMMETRY_MODE_LABELS,
@@ -17716,6 +18722,7 @@ export {
17716
18722
  createVectorEffect,
17717
18723
  createVectorEffectsToolProvider,
17718
18724
  createVectorFilter,
18725
+ createVectorGenerationToolProvider,
17719
18726
  createVectorMeshGradient,
17720
18727
  createVectorMorph,
17721
18728
  cssFontFamily,
@@ -17725,6 +18732,7 @@ export {
17725
18732
  decomposeRigid,
17726
18733
  defaultVectorGradient,
17727
18734
  distortVectorNode,
18735
+ distortVectorPath,
17728
18736
  dragWarpMeshPoint as dragVectorMeshPoint,
17729
18737
  effectPathLength,
17730
18738
  effectPathVertexCount,
@@ -17751,6 +18759,7 @@ export {
17751
18759
  fitVectorTextRuns,
17752
18760
  foldVectorBooleanOperands,
17753
18761
  formatVectorValue,
18762
+ generateVectorArt,
17754
18763
  generateVectorBlendChildren,
17755
18764
  generateWidthProfile,
17756
18765
  getAnchor,
@@ -17820,6 +18829,7 @@ export {
17820
18829
  isValidVectorParameterName,
17821
18830
  isVectorBlendGroup,
17822
18831
  isVectorBlendMode,
18832
+ isVectorBrushstrokeLayer,
17823
18833
  isVectorConnector,
17824
18834
  isVectorFilterType,
17825
18835
  isVectorFrame,
@@ -17901,6 +18911,7 @@ export {
17901
18911
  rectSubpath,
17902
18912
  refineMorphContour,
17903
18913
  refreshVectorBlends,
18914
+ regenerateVectorArt,
17904
18915
  releaseVectorBlend,
17905
18916
  removeAnchorKeepingShape,
17906
18917
  removeAnchors,
@@ -17934,6 +18945,7 @@ export {
17934
18945
  sampleVectorGradient,
17935
18946
  sampleVectorGradientAt,
17936
18947
  sampleVectorPaintDefAt,
18948
+ sanitizeVectorBrushstroke,
17937
18949
  sanitizeVectorConnector,
17938
18950
  sanitizeVectorEffects,
17939
18951
  sanitizeVectorFills,
@@ -17977,6 +18989,7 @@ export {
17977
18989
  strokePositionPoint,
17978
18990
  strokeWidthProfileOf,
17979
18991
  styleVectorText,
18992
+ stylizeVectorImage,
17980
18993
  subpathEnclosesArea,
17981
18994
  subpathPointAt,
17982
18995
  subpathSegment,
@@ -18011,6 +19024,7 @@ export {
18011
19024
  vectorAppearanceNodes,
18012
19025
  vectorAppearanceOutline,
18013
19026
  vectorAppearanceShapes,
19027
+ vectorArtModels,
18014
19028
  vectorArtboards,
18015
19029
  vectorBevelFaces,
18016
19030
  vectorBlendColorDistance,
@@ -18025,6 +19039,10 @@ export {
18025
19039
  vectorBoundsInside,
18026
19040
  vectorBoxCorners,
18027
19041
  vectorBoxMatrix,
19042
+ vectorBrushstrokeBrush,
19043
+ vectorBrushstrokeBrushes,
19044
+ vectorBrushstrokeImages,
19045
+ vectorBrushstrokeReach,
18028
19046
  vectorBuilderErasedEdges,
18029
19047
  vectorChildAppearanceShapes,
18030
19048
  vectorConnectionSites,
@@ -18041,6 +19059,8 @@ export {
18041
19059
  vectorCorners,
18042
19060
  vectorCornersAttribute,
18043
19061
  vectorDiamondPath,
19062
+ vectorDistortDabMap,
19063
+ vectorDistortFalloff,
18044
19064
  vectorDistortQuad,
18045
19065
  vectorDocumentToLottie,
18046
19066
  vectorDocumentToPdf,
@@ -18083,6 +19103,8 @@ export {
18083
19103
  vectorGradientTAt,
18084
19104
  vectorGradientTargetPath,
18085
19105
  vectorGradientUnitsMatrix,
19106
+ vectorHalftone,
19107
+ vectorHatch,
18086
19108
  homographyFromPoints as vectorHomography,
18087
19109
  homographyKeepsQuad as vectorHomographyKeepsQuad,
18088
19110
  identityWarpMesh as vectorIdentityMesh,
@@ -18092,6 +19114,7 @@ export {
18092
19114
  vectorLensColorMap,
18093
19115
  vectorLensDrawn,
18094
19116
  vectorLiveBooleanSteps,
19117
+ vectorLowPoly,
18095
19118
  vectorMaskContains,
18096
19119
  vectorMaskContentBounds,
18097
19120
  vectorMaskContents,
@@ -18118,6 +19141,7 @@ export {
18118
19141
  warpMeshParamAt as vectorMeshParamAt,
18119
19142
  vectorMeshPointAt,
18120
19143
  warpMeshRows as vectorMeshRows,
19144
+ vectorMosaic,
18121
19145
  vectorNodeBounds,
18122
19146
  vectorNodeEnvelope,
18123
19147
  vectorNodeFilterSvg,