@pixerate/editor 0.22.0 → 0.23.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 (83) hide show
  1. package/README.md +80 -2
  2. package/THIRD_PARTY_NOTICES.md +88 -0
  3. package/dist/agent/index.cjs +83 -108
  4. package/dist/agent/index.cjs.map +1 -1
  5. package/dist/agent/index.d.cts +69 -211
  6. package/dist/agent/index.d.ts +69 -211
  7. package/dist/agent/index.js +76 -101
  8. package/dist/agent/index.js.map +1 -1
  9. package/dist/arrangeShortcuts-B8O3f8-P.d.cts +184 -0
  10. package/dist/arrangeShortcuts-sLlD2rSX.d.ts +184 -0
  11. package/dist/canvas/index.cjs +879 -83
  12. package/dist/canvas/index.cjs.map +1 -1
  13. package/dist/canvas/index.d.cts +291 -186
  14. package/dist/canvas/index.d.ts +291 -186
  15. package/dist/canvas/index.js +865 -69
  16. package/dist/canvas/index.js.map +1 -1
  17. package/dist/chunk-26BVFKHU.cjs +453 -0
  18. package/dist/chunk-26BVFKHU.cjs.map +1 -0
  19. package/dist/chunk-2H7ZHPEJ.js +762 -0
  20. package/dist/chunk-2H7ZHPEJ.js.map +1 -0
  21. package/dist/chunk-7DWMJCAT.js +453 -0
  22. package/dist/chunk-7DWMJCAT.js.map +1 -0
  23. package/dist/chunk-ARRTP33I.js +101 -0
  24. package/dist/chunk-ARRTP33I.js.map +1 -0
  25. package/dist/chunk-CPOKFNBG.cjs +101 -0
  26. package/dist/chunk-CPOKFNBG.cjs.map +1 -0
  27. package/dist/chunk-G2I4QJOT.cjs +762 -0
  28. package/dist/chunk-G2I4QJOT.cjs.map +1 -0
  29. package/dist/{chunk-RU52I7QO.js → chunk-K3R5NG2D.js} +147 -42
  30. package/dist/chunk-K3R5NG2D.js.map +1 -0
  31. package/dist/{chunk-DENSCKFC.js → chunk-QBJ22YZR.js} +423 -30
  32. package/dist/chunk-QBJ22YZR.js.map +1 -0
  33. package/dist/{chunk-SAZRENEZ.cjs → chunk-RIKBJLS2.cjs} +424 -31
  34. package/dist/chunk-RIKBJLS2.cjs.map +1 -0
  35. package/dist/{chunk-6M3BCTBD.cjs → chunk-XDMXXC4Z.cjs} +230 -125
  36. package/dist/chunk-XDMXXC4Z.cjs.map +1 -0
  37. package/dist/commands/index.cjs +860 -0
  38. package/dist/commands/index.cjs.map +1 -0
  39. package/dist/commands/index.d.cts +592 -0
  40. package/dist/commands/index.d.ts +592 -0
  41. package/dist/commands/index.js +860 -0
  42. package/dist/commands/index.js.map +1 -0
  43. package/dist/{controller-B7GIybc4.d.cts → controller-pePCJLT1.d.cts} +78 -5
  44. package/dist/{controller-B7GIybc4.d.ts → controller-pePCJLT1.d.ts} +78 -5
  45. package/dist/history/index.cjs +12 -2
  46. package/dist/history/index.cjs.map +1 -1
  47. package/dist/history/index.d.cts +211 -7
  48. package/dist/history/index.d.ts +211 -7
  49. package/dist/history/index.js +13 -3
  50. package/dist/image-editor/index.cjs +418 -13
  51. package/dist/image-editor/index.cjs.map +1 -1
  52. package/dist/image-editor/index.d.cts +154 -3
  53. package/dist/image-editor/index.d.ts +154 -3
  54. package/dist/image-editor/index.js +417 -12
  55. package/dist/image-editor/index.js.map +1 -1
  56. package/dist/index.cjs +34 -76
  57. package/dist/index.cjs.map +1 -1
  58. package/dist/index.d.cts +5 -42
  59. package/dist/index.d.ts +5 -42
  60. package/dist/index.js +27 -69
  61. package/dist/index.js.map +1 -1
  62. package/dist/registry-D5-R90p0.d.cts +209 -0
  63. package/dist/registry-D5-R90p0.d.ts +209 -0
  64. package/dist/spreadsheet/index.cjs +19 -3
  65. package/dist/spreadsheet/index.cjs.map +1 -1
  66. package/dist/spreadsheet/index.d.cts +67 -9
  67. package/dist/spreadsheet/index.d.ts +67 -9
  68. package/dist/spreadsheet/index.js +18 -2
  69. package/dist/{types-CJ4U-HHI.d.cts → types-DX4Mh4SL.d.cts} +1 -1
  70. package/dist/{types-CJ4U-HHI.d.ts → types-DX4Mh4SL.d.ts} +1 -1
  71. package/dist/types-GT5a5YAX.d.cts +86 -0
  72. package/dist/types-GT5a5YAX.d.ts +86 -0
  73. package/package.json +8 -2
  74. package/dist/chunk-2TWGLC5K.cjs +0 -262
  75. package/dist/chunk-2TWGLC5K.cjs.map +0 -1
  76. package/dist/chunk-6M3BCTBD.cjs.map +0 -1
  77. package/dist/chunk-DENSCKFC.js.map +0 -1
  78. package/dist/chunk-KAADNAPF.js +0 -262
  79. package/dist/chunk-KAADNAPF.js.map +0 -1
  80. package/dist/chunk-RU52I7QO.js.map +0 -1
  81. package/dist/chunk-SAZRENEZ.cjs.map +0 -1
  82. package/dist/types-BZgcDsC5.d.cts +0 -42
  83. package/dist/types-BZgcDsC5.d.ts +0 -42
@@ -1,3 +1,7 @@
1
+ import {
2
+ DEFAULT_ARRANGE_SHORTCUTS,
3
+ matchArrangeShortcut
4
+ } from "../chunk-7DWMJCAT.js";
1
5
  import {
2
6
  DEFAULT_NODE_HEIGHT,
3
7
  DEFAULT_NODE_WIDTH,
@@ -1073,6 +1077,10 @@ function invertNodeUpdates(nodes, updates) {
1073
1077
  }
1074
1078
 
1075
1079
  // src/canvas/guides.ts
1080
+ function snapToGrid(value, spacing, origin = 0) {
1081
+ if (!(spacing > 0) || !Number.isFinite(spacing)) return value;
1082
+ return Math.round((value - origin) / spacing) * spacing + origin;
1083
+ }
1076
1084
  function buildAxis(entries) {
1077
1085
  entries.sort((a, b) => a.value - b.value);
1078
1086
  return {
@@ -1229,8 +1237,10 @@ function computeSnapGuides(moving, targets, options = {}) {
1229
1237
  };
1230
1238
  const sx = snapAxis(true);
1231
1239
  const sy = snapAxis(false);
1232
- const dx = sx.delta ?? 0;
1233
- const dy = sy.delta ?? 0;
1240
+ const grid = options.gridSize ?? 0;
1241
+ const gridDelta = (v, origin) => snapToGrid(v, grid, origin) - v;
1242
+ const dx = sx.delta ?? (grid > 0 ? gridDelta(moving.x, options.gridOrigin?.x ?? 0) : 0);
1243
+ const dy = sy.delta ?? (grid > 0 ? gridDelta(moving.y, options.gridOrigin?.y ?? 0) : 0);
1234
1244
  const snapped = { x: moving.x + dx, y: moving.y + dy, width: moving.width, height: moving.height };
1235
1245
  const guides = [];
1236
1246
  const alignmentGuides = (horizontal) => {
@@ -1244,12 +1254,12 @@ function computeSnapGuides(moving, targets, options = {}) {
1244
1254
  if (useCentres) {
1245
1255
  lines.push({ value: start + size / 2, kind: "centre", axis: horizontal ? index.centreX : index.centreY });
1246
1256
  }
1247
- for (const line of lines) {
1248
- const aligned = matches(line.axis, line.value, eps);
1257
+ for (const line2 of lines) {
1258
+ const aligned = matches(line2.axis, line2.value, eps);
1249
1259
  if (aligned.length === 0) continue;
1250
1260
  const span = boundsOf([snapped, ...aligned]);
1251
1261
  guides.push(
1252
- horizontal ? { orientation: "vertical", kind: line.kind, x1: line.value, y1: span.y, x2: line.value, y2: span.y + span.height } : { orientation: "horizontal", kind: line.kind, x1: span.x, y1: line.value, x2: span.x + span.width, y2: line.value }
1262
+ horizontal ? { orientation: "vertical", kind: line2.kind, x1: line2.value, y1: span.y, x2: line2.value, y2: span.y + span.height } : { orientation: "horizontal", kind: line2.kind, x1: span.x, y1: line2.value, x2: span.x + span.width, y2: line2.value }
1253
1263
  );
1254
1264
  }
1255
1265
  };
@@ -1390,16 +1400,16 @@ function findConnectorSnap(pointer, targets, options = {}) {
1390
1400
  if (pointer.x < rect.x - radius || pointer.x > rect.x + rect.width + radius || pointer.y < rect.y - radius || pointer.y > rect.y + rect.height + radius) {
1391
1401
  continue;
1392
1402
  }
1393
- const inside = pointer.x >= rect.x && pointer.x <= rect.x + rect.width && pointer.y >= rect.y && pointer.y <= rect.y + rect.height;
1403
+ const inside2 = pointer.x >= rect.x && pointer.x <= rect.x + rect.width && pointer.y >= rect.y && pointer.y <= rect.y + rect.height;
1394
1404
  for (const position of target.positions ?? all) {
1395
1405
  const point = options.mode === "edge" ? closestPointOnSide(rect, position, pointer) : handlePoint(rect, position);
1396
1406
  const sidePoint = closestPointOnSide(rect, position, pointer);
1397
1407
  const rank = Math.hypot(pointer.x - sidePoint.x, pointer.y - sidePoint.y);
1398
1408
  const distance = Math.hypot(pointer.x - point.x, pointer.y - point.y);
1399
- if (!inside && rank > radius) continue;
1400
- const better = !best || inside && !best.inside || inside === best.inside && rank < best.rank;
1409
+ if (!inside2 && rank > radius) continue;
1410
+ const better = !best || inside2 && !best.inside || inside2 === best.inside && rank < best.rank;
1401
1411
  if (better) {
1402
- best = { nodeId: target.id, position, side: positionToSide(position), point, distance, inside, rank };
1412
+ best = { nodeId: target.id, position, side: positionToSide(position), point, distance, inside: inside2, rank };
1403
1413
  }
1404
1414
  }
1405
1415
  }
@@ -1662,49 +1672,6 @@ function fitViewportToNodes(nodes, size, ids, options = {}) {
1662
1672
  return bounds ? fitViewportToRect(bounds, size, options) : null;
1663
1673
  }
1664
1674
 
1665
- // src/canvas/arrangeShortcuts.ts
1666
- var DEFAULT_ARRANGE_SHORTCUTS = [
1667
- { action: "alignLeft", code: "KeyA", alt: true, label: "Alt+A" },
1668
- { action: "alignCenter", code: "KeyH", alt: true, label: "Alt+H" },
1669
- { action: "alignRight", code: "KeyD", alt: true, label: "Alt+D" },
1670
- { action: "alignTop", code: "KeyW", alt: true, label: "Alt+W" },
1671
- { action: "alignMiddle", code: "KeyV", alt: true, label: "Alt+V" },
1672
- { action: "alignBottom", code: "KeyS", alt: true, label: "Alt+S" },
1673
- { action: "distributeHorizontal", code: "KeyH", alt: true, shift: true, label: "Alt+Shift+H" },
1674
- { action: "distributeVertical", code: "KeyV", alt: true, shift: true, label: "Alt+Shift+V" },
1675
- { action: "lock", code: "KeyL", mod: true, label: "Mod+L" },
1676
- { action: "unlock", code: "KeyL", mod: true, alt: true, label: "Mod+Alt+L" },
1677
- { action: "bringForward", code: "BracketRight", mod: true, label: "Mod+]" },
1678
- { action: "sendBackward", code: "BracketLeft", mod: true, label: "Mod+[" },
1679
- { action: "bringToFront", code: "BracketRight", mod: true, alt: true, label: "Mod+Alt+]" },
1680
- { action: "sendToBack", code: "BracketLeft", mod: true, alt: true, label: "Mod+Alt+[" },
1681
- { action: "nextScene", code: "PageDown", label: "PageDown" },
1682
- { action: "previousScene", code: "PageUp", label: "PageUp" }
1683
- ];
1684
- var KEY_TO_CODE = {
1685
- "]": "BracketRight",
1686
- "[": "BracketLeft",
1687
- PageDown: "PageDown",
1688
- PageUp: "PageUp"
1689
- };
1690
- function eventCode(event) {
1691
- if (event.code) return event.code;
1692
- if (KEY_TO_CODE[event.key]) return KEY_TO_CODE[event.key];
1693
- if (/^[a-z]$/i.test(event.key)) return `Key${event.key.toUpperCase()}`;
1694
- return event.key;
1695
- }
1696
- function matchArrangeShortcut(event, bindings = DEFAULT_ARRANGE_SHORTCUTS) {
1697
- if (isEventFromTextInput(event)) return null;
1698
- const code = eventCode(event);
1699
- const mod = event.metaKey || event.ctrlKey;
1700
- for (const binding of bindings) {
1701
- if (binding.code === code && !!binding.mod === mod && !!binding.alt === event.altKey && !!binding.shift === event.shiftKey) {
1702
- return binding.action;
1703
- }
1704
- }
1705
- return null;
1706
- }
1707
-
1708
1675
  // src/canvas/polygonBoolean.ts
1709
1676
  function ringSignedArea(ring) {
1710
1677
  let sum = 0;
@@ -1885,9 +1852,9 @@ function splitPass(segments, pool, eps) {
1885
1852
  ]) {
1886
1853
  for (const p of [other.a, other.b]) {
1887
1854
  if (p.id === seg.a.id || p.id === seg.b.id) continue;
1888
- const { t: t2, dist } = project(seg.a, seg.b, p);
1855
+ const { t: t2, dist: dist2 } = project(seg.a, seg.b, p);
1889
1856
  const len = Math.hypot(seg.b.x - seg.a.x, seg.b.y - seg.a.y);
1890
- if (dist <= eps && t2 * len > eps && (1 - t2) * len > eps) {
1857
+ if (dist2 <= eps && t2 * len > eps && (1 - t2) * len > eps) {
1891
1858
  addSplit(split, t2, p);
1892
1859
  touched = true;
1893
1860
  }
@@ -2077,8 +2044,8 @@ function removeCollinear(ring, eps) {
2077
2044
  const dy = nxt[1] - prev[1];
2078
2045
  const len = Math.hypot(dx, dy);
2079
2046
  const cross = (cur[0] - prev[0]) * dy - (cur[1] - prev[1]) * dx;
2080
- const dot = (cur[0] - prev[0]) * dx + (cur[1] - prev[1]) * dy;
2081
- if (len > 0 && Math.abs(cross) / len <= eps && dot > 0 && dot < len * len) {
2047
+ const dot2 = (cur[0] - prev[0]) * dx + (cur[1] - prev[1]) * dy;
2048
+ if (len > 0 && Math.abs(cross) / len <= eps && dot2 > 0 && dot2 < len * len) {
2082
2049
  changed = true;
2083
2050
  continue;
2084
2051
  }
@@ -2174,6 +2141,347 @@ function dividePolygons(operands, options = {}) {
2174
2141
  return pieces;
2175
2142
  }
2176
2143
 
2144
+ // src/canvas/curveFit.ts
2145
+ function formatNumber(value, precision) {
2146
+ const factor = 10 ** precision;
2147
+ const rounded = Math.round(value * factor) / factor;
2148
+ return Object.is(rounded, -0) ? "0" : String(rounded);
2149
+ }
2150
+ var MAX_REPARAM = 6;
2151
+ var sub = (a, b) => [a[0] - b[0], a[1] - b[1]];
2152
+ var add = (a, v, s = 1) => [a[0] + v[0] * s, a[1] + v[1] * s];
2153
+ var dot = (a, b) => a[0] * b[0] + a[1] * b[1];
2154
+ var dist = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1]);
2155
+ function unit(v) {
2156
+ const l = Math.hypot(v[0], v[1]);
2157
+ return l > 1e-12 && Number.isFinite(l) ? [v[0] / l, v[1] / l] : null;
2158
+ }
2159
+ function evalCubic(c, t) {
2160
+ const s = 1 - t;
2161
+ const b0 = s * s * s;
2162
+ const b1 = 3 * s * s * t;
2163
+ const b2 = 3 * s * t * t;
2164
+ const b3 = t * t * t;
2165
+ return [
2166
+ b0 * c.p0[0] + b1 * c.p1[0] + b2 * c.p2[0] + b3 * c.p3[0],
2167
+ b0 * c.p0[1] + b1 * c.p1[1] + b2 * c.p2[1] + b3 * c.p3[1]
2168
+ ];
2169
+ }
2170
+ function cubicPointAt(c, t) {
2171
+ return evalCubic(c, t);
2172
+ }
2173
+ function dedup(points) {
2174
+ const out = [];
2175
+ for (const p of points) {
2176
+ if (!Number.isFinite(p[0]) || !Number.isFinite(p[1])) continue;
2177
+ if (out.length === 0 || dist(out[out.length - 1], p) > 1e-12) out.push(p);
2178
+ }
2179
+ return out;
2180
+ }
2181
+ function chordParams(pts) {
2182
+ const u = [0];
2183
+ let acc = 0;
2184
+ for (let i = 1; i < pts.length; i++) {
2185
+ acc += dist(pts[i - 1], pts[i]);
2186
+ u.push(acc);
2187
+ }
2188
+ return acc > 0 ? u.map((x) => x / acc) : u;
2189
+ }
2190
+ function generate(pts, u, t0, t1) {
2191
+ const p0 = pts[0];
2192
+ const p3 = pts[pts.length - 1];
2193
+ const dirIn = [-t1[0], -t1[1]];
2194
+ let c00 = 0;
2195
+ let c01 = 0;
2196
+ let c11 = 0;
2197
+ let x0 = 0;
2198
+ let x1 = 0;
2199
+ for (let i = 0; i < pts.length; i++) {
2200
+ const t = u[i];
2201
+ const s = 1 - t;
2202
+ const b0 = s * s * s;
2203
+ const b1 = 3 * s * s * t;
2204
+ const b2 = 3 * s * t * t;
2205
+ const b3 = t * t * t;
2206
+ const a0 = [t0[0] * b1, t0[1] * b1];
2207
+ const a1 = [dirIn[0] * b2, dirIn[1] * b2];
2208
+ c00 += dot(a0, a0);
2209
+ c01 += dot(a0, a1);
2210
+ c11 += dot(a1, a1);
2211
+ const tmp = [
2212
+ pts[i][0] - (p0[0] * (b0 + b1) + p3[0] * (b2 + b3)),
2213
+ pts[i][1] - (p0[1] * (b0 + b1) + p3[1] * (b2 + b3))
2214
+ ];
2215
+ x0 += dot(a0, tmp);
2216
+ x1 += dot(a1, tmp);
2217
+ }
2218
+ const det = c00 * c11 - c01 * c01;
2219
+ const seg = dist(p0, p3);
2220
+ const eps = 1e-6 * seg;
2221
+ let al = seg / 3;
2222
+ let ar = seg / 3;
2223
+ if (Math.abs(det) > 1e-12) {
2224
+ al = (x0 * c11 - x1 * c01) / det;
2225
+ ar = (c00 * x1 - c01 * x0) / det;
2226
+ }
2227
+ if (!Number.isFinite(al) || !Number.isFinite(ar) || al < eps || ar < eps) {
2228
+ al = seg / 3;
2229
+ ar = seg / 3;
2230
+ }
2231
+ const cap = seg * 4 + 1e-9;
2232
+ al = Math.min(al, cap);
2233
+ ar = Math.min(ar, cap);
2234
+ return { p0, p1: add(p0, t0, al), p2: add(p3, dirIn, ar), p3 };
2235
+ }
2236
+ function maxError(pts, c, u) {
2237
+ let e = 0;
2238
+ let idx = pts.length >> 1;
2239
+ for (let i = 0; i < pts.length; i++) {
2240
+ const d = dist(evalCubic(c, u[i]), pts[i]);
2241
+ if (d > e) {
2242
+ e = d;
2243
+ idx = i;
2244
+ }
2245
+ }
2246
+ return [e, idx];
2247
+ }
2248
+ function newton(c, p, t) {
2249
+ const s = 1 - t;
2250
+ const q = evalCubic(c, t);
2251
+ const d1 = [
2252
+ 3 * (s * s * (c.p1[0] - c.p0[0]) + 2 * s * t * (c.p2[0] - c.p1[0]) + t * t * (c.p3[0] - c.p2[0])),
2253
+ 3 * (s * s * (c.p1[1] - c.p0[1]) + 2 * s * t * (c.p2[1] - c.p1[1]) + t * t * (c.p3[1] - c.p2[1]))
2254
+ ];
2255
+ const d2 = [
2256
+ 6 * (s * (c.p2[0] - 2 * c.p1[0] + c.p0[0]) + t * (c.p3[0] - 2 * c.p2[0] + c.p1[0])),
2257
+ 6 * (s * (c.p2[1] - 2 * c.p1[1] + c.p0[1]) + t * (c.p3[1] - 2 * c.p2[1] + c.p1[1]))
2258
+ ];
2259
+ const diff = [q[0] - p[0], q[1] - p[1]];
2260
+ const num = dot(diff, d1);
2261
+ const den = dot(d1, d1) + dot(diff, d2);
2262
+ if (Math.abs(den) < 1e-12) return t;
2263
+ const nt = t - num / den;
2264
+ return Number.isFinite(nt) ? Math.min(1, Math.max(0, nt)) : t;
2265
+ }
2266
+ function fitSingleFrom(pts, u, t0, t1) {
2267
+ let best = generate(pts, u, t0, t1);
2268
+ let [bestErr, bestSplit] = maxError(pts, best, u);
2269
+ const params = u.slice();
2270
+ for (let round = 0; round < MAX_REPARAM; round++) {
2271
+ for (let i = 0; i < params.length; i++) params[i] = newton(best, pts[i], params[i]);
2272
+ const c = generate(pts, params, t0, t1);
2273
+ const [e, s] = maxError(pts, c, params);
2274
+ if (e < bestErr) {
2275
+ best = c;
2276
+ bestErr = e;
2277
+ bestSplit = s;
2278
+ } else break;
2279
+ }
2280
+ return [best, bestErr, bestSplit];
2281
+ }
2282
+ function fitRec(pts, t0, t1, tol, out, depth) {
2283
+ const n = pts.length;
2284
+ if (n === 2) {
2285
+ const d = dist(pts[0], pts[1]) / 3;
2286
+ out.push({ p0: pts[0], p1: add(pts[0], t0, d), p2: add(pts[1], t1, -d), p3: pts[1] });
2287
+ return;
2288
+ }
2289
+ const [c, err, splitAt] = fitSingleFrom(pts, chordParams(pts), t0, t1);
2290
+ if (err <= tol || depth > 40) {
2291
+ out.push(c);
2292
+ return;
2293
+ }
2294
+ const split = Math.min(n - 2, Math.max(1, splitAt));
2295
+ const tc = unit(sub(pts[split + 1], pts[split - 1])) ?? t0;
2296
+ fitRec(pts.slice(0, split + 1), t0, tc, tol, out, depth + 1);
2297
+ fitRec(pts.slice(split), tc, t1, tol, out, depth + 1);
2298
+ }
2299
+ function tangentFrom(a, b, c) {
2300
+ const chord = unit(sub(b, a));
2301
+ if (!c) return chord;
2302
+ const d1 = dist(a, b);
2303
+ const d2 = dist(b, c);
2304
+ if (!(d1 > 0) || !(d2 > 0)) return chord;
2305
+ const ka = -(2 * d1 + d2) / (d1 * (d1 + d2));
2306
+ const kb = (d1 + d2) / (d1 * d2);
2307
+ const kc = -d1 / (d2 * (d1 + d2));
2308
+ const est = unit([ka * a[0] + kb * b[0] + kc * c[0], ka * a[1] + kb * b[1] + kc * c[1]]);
2309
+ return est && chord && dot(est, chord) > Math.cos(Math.PI / 4) ? est : chord;
2310
+ }
2311
+ function fitCubicRun(points, tolerance, t0, t1) {
2312
+ const pts = dedup(points);
2313
+ const out = [];
2314
+ if (pts.length < 2) return out;
2315
+ const n = pts.length;
2316
+ const start = (t0 && unit([t0[0], t0[1]])) ?? tangentFrom(pts[0], pts[1], pts[2]) ?? [1, 0];
2317
+ const back = tangentFrom(pts[n - 1], pts[n - 2], pts[n - 3]);
2318
+ const end = (t1 && unit([t1[0], t1[1]])) ?? (back ? [-back[0], -back[1]] : [1, 0]);
2319
+ fitRec(pts, start, end, Math.max(tolerance, 1e-9), out, 0);
2320
+ return out;
2321
+ }
2322
+ function turnDeg(a, b, c) {
2323
+ const u = unit(sub(b, a));
2324
+ const v = unit(sub(c, b));
2325
+ if (!u || !v) return 0;
2326
+ return Math.acos(Math.min(1, Math.max(-1, dot(u, v)))) * 180 / Math.PI;
2327
+ }
2328
+ function findCorners(pts, closed, cornerAngle, span) {
2329
+ const n = pts.length;
2330
+ const at = (i) => pts[closed ? (i % n + n) % n : i];
2331
+ const reach = (i, step) => {
2332
+ for (let k = 1; k < (closed ? n : Infinity); k++) {
2333
+ const j = i + step * k;
2334
+ if (!closed && (j < 0 || j >= n)) return k > 1 ? j - step : null;
2335
+ if (dist(at(j), pts[i]) >= span) return j;
2336
+ }
2337
+ return null;
2338
+ };
2339
+ const turns = new Float64Array(n);
2340
+ const windows = [];
2341
+ for (let i = 0; i < n; i++) {
2342
+ windows.push([i, i]);
2343
+ if (!closed && (i === 0 || i === n - 1)) continue;
2344
+ const prev = reach(i, -1);
2345
+ const next = reach(i, 1);
2346
+ if (prev === null || next === null) continue;
2347
+ windows[i] = [prev, next];
2348
+ turns[i] = turnDeg(at(prev), pts[i], at(next));
2349
+ }
2350
+ const corners = [];
2351
+ for (let i = 0; i < n; i++) {
2352
+ if (!(turns[i] > cornerAngle)) continue;
2353
+ const [lo, hi] = windows[i];
2354
+ let sharpest = true;
2355
+ for (let j = lo; j <= hi && sharpest; j++) {
2356
+ const k = closed ? (j % n + n) % n : j;
2357
+ if (k === i || dist(pts[k], pts[i]) >= span) continue;
2358
+ if (turns[k] > turns[i] || turns[k] === turns[i] && k < i) sharpest = false;
2359
+ }
2360
+ if (sharpest) corners.push(i);
2361
+ }
2362
+ return corners;
2363
+ }
2364
+ function densify(run) {
2365
+ if (run.length < 3) return run;
2366
+ const lengths = [];
2367
+ for (let i = 1; i < run.length; i++) lengths.push(dist(run[i - 1], run[i]));
2368
+ const sorted = [...lengths].sort((a, b) => a - b);
2369
+ const median = sorted[sorted.length >> 1];
2370
+ if (!(median > 0)) return run;
2371
+ const out = [run[0]];
2372
+ for (let i = 1; i < run.length; i++) {
2373
+ const len = lengths[i - 1];
2374
+ if (len > 2 * median) {
2375
+ const steps = Math.min(64, Math.ceil(len / median));
2376
+ const a = run[i - 1];
2377
+ const b = run[i];
2378
+ for (let k = 1; k < steps; k++) {
2379
+ const t = k / steps;
2380
+ out.push([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]);
2381
+ }
2382
+ }
2383
+ out.push(run[i]);
2384
+ }
2385
+ return out;
2386
+ }
2387
+ function fitCubics(points, tolerance, options = {}) {
2388
+ let pts = dedup(points);
2389
+ const closed = options.closed === true;
2390
+ if (closed && pts.length > 1 && dist(pts[0], pts[pts.length - 1]) <= 1e-12) pts = pts.slice(0, -1);
2391
+ if (pts.length < 2) return [];
2392
+ const tol = Math.max(tolerance, 1e-6);
2393
+ const cornerAngle = options.cornerAngleDeg ?? 30;
2394
+ const n = pts.length;
2395
+ const span = 4 * tol;
2396
+ if (!closed || n < 3) {
2397
+ const corners2 = [0, ...n > 2 ? findCorners(pts, false, cornerAngle, span) : [], n - 1];
2398
+ const out2 = [];
2399
+ for (let k = 1; k < corners2.length; k++) {
2400
+ const run = pts.slice(corners2[k - 1], corners2[k] + 1);
2401
+ out2.push(...fitCubicRun(densify(run), tol));
2402
+ }
2403
+ if (closed && n === 2) out2.push(...fitCubicRun([pts[1], pts[0]], tol));
2404
+ return out2;
2405
+ }
2406
+ const corners = findCorners(pts, true, cornerAngle, span);
2407
+ if (corners.length === 0) {
2408
+ const ring = [...pts, pts[0]];
2409
+ const t = unit(sub(pts[1], pts[n - 1])) ?? [1, 0];
2410
+ return fitCubicRun(densify(ring), tol, t, t);
2411
+ }
2412
+ const out = [];
2413
+ for (let k = 0; k < corners.length; k++) {
2414
+ const from = corners[k];
2415
+ const to = corners[(k + 1) % corners.length];
2416
+ const run = [];
2417
+ for (let i = from; ; i = (i + 1) % n) {
2418
+ run.push(pts[i]);
2419
+ if (run.length > 1 && i === to) break;
2420
+ }
2421
+ out.push(...fitCubicRun(densify(run), tol));
2422
+ }
2423
+ return out;
2424
+ }
2425
+ function isStraightCubic(c, tol) {
2426
+ const chord = sub(c.p3, c.p0);
2427
+ const len = Math.hypot(chord[0], chord[1]);
2428
+ if (len < 1e-12) return dist(c.p1, c.p0) <= tol && dist(c.p2, c.p0) <= tol;
2429
+ const dir = [chord[0] / len, chord[1] / len];
2430
+ return [c.p1, c.p2].every((h) => {
2431
+ const v = sub(h, c.p0);
2432
+ const along = dot(v, dir);
2433
+ const across = Math.abs(v[0] * dir[1] - v[1] * dir[0]);
2434
+ return across <= tol && along >= -tol && along <= len + tol;
2435
+ });
2436
+ }
2437
+ function cubicsToPathData(segments, options = {}) {
2438
+ if (segments.length === 0) return "";
2439
+ const f = (n) => formatNumber(n, options.precision ?? 3);
2440
+ const dx = options.offset?.x ?? 0;
2441
+ const dy = options.offset?.y ?? 0;
2442
+ const pt = (p) => `${f(p[0] + dx)} ${f(p[1] + dy)}`;
2443
+ const straight = options.straightTolerance ?? 1e-3;
2444
+ let d = `M${pt(segments[0].p0)}`;
2445
+ segments.forEach((c, i) => {
2446
+ const last = options.closed && i === segments.length - 1;
2447
+ if (isStraightCubic(c, straight)) {
2448
+ if (!last) d += `L${pt(c.p3)}`;
2449
+ } else d += `C${pt(c.p1)} ${pt(c.p2)} ${pt(c.p3)}`;
2450
+ });
2451
+ return options.closed ? `${d}Z` : d;
2452
+ }
2453
+ function polylineToPathData(points, closed, options) {
2454
+ if (points.length < 2) return "";
2455
+ const f = (n) => formatNumber(n, options.precision ?? 3);
2456
+ const dx = options.offset?.x ?? 0;
2457
+ const dy = options.offset?.y ?? 0;
2458
+ let d = `M${f(points[0][0] + dx)} ${f(points[0][1] + dy)}`;
2459
+ for (let i = 1; i < points.length; i++) d += `L${f(points[i][0] + dx)} ${f(points[i][1] + dy)}`;
2460
+ return closed ? `${d}Z` : d;
2461
+ }
2462
+ function fitPathData(ring, options = {}) {
2463
+ const closed = options.closed ?? true;
2464
+ const tolerance = options.tolerance ?? 0.5;
2465
+ const pts = dedup(ring);
2466
+ const segments = fitCubics(pts, tolerance, { ...options, closed });
2467
+ if (segments.length === 0) return "";
2468
+ const edges = closed ? pts.length : pts.length - 1;
2469
+ if (options.noWorse !== false) {
2470
+ const straight = segments.filter((c) => isStraightCubic(c, tolerance * 0.02)).length;
2471
+ if (segments.length >= edges && straight === segments.length) return polylineToPathData(pts, closed, options);
2472
+ if (segments.length > edges) return polylineToPathData(pts, closed, options);
2473
+ }
2474
+ return cubicsToPathData(segments, {
2475
+ precision: options.precision,
2476
+ offset: options.offset,
2477
+ closed,
2478
+ straightTolerance: tolerance * 0.02
2479
+ });
2480
+ }
2481
+ function fitFreehandPath(points, options = {}) {
2482
+ return fitPathData(points, { tolerance: 1, ...options, closed: false });
2483
+ }
2484
+
2177
2485
  // src/canvas/shapeGeometry.ts
2178
2486
  var SHAPE_KINDS = ["rect", "ellipse", "polygon", "star", "path"];
2179
2487
  function isShapeDescription(value) {
@@ -2540,7 +2848,7 @@ function rotationTransform(shape, size) {
2540
2848
  }
2541
2849
  function getShapeSvg(shape, size) {
2542
2850
  const { width: w, height: h } = size;
2543
- const f = (n) => formatNumber(n, 3);
2851
+ const f = (n) => formatNumber2(n, 3);
2544
2852
  const transforms = [];
2545
2853
  if (shape.rotation) transforms.push(`rotate(${f(shape.rotation)} ${f(w / 2)} ${f(h / 2)})`);
2546
2854
  let d = "";
@@ -2589,7 +2897,7 @@ function getShapeRenderData(shape, size = {}) {
2589
2897
  const height = size.height && size.height > 0 ? size.height : vb ? vb[3] : 100;
2590
2898
  const svg = getShapeSvg(shape, { width, height });
2591
2899
  const data = {
2592
- viewBox: `0 0 ${formatNumber(width, 3)} ${formatNumber(height, 3)}`,
2900
+ viewBox: `0 0 ${formatNumber2(width, 3)} ${formatNumber2(height, 3)}`,
2593
2901
  width,
2594
2902
  height,
2595
2903
  d: svg.d,
@@ -2636,7 +2944,7 @@ function shapeToPolygon(shape, size, options = {}) {
2636
2944
  if (rotate) rings = rings.map((ring) => ring.map(rotate));
2637
2945
  return { rings, fillRule };
2638
2946
  }
2639
- function formatNumber(value, precision = 3) {
2947
+ function formatNumber2(value, precision = 3) {
2640
2948
  const factor = 10 ** precision;
2641
2949
  const rounded = Math.round(value * factor) / factor;
2642
2950
  return Object.is(rounded, -0) ? "0" : String(rounded);
@@ -2670,9 +2978,9 @@ function rdp(points, tolerance) {
2670
2978
  let maxDist = -1;
2671
2979
  let index = -1;
2672
2980
  for (let i = s + 1; i < e; i++) {
2673
- const dist = segmentDistance(points[i], points[s], points[e]);
2674
- if (dist > maxDist) {
2675
- maxDist = dist;
2981
+ const dist2 = segmentDistance(points[i], points[s], points[e]);
2982
+ if (dist2 > maxDist) {
2983
+ maxDist = dist2;
2676
2984
  index = i;
2677
2985
  }
2678
2986
  }
@@ -2688,9 +2996,9 @@ function simplifyRing(ring, tolerance) {
2688
2996
  let far = 0;
2689
2997
  let farDist = -1;
2690
2998
  for (let i = 1; i < ring.length; i++) {
2691
- const dist = Math.hypot(ring[i][0] - ring[0][0], ring[i][1] - ring[0][1]);
2692
- if (dist > farDist) {
2693
- farDist = dist;
2999
+ const dist2 = Math.hypot(ring[i][0] - ring[0][0], ring[i][1] - ring[0][1]);
3000
+ if (dist2 > farDist) {
3001
+ farDist = dist2;
2694
3002
  far = i;
2695
3003
  }
2696
3004
  }
@@ -2701,20 +3009,31 @@ function simplifyRing(ring, tolerance) {
2701
3009
  }
2702
3010
  function polygonToSvgPath(input, options = {}) {
2703
3011
  const precision = options.precision ?? 3;
2704
- const f = (n) => formatNumber(n, precision);
3012
+ const f = (n) => formatNumber2(n, precision);
2705
3013
  const dx = options.offset?.x ?? 0;
2706
3014
  const dy = options.offset?.y ?? 0;
2707
3015
  const rings = [];
3016
+ const fit = options.curves === "fit";
3017
+ let fitted = "";
2708
3018
  for (const item of input) {
2709
3019
  const list = item.length > 0 && Array.isArray(item[0]) && Array.isArray(item[0][0]) ? item : [item];
2710
3020
  for (const ring of list) {
2711
3021
  let r = ring;
2712
3022
  if (options.simplify && options.simplify > 0) r = simplifyRing(r, options.simplify);
2713
3023
  if (r.length < 3) continue;
3024
+ if (fit) {
3025
+ fitted += fitPathData(r, {
3026
+ tolerance: options.curveTolerance ?? 0.25,
3027
+ cornerAngleDeg: options.cornerAngleDeg,
3028
+ precision,
3029
+ offset: { x: dx, y: dy }
3030
+ });
3031
+ continue;
3032
+ }
2714
3033
  rings.push(dx || dy ? r.map(([x, y]) => [x + dx, y + dy]) : r);
2715
3034
  }
2716
3035
  }
2717
- return ringsToPath(rings, f);
3036
+ return fit ? fitted : ringsToPath(rings, f);
2718
3037
  }
2719
3038
 
2720
3039
  // src/canvas/shapeNodes.ts
@@ -2792,7 +3111,10 @@ function combineShapeNodes(nodes, ids, operation, options = {}) {
2792
3111
  const d = polygonToSvgPath(polygon, {
2793
3112
  precision: options.precision,
2794
3113
  simplify: options.simplify,
2795
- offset: { x: -bounds.minX, y: -bounds.minY }
3114
+ offset: { x: -bounds.minX, y: -bounds.minY },
3115
+ curves: options.curves ?? "fit",
3116
+ curveTolerance: options.curveTolerance ?? options.tolerance ?? 0.25,
3117
+ cornerAngleDeg: options.cornerAngleDeg
2796
3118
  });
2797
3119
  if (!d) return null;
2798
3120
  const sourceShape = getNodeShape(source);
@@ -2876,12 +3198,206 @@ function revertShapeCombination(nodes, change, previous) {
2876
3198
  return out;
2877
3199
  }
2878
3200
 
3201
+ // src/canvas/resizeSnap.ts
3202
+ var RESIZE_HANDLES = [
3203
+ "top-left",
3204
+ "top",
3205
+ "top-right",
3206
+ "right",
3207
+ "bottom-right",
3208
+ "bottom",
3209
+ "bottom-left",
3210
+ "left"
3211
+ ];
3212
+ function resizeHandleAxes(handle) {
3213
+ const x = handle.includes("left") ? -1 : handle.includes("right") ? 1 : 0;
3214
+ const y = handle.includes("top") ? -1 : handle.includes("bottom") ? 1 : 0;
3215
+ return { x, y };
3216
+ }
3217
+ function inferResizeHandle(start, next, eps = 1e-6) {
3218
+ const moved = (a, b) => Math.abs(a - b) > eps;
3219
+ const left = moved(start.x, next.x);
3220
+ const right = moved(start.x + start.width, next.x + next.width);
3221
+ const top = moved(start.y, next.y);
3222
+ const bottom = moved(start.y + start.height, next.y + next.height);
3223
+ const h = left ? "left" : right ? "right" : "";
3224
+ const v = top ? "top" : bottom ? "bottom" : "";
3225
+ if (h && v) return `${v}-${h}`;
3226
+ return h || v || null;
3227
+ }
3228
+ function within(axis, v, tolerance) {
3229
+ const { values } = axis;
3230
+ let lo = 0;
3231
+ let hi = values.length;
3232
+ while (lo < hi) {
3233
+ const mid = lo + hi >>> 1;
3234
+ if (values[mid] < v - tolerance) lo = mid + 1;
3235
+ else hi = mid;
3236
+ }
3237
+ const out = [];
3238
+ for (let i = lo; i < values.length && values[i] <= v + tolerance; i++) out.push(values[i]);
3239
+ return out;
3240
+ }
3241
+ function matching(axis, v, eps) {
3242
+ const { values, rects } = axis;
3243
+ const out = [];
3244
+ for (let i = 0; i < values.length; i++) if (Math.abs(values[i] - v) <= eps) out.push(rects[i]);
3245
+ return out;
3246
+ }
3247
+ function snapResize(rect, handle, targets, options = {}) {
3248
+ const index = Array.isArray(targets) ? createGuideIndex(targets) : targets;
3249
+ const zoom = options.zoom && options.zoom > 0 ? options.zoom : 1;
3250
+ const tolerance = (options.threshold ?? 6) / zoom;
3251
+ const eps = 0.5 / zoom;
3252
+ const useEdges = options.edges !== false;
3253
+ const useCentres = options.centres !== false;
3254
+ const grid = options.gridSize ?? 0;
3255
+ const axes = resizeHandleAxes(handle);
3256
+ const unchanged = { rect: { ...rect }, guides: [], snappedX: false, snappedY: false, changed: false };
3257
+ if (![rect.x, rect.y, rect.width, rect.height].every(Number.isFinite)) return unchanged;
3258
+ const x0 = rect.x;
3259
+ const x1 = rect.x + rect.width;
3260
+ const y0 = rect.y;
3261
+ const y1 = rect.y + rect.height;
3262
+ const bestHit = (horizontal) => {
3263
+ const dir = horizontal ? axes.x : axes.y;
3264
+ if (dir === 0) return null;
3265
+ const moving = horizontal ? dir < 0 ? x0 : x1 : dir < 0 ? y0 : y1;
3266
+ const fixed = horizontal ? dir < 0 ? x1 : x0 : dir < 0 ? y1 : y0;
3267
+ const min = (horizontal ? options.minWidth : options.minHeight) ?? 0;
3268
+ const valid = (t) => {
3269
+ const size = dir < 0 ? fixed - t : t - fixed;
3270
+ return Number.isFinite(size) && size > 1e-9 && size >= min;
3271
+ };
3272
+ let best = null;
3273
+ const consider = (values, kind) => {
3274
+ for (const t of values) {
3275
+ if (!valid(t)) continue;
3276
+ if (!best || Math.abs(t - moving) < Math.abs(best.value - moving)) best = { value: t, kind };
3277
+ }
3278
+ };
3279
+ if (useEdges) consider(within(horizontal ? index.edgeX : index.edgeY, moving, tolerance), "edge");
3280
+ if (useCentres) consider(within(horizontal ? index.centreX : index.centreY, moving, tolerance), "centre");
3281
+ if (!best && grid > 0) {
3282
+ const origin = horizontal ? options.gridOrigin?.x ?? 0 : options.gridOrigin?.y ?? 0;
3283
+ consider([snapToGrid(moving, grid, origin)], "grid");
3284
+ }
3285
+ return best;
3286
+ };
3287
+ let nx0 = x0;
3288
+ let nx1 = x1;
3289
+ let ny0 = y0;
3290
+ let ny1 = y1;
3291
+ const setEdge = (horizontal, value) => {
3292
+ const dir = horizontal ? axes.x : axes.y;
3293
+ if (horizontal) {
3294
+ if (dir < 0) nx0 = value;
3295
+ else nx1 = value;
3296
+ } else if (dir < 0) ny0 = value;
3297
+ else ny1 = value;
3298
+ };
3299
+ let hitX = bestHit(true);
3300
+ let hitY = bestHit(false);
3301
+ const ratio = rect.height > 0 ? rect.width / rect.height : 0;
3302
+ if (options.keepAspectRatio && ratio > 0 && (hitX || hitY)) {
3303
+ if (axes.x !== 0 && axes.y !== 0) {
3304
+ const dist2 = (h, horizontal) => h ? Math.abs(h.value - (horizontal ? axes.x < 0 ? x0 : x1 : axes.y < 0 ? y0 : y1)) : Infinity;
3305
+ const useX = dist2(hitX, true) <= dist2(hitY, false);
3306
+ if (useX && hitX) {
3307
+ setEdge(true, hitX.value);
3308
+ const height = (nx1 - nx0) / ratio;
3309
+ if (axes.y < 0) ny0 = ny1 - height;
3310
+ else ny1 = ny0 + height;
3311
+ hitY = null;
3312
+ } else if (hitY) {
3313
+ setEdge(false, hitY.value);
3314
+ const width = (ny1 - ny0) * ratio;
3315
+ if (axes.x < 0) nx0 = nx1 - width;
3316
+ else nx1 = nx0 + width;
3317
+ hitX = null;
3318
+ }
3319
+ const min = (v) => v ?? 0;
3320
+ if (nx1 - nx0 < min(options.minWidth) || ny1 - ny0 < min(options.minHeight) || !(nx1 > nx0 && ny1 > ny0)) {
3321
+ return unchanged;
3322
+ }
3323
+ } else if (axes.x !== 0 && hitX) {
3324
+ setEdge(true, hitX.value);
3325
+ const height = (nx1 - nx0) / ratio;
3326
+ const cy = (y0 + y1) / 2;
3327
+ ny0 = cy - height / 2;
3328
+ ny1 = cy + height / 2;
3329
+ } else if (axes.y !== 0 && hitY) {
3330
+ setEdge(false, hitY.value);
3331
+ const width = (ny1 - ny0) * ratio;
3332
+ const cx = (x0 + x1) / 2;
3333
+ nx0 = cx - width / 2;
3334
+ nx1 = cx + width / 2;
3335
+ }
3336
+ } else {
3337
+ if (hitX) setEdge(true, hitX.value);
3338
+ if (hitY) setEdge(false, hitY.value);
3339
+ }
3340
+ const out = { x: nx0, y: ny0, width: nx1 - nx0, height: ny1 - ny0 };
3341
+ const guides = [];
3342
+ const guideFor = (horizontal, hit) => {
3343
+ if (!hit || hit.kind === "grid") return;
3344
+ const axis = hit.kind === "edge" ? horizontal ? index.edgeX : index.edgeY : horizontal ? index.centreX : index.centreY;
3345
+ const aligned = matching(axis, hit.value, eps);
3346
+ const span = boundsOf([out, ...aligned]);
3347
+ guides.push(
3348
+ horizontal ? { orientation: "vertical", kind: hit.kind, x1: hit.value, y1: span.y, x2: hit.value, y2: span.y + span.height } : { orientation: "horizontal", kind: hit.kind, x1: span.x, y1: hit.value, x2: span.x + span.width, y2: hit.value }
3349
+ );
3350
+ };
3351
+ guideFor(true, hitX);
3352
+ guideFor(false, hitY);
3353
+ const changed = out.x !== rect.x || out.y !== rect.y || out.width !== rect.width || out.height !== rect.height;
3354
+ return {
3355
+ rect: out,
3356
+ guides,
3357
+ snappedX: !!hitX && hitX.kind !== "grid",
3358
+ snappedY: !!hitY && hitY.kind !== "grid",
3359
+ changed
3360
+ };
3361
+ }
3362
+ function snapNodeResize(nodes, id, rect, handle, options = {}) {
3363
+ const lookup = createNodeLookup(nodes);
3364
+ const node = lookup.get(id);
3365
+ if (!node || isLocked(node)) {
3366
+ return { rect: { ...rect }, guides: [], snappedX: false, snappedY: false, changed: false, updates: [] };
3367
+ }
3368
+ const index = options.index ?? createNodeGuideIndex(nodes, [id], options);
3369
+ const result = snapResize(rect, handle, index, options);
3370
+ if (!result.changed) return { ...result, updates: [] };
3371
+ const parent = node.parentId ? lookup.get(node.parentId) : void 0;
3372
+ const origin = parent ? getAbsolutePosition(parent, lookup) : { x: 0, y: 0 };
3373
+ return {
3374
+ ...result,
3375
+ updates: [
3376
+ {
3377
+ id,
3378
+ position: { x: result.rect.x - origin.x, y: result.rect.y - origin.y },
3379
+ width: result.rect.width,
3380
+ height: result.rect.height
3381
+ }
3382
+ ]
3383
+ };
3384
+ }
3385
+ function getResizingNodeRect(node, lookup, options = {}) {
3386
+ const { x, y } = node.parentId ? getAbsolutePosition(node, lookup) : node.position;
3387
+ const size = getNodeSize(node, options);
3388
+ return { x, y, width: node.width ?? size.width, height: node.height ?? size.height };
3389
+ }
3390
+
2879
3391
  // src/canvas/arrangeController.ts
2880
3392
  function createCanvasArrangeController(options) {
2881
3393
  let guides = [];
2882
3394
  let dragIndex = null;
2883
3395
  let dragKey = "";
2884
3396
  let lastSnapped = /* @__PURE__ */ new Map();
3397
+ let resizeId = null;
3398
+ let resizeStart = null;
3399
+ let resizeIndex = null;
3400
+ let lastResized = null;
2885
3401
  const selectedIds = (ids) => ids ? [...ids] : options.getSelectedIds?.() ?? options.getNodes().filter((n) => n.selected).map((n) => n.id);
2886
3402
  const setGuides = (next) => {
2887
3403
  if (next.length === 0 && guides.length === 0) return;
@@ -3098,6 +3614,150 @@ function createCanvasArrangeController(options) {
3098
3614
  return { ...c, position };
3099
3615
  });
3100
3616
  },
3617
+ /**
3618
+ * Starts resize snapping for node `id`: records its box (to tell which handle is
3619
+ * dragged) and caches the snap targets. Optional: `resize` starts lazily.
3620
+ */
3621
+ beginResize(id) {
3622
+ const nodes = options.getNodes();
3623
+ const lookup = createNodeLookup(nodes);
3624
+ const node = lookup.get(id);
3625
+ resizeId = id;
3626
+ resizeStart = node ? getResizingNodeRect(node, lookup, rect()) : null;
3627
+ resizeIndex = createNodeGuideIndex(nodes, [id], rect());
3628
+ lastResized = null;
3629
+ },
3630
+ /**
3631
+ * Snaps node `id` as currently sized in `getNodes()` (call it after the flow wrote the
3632
+ * new size, e.g. SvelteFlow's `NodeResizer` `onResize`, deferred by a microtask) and
3633
+ * writes the snapped box back through `setNodes` (not recorded in history). `handle`
3634
+ * defaults to the one inferred from the edges that moved since `beginResize`. A locked
3635
+ * node is restored to its starting box.
3636
+ */
3637
+ resize(id, handle) {
3638
+ if (resizeId !== id) controller.beginResize(id);
3639
+ const nodes = options.getNodes();
3640
+ const lookup = createNodeLookup(nodes);
3641
+ const node = lookup.get(id);
3642
+ if (!node || !resizeStart) return null;
3643
+ const proposed = getResizingNodeRect(node, lookup, rect());
3644
+ if (isLocked(node)) {
3645
+ const start = resizeStart;
3646
+ const parent = node.parentId ? lookup.get(node.parentId) : void 0;
3647
+ const origin = parent ? getResizingNodeRect(parent, lookup, rect()) : { x: 0, y: 0 };
3648
+ if (proposed.x !== start.x || proposed.y !== start.y || proposed.width !== start.width || proposed.height !== start.height) {
3649
+ options.setNodes(
3650
+ applyNodeUpdates(nodes, [
3651
+ { id, position: { x: start.x - origin.x, y: start.y - origin.y }, width: start.width, height: start.height }
3652
+ ])
3653
+ );
3654
+ }
3655
+ return null;
3656
+ }
3657
+ if ((options.resizeSnapping ?? options.snapping) === false) return null;
3658
+ const h = handle ?? inferResizeHandle(resizeStart, proposed);
3659
+ if (!h) {
3660
+ setGuides([]);
3661
+ return null;
3662
+ }
3663
+ const result = snapNodeResize(nodes, id, proposed, h, {
3664
+ ...rect(),
3665
+ ...options.guides,
3666
+ ...options.resize,
3667
+ zoom: options.getZoom?.() ?? 1,
3668
+ index: resizeIndex
3669
+ });
3670
+ lastResized = result.updates.length > 0 ? result : null;
3671
+ if (result.updates.length > 0) options.setNodes(applyNodeUpdates(nodes, result.updates));
3672
+ setGuides(result.guides);
3673
+ return result;
3674
+ },
3675
+ /**
3676
+ * Ends resize snapping: clears guides and re-applies the last snapped box if the flow
3677
+ * wrote the raw pointer size afterwards.
3678
+ */
3679
+ endResize() {
3680
+ const last = lastResized;
3681
+ resizeId = null;
3682
+ resizeStart = null;
3683
+ resizeIndex = null;
3684
+ lastResized = null;
3685
+ setGuides([]);
3686
+ const update = last?.updates[0];
3687
+ if (!update) return;
3688
+ const node = options.getNodes().find((n) => n.id === update.id);
3689
+ if (!node) return;
3690
+ const drift = node.position.x !== update.position.x || node.position.y !== update.position.y || node.width !== update.width || node.height !== update.height;
3691
+ if (drift) options.setNodes(applyNodeUpdates(options.getNodes(), [update]));
3692
+ },
3693
+ /**
3694
+ * React Flow style: snaps the `dimensions` (and matching `position`) changes a
3695
+ * `NodeResizer` emits in an `onNodesChange` batch and updates `guides`. The resize-end
3696
+ * change (`resizing: false`) carries the raw pointer size, so the last snapped size is
3697
+ * kept. Resize changes of locked nodes are dropped. Returns the (possibly new) changes.
3698
+ */
3699
+ snapResizeChanges(changes) {
3700
+ const dims = changes.find(
3701
+ (c) => c.type === "dimensions" && typeof c.id === "string" && !!c.dimensions && typeof c.resizing === "boolean"
3702
+ );
3703
+ if (!dims) return changes;
3704
+ const id = dims.id;
3705
+ const nodes = options.getNodes();
3706
+ const lookup = createNodeLookup(nodes);
3707
+ const node = lookup.get(id);
3708
+ const isResizeChange = (c) => c.id === id && (c.type === "dimensions" || c.type === "position");
3709
+ if (node && isLocked(node)) {
3710
+ if (dims.resizing === false) controller.endResize();
3711
+ return changes.filter((c) => !isResizeChange(c));
3712
+ }
3713
+ if (dims.resizing === false) {
3714
+ const last = lastResized?.updates[0];
3715
+ lastResized = null;
3716
+ controller.endResize();
3717
+ if (!last || last.id !== id) return changes;
3718
+ return changes.map(
3719
+ (c) => c === dims ? { ...c, dimensions: { width: last.width, height: last.height } } : c
3720
+ );
3721
+ }
3722
+ if (!node || (options.resizeSnapping ?? options.snapping) === false) return changes;
3723
+ if (resizeId !== id) controller.beginResize(id);
3724
+ if (!resizeStart) return changes;
3725
+ const posChange = changes.find((c) => c.id === id && c.type === "position" && !!c.position);
3726
+ const parent = node.parentId ? lookup.get(node.parentId) : void 0;
3727
+ const origin = parent ? getResizingNodeRect(parent, lookup, rect()) : { x: 0, y: 0 };
3728
+ const position = posChange?.position ?? node.position;
3729
+ const proposed = {
3730
+ x: position.x + origin.x,
3731
+ y: position.y + origin.y,
3732
+ width: dims.dimensions.width,
3733
+ height: dims.dimensions.height
3734
+ };
3735
+ const handle = inferResizeHandle(resizeStart, proposed);
3736
+ if (!handle) {
3737
+ setGuides([]);
3738
+ return changes;
3739
+ }
3740
+ const result = snapNodeResize(nodes, id, proposed, handle, {
3741
+ ...rect(),
3742
+ ...options.guides,
3743
+ ...options.resize,
3744
+ zoom: options.getZoom?.() ?? 1,
3745
+ index: resizeIndex
3746
+ });
3747
+ setGuides(result.guides);
3748
+ const update = result.updates[0];
3749
+ lastResized = update ? result : null;
3750
+ if (!update) return changes;
3751
+ const next = changes.map((c) => {
3752
+ if (c === dims) return { ...c, dimensions: { width: update.width, height: update.height } };
3753
+ if (c === posChange) return { ...c, position: update.position };
3754
+ return c;
3755
+ });
3756
+ if (!posChange && (update.position.x !== node.position.x || update.position.y !== node.position.y)) {
3757
+ next.push({ id, type: "position", position: update.position });
3758
+ }
3759
+ return next;
3760
+ },
3101
3761
  /**
3102
3762
  * Clears guides and the cached snap targets. Call it from the flow's drag-stop
3103
3763
  * handler: flow libraries write the raw pointer position when a drag ends, so the
@@ -3122,6 +3782,123 @@ function createCanvasArrangeController(options) {
3122
3782
  };
3123
3783
  return controller;
3124
3784
  }
3785
+
3786
+ // src/canvas/measure.ts
3787
+ var EPS = 1e-6;
3788
+ function measureLabel(distance) {
3789
+ const v = Math.round(Math.abs(distance) * 100) / 100;
3790
+ return Object.is(v, -0) ? "0" : String(v);
3791
+ }
3792
+ var toBox = (r) => ({ x0: r.x, y0: r.y, x1: r.x + r.width, y1: r.y + r.height });
3793
+ var finiteBox = (b) => [b.x0, b.y0, b.x1, b.y1].every(Number.isFinite);
3794
+ function line(kind, x1, y1, x2, y2) {
3795
+ return { orientation: x1 === x2 ? "vertical" : "horizontal", kind, x1, y1, x2, y2 };
3796
+ }
3797
+ function measured(out, x1, y1, x2, y2) {
3798
+ const d = Math.hypot(x2 - x1, y2 - y1);
3799
+ if (d > EPS && Number.isFinite(d)) out.push({ ...line("measure", x1, y1, x2, y2), label: measureLabel(d) });
3800
+ }
3801
+ function extension(out, x1, y1, x2, y2) {
3802
+ if (Math.hypot(x2 - x1, y2 - y1) > EPS) out.push(line("extension", x1, y1, x2, y2));
3803
+ }
3804
+ function contains(outer, inner) {
3805
+ return outer.x0 <= inner.x0 + EPS && outer.y0 <= inner.y0 + EPS && outer.x1 >= inner.x1 - EPS && outer.y1 >= inner.y1 - EPS;
3806
+ }
3807
+ function overlapMid(a0, a1, b0, b1) {
3808
+ const lo = Math.max(a0, b0);
3809
+ const hi = Math.min(a1, b1);
3810
+ return lo <= hi ? (lo + hi) / 2 : null;
3811
+ }
3812
+ function inside(out, inner, outer) {
3813
+ const cx = (inner.x0 + inner.x1) / 2;
3814
+ const cy = (inner.y0 + inner.y1) / 2;
3815
+ measured(out, outer.x0, cy, inner.x0, cy);
3816
+ measured(out, inner.x1, cy, outer.x1, cy);
3817
+ measured(out, cx, outer.y0, cx, inner.y0);
3818
+ measured(out, cx, inner.y1, cx, outer.y1);
3819
+ }
3820
+ function measureBetween(a, b) {
3821
+ const out = [];
3822
+ const A = toBox(a);
3823
+ const B = toBox(b);
3824
+ if (!finiteBox(A) || !finiteBox(B)) return out;
3825
+ if (contains(B, A)) {
3826
+ inside(out, A, B);
3827
+ return out;
3828
+ }
3829
+ if (contains(A, B)) {
3830
+ inside(out, B, A);
3831
+ return out;
3832
+ }
3833
+ const ox = overlapMid(A.x0, A.x1, B.x0, B.x1);
3834
+ const oy = overlapMid(A.y0, A.y1, B.y0, B.y1);
3835
+ const overlapping = A.x0 < B.x1 && B.x0 < A.x1 && A.y0 < B.y1 && B.y0 < A.y1;
3836
+ if (overlapping && ox !== null && oy !== null) {
3837
+ measured(out, A.x0, oy, B.x0, oy);
3838
+ measured(out, A.x1, oy, B.x1, oy);
3839
+ measured(out, ox, A.y0, ox, B.y0);
3840
+ measured(out, ox, A.y1, ox, B.y1);
3841
+ return out;
3842
+ }
3843
+ const cx = (A.x0 + A.x1) / 2;
3844
+ const cy = (A.y0 + A.y1) / 2;
3845
+ const gapX = B.x0 >= A.x1 ? [A.x1, B.x0] : B.x1 <= A.x0 ? [A.x0, B.x1] : null;
3846
+ if (gapX) {
3847
+ const [from, to] = gapX;
3848
+ const y = oy ?? cy;
3849
+ measured(out, from, y, to, y);
3850
+ if (oy === null) extension(out, to, y, to, y < B.y0 ? B.y0 : B.y1);
3851
+ }
3852
+ const gapY = B.y0 >= A.y1 ? [A.y1, B.y0] : B.y1 <= A.y0 ? [A.y0, B.y1] : null;
3853
+ if (gapY) {
3854
+ const [from, to] = gapY;
3855
+ const x = ox ?? cx;
3856
+ measured(out, x, from, x, to);
3857
+ if (ox === null) extension(out, x, to, x < B.x0 ? B.x0 : B.x1, to);
3858
+ }
3859
+ return out;
3860
+ }
3861
+ function measureToLine(a, orientation, at) {
3862
+ const out = [];
3863
+ const A = toBox(a);
3864
+ if (!finiteBox(A) || !Number.isFinite(at)) return out;
3865
+ const vertical = orientation === "vertical";
3866
+ const cx = (A.x0 + A.x1) / 2;
3867
+ const cy = (A.y0 + A.y1) / 2;
3868
+ const [lo, hi] = vertical ? [A.x0, A.x1] : [A.y0, A.y1];
3869
+ const seg = (from, to) => vertical ? measured(out, from, cy, to, cy) : measured(out, cx, from, cx, to);
3870
+ if (at >= hi) seg(hi, at);
3871
+ else if (at <= lo) seg(at, lo);
3872
+ else {
3873
+ seg(lo, at);
3874
+ seg(at, hi);
3875
+ }
3876
+ return out;
3877
+ }
3878
+ function measureSelection(nodes, selectedIds, hoveredId, options = {}) {
3879
+ const none = (selection2 = null) => ({ guides: [], target: null, selection: selection2 });
3880
+ const lookup = createNodeLookup(nodes);
3881
+ const selected = [...new Set(selectedIds)].map((id) => lookup.get(id)).filter((n) => !!n && n.hidden !== true);
3882
+ if (selected.length === 0) return none();
3883
+ const selection = boundsOf(selected.map((n) => getNodeRect(n, lookup, options)));
3884
+ if (!selection) return none();
3885
+ const selectedSet = new Set(selected.map((n) => n.id));
3886
+ let targetId = hoveredId ?? null;
3887
+ if (targetId === null || selectedSet.has(targetId)) {
3888
+ const parentId = selected[0].parentId;
3889
+ const sameParent = selected.every((n) => n.parentId === parentId);
3890
+ targetId = parentId && sameParent && !selectedSet.has(parentId) ? parentId : null;
3891
+ }
3892
+ const target = targetId !== null ? lookup.get(targetId) : void 0;
3893
+ if (!target || target.hidden === true) return none(selection);
3894
+ const rect = getNodeRect(target, lookup, options);
3895
+ return { guides: measureBetween(selection, rect), target: rect, selection };
3896
+ }
3897
+ function isMeasureModifierHeld(event) {
3898
+ if (!event) return false;
3899
+ if (event.type === "keyup" && event.key === "Alt") return false;
3900
+ return event.altKey === true || event.type === "keydown" && event.key === "Alt";
3901
+ }
3125
3902
  export {
3126
3903
  CONNECTION_LINE_TYPES,
3127
3904
  CanvasScenesStore,
@@ -3132,6 +3909,7 @@ export {
3132
3909
  LOCKED_DATA_KEY,
3133
3910
  LOCKED_PREVIOUS_FLAGS_KEY,
3134
3911
  ManualClock,
3912
+ RESIZE_HANDLES,
3135
3913
  SHAPE_DATA_KEY,
3136
3914
  SHAPE_KINDS,
3137
3915
  SHAPE_NODE_TYPE,
@@ -3160,6 +3938,8 @@ export {
3160
3938
  createGuideIndex,
3161
3939
  createNodeGuideIndex,
3162
3940
  createNodeLookup,
3941
+ cubicPointAt,
3942
+ cubicsToPathData,
3163
3943
  defaultEasing,
3164
3944
  distributeNodes,
3165
3945
  dividePolygons,
@@ -3167,26 +3947,34 @@ export {
3167
3947
  estimateCanvasHeight,
3168
3948
  estimateNodeSize,
3169
3949
  findConnectorSnap,
3950
+ fitCubicRun,
3951
+ fitCubics,
3952
+ fitFreehandPath,
3953
+ fitPathData,
3170
3954
  fitViewportToNodes,
3171
3955
  fitViewportToRect,
3172
3956
  flattenSvgPath,
3173
3957
  flowSide,
3174
- formatNumber,
3958
+ formatNumber2 as formatNumber,
3175
3959
  getAbsolutePosition,
3176
3960
  getCenteredNodePosition,
3177
3961
  getLayoutedNodes,
3178
3962
  getNodeRect,
3179
3963
  getNodeShape,
3180
3964
  getNodeSize,
3965
+ getResizingNodeRect,
3181
3966
  getShapeRenderData,
3182
3967
  getShapeSvg,
3183
3968
  handlePoint,
3969
+ inferResizeHandle,
3184
3970
  intersectPolygons,
3185
3971
  invertNodeUpdates,
3186
3972
  isEventFromTextInput,
3187
3973
  isLocked,
3974
+ isMeasureModifierHeld,
3188
3975
  isShapeDescription,
3189
3976
  isShapeNode,
3977
+ isStraightCubic,
3190
3978
  isValidCanvasEdge,
3191
3979
  isValidCanvasNode,
3192
3980
  isValidCanvasScene,
@@ -3197,6 +3985,10 @@ export {
3197
3985
  lockNodes,
3198
3986
  matchArrangeShortcut,
3199
3987
  matchSize,
3988
+ measureBetween,
3989
+ measureLabel,
3990
+ measureSelection,
3991
+ measureToLine,
3200
3992
  multiPolygonArea,
3201
3993
  multiPolygonBounds,
3202
3994
  nearestHandlePositions,
@@ -3218,6 +4010,7 @@ export {
3218
4010
  remapPastedNodes,
3219
4011
  reorderNodes,
3220
4012
  repositionNodesTo,
4013
+ resizeHandleAxes,
3221
4014
  revertShapeCombination,
3222
4015
  ringSignedArea,
3223
4016
  runMultiNodeTransition,
@@ -3227,6 +4020,9 @@ export {
3227
4020
  sideToPosition,
3228
4021
  simplifyRing,
3229
4022
  snapNodeDrag,
4023
+ snapNodeResize,
4024
+ snapResize,
4025
+ snapToGrid,
3230
4026
  subtractPolygons,
3231
4027
  toggleLockNodes,
3232
4028
  unionPolygons,