@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.
- package/README.md +20 -11
- package/dist/agent/index.cjs +8 -8
- package/dist/agent/index.d.cts +2 -2
- package/dist/agent/index.d.ts +2 -2
- package/dist/agent/index.js +2 -2
- package/dist/canvas/index.cjs +1269 -13
- package/dist/canvas/index.cjs.map +1 -1
- package/dist/canvas/index.d.cts +432 -6
- package/dist/canvas/index.d.ts +432 -6
- package/dist/canvas/index.js +1264 -8
- package/dist/canvas/index.js.map +1 -1
- package/dist/{chunk-U6NDQFOK.cjs → chunk-6M3BCTBD.cjs} +427 -138
- package/dist/chunk-6M3BCTBD.cjs.map +1 -0
- package/dist/{chunk-TVRC4KVT.cjs → chunk-AA4ZEVCC.cjs} +845 -90
- package/dist/chunk-AA4ZEVCC.cjs.map +1 -0
- package/dist/{chunk-LA7PEGU3.cjs → chunk-CL37JGFW.cjs} +4 -4
- package/dist/{chunk-LA7PEGU3.cjs.map → chunk-CL37JGFW.cjs.map} +1 -1
- package/dist/{chunk-A7DKIA72.js → chunk-DENSCKFC.js} +642 -69
- package/dist/chunk-DENSCKFC.js.map +1 -0
- package/dist/{chunk-4QPLEUL2.cjs → chunk-GVSMRPHB.cjs} +1046 -54
- package/dist/chunk-GVSMRPHB.cjs.map +1 -0
- package/dist/{chunk-KYRZDWM4.js → chunk-JJDLPEWL.js} +2 -2
- package/dist/{chunk-3TYMNULT.js → chunk-MJZBNWG7.js} +807 -52
- package/dist/chunk-MJZBNWG7.js.map +1 -0
- package/dist/{chunk-CYFJCKC4.js → chunk-QGHGJ56O.js} +997 -5
- package/dist/chunk-QGHGJ56O.js.map +1 -0
- package/dist/{chunk-GVW4P7BG.js → chunk-RU52I7QO.js} +305 -16
- package/dist/chunk-RU52I7QO.js.map +1 -0
- package/dist/{chunk-JYRIOFEN.cjs → chunk-SAZRENEZ.cjs} +647 -74
- package/dist/chunk-SAZRENEZ.cjs.map +1 -0
- package/dist/{controller-OUdxPY-X.d.ts → controller-B7GIybc4.d.cts} +139 -7
- package/dist/{controller-OUdxPY-X.d.cts → controller-B7GIybc4.d.ts} +139 -7
- package/dist/extensions/index.cjs +14 -4
- package/dist/extensions/index.cjs.map +1 -1
- package/dist/extensions/index.d.cts +3 -3
- package/dist/extensions/index.d.ts +3 -3
- package/dist/extensions/index.js +13 -3
- package/dist/{index-k3mFbPx_.d.cts → index-CCr_z9kF.d.cts} +3 -3
- package/dist/{index-Ng1pG7JE.d.ts → index-Ck6ajwXh.d.ts} +3 -3
- package/dist/index.cjs +250 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +294 -8
- package/dist/index.d.ts +294 -8
- package/dist/index.js +241 -7
- package/dist/index.js.map +1 -1
- package/dist/{registry-iAHbqkYt.d.cts → registry-GJI4gToU.d.cts} +15 -4
- package/dist/{registry-iAHbqkYt.d.ts → registry-GJI4gToU.d.ts} +15 -4
- package/dist/serializers/index.cjs +3 -3
- package/dist/serializers/index.d.cts +1 -1
- package/dist/serializers/index.d.ts +1 -1
- package/dist/serializers/index.js +2 -2
- package/dist/spreadsheet/index.cjs +7 -3
- package/dist/spreadsheet/index.cjs.map +1 -1
- package/dist/spreadsheet/index.d.cts +33 -4
- package/dist/spreadsheet/index.d.ts +33 -4
- package/dist/spreadsheet/index.js +6 -2
- package/dist/{track-changes-G8JcWB8b.d.cts → track-changes-BSF58RK0.d.cts} +92 -9
- package/dist/{track-changes-G8JcWB8b.d.ts → track-changes-BSF58RK0.d.ts} +92 -9
- package/dist/{types-DMe0vFB1.d.cts → types-CJ4U-HHI.d.cts} +1 -1
- package/dist/{types-DMe0vFB1.d.ts → types-CJ4U-HHI.d.ts} +1 -1
- package/package.json +1 -1
- package/dist/chunk-3TYMNULT.js.map +0 -1
- package/dist/chunk-4QPLEUL2.cjs.map +0 -1
- package/dist/chunk-A7DKIA72.js.map +0 -1
- package/dist/chunk-CYFJCKC4.js.map +0 -1
- package/dist/chunk-GVW4P7BG.js.map +0 -1
- package/dist/chunk-JYRIOFEN.cjs.map +0 -1
- package/dist/chunk-TVRC4KVT.cjs.map +0 -1
- package/dist/chunk-U6NDQFOK.cjs.map +0 -1
- /package/dist/{chunk-KYRZDWM4.js.map → chunk-JJDLPEWL.js.map} +0 -0
package/dist/canvas/index.js
CHANGED
|
@@ -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(
|
|
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))
|
|
1869
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|