@pixerate/editor 0.21.0 → 0.22.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 (70) hide show
  1. package/README.md +20 -11
  2. package/dist/agent/index.cjs +8 -8
  3. package/dist/agent/index.d.cts +2 -2
  4. package/dist/agent/index.d.ts +2 -2
  5. package/dist/agent/index.js +2 -2
  6. package/dist/canvas/index.cjs +1269 -13
  7. package/dist/canvas/index.cjs.map +1 -1
  8. package/dist/canvas/index.d.cts +432 -6
  9. package/dist/canvas/index.d.ts +432 -6
  10. package/dist/canvas/index.js +1264 -8
  11. package/dist/canvas/index.js.map +1 -1
  12. package/dist/{chunk-U6NDQFOK.cjs → chunk-6M3BCTBD.cjs} +427 -138
  13. package/dist/chunk-6M3BCTBD.cjs.map +1 -0
  14. package/dist/{chunk-TVRC4KVT.cjs → chunk-AA4ZEVCC.cjs} +845 -90
  15. package/dist/chunk-AA4ZEVCC.cjs.map +1 -0
  16. package/dist/{chunk-LA7PEGU3.cjs → chunk-CL37JGFW.cjs} +4 -4
  17. package/dist/{chunk-LA7PEGU3.cjs.map → chunk-CL37JGFW.cjs.map} +1 -1
  18. package/dist/{chunk-A7DKIA72.js → chunk-DENSCKFC.js} +642 -69
  19. package/dist/chunk-DENSCKFC.js.map +1 -0
  20. package/dist/{chunk-4QPLEUL2.cjs → chunk-GVSMRPHB.cjs} +1046 -54
  21. package/dist/chunk-GVSMRPHB.cjs.map +1 -0
  22. package/dist/{chunk-KYRZDWM4.js → chunk-JJDLPEWL.js} +2 -2
  23. package/dist/{chunk-3TYMNULT.js → chunk-MJZBNWG7.js} +807 -52
  24. package/dist/chunk-MJZBNWG7.js.map +1 -0
  25. package/dist/{chunk-CYFJCKC4.js → chunk-QGHGJ56O.js} +997 -5
  26. package/dist/chunk-QGHGJ56O.js.map +1 -0
  27. package/dist/{chunk-GVW4P7BG.js → chunk-RU52I7QO.js} +305 -16
  28. package/dist/chunk-RU52I7QO.js.map +1 -0
  29. package/dist/{chunk-JYRIOFEN.cjs → chunk-SAZRENEZ.cjs} +647 -74
  30. package/dist/chunk-SAZRENEZ.cjs.map +1 -0
  31. package/dist/{controller-OUdxPY-X.d.ts → controller-B7GIybc4.d.cts} +139 -7
  32. package/dist/{controller-OUdxPY-X.d.cts → controller-B7GIybc4.d.ts} +139 -7
  33. package/dist/extensions/index.cjs +14 -4
  34. package/dist/extensions/index.cjs.map +1 -1
  35. package/dist/extensions/index.d.cts +3 -3
  36. package/dist/extensions/index.d.ts +3 -3
  37. package/dist/extensions/index.js +13 -3
  38. package/dist/{index-k3mFbPx_.d.cts → index-CCr_z9kF.d.cts} +3 -3
  39. package/dist/{index-Ng1pG7JE.d.ts → index-Ck6ajwXh.d.ts} +3 -3
  40. package/dist/index.cjs +250 -16
  41. package/dist/index.cjs.map +1 -1
  42. package/dist/index.d.cts +294 -8
  43. package/dist/index.d.ts +294 -8
  44. package/dist/index.js +241 -7
  45. package/dist/index.js.map +1 -1
  46. package/dist/{registry-iAHbqkYt.d.cts → registry-GJI4gToU.d.cts} +15 -4
  47. package/dist/{registry-iAHbqkYt.d.ts → registry-GJI4gToU.d.ts} +15 -4
  48. package/dist/serializers/index.cjs +3 -3
  49. package/dist/serializers/index.d.cts +1 -1
  50. package/dist/serializers/index.d.ts +1 -1
  51. package/dist/serializers/index.js +2 -2
  52. package/dist/spreadsheet/index.cjs +7 -3
  53. package/dist/spreadsheet/index.cjs.map +1 -1
  54. package/dist/spreadsheet/index.d.cts +33 -4
  55. package/dist/spreadsheet/index.d.ts +33 -4
  56. package/dist/spreadsheet/index.js +6 -2
  57. package/dist/{track-changes-G8JcWB8b.d.cts → track-changes-BSF58RK0.d.cts} +92 -9
  58. package/dist/{track-changes-G8JcWB8b.d.ts → track-changes-BSF58RK0.d.ts} +92 -9
  59. package/dist/{types-DMe0vFB1.d.cts → types-CJ4U-HHI.d.cts} +1 -1
  60. package/dist/{types-DMe0vFB1.d.ts → types-CJ4U-HHI.d.ts} +1 -1
  61. package/package.json +1 -1
  62. package/dist/chunk-3TYMNULT.js.map +0 -1
  63. package/dist/chunk-4QPLEUL2.cjs.map +0 -1
  64. package/dist/chunk-A7DKIA72.js.map +0 -1
  65. package/dist/chunk-CYFJCKC4.js.map +0 -1
  66. package/dist/chunk-GVW4P7BG.js.map +0 -1
  67. package/dist/chunk-JYRIOFEN.cjs.map +0 -1
  68. package/dist/chunk-TVRC4KVT.cjs.map +0 -1
  69. package/dist/chunk-U6NDQFOK.cjs.map +0 -1
  70. /package/dist/{chunk-KYRZDWM4.js.map → chunk-JJDLPEWL.js.map} +0 -0
@@ -1705,11 +1705,1183 @@ function matchArrangeShortcut(event, bindings = DEFAULT_ARRANGE_SHORTCUTS) {
1705
1705
  return null;
1706
1706
  }
1707
1707
 
1708
+ // src/canvas/polygonBoolean.ts
1709
+ function ringSignedArea(ring) {
1710
+ let sum = 0;
1711
+ const n = ring.length;
1712
+ for (let i = 0; i < n; i++) {
1713
+ const [x1, y1] = ring[i];
1714
+ const [x2, y2] = ring[(i + 1) % n];
1715
+ sum += x1 * y2 - x2 * y1;
1716
+ }
1717
+ return sum / 2;
1718
+ }
1719
+ function polygonArea(polygon) {
1720
+ if (polygon.length === 0) return 0;
1721
+ let area = Math.abs(ringSignedArea(polygon[0]));
1722
+ for (let i = 1; i < polygon.length; i++) area -= Math.abs(ringSignedArea(polygon[i]));
1723
+ return area;
1724
+ }
1725
+ function multiPolygonArea(multi) {
1726
+ let total = 0;
1727
+ for (const polygon of multi) total += polygonArea(polygon);
1728
+ return total;
1729
+ }
1730
+ function multiPolygonBounds(input) {
1731
+ let minX = Infinity;
1732
+ let minY = Infinity;
1733
+ let maxX = -Infinity;
1734
+ let maxY = -Infinity;
1735
+ const visit = (ring) => {
1736
+ for (const [x, y] of ring) {
1737
+ if (x < minX) minX = x;
1738
+ if (y < minY) minY = y;
1739
+ if (x > maxX) maxX = x;
1740
+ if (y > maxY) maxY = y;
1741
+ }
1742
+ };
1743
+ for (const item of input) {
1744
+ if (item.length > 0 && Array.isArray(item[0]) && Array.isArray(item[0][0])) {
1745
+ for (const ring of item) visit(ring);
1746
+ } else {
1747
+ visit(item);
1748
+ }
1749
+ }
1750
+ return minX === Infinity ? null : { minX, minY, maxX, maxY };
1751
+ }
1752
+ function pointInRings(point, rings, fillRule = "nonzero") {
1753
+ const [px, py] = point;
1754
+ let winding = 0;
1755
+ for (const ring of rings) {
1756
+ const n = ring.length;
1757
+ for (let i = 0; i < n; i++) {
1758
+ const [ax, ay] = ring[i];
1759
+ const [bx, by] = ring[(i + 1) % n];
1760
+ if (ay <= py && by > py) {
1761
+ if (ax + (py - ay) / (by - ay) * (bx - ax) > px) winding++;
1762
+ } else if (by <= py && ay > py) {
1763
+ if (ax + (py - ay) / (by - ay) * (bx - ax) > px) winding--;
1764
+ }
1765
+ }
1766
+ }
1767
+ return fillRule === "evenodd" ? (winding & 1) === 1 : winding !== 0;
1768
+ }
1769
+ function pointInMultiPolygon(point, multi) {
1770
+ return multi.some((polygon) => pointInRings(point, polygon, "evenodd"));
1771
+ }
1772
+ function cleanRing(ring) {
1773
+ const out = [];
1774
+ for (const p of ring) {
1775
+ if (!Number.isFinite(p[0]) || !Number.isFinite(p[1])) continue;
1776
+ const last = out[out.length - 1];
1777
+ if (last && last[0] === p[0] && last[1] === p[1]) continue;
1778
+ out.push([p[0], p[1]]);
1779
+ }
1780
+ while (out.length > 1) {
1781
+ const first = out[0];
1782
+ const last = out[out.length - 1];
1783
+ if (first[0] === last[0] && first[1] === last[1]) out.pop();
1784
+ else break;
1785
+ }
1786
+ return out;
1787
+ }
1788
+ function isOperand(input) {
1789
+ return !Array.isArray(input) && typeof input === "object" && input !== null && Array.isArray(input.rings);
1790
+ }
1791
+ function normalizeInput(input) {
1792
+ if (isOperand(input)) {
1793
+ return {
1794
+ rings: input.rings.map(cleanRing).filter((r) => r.length >= 3),
1795
+ fillRule: input.fillRule ?? "nonzero"
1796
+ };
1797
+ }
1798
+ const rings = [];
1799
+ for (const polygon of input ?? []) {
1800
+ polygon.forEach((raw, index) => {
1801
+ const ring = cleanRing(raw);
1802
+ if (ring.length < 3) return;
1803
+ const area = ringSignedArea(ring);
1804
+ if (area === 0) return;
1805
+ const wantPositive = index === 0;
1806
+ rings.push(area > 0 === wantPositive ? ring : ring.reverse());
1807
+ });
1808
+ }
1809
+ return { rings, fillRule: "nonzero" };
1810
+ }
1811
+ var PointPool = class {
1812
+ constructor(eps) {
1813
+ this.eps = eps;
1814
+ }
1815
+ eps;
1816
+ vertices = [];
1817
+ cells = /* @__PURE__ */ new Map();
1818
+ get(x, y) {
1819
+ const cx = Math.floor(x / this.eps);
1820
+ const cy = Math.floor(y / this.eps);
1821
+ let best = null;
1822
+ let bestDist = Infinity;
1823
+ for (let dx = -1; dx <= 1; dx++) {
1824
+ for (let dy = -1; dy <= 1; dy++) {
1825
+ const ids = this.cells.get(`${cx + dx},${cy + dy}`);
1826
+ if (!ids) continue;
1827
+ for (const id of ids) {
1828
+ const v = this.vertices[id];
1829
+ const d = Math.hypot(v.x - x, v.y - y);
1830
+ if (d <= this.eps && d < bestDist) {
1831
+ best = v;
1832
+ bestDist = d;
1833
+ }
1834
+ }
1835
+ }
1836
+ }
1837
+ if (best) return best;
1838
+ const vertex = { id: this.vertices.length, x, y };
1839
+ this.vertices.push(vertex);
1840
+ const key = `${cx},${cy}`;
1841
+ const bucket = this.cells.get(key);
1842
+ if (bucket) bucket.push(vertex.id);
1843
+ else this.cells.set(key, [vertex.id]);
1844
+ return vertex;
1845
+ }
1846
+ };
1847
+ function project(a, b, p) {
1848
+ const dx = b.x - a.x;
1849
+ const dy = b.y - a.y;
1850
+ const len2 = dx * dx + dy * dy;
1851
+ const t = len2 === 0 ? 0 : ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2;
1852
+ const cx = a.x + t * dx;
1853
+ const cy = a.y + t * dy;
1854
+ return { t, dist: Math.hypot(p.x - cx, p.y - cy) };
1855
+ }
1856
+ function addSplit(splits, t, v) {
1857
+ const { a, b } = splits.segment;
1858
+ if (v.id === a.id || v.id === b.id) return;
1859
+ if (!splits.points.has(v.id)) splits.points.set(v.id, { t, v });
1860
+ }
1861
+ function splitPass(segments, pool, eps) {
1862
+ const order = segments.map((s, i) => ({
1863
+ i,
1864
+ minX: Math.min(s.a.x, s.b.x),
1865
+ maxX: Math.max(s.a.x, s.b.x),
1866
+ minY: Math.min(s.a.y, s.b.y),
1867
+ maxY: Math.max(s.a.y, s.b.y)
1868
+ })).sort((p, q) => p.minX - q.minX);
1869
+ const splits = segments.map((segment) => ({ segment, points: /* @__PURE__ */ new Map() }));
1870
+ for (let oi = 0; oi < order.length; oi++) {
1871
+ const bi = order[oi];
1872
+ const s1 = segments[bi.i];
1873
+ for (let oj = oi + 1; oj < order.length; oj++) {
1874
+ const bj = order[oj];
1875
+ if (bj.minX > bi.maxX + eps) break;
1876
+ if (bj.minY > bi.maxY + eps || bj.maxY < bi.minY - eps) continue;
1877
+ const s2 = segments[bj.i];
1878
+ const sharedA = s1.a.id === s2.a.id || s1.a.id === s2.b.id;
1879
+ const sharedB = s1.b.id === s2.a.id || s1.b.id === s2.b.id;
1880
+ if (sharedA && sharedB) continue;
1881
+ let touched = false;
1882
+ for (const [seg, other, split] of [
1883
+ [s1, s2, splits[bi.i]],
1884
+ [s2, s1, splits[bj.i]]
1885
+ ]) {
1886
+ for (const p of [other.a, other.b]) {
1887
+ if (p.id === seg.a.id || p.id === seg.b.id) continue;
1888
+ const { t: t2, dist } = project(seg.a, seg.b, p);
1889
+ 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) {
1891
+ addSplit(split, t2, p);
1892
+ touched = true;
1893
+ }
1894
+ }
1895
+ }
1896
+ if (touched) continue;
1897
+ const d1x = s1.b.x - s1.a.x;
1898
+ const d1y = s1.b.y - s1.a.y;
1899
+ const d2x = s2.b.x - s2.a.x;
1900
+ const d2y = s2.b.y - s2.a.y;
1901
+ const denom = d1x * d2y - d1y * d2x;
1902
+ const len1 = Math.hypot(d1x, d1y);
1903
+ const len2 = Math.hypot(d2x, d2y);
1904
+ if (Math.abs(denom) <= 1e-12 * len1 * len2) continue;
1905
+ const ex = s2.a.x - s1.a.x;
1906
+ const ey = s2.a.y - s1.a.y;
1907
+ const t = (ex * d2y - ey * d2x) / denom;
1908
+ const u = (ex * d1y - ey * d1x) / denom;
1909
+ const tolT = eps / len1;
1910
+ const tolU = eps / len2;
1911
+ if (t < -tolT || t > 1 + tolT || u < -tolU || u > 1 + tolU) continue;
1912
+ if (sharedA || sharedB) continue;
1913
+ const v = pool.get(s1.a.x + t * d1x, s1.a.y + t * d1y);
1914
+ addSplit(splits[bi.i], project(s1.a, s1.b, v).t, v);
1915
+ addSplit(splits[bj.i], project(s2.a, s2.b, v).t, v);
1916
+ }
1917
+ }
1918
+ let changed = false;
1919
+ const out = [];
1920
+ for (const { segment, points } of splits) {
1921
+ if (points.size === 0) {
1922
+ out.push(segment);
1923
+ continue;
1924
+ }
1925
+ changed = true;
1926
+ const ordered = [...points.values()].sort((p, q) => p.t - q.t);
1927
+ let prev = segment.a;
1928
+ for (const { v } of ordered) {
1929
+ if (v.id !== prev.id) out.push({ a: prev, b: v, operand: segment.operand });
1930
+ prev = v;
1931
+ }
1932
+ if (prev.id !== segment.b.id) out.push({ a: prev, b: segment.b, operand: segment.operand });
1933
+ }
1934
+ return { segments: out, changed };
1935
+ }
1936
+ function defaultEpsilon(operands) {
1937
+ let max = 0;
1938
+ for (const op of operands) {
1939
+ for (const ring of op.rings) {
1940
+ for (const [x, y] of ring) {
1941
+ const m = Math.max(Math.abs(x), Math.abs(y));
1942
+ if (m > max) max = m;
1943
+ }
1944
+ }
1945
+ }
1946
+ return Math.max(1e-9, max * 1e-9);
1947
+ }
1948
+ function buildOverlay(inputs, options) {
1949
+ const operands = inputs.map(normalizeInput);
1950
+ const eps = options.epsilon && options.epsilon > 0 ? options.epsilon : defaultEpsilon(operands);
1951
+ const pool = new PointPool(eps);
1952
+ let segments = [];
1953
+ operands.forEach((op, operand) => {
1954
+ for (const ring of op.rings) {
1955
+ const verts = ring.map(([x, y]) => pool.get(x, y));
1956
+ for (let i = 0; i < verts.length; i++) {
1957
+ const a = verts[i];
1958
+ const b = verts[(i + 1) % verts.length];
1959
+ if (a.id !== b.id) segments.push({ a, b, operand });
1960
+ }
1961
+ }
1962
+ });
1963
+ for (let pass = 0; pass < 8; pass++) {
1964
+ const result = splitPass(segments, pool, eps);
1965
+ segments = result.segments;
1966
+ if (!result.changed) break;
1967
+ }
1968
+ const count = operands.length;
1969
+ const byKey = /* @__PURE__ */ new Map();
1970
+ for (const s of segments) {
1971
+ const forward = s.a.id < s.b.id;
1972
+ const a = forward ? s.a : s.b;
1973
+ const b = forward ? s.b : s.a;
1974
+ const key = `${a.id},${b.id}`;
1975
+ let edge = byKey.get(key);
1976
+ if (!edge) {
1977
+ edge = { a, b, mult: new Array(count).fill(0) };
1978
+ byKey.set(key, edge);
1979
+ }
1980
+ edge.mult[s.operand] += forward ? 1 : -1;
1981
+ }
1982
+ const edges = [...byKey.values()].filter((e) => e.mult.some((m) => m !== 0));
1983
+ const right = [];
1984
+ const left = [];
1985
+ for (let i = 0; i < edges.length; i++) {
1986
+ const edge = edges[i];
1987
+ const horizontal = Math.abs(edge.b.y - edge.a.y) < Math.abs(edge.b.x - edge.a.x);
1988
+ const tx = (v) => horizontal ? v.y : v.x;
1989
+ const ty = (v) => horizontal ? -v.x : v.y;
1990
+ const ax = tx(edge.a);
1991
+ const ay = ty(edge.a);
1992
+ const bx = tx(edge.b);
1993
+ const by = ty(edge.b);
1994
+ const mx = (ax + bx) / 2;
1995
+ const my = (ay + by) / 2;
1996
+ const winding = new Array(count).fill(0);
1997
+ for (let j = 0; j < edges.length; j++) {
1998
+ if (j === i) continue;
1999
+ const other = edges[j];
2000
+ const oax = tx(other.a);
2001
+ const oay = ty(other.a);
2002
+ const obx = tx(other.b);
2003
+ const oby = ty(other.b);
2004
+ let dir = 0;
2005
+ if (oay <= my && oby > my) dir = 1;
2006
+ else if (oby <= my && oay > my) dir = -1;
2007
+ else continue;
2008
+ const x = oax + (my - oay) / (oby - oay) * (obx - oax);
2009
+ if (x <= mx) continue;
2010
+ for (let k = 0; k < count; k++) {
2011
+ if (other.mult[k] !== 0) winding[k] += dir * other.mult[k];
2012
+ }
2013
+ }
2014
+ const sideIsRight = by > ay;
2015
+ const r = sideIsRight ? winding : winding.map((w, k) => w - edge.mult[k]);
2016
+ right.push(r);
2017
+ left.push(r.map((w, k) => w + edge.mult[k]));
2018
+ }
2019
+ return { edges, right, left, fillRules: operands.map((o) => o.fillRule), eps };
2020
+ }
2021
+ function insideFlags(windings, fillRules) {
2022
+ return windings.map((w, k) => fillRules[k] === "evenodd" ? (w & 1) === 1 : w !== 0);
2023
+ }
2024
+ function traceRings(directed) {
2025
+ const outgoing = /* @__PURE__ */ new Map();
2026
+ for (const e of directed) {
2027
+ const list = outgoing.get(e.from.id);
2028
+ if (list) list.push(e);
2029
+ else outgoing.set(e.from.id, [e]);
2030
+ }
2031
+ const TWO_PI = Math.PI * 2;
2032
+ const next = (incoming) => {
2033
+ const candidates = outgoing.get(incoming.to.id);
2034
+ if (!candidates) return void 0;
2035
+ const back = Math.atan2(incoming.from.y - incoming.to.y, incoming.from.x - incoming.to.x);
2036
+ let best;
2037
+ let bestTurn = Infinity;
2038
+ for (const c of candidates) {
2039
+ let turn = back - c.angle;
2040
+ turn -= Math.floor(turn / TWO_PI) * TWO_PI;
2041
+ if (turn <= 1e-15) turn = TWO_PI;
2042
+ if (turn < bestTurn) {
2043
+ bestTurn = turn;
2044
+ best = c;
2045
+ }
2046
+ }
2047
+ return best;
2048
+ };
2049
+ const used = /* @__PURE__ */ new Set();
2050
+ const rings = [];
2051
+ for (const start of directed) {
2052
+ if (used.has(start)) continue;
2053
+ const ring = [];
2054
+ let edge = start;
2055
+ let guard = directed.length + 1;
2056
+ while (edge && !used.has(edge) && guard-- > 0) {
2057
+ used.add(edge);
2058
+ ring.push(edge.from);
2059
+ edge = next(edge);
2060
+ }
2061
+ if (ring.length >= 3) rings.push(ring);
2062
+ }
2063
+ return rings;
2064
+ }
2065
+ function removeCollinear(ring, eps) {
2066
+ let points = ring;
2067
+ let changed = true;
2068
+ while (changed && points.length > 3) {
2069
+ changed = false;
2070
+ const out = [];
2071
+ const n = points.length;
2072
+ for (let i = 0; i < n; i++) {
2073
+ const prev = out.length > 0 ? out[out.length - 1] : points[(i - 1 + n) % n];
2074
+ const cur = points[i];
2075
+ const nxt = points[(i + 1) % n];
2076
+ const dx = nxt[0] - prev[0];
2077
+ const dy = nxt[1] - prev[1];
2078
+ const len = Math.hypot(dx, dy);
2079
+ 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) {
2082
+ changed = true;
2083
+ continue;
2084
+ }
2085
+ out.push(cur);
2086
+ }
2087
+ if (out.length < 3) return out;
2088
+ points = out;
2089
+ }
2090
+ return points;
2091
+ }
2092
+ function assemble(rings, eps) {
2093
+ const outers = [];
2094
+ const holes = [];
2095
+ const minArea = eps * eps;
2096
+ for (const vertices of rings) {
2097
+ const ring = removeCollinear(
2098
+ vertices.map((v) => [v.x, v.y]),
2099
+ eps
2100
+ );
2101
+ if (ring.length < 3) continue;
2102
+ const area = ringSignedArea(ring);
2103
+ if (Math.abs(area) <= minArea) continue;
2104
+ (area > 0 ? outers : holes).push({ ring, area: Math.abs(area) });
2105
+ }
2106
+ outers.sort((a, b) => b.area - a.area);
2107
+ const polygons = outers.map((o) => [o.ring]);
2108
+ for (const hole of holes) {
2109
+ const [p, q] = [hole.ring[0], hole.ring[1]];
2110
+ const sample = [(p[0] + q[0]) / 2, (p[1] + q[1]) / 2];
2111
+ let owner = -1;
2112
+ for (let i = outers.length - 1; i >= 0; i--) {
2113
+ if (outers[i].area >= hole.area && pointInRings(sample, [outers[i].ring], "evenodd")) {
2114
+ owner = i;
2115
+ break;
2116
+ }
2117
+ }
2118
+ if (owner >= 0) polygons[owner].push(hole.ring);
2119
+ }
2120
+ return polygons;
2121
+ }
2122
+ function extract(overlay, isInside) {
2123
+ const directed = [];
2124
+ overlay.edges.forEach((edge, i) => {
2125
+ const l = isInside(insideFlags(overlay.left[i], overlay.fillRules));
2126
+ const r = isInside(insideFlags(overlay.right[i], overlay.fillRules));
2127
+ if (l === r) return;
2128
+ const [from, to] = l ? [edge.a, edge.b] : [edge.b, edge.a];
2129
+ directed.push({ from, to, angle: Math.atan2(to.y - from.y, to.x - from.x) });
2130
+ });
2131
+ return assemble(traceRings(directed), overlay.eps);
2132
+ }
2133
+ function predicate(operation) {
2134
+ switch (operation) {
2135
+ case "union":
2136
+ return (f) => f.some(Boolean);
2137
+ case "intersection":
2138
+ return (f) => f.length > 0 && f.every(Boolean);
2139
+ case "difference":
2140
+ return (f) => f[0] === true && !f.slice(1).some(Boolean);
2141
+ case "xor":
2142
+ return (f) => f.filter(Boolean).length % 2 === 1;
2143
+ }
2144
+ }
2145
+ function polygonBoolean(operation, operands, options = {}) {
2146
+ if (operands.length === 0) return [];
2147
+ return extract(buildOverlay(operands, options), predicate(operation));
2148
+ }
2149
+ var unionPolygons = (a, b, options) => polygonBoolean("union", [a, b], options);
2150
+ var intersectPolygons = (a, b, options) => polygonBoolean("intersection", [a, b], options);
2151
+ var subtractPolygons = (a, b, options) => polygonBoolean("difference", [a, b], options);
2152
+ var xorPolygons = (a, b, options) => polygonBoolean("xor", [a, b], options);
2153
+ function dividePolygons(operands, options = {}) {
2154
+ if (operands.length === 0) return [];
2155
+ const overlay = buildOverlay(operands, options);
2156
+ const signatures = /* @__PURE__ */ new Map();
2157
+ const note = (windings) => {
2158
+ const flags = insideFlags(windings, overlay.fillRules);
2159
+ if (!flags.some(Boolean)) return;
2160
+ const key = flags.map((f) => f ? "1" : "0").join("");
2161
+ if (!signatures.has(key)) signatures.set(key, flags.flatMap((f, k) => f ? [k] : []));
2162
+ };
2163
+ overlay.edges.forEach((_, i) => {
2164
+ note(overlay.left[i]);
2165
+ note(overlay.right[i]);
2166
+ });
2167
+ const pieces = [];
2168
+ const keys = [...signatures.keys()].sort();
2169
+ for (const key of keys) {
2170
+ const polygons = extract(overlay, (flags) => flags.map((f) => f ? "1" : "0").join("") === key);
2171
+ polygons.sort((a, b) => polygonArea(b) - polygonArea(a));
2172
+ for (const polygon of polygons) pieces.push({ polygon, operands: signatures.get(key) });
2173
+ }
2174
+ return pieces;
2175
+ }
2176
+
2177
+ // src/canvas/shapeGeometry.ts
2178
+ var SHAPE_KINDS = ["rect", "ellipse", "polygon", "star", "path"];
2179
+ function isShapeDescription(value) {
2180
+ if (!value || typeof value !== "object") return false;
2181
+ const kind = value.kind;
2182
+ if (!SHAPE_KINDS.includes(kind)) return false;
2183
+ return kind !== "path" || typeof value.d === "string";
2184
+ }
2185
+ var PARAM_COUNTS = { m: 2, l: 2, h: 1, v: 1, c: 6, s: 4, q: 4, t: 2, a: 7, z: 0 };
2186
+ var PathScanner = class {
2187
+ constructor(text) {
2188
+ this.text = text;
2189
+ }
2190
+ text;
2191
+ pos = 0;
2192
+ skip() {
2193
+ while (this.pos < this.text.length && /[\s,]/.test(this.text[this.pos])) this.pos++;
2194
+ }
2195
+ /** The next command letter, or `null` when a number (or the end) follows. */
2196
+ command() {
2197
+ this.skip();
2198
+ const ch = this.text[this.pos];
2199
+ if (ch && /[MmLlHhVvCcSsQqTtAaZz]/.test(ch)) {
2200
+ this.pos++;
2201
+ return ch;
2202
+ }
2203
+ return null;
2204
+ }
2205
+ done() {
2206
+ this.skip();
2207
+ return this.pos >= this.text.length;
2208
+ }
2209
+ number() {
2210
+ this.skip();
2211
+ const match = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/.exec(this.text.slice(this.pos, this.pos + 64));
2212
+ if (!match) return null;
2213
+ this.pos += match[0].length;
2214
+ const value = Number(match[0]);
2215
+ return Number.isFinite(value) ? value : null;
2216
+ }
2217
+ flag() {
2218
+ this.skip();
2219
+ const ch = this.text[this.pos];
2220
+ if (ch === "0" || ch === "1") {
2221
+ this.pos++;
2222
+ return ch === "1" ? 1 : 0;
2223
+ }
2224
+ return null;
2225
+ }
2226
+ };
2227
+ function parseSvgPath(d) {
2228
+ const scanner = new PathScanner(d ?? "");
2229
+ const commands = [];
2230
+ let cx = 0;
2231
+ let cy = 0;
2232
+ let startX = 0;
2233
+ let startY = 0;
2234
+ let lastControl = null;
2235
+ let command = "";
2236
+ while (!scanner.done()) {
2237
+ const letter = scanner.command();
2238
+ if (letter) {
2239
+ command = letter;
2240
+ if (command === "z" || command === "Z") {
2241
+ commands.push({ type: "Z" });
2242
+ cx = startX;
2243
+ cy = startY;
2244
+ lastControl = null;
2245
+ continue;
2246
+ }
2247
+ } else if (!command || command === "z" || command === "Z") {
2248
+ break;
2249
+ }
2250
+ const lower = command.toLowerCase();
2251
+ const count = PARAM_COUNTS[lower];
2252
+ const args = [];
2253
+ for (let k = 0; k < count; k++) {
2254
+ const value = lower === "a" && (k === 3 || k === 4) ? scanner.flag() : scanner.number();
2255
+ if (value === null) return commands;
2256
+ args.push(value);
2257
+ }
2258
+ const rel = command !== command.toUpperCase();
2259
+ const ox = rel ? cx : 0;
2260
+ const oy = rel ? cy : 0;
2261
+ switch (lower) {
2262
+ case "m": {
2263
+ cx = args[0] + ox;
2264
+ cy = args[1] + oy;
2265
+ startX = cx;
2266
+ startY = cy;
2267
+ commands.push({ type: "M", x: cx, y: cy });
2268
+ lastControl = null;
2269
+ command = rel ? "l" : "L";
2270
+ break;
2271
+ }
2272
+ case "l":
2273
+ cx = args[0] + ox;
2274
+ cy = args[1] + oy;
2275
+ commands.push({ type: "L", x: cx, y: cy });
2276
+ lastControl = null;
2277
+ break;
2278
+ case "h":
2279
+ cx = args[0] + (rel ? cx : 0);
2280
+ commands.push({ type: "L", x: cx, y: cy });
2281
+ lastControl = null;
2282
+ break;
2283
+ case "v":
2284
+ cy = args[0] + (rel ? cy : 0);
2285
+ commands.push({ type: "L", x: cx, y: cy });
2286
+ lastControl = null;
2287
+ break;
2288
+ case "c": {
2289
+ const c = {
2290
+ type: "C",
2291
+ x1: args[0] + ox,
2292
+ y1: args[1] + oy,
2293
+ x2: args[2] + ox,
2294
+ y2: args[3] + oy,
2295
+ x: args[4] + ox,
2296
+ y: args[5] + oy
2297
+ };
2298
+ commands.push(c);
2299
+ lastControl = { x: c.x2, y: c.y2, type: "C" };
2300
+ cx = c.x;
2301
+ cy = c.y;
2302
+ break;
2303
+ }
2304
+ case "s": {
2305
+ const x1 = lastControl?.type === "C" ? 2 * cx - lastControl.x : cx;
2306
+ const y1 = lastControl?.type === "C" ? 2 * cy - lastControl.y : cy;
2307
+ const c = {
2308
+ type: "C",
2309
+ x1,
2310
+ y1,
2311
+ x2: args[0] + ox,
2312
+ y2: args[1] + oy,
2313
+ x: args[2] + ox,
2314
+ y: args[3] + oy
2315
+ };
2316
+ commands.push(c);
2317
+ lastControl = { x: c.x2, y: c.y2, type: "C" };
2318
+ cx = c.x;
2319
+ cy = c.y;
2320
+ break;
2321
+ }
2322
+ case "q": {
2323
+ const c = { type: "Q", x1: args[0] + ox, y1: args[1] + oy, x: args[2] + ox, y: args[3] + oy };
2324
+ commands.push(c);
2325
+ lastControl = { x: c.x1, y: c.y1, type: "Q" };
2326
+ cx = c.x;
2327
+ cy = c.y;
2328
+ break;
2329
+ }
2330
+ case "t": {
2331
+ const x1 = lastControl?.type === "Q" ? 2 * cx - lastControl.x : cx;
2332
+ const y1 = lastControl?.type === "Q" ? 2 * cy - lastControl.y : cy;
2333
+ const c = { type: "Q", x1, y1, x: args[0] + ox, y: args[1] + oy };
2334
+ commands.push(c);
2335
+ lastControl = { x: x1, y: y1, type: "Q" };
2336
+ cx = c.x;
2337
+ cy = c.y;
2338
+ break;
2339
+ }
2340
+ case "a": {
2341
+ const c = {
2342
+ type: "A",
2343
+ rx: Math.abs(args[0]),
2344
+ ry: Math.abs(args[1]),
2345
+ rotation: args[2],
2346
+ largeArc: args[3] !== 0,
2347
+ sweep: args[4] !== 0,
2348
+ x: args[5] + ox,
2349
+ y: args[6] + oy
2350
+ };
2351
+ commands.push(c);
2352
+ lastControl = null;
2353
+ cx = c.x;
2354
+ cy = c.y;
2355
+ break;
2356
+ }
2357
+ }
2358
+ }
2359
+ return commands;
2360
+ }
2361
+ function segmentCount(curvature, tolerance, max) {
2362
+ if (!(curvature > 0)) return 1;
2363
+ return Math.max(1, Math.min(max, Math.ceil(Math.sqrt(curvature / tolerance))));
2364
+ }
2365
+ function flattenCubic(out, p0, x1, y1, x2, y2, x, y, tolerance, max) {
2366
+ const dd = Math.max(
2367
+ Math.hypot(p0[0] - 2 * x1 + x2, p0[1] - 2 * y1 + y2),
2368
+ Math.hypot(x1 - 2 * x2 + x, y1 - 2 * y2 + y)
2369
+ );
2370
+ const n = segmentCount(6 * dd / 8, tolerance, max);
2371
+ for (let k = 1; k <= n; k++) {
2372
+ const t = k / n;
2373
+ const mt = 1 - t;
2374
+ const a = mt * mt * mt;
2375
+ const b = 3 * mt * mt * t;
2376
+ const c = 3 * mt * t * t;
2377
+ const e = t * t * t;
2378
+ out.push([a * p0[0] + b * x1 + c * x2 + e * x, a * p0[1] + b * y1 + c * y2 + e * y]);
2379
+ }
2380
+ }
2381
+ function flattenQuadratic(out, p0, x1, y1, x, y, tolerance, max) {
2382
+ const dd = Math.hypot(p0[0] - 2 * x1 + x, p0[1] - 2 * y1 + y);
2383
+ const n = segmentCount(2 * dd / 8, tolerance, max);
2384
+ for (let k = 1; k <= n; k++) {
2385
+ const t = k / n;
2386
+ const mt = 1 - t;
2387
+ out.push([mt * mt * p0[0] + 2 * mt * t * x1 + t * t * x, mt * mt * p0[1] + 2 * mt * t * y1 + t * t * y]);
2388
+ }
2389
+ }
2390
+ function arcSteps(radius, sweep, tolerance, max) {
2391
+ if (radius <= tolerance) return Math.max(1, Math.ceil(Math.abs(sweep) / (Math.PI / 2)));
2392
+ const step = 2 * Math.acos(1 - tolerance / radius);
2393
+ return Math.max(1, Math.min(max, Math.ceil(Math.abs(sweep) / step)));
2394
+ }
2395
+ function flattenArc(out, p0, cmd, tolerance, max) {
2396
+ const [x0, y0] = p0;
2397
+ let { rx, ry } = cmd;
2398
+ const { x, y } = cmd;
2399
+ if (x0 === x && y0 === y) return;
2400
+ if (rx === 0 || ry === 0) {
2401
+ out.push([x, y]);
2402
+ return;
2403
+ }
2404
+ const phi = cmd.rotation * Math.PI / 180;
2405
+ const cos = Math.cos(phi);
2406
+ const sin = Math.sin(phi);
2407
+ const dx = (x0 - x) / 2;
2408
+ const dy = (y0 - y) / 2;
2409
+ const x1p = cos * dx + sin * dy;
2410
+ const y1p = -sin * dx + cos * dy;
2411
+ const lambda = x1p * x1p / (rx * rx) + y1p * y1p / (ry * ry);
2412
+ if (lambda > 1) {
2413
+ const s = Math.sqrt(lambda);
2414
+ rx *= s;
2415
+ ry *= s;
2416
+ }
2417
+ const num = rx * rx * ry * ry - rx * rx * y1p * y1p - ry * ry * x1p * x1p;
2418
+ const den = rx * rx * y1p * y1p + ry * ry * x1p * x1p;
2419
+ let coef = den === 0 ? 0 : Math.sqrt(Math.max(0, num / den));
2420
+ if (cmd.largeArc === cmd.sweep) coef = -coef;
2421
+ const cxp = coef * rx * y1p / ry;
2422
+ const cyp = -coef * ry * x1p / rx;
2423
+ const cx = cos * cxp - sin * cyp + (x0 + x) / 2;
2424
+ const cy = sin * cxp + cos * cyp + (y0 + y) / 2;
2425
+ const angle = (ux, uy, vx, vy) => {
2426
+ const a = Math.atan2(ux * vy - uy * vx, ux * vx + uy * vy);
2427
+ return a;
2428
+ };
2429
+ const theta1 = angle(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry);
2430
+ let delta = angle((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, (-y1p - cyp) / ry);
2431
+ if (!cmd.sweep && delta > 0) delta -= 2 * Math.PI;
2432
+ else if (cmd.sweep && delta < 0) delta += 2 * Math.PI;
2433
+ const n = arcSteps(Math.max(rx, ry), delta, tolerance, max);
2434
+ for (let k = 1; k < n; k++) {
2435
+ const t = theta1 + delta * k / n;
2436
+ const ex = rx * Math.cos(t);
2437
+ const ey = ry * Math.sin(t);
2438
+ out.push([cos * ex - sin * ey + cx, sin * ex + cos * ey + cy]);
2439
+ }
2440
+ out.push([x, y]);
2441
+ }
2442
+ function flattenSvgPath(d, options = {}) {
2443
+ const tolerance = options.tolerance && options.tolerance > 0 ? options.tolerance : 0.25;
2444
+ const max = options.maxSegments ?? 512;
2445
+ const commands = typeof d === "string" ? parseSvgPath(d) : d;
2446
+ const rings = [];
2447
+ let current = [];
2448
+ let start = [0, 0];
2449
+ const flush = () => {
2450
+ if (current.length >= 3) rings.push(current);
2451
+ current = [];
2452
+ };
2453
+ const last = () => current[current.length - 1] ?? start;
2454
+ for (const cmd of commands) {
2455
+ switch (cmd.type) {
2456
+ case "M":
2457
+ flush();
2458
+ start = [cmd.x, cmd.y];
2459
+ current = [start];
2460
+ break;
2461
+ case "L":
2462
+ if (current.length === 0) current = [start];
2463
+ current.push([cmd.x, cmd.y]);
2464
+ break;
2465
+ case "C":
2466
+ if (current.length === 0) current = [start];
2467
+ flattenCubic(current, last(), cmd.x1, cmd.y1, cmd.x2, cmd.y2, cmd.x, cmd.y, tolerance, max);
2468
+ break;
2469
+ case "Q":
2470
+ if (current.length === 0) current = [start];
2471
+ flattenQuadratic(current, last(), cmd.x1, cmd.y1, cmd.x, cmd.y, tolerance, max);
2472
+ break;
2473
+ case "A":
2474
+ if (current.length === 0) current = [start];
2475
+ flattenArc(current, last(), cmd, tolerance, max);
2476
+ break;
2477
+ case "Z":
2478
+ flush();
2479
+ break;
2480
+ }
2481
+ }
2482
+ flush();
2483
+ return rings.map(dedupeRing).filter((r) => r.length >= 3);
2484
+ }
2485
+ function dedupeRing(ring) {
2486
+ const out = [];
2487
+ for (const p of ring) {
2488
+ const prev = out[out.length - 1];
2489
+ if (!prev || prev[0] !== p[0] || prev[1] !== p[1]) out.push(p);
2490
+ }
2491
+ while (out.length > 1 && out[0][0] === out[out.length - 1][0] && out[0][1] === out[out.length - 1][1]) out.pop();
2492
+ return out;
2493
+ }
2494
+ function cornerRadii(shape, width, height) {
2495
+ const raw = shape.cornerRadius ?? 0;
2496
+ let radii = (typeof raw === "number" ? [raw, raw, raw, raw] : [...raw]).map(
2497
+ (r) => Number.isFinite(r) && r > 0 ? r : 0
2498
+ );
2499
+ const factor = Math.min(
2500
+ 1,
2501
+ width / Math.max(radii[0] + radii[1], 1e-12),
2502
+ height / Math.max(radii[1] + radii[2], 1e-12),
2503
+ width / Math.max(radii[2] + radii[3], 1e-12),
2504
+ height / Math.max(radii[3] + radii[0], 1e-12)
2505
+ );
2506
+ if (factor < 1) radii = radii.map((r) => r * factor);
2507
+ return radii;
2508
+ }
2509
+ function polygonVertices(count, inner, width, height) {
2510
+ const raw = [];
2511
+ const total = inner === null ? count : count * 2;
2512
+ for (let k = 0; k < total; k++) {
2513
+ const angle = -Math.PI / 2 + k * 2 * Math.PI / total;
2514
+ const r = inner !== null && k % 2 === 1 ? inner : 1;
2515
+ raw.push([r * Math.cos(angle), r * Math.sin(angle)]);
2516
+ }
2517
+ let minX = Infinity;
2518
+ let minY = Infinity;
2519
+ let maxX = -Infinity;
2520
+ let maxY = -Infinity;
2521
+ for (const [x, y] of raw) {
2522
+ minX = Math.min(minX, x);
2523
+ minY = Math.min(minY, y);
2524
+ maxX = Math.max(maxX, x);
2525
+ maxY = Math.max(maxY, y);
2526
+ }
2527
+ const sx = width / (maxX - minX || 1);
2528
+ const sy = height / (maxY - minY || 1);
2529
+ return raw.map(([x, y]) => [(x - minX) * sx, (y - minY) * sy]);
2530
+ }
2531
+ function rotationTransform(shape, size) {
2532
+ const deg = shape.rotation ?? 0;
2533
+ if (!deg || !Number.isFinite(deg)) return null;
2534
+ const rad = deg * Math.PI / 180;
2535
+ const cos = Math.cos(rad);
2536
+ const sin = Math.sin(rad);
2537
+ const cx = size.width / 2;
2538
+ const cy = size.height / 2;
2539
+ return ([x, y]) => [cx + (x - cx) * cos - (y - cy) * sin, cy + (x - cx) * sin + (y - cy) * cos];
2540
+ }
2541
+ function getShapeSvg(shape, size) {
2542
+ const { width: w, height: h } = size;
2543
+ const f = (n) => formatNumber(n, 3);
2544
+ const transforms = [];
2545
+ if (shape.rotation) transforms.push(`rotate(${f(shape.rotation)} ${f(w / 2)} ${f(h / 2)})`);
2546
+ let d = "";
2547
+ let fillRule = "nonzero";
2548
+ switch (shape.kind) {
2549
+ case "rect": {
2550
+ const [tl, tr, br, bl] = cornerRadii(shape, w, h);
2551
+ if (tl + tr + br + bl === 0) {
2552
+ d = `M0 0H${f(w)}V${f(h)}H0Z`;
2553
+ } else {
2554
+ const arc = (r, x, y) => r > 0 ? `A${f(r)} ${f(r)} 0 0 1 ${f(x)} ${f(y)}` : "";
2555
+ d = `M${f(tl)} 0H${f(w - tr)}${arc(tr, w, tr)}V${f(h - br)}${arc(br, w - br, h)}H${f(bl)}${arc(bl, 0, h - bl)}V${f(tl)}${arc(tl, tl, 0)}Z`;
2556
+ }
2557
+ break;
2558
+ }
2559
+ case "ellipse": {
2560
+ const rx = w / 2;
2561
+ const ry = h / 2;
2562
+ d = `M0 ${f(ry)}A${f(rx)} ${f(ry)} 0 1 1 ${f(w)} ${f(ry)}A${f(rx)} ${f(ry)} 0 1 1 0 ${f(ry)}Z`;
2563
+ break;
2564
+ }
2565
+ case "polygon":
2566
+ case "star": {
2567
+ const points = shape.kind === "polygon" ? polygonVertices(Math.max(3, Math.floor(shape.sides ?? 6)), null, w, h) : polygonVertices(Math.max(3, Math.floor(shape.points ?? 5)), clampRatio(shape.innerRadius), w, h);
2568
+ d = ringsToPath([points], f);
2569
+ break;
2570
+ }
2571
+ case "path": {
2572
+ d = shape.d;
2573
+ fillRule = shape.fillRule ?? "nonzero";
2574
+ const vb = validViewBox(shape.viewBox);
2575
+ if (vb) {
2576
+ transforms.push(
2577
+ `scale(${f(w / vb[2])} ${f(h / vb[3])})`,
2578
+ `translate(${f(-vb[0])} ${f(-vb[1])})`
2579
+ );
2580
+ }
2581
+ break;
2582
+ }
2583
+ }
2584
+ return transforms.length > 0 ? { d, transform: transforms.join(" "), fillRule } : { d, fillRule };
2585
+ }
2586
+ function getShapeRenderData(shape, size = {}) {
2587
+ const vb = shape.kind === "path" ? validViewBox(shape.viewBox) : null;
2588
+ const width = size.width && size.width > 0 ? size.width : vb ? vb[2] : 100;
2589
+ const height = size.height && size.height > 0 ? size.height : vb ? vb[3] : 100;
2590
+ const svg = getShapeSvg(shape, { width, height });
2591
+ const data = {
2592
+ viewBox: `0 0 ${formatNumber(width, 3)} ${formatNumber(height, 3)}`,
2593
+ width,
2594
+ height,
2595
+ d: svg.d,
2596
+ fillRule: svg.fillRule,
2597
+ fill: shape.fill ?? "currentColor",
2598
+ stroke: shape.stroke ?? "none",
2599
+ strokeWidth: shape.strokeWidth ?? 1
2600
+ };
2601
+ if (svg.transform) data.transform = svg.transform;
2602
+ if (shape.opacity !== void 0) data.opacity = shape.opacity;
2603
+ return data;
2604
+ }
2605
+ function clampRatio(value) {
2606
+ if (value === void 0 || !Number.isFinite(value)) return 0.5;
2607
+ return Math.min(1, Math.max(0.01, value));
2608
+ }
2609
+ function validViewBox(vb) {
2610
+ if (!vb || vb.length !== 4 || !vb.every(Number.isFinite) || vb[2] <= 0 || vb[3] <= 0) return null;
2611
+ return vb;
2612
+ }
2613
+ function shapeToPolygon(shape, size, options = {}) {
2614
+ const { width: w, height: h } = size;
2615
+ if (!(w > 0) || !(h > 0)) return { rings: [], fillRule: "nonzero" };
2616
+ let rings;
2617
+ let fillRule = "nonzero";
2618
+ if (shape.kind === "path") {
2619
+ fillRule = shape.fillRule ?? "nonzero";
2620
+ const vb = validViewBox(shape.viewBox);
2621
+ if (vb) {
2622
+ const sx = w / vb[2];
2623
+ const sy = h / vb[3];
2624
+ const tolerance = (options.tolerance ?? 0.25) / Math.max(sx, sy);
2625
+ rings = flattenSvgPath(shape.d, { ...options, tolerance }).map(
2626
+ (ring) => ring.map(([x, y]) => [(x - vb[0]) * sx, (y - vb[1]) * sy])
2627
+ );
2628
+ } else {
2629
+ rings = flattenSvgPath(shape.d, options);
2630
+ }
2631
+ } else {
2632
+ const { d } = getShapeSvg({ ...shape, rotation: 0 }, size);
2633
+ rings = flattenSvgPath(d, options);
2634
+ }
2635
+ const rotate = rotationTransform(shape, size);
2636
+ if (rotate) rings = rings.map((ring) => ring.map(rotate));
2637
+ return { rings, fillRule };
2638
+ }
2639
+ function formatNumber(value, precision = 3) {
2640
+ const factor = 10 ** precision;
2641
+ const rounded = Math.round(value * factor) / factor;
2642
+ return Object.is(rounded, -0) ? "0" : String(rounded);
2643
+ }
2644
+ function ringsToPath(rings, f) {
2645
+ let d = "";
2646
+ for (const ring of rings) {
2647
+ if (ring.length < 2) continue;
2648
+ d += `M${f(ring[0][0])} ${f(ring[0][1])}`;
2649
+ for (let i = 1; i < ring.length; i++) d += `L${f(ring[i][0])} ${f(ring[i][1])}`;
2650
+ d += "Z";
2651
+ }
2652
+ return d;
2653
+ }
2654
+ function segmentDistance(p, a, b) {
2655
+ const dx = b[0] - a[0];
2656
+ const dy = b[1] - a[1];
2657
+ const len2 = dx * dx + dy * dy;
2658
+ if (len2 === 0) return Math.hypot(p[0] - a[0], p[1] - a[1]);
2659
+ const t = Math.max(0, Math.min(1, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / len2));
2660
+ return Math.hypot(p[0] - (a[0] + t * dx), p[1] - (a[1] + t * dy));
2661
+ }
2662
+ function rdp(points, tolerance) {
2663
+ if (points.length <= 2) return points;
2664
+ const keep = new Uint8Array(points.length);
2665
+ keep[0] = 1;
2666
+ keep[points.length - 1] = 1;
2667
+ const stack = [[0, points.length - 1]];
2668
+ while (stack.length) {
2669
+ const [s, e] = stack.pop();
2670
+ let maxDist = -1;
2671
+ let index = -1;
2672
+ 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;
2676
+ index = i;
2677
+ }
2678
+ }
2679
+ if (index !== -1 && maxDist > tolerance) {
2680
+ keep[index] = 1;
2681
+ stack.push([s, index], [index, e]);
2682
+ }
2683
+ }
2684
+ return points.filter((_, i) => keep[i]);
2685
+ }
2686
+ function simplifyRing(ring, tolerance) {
2687
+ if (ring.length <= 3 || !(tolerance > 0)) return [...ring];
2688
+ let far = 0;
2689
+ let farDist = -1;
2690
+ 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;
2694
+ far = i;
2695
+ }
2696
+ }
2697
+ const first = rdp(ring.slice(0, far + 1), tolerance);
2698
+ const second = rdp([...ring.slice(far), ring[0]], tolerance);
2699
+ const out = [...first.slice(0, -1), ...second.slice(0, -1)];
2700
+ return out.length >= 3 ? out : [];
2701
+ }
2702
+ function polygonToSvgPath(input, options = {}) {
2703
+ const precision = options.precision ?? 3;
2704
+ const f = (n) => formatNumber(n, precision);
2705
+ const dx = options.offset?.x ?? 0;
2706
+ const dy = options.offset?.y ?? 0;
2707
+ const rings = [];
2708
+ for (const item of input) {
2709
+ const list = item.length > 0 && Array.isArray(item[0]) && Array.isArray(item[0][0]) ? item : [item];
2710
+ for (const ring of list) {
2711
+ let r = ring;
2712
+ if (options.simplify && options.simplify > 0) r = simplifyRing(r, options.simplify);
2713
+ if (r.length < 3) continue;
2714
+ rings.push(dx || dy ? r.map(([x, y]) => [x + dx, y + dy]) : r);
2715
+ }
2716
+ }
2717
+ return ringsToPath(rings, f);
2718
+ }
2719
+
2720
+ // src/canvas/shapeNodes.ts
2721
+ var SHAPE_DATA_KEY = "shape";
2722
+ var SHAPE_NODE_TYPE = "shape";
2723
+ var SHAPE_OPERATIONS = ["unite", "intersect", "subtract", "exclude", "divide"];
2724
+ var BOOLEAN_FOR = {
2725
+ unite: "union",
2726
+ intersect: "intersection",
2727
+ subtract: "difference",
2728
+ exclude: "xor"
2729
+ };
2730
+ function isShapeNode(node) {
2731
+ return !!node && isShapeDescription(node.data?.[SHAPE_DATA_KEY]);
2732
+ }
2733
+ function getNodeShape(node) {
2734
+ const shape = node?.data?.[SHAPE_DATA_KEY];
2735
+ return isShapeDescription(shape) ? shape : null;
2736
+ }
2737
+ function shapeNodeToPolygon(node, lookup, options = {}) {
2738
+ const shape = getNodeShape(node);
2739
+ if (!shape) return null;
2740
+ const size = getNodeSize(node, options);
2741
+ const origin = node.parentId && lookup ? getAbsolutePosition(node, lookup) : node.position;
2742
+ const { rings, fillRule } = shapeToPolygon(shape, size, { tolerance: options.tolerance });
2743
+ return {
2744
+ rings: rings.map((ring) => ring.map(([x, y]) => [x + origin.x, y + origin.y])),
2745
+ fillRule
2746
+ };
2747
+ }
2748
+ var idCounter = 0;
2749
+ function defaultId2(index) {
2750
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
2751
+ idCounter += 1;
2752
+ return `shape-${Date.now().toString(36)}-${idCounter}-${index}`;
2753
+ }
2754
+ function stackOrder(nodes, ids) {
2755
+ return nodes.map((node, index) => ({ node, index, z: typeof node.zIndex === "number" ? node.zIndex : 0 })).filter((entry) => ids.has(entry.node.id)).sort((a, b) => a.z - b.z || a.index - b.index).map((entry) => entry.node);
2756
+ }
2757
+ function canCombineShapeNodes(nodes, ids) {
2758
+ const idSet = new Set(ids);
2759
+ const shapes = nodes.filter((n) => idSet.has(n.id) && isShapeNode(n));
2760
+ if (shapes.some((n) => isLocked(n))) return { ok: false, reason: "locked" };
2761
+ if (shapes.length < 2) return { ok: false, reason: "too-few" };
2762
+ return { ok: true, shapeIds: shapes.map((n) => n.id) };
2763
+ }
2764
+ function combineShapeNodes(nodes, ids, operation, options = {}) {
2765
+ const idList = [...ids];
2766
+ const check = canCombineShapeNodes(nodes, idList);
2767
+ if (!check.ok) return { ok: false, operation, reason: check.reason };
2768
+ const lookup = createNodeLookup(nodes);
2769
+ const shapeIds = new Set(check.shapeIds);
2770
+ const ordered = stackOrder(nodes, shapeIds);
2771
+ const operands = ordered.map((node) => shapeNodeToPolygon(node, lookup, options));
2772
+ const parentIds = new Set(ordered.map((n) => n.parentId ?? ""));
2773
+ const sharedParent = parentIds.size === 1 ? ordered[0].parentId : void 0;
2774
+ const parentOrigin = sharedParent && lookup.get(sharedParent) ? getAbsolutePosition(lookup.get(sharedParent), lookup) : { x: 0, y: 0 };
2775
+ const pickSource = () => {
2776
+ const style = options.styleFrom ?? "bottom";
2777
+ if (style === "top") return ordered[ordered.length - 1];
2778
+ if (style === "first") {
2779
+ const first = idList.find((id) => shapeIds.has(id));
2780
+ return first && lookup.get(first) || ordered[0];
2781
+ }
2782
+ if (typeof style === "object") return shapeIds.has(style.id) && lookup.get(style.id) || ordered[0];
2783
+ return ordered[0];
2784
+ };
2785
+ const createId = options.createId ?? defaultId2;
2786
+ const build = (polygon, source, index) => {
2787
+ const bounds = multiPolygonBounds(polygon);
2788
+ if (!bounds) return null;
2789
+ const width = bounds.maxX - bounds.minX;
2790
+ const height = bounds.maxY - bounds.minY;
2791
+ if (!(width > 0) || !(height > 0)) return null;
2792
+ const d = polygonToSvgPath(polygon, {
2793
+ precision: options.precision,
2794
+ simplify: options.simplify,
2795
+ offset: { x: -bounds.minX, y: -bounds.minY }
2796
+ });
2797
+ if (!d) return null;
2798
+ const sourceShape = getNodeShape(source);
2799
+ const shape = {
2800
+ kind: "path",
2801
+ d,
2802
+ viewBox: [0, 0, roundSize(width), roundSize(height)],
2803
+ fillRule: "nonzero"
2804
+ };
2805
+ for (const key of ["fill", "stroke", "strokeWidth", "opacity"]) {
2806
+ if (sourceShape[key] !== void 0) shape[key] = sourceShape[key];
2807
+ }
2808
+ const {
2809
+ [LOCKED_DATA_KEY]: _locked,
2810
+ [LOCKED_PREVIOUS_FLAGS_KEY]: _previous,
2811
+ ...data
2812
+ } = source.data ?? {};
2813
+ const {
2814
+ measured: _measured,
2815
+ selected: _selected,
2816
+ dragging: _dragging,
2817
+ positionAbsolute: _positionAbsolute,
2818
+ internals: _internals,
2819
+ parentId: _parentId,
2820
+ ...rest
2821
+ } = source;
2822
+ const node = {
2823
+ ...rest,
2824
+ id: createId(index),
2825
+ position: { x: bounds.minX - parentOrigin.x, y: bounds.minY - parentOrigin.y },
2826
+ width,
2827
+ height,
2828
+ data: { ...data, [SHAPE_DATA_KEY]: shape }
2829
+ };
2830
+ if (sharedParent) node.parentId = sharedParent;
2831
+ if (options.nodeType !== void 0) node.type = options.nodeType;
2832
+ if (source.style) node.style = withStyleSize(source.style, { width, height });
2833
+ if (options.select !== false) node.selected = true;
2834
+ return node;
2835
+ };
2836
+ const created = [];
2837
+ if (operation === "divide") {
2838
+ const pieces = dividePolygons(operands, { epsilon: options.epsilon });
2839
+ pieces.forEach((piece) => {
2840
+ const source = ordered[piece.operands[piece.operands.length - 1]];
2841
+ const node = build([piece.polygon], source, created.length);
2842
+ if (node) created.push(node);
2843
+ });
2844
+ } else {
2845
+ const result = polygonBoolean(BOOLEAN_FOR[operation], operands, { epsilon: options.epsilon });
2846
+ const node = build(result, pickSource(), 0);
2847
+ if (node) created.push(node);
2848
+ }
2849
+ if (created.length === 0) return { ok: false, operation, reason: "empty" };
2850
+ return { ok: true, operation, removeIds: ordered.map((n) => n.id), nodes: created };
2851
+ }
2852
+ function roundSize(value) {
2853
+ return Math.round(value * 1e3) / 1e3;
2854
+ }
2855
+ function applyShapeCombination(nodes, change, options = {}) {
2856
+ const remove = new Set(change.removeIds);
2857
+ const insertAt = nodes.findIndex((n) => remove.has(n.id));
2858
+ const deselect = options.deselect !== false && change.nodes.some((n) => n.selected);
2859
+ const out = [];
2860
+ nodes.forEach((node, index) => {
2861
+ if (index === insertAt) out.push(...change.nodes);
2862
+ if (remove.has(node.id)) return;
2863
+ out.push(deselect && node.selected ? { ...node, selected: false } : node);
2864
+ });
2865
+ if (insertAt === -1) out.push(...change.nodes);
2866
+ return out;
2867
+ }
2868
+ function revertShapeCombination(nodes, change, previous) {
2869
+ const created = new Set(change.nodes.map((n) => n.id));
2870
+ const removed = new Set(change.removeIds);
2871
+ const out = nodes.filter((n) => !created.has(n.id));
2872
+ previous.forEach((node, index) => {
2873
+ if (!removed.has(node.id) || out.some((n) => n.id === node.id)) return;
2874
+ out.splice(Math.min(index, out.length), 0, node);
2875
+ });
2876
+ return out;
2877
+ }
2878
+
1708
2879
  // src/canvas/arrangeController.ts
1709
2880
  function createCanvasArrangeController(options) {
1710
2881
  let guides = [];
1711
2882
  let dragIndex = null;
1712
2883
  let dragKey = "";
2884
+ let lastSnapped = /* @__PURE__ */ new Map();
1713
2885
  const selectedIds = (ids) => ids ? [...ids] : options.getSelectedIds?.() ?? options.getNodes().filter((n) => n.selected).map((n) => n.id);
1714
2886
  const setGuides = (next) => {
1715
2887
  if (next.length === 0 && guides.length === 0) return;
@@ -1777,6 +2949,30 @@ function createCanvasArrangeController(options) {
1777
2949
  return anyUnlocked ? controller.lock(list) : controller.unlock(list);
1778
2950
  },
1779
2951
  isLocked: (id) => isLocked(options.getNodes().find((n) => n.id === id)),
2952
+ /**
2953
+ * Freeform shape operation (Unite / Intersect / Subtract / Exclude / Divide) on the
2954
+ * shape nodes among `ids` (default: the selection). Replaces them with the result as
2955
+ * one undoable step. Refuses without changing anything when a selected shape is
2956
+ * locked, fewer than two shapes are selected, or the result is empty.
2957
+ */
2958
+ combineShapes(operation, ids, combineOptions = {}) {
2959
+ const before = options.getNodes();
2960
+ const result = combineShapeNodes(before, selectedIds(ids), operation, { ...rect(), ...combineOptions });
2961
+ if (!result.ok) return result;
2962
+ const name = `${operation[0].toUpperCase()}${operation.slice(1)} Shapes`;
2963
+ const run = () => options.setNodes(applyShapeCombination(options.getNodes(), result));
2964
+ const undo = () => options.setNodes(revertShapeCombination(options.getNodes(), result, before));
2965
+ if (options.history) options.history.execute({ name, execute: run, undo });
2966
+ else run();
2967
+ return result;
2968
+ },
2969
+ unite: (ids, o) => controller.combineShapes("unite", ids, o),
2970
+ intersect: (ids, o) => controller.combineShapes("intersect", ids, o),
2971
+ subtract: (ids, o) => controller.combineShapes("subtract", ids, o),
2972
+ exclude: (ids, o) => controller.combineShapes("exclude", ids, o),
2973
+ divide: (ids, o) => controller.combineShapes("divide", ids, o),
2974
+ /** Whether the shape operations can run on `ids` (default: the selection). */
2975
+ canCombineShapes: (ids) => canCombineShapeNodes(options.getNodes(), selectedIds(ids)).ok,
1780
2976
  /**
1781
2977
  * Runs a shortcut action. Scene actions are not handled here (see the scenes store);
1782
2978
  * returns `false` for them and for unknown actions.
@@ -1851,8 +3047,10 @@ function createCanvasArrangeController(options) {
1851
3047
  zoom: options.getZoom?.() ?? 1,
1852
3048
  index: dragIndex
1853
3049
  });
3050
+ lastSnapped = /* @__PURE__ */ new Map();
1854
3051
  if (result.updates.length > 0) {
1855
3052
  options.setNodes(applyNodeUpdates(options.getNodes(), result.updates));
3053
+ for (const u of result.updates) if (u.position) lastSnapped.set(u.id, u.position);
1856
3054
  }
1857
3055
  setGuides(result.guides);
1858
3056
  return result;
@@ -1863,10 +3061,17 @@ function createCanvasArrangeController(options) {
1863
3061
  * A batch without a dragging position change ends the drag and clears the guides.
1864
3062
  */
1865
3063
  snapPositionChanges(changes) {
1866
- const dragging = changes.filter((c) => c.type === "position" && c.position && c.dragging);
3064
+ const dragging = changes.filter(
3065
+ (c) => c.type === "position" && typeof c.id === "string" && !!c.position && !!c.dragging
3066
+ );
1867
3067
  if (dragging.length === 0) {
1868
- if (changes.some((c) => c.type === "position" && c.dragging === false)) controller.endDrag();
1869
- return changes;
3068
+ if (!changes.some((c) => c.type === "position" && c.dragging === false)) return changes;
3069
+ const snapped = lastSnapped;
3070
+ lastSnapped = /* @__PURE__ */ new Map();
3071
+ controller.endDrag();
3072
+ const keep = (c) => c.type === "position" && c.dragging === false && !!c.position && c.id !== void 0 && snapped.has(c.id);
3073
+ if (!changes.some(keep)) return changes;
3074
+ return changes.map((c) => keep(c) ? { ...c, position: snapped.get(c.id) } : c);
1870
3075
  }
1871
3076
  if (options.snapping === false) return changes;
1872
3077
  const ids = dragging.map((c) => c.id);
@@ -1882,18 +3087,37 @@ function createCanvasArrangeController(options) {
1882
3087
  });
1883
3088
  setGuides(result.guides);
1884
3089
  const { x: dx, y: dy } = result.offset;
3090
+ lastSnapped = /* @__PURE__ */ new Map();
1885
3091
  if (dx === 0 && dy === 0) return changes;
1886
3092
  const parents = new Map(nodes.map((n) => [n.id, n.parentId]));
1887
3093
  const shift = new Set(ids.filter((id) => !proposed.has(parents.get(id) ?? "")));
1888
- return changes.map(
1889
- (c) => c.type === "position" && c.position && c.dragging && shift.has(c.id) ? { ...c, position: { x: c.position.x + dx, y: c.position.y + dy } } : c
1890
- );
3094
+ return changes.map((c) => {
3095
+ if (!(c.type === "position" && c.position && c.dragging && c.id !== void 0 && shift.has(c.id))) return c;
3096
+ const position = { x: c.position.x + dx, y: c.position.y + dy };
3097
+ lastSnapped.set(c.id, position);
3098
+ return { ...c, position };
3099
+ });
1891
3100
  },
1892
- /** Clears guides and the cached snap targets. */
3101
+ /**
3102
+ * Clears guides and the cached snap targets. Call it from the flow's drag-stop
3103
+ * handler: flow libraries write the raw pointer position when a drag ends, so the
3104
+ * last snapped positions from `drag` are written back here.
3105
+ */
1893
3106
  endDrag() {
3107
+ const snapped = lastSnapped;
1894
3108
  dragIndex = null;
1895
3109
  dragKey = "";
3110
+ lastSnapped = /* @__PURE__ */ new Map();
1896
3111
  setGuides([]);
3112
+ if (snapped.size === 0) return;
3113
+ const nodes = options.getNodes();
3114
+ const drift = nodes.some((n) => {
3115
+ const p = snapped.get(n.id);
3116
+ return !!p && (n.position.x !== p.x || n.position.y !== p.y);
3117
+ });
3118
+ if (drift) {
3119
+ options.setNodes(applyNodeUpdates(nodes, [...snapped].map(([id, position]) => ({ id, position }))));
3120
+ }
1897
3121
  }
1898
3122
  };
1899
3123
  return controller;
@@ -1908,17 +3132,24 @@ export {
1908
3132
  LOCKED_DATA_KEY,
1909
3133
  LOCKED_PREVIOUS_FLAGS_KEY,
1910
3134
  ManualClock,
3135
+ SHAPE_DATA_KEY,
3136
+ SHAPE_KINDS,
3137
+ SHAPE_NODE_TYPE,
3138
+ SHAPE_OPERATIONS,
1911
3139
  StreamingLayoutManager,
1912
3140
  alignNodes,
1913
3141
  applyNodeUpdates,
3142
+ applyShapeCombination,
1914
3143
  blendLayout,
1915
3144
  boundsOf,
1916
3145
  calculateDagreReflowDisplacement,
1917
3146
  calculateDirectionalDisplacement,
3147
+ canCombineShapeNodes,
1918
3148
  canvasHeight,
1919
3149
  centerNodes,
1920
3150
  centre,
1921
3151
  collectDescendantIds,
3152
+ combineShapeNodes,
1922
3153
  computeNodesBoundingBox,
1923
3154
  computeSnapGuides,
1924
3155
  connectedOnly,
@@ -1931,22 +3162,31 @@ export {
1931
3162
  createNodeLookup,
1932
3163
  defaultEasing,
1933
3164
  distributeNodes,
3165
+ dividePolygons,
1934
3166
  ensureLayout,
1935
3167
  estimateCanvasHeight,
1936
3168
  estimateNodeSize,
1937
3169
  findConnectorSnap,
1938
3170
  fitViewportToNodes,
1939
3171
  fitViewportToRect,
3172
+ flattenSvgPath,
1940
3173
  flowSide,
3174
+ formatNumber,
1941
3175
  getAbsolutePosition,
1942
3176
  getCenteredNodePosition,
1943
3177
  getLayoutedNodes,
1944
3178
  getNodeRect,
3179
+ getNodeShape,
1945
3180
  getNodeSize,
3181
+ getShapeRenderData,
3182
+ getShapeSvg,
1946
3183
  handlePoint,
3184
+ intersectPolygons,
1947
3185
  invertNodeUpdates,
1948
3186
  isEventFromTextInput,
1949
3187
  isLocked,
3188
+ isShapeDescription,
3189
+ isShapeNode,
1950
3190
  isValidCanvasEdge,
1951
3191
  isValidCanvasNode,
1952
3192
  isValidCanvasScene,
@@ -1957,26 +3197,42 @@ export {
1957
3197
  lockNodes,
1958
3198
  matchArrangeShortcut,
1959
3199
  matchSize,
3200
+ multiPolygonArea,
3201
+ multiPolygonBounds,
1960
3202
  nearestHandlePositions,
1961
3203
  oppositePosition,
1962
3204
  overlaps,
1963
3205
  pageHidden,
1964
3206
  parseCanvasClipboardData,
1965
3207
  parseCanvasScenes,
3208
+ parseSvgPath,
1966
3209
  placeBeside,
3210
+ pointInMultiPolygon,
3211
+ pointInRings,
3212
+ polygonArea,
3213
+ polygonBoolean,
3214
+ polygonToSvgPath,
1967
3215
  positionToSide,
1968
3216
  readStyleSize,
1969
3217
  realClock,
1970
3218
  remapPastedNodes,
1971
3219
  reorderNodes,
1972
3220
  repositionNodesTo,
3221
+ revertShapeCombination,
3222
+ ringSignedArea,
1973
3223
  runMultiNodeTransition,
1974
3224
  serializeCanvasNodes,
3225
+ shapeNodeToPolygon,
3226
+ shapeToPolygon,
1975
3227
  sideToPosition,
3228
+ simplifyRing,
1976
3229
  snapNodeDrag,
3230
+ subtractPolygons,
1977
3231
  toggleLockNodes,
3232
+ unionPolygons,
1978
3233
  unlockNodes,
1979
3234
  withStyleOpacity,
1980
- withStyleSize
3235
+ withStyleSize,
3236
+ xorPolygons
1981
3237
  };
1982
3238
  //# sourceMappingURL=index.js.map