@pixerate/editor 0.21.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -13
- package/THIRD_PARTY_NOTICES.md +88 -0
- package/dist/agent/index.cjs +86 -111
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +69 -211
- package/dist/agent/index.d.ts +69 -211
- package/dist/agent/index.js +77 -102
- package/dist/agent/index.js.map +1 -1
- package/dist/arrangeShortcuts-B8O3f8-P.d.cts +184 -0
- package/dist/arrangeShortcuts-sLlD2rSX.d.ts +184 -0
- package/dist/canvas/index.cjs +2112 -60
- package/dist/canvas/index.cjs.map +1 -1
- package/dist/canvas/index.d.cts +712 -181
- package/dist/canvas/index.d.ts +712 -181
- package/dist/canvas/index.js +2105 -53
- package/dist/canvas/index.js.map +1 -1
- package/dist/chunk-26BVFKHU.cjs +453 -0
- package/dist/chunk-26BVFKHU.cjs.map +1 -0
- package/dist/chunk-2H7ZHPEJ.js +762 -0
- package/dist/chunk-2H7ZHPEJ.js.map +1 -0
- package/dist/chunk-7DWMJCAT.js +453 -0
- package/dist/chunk-7DWMJCAT.js.map +1 -0
- package/dist/{chunk-TVRC4KVT.cjs → chunk-AA4ZEVCC.cjs} +845 -90
- package/dist/chunk-AA4ZEVCC.cjs.map +1 -0
- package/dist/chunk-ARRTP33I.js +101 -0
- package/dist/chunk-ARRTP33I.js.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-CPOKFNBG.cjs +101 -0
- package/dist/chunk-CPOKFNBG.cjs.map +1 -0
- package/dist/chunk-G2I4QJOT.cjs +762 -0
- package/dist/chunk-G2I4QJOT.cjs.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-GVW4P7BG.js → chunk-K3R5NG2D.js} +448 -54
- package/dist/chunk-K3R5NG2D.js.map +1 -0
- package/dist/{chunk-3TYMNULT.js → chunk-MJZBNWG7.js} +807 -52
- package/dist/chunk-MJZBNWG7.js.map +1 -0
- package/dist/{chunk-A7DKIA72.js → chunk-QBJ22YZR.js} +1059 -93
- package/dist/chunk-QBJ22YZR.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-JYRIOFEN.cjs → chunk-RIKBJLS2.cjs} +1064 -98
- package/dist/chunk-RIKBJLS2.cjs.map +1 -0
- package/dist/{chunk-U6NDQFOK.cjs → chunk-XDMXXC4Z.cjs} +569 -175
- package/dist/chunk-XDMXXC4Z.cjs.map +1 -0
- package/dist/commands/index.cjs +860 -0
- package/dist/commands/index.cjs.map +1 -0
- package/dist/commands/index.d.cts +592 -0
- package/dist/commands/index.d.ts +592 -0
- package/dist/commands/index.js +860 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/{controller-OUdxPY-X.d.ts → controller-pePCJLT1.d.cts} +216 -11
- package/dist/{controller-OUdxPY-X.d.cts → controller-pePCJLT1.d.ts} +216 -11
- 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/history/index.cjs +12 -2
- package/dist/history/index.cjs.map +1 -1
- package/dist/history/index.d.cts +211 -7
- package/dist/history/index.d.ts +211 -7
- package/dist/history/index.js +13 -3
- package/dist/image-editor/index.cjs +418 -13
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +154 -3
- package/dist/image-editor/index.d.ts +154 -3
- package/dist/image-editor/index.js +417 -12
- package/dist/image-editor/index.js.map +1 -1
- 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 +275 -83
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +296 -47
- package/dist/index.d.ts +296 -47
- package/dist/index.js +266 -74
- package/dist/index.js.map +1 -1
- package/dist/registry-D5-R90p0.d.cts +209 -0
- package/dist/registry-D5-R90p0.d.ts +209 -0
- 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 +23 -3
- package/dist/spreadsheet/index.cjs.map +1 -1
- package/dist/spreadsheet/index.d.cts +97 -10
- package/dist/spreadsheet/index.d.ts +97 -10
- package/dist/spreadsheet/index.js +22 -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-DX4Mh4SL.d.cts} +1 -1
- package/dist/{types-DMe0vFB1.d.ts → types-DX4Mh4SL.d.ts} +1 -1
- package/dist/types-GT5a5YAX.d.cts +86 -0
- package/dist/types-GT5a5YAX.d.ts +86 -0
- package/package.json +8 -2
- package/dist/chunk-2TWGLC5K.cjs +0 -262
- package/dist/chunk-2TWGLC5K.cjs.map +0 -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-KAADNAPF.js +0 -262
- package/dist/chunk-KAADNAPF.js.map +0 -1
- package/dist/chunk-TVRC4KVT.cjs.map +0 -1
- package/dist/chunk-U6NDQFOK.cjs.map +0 -1
- package/dist/types-BZgcDsC5.d.cts +0 -42
- package/dist/types-BZgcDsC5.d.ts +0 -42
- /package/dist/{chunk-KYRZDWM4.js.map → chunk-JJDLPEWL.js.map} +0 -0
package/dist/canvas/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_ARRANGE_SHORTCUTS,
|
|
3
|
+
matchArrangeShortcut
|
|
4
|
+
} from "../chunk-7DWMJCAT.js";
|
|
1
5
|
import {
|
|
2
6
|
DEFAULT_NODE_HEIGHT,
|
|
3
7
|
DEFAULT_NODE_WIDTH,
|
|
@@ -1073,6 +1077,10 @@ function invertNodeUpdates(nodes, updates) {
|
|
|
1073
1077
|
}
|
|
1074
1078
|
|
|
1075
1079
|
// src/canvas/guides.ts
|
|
1080
|
+
function snapToGrid(value, spacing, origin = 0) {
|
|
1081
|
+
if (!(spacing > 0) || !Number.isFinite(spacing)) return value;
|
|
1082
|
+
return Math.round((value - origin) / spacing) * spacing + origin;
|
|
1083
|
+
}
|
|
1076
1084
|
function buildAxis(entries) {
|
|
1077
1085
|
entries.sort((a, b) => a.value - b.value);
|
|
1078
1086
|
return {
|
|
@@ -1229,8 +1237,10 @@ function computeSnapGuides(moving, targets, options = {}) {
|
|
|
1229
1237
|
};
|
|
1230
1238
|
const sx = snapAxis(true);
|
|
1231
1239
|
const sy = snapAxis(false);
|
|
1232
|
-
const
|
|
1233
|
-
const
|
|
1240
|
+
const grid = options.gridSize ?? 0;
|
|
1241
|
+
const gridDelta = (v, origin) => snapToGrid(v, grid, origin) - v;
|
|
1242
|
+
const dx = sx.delta ?? (grid > 0 ? gridDelta(moving.x, options.gridOrigin?.x ?? 0) : 0);
|
|
1243
|
+
const dy = sy.delta ?? (grid > 0 ? gridDelta(moving.y, options.gridOrigin?.y ?? 0) : 0);
|
|
1234
1244
|
const snapped = { x: moving.x + dx, y: moving.y + dy, width: moving.width, height: moving.height };
|
|
1235
1245
|
const guides = [];
|
|
1236
1246
|
const alignmentGuides = (horizontal) => {
|
|
@@ -1244,12 +1254,12 @@ function computeSnapGuides(moving, targets, options = {}) {
|
|
|
1244
1254
|
if (useCentres) {
|
|
1245
1255
|
lines.push({ value: start + size / 2, kind: "centre", axis: horizontal ? index.centreX : index.centreY });
|
|
1246
1256
|
}
|
|
1247
|
-
for (const
|
|
1248
|
-
const aligned = matches(
|
|
1257
|
+
for (const line2 of lines) {
|
|
1258
|
+
const aligned = matches(line2.axis, line2.value, eps);
|
|
1249
1259
|
if (aligned.length === 0) continue;
|
|
1250
1260
|
const span = boundsOf([snapped, ...aligned]);
|
|
1251
1261
|
guides.push(
|
|
1252
|
-
horizontal ? { orientation: "vertical", kind:
|
|
1262
|
+
horizontal ? { orientation: "vertical", kind: line2.kind, x1: line2.value, y1: span.y, x2: line2.value, y2: span.y + span.height } : { orientation: "horizontal", kind: line2.kind, x1: span.x, y1: line2.value, x2: span.x + span.width, y2: line2.value }
|
|
1253
1263
|
);
|
|
1254
1264
|
}
|
|
1255
1265
|
};
|
|
@@ -1390,16 +1400,16 @@ function findConnectorSnap(pointer, targets, options = {}) {
|
|
|
1390
1400
|
if (pointer.x < rect.x - radius || pointer.x > rect.x + rect.width + radius || pointer.y < rect.y - radius || pointer.y > rect.y + rect.height + radius) {
|
|
1391
1401
|
continue;
|
|
1392
1402
|
}
|
|
1393
|
-
const
|
|
1403
|
+
const inside2 = pointer.x >= rect.x && pointer.x <= rect.x + rect.width && pointer.y >= rect.y && pointer.y <= rect.y + rect.height;
|
|
1394
1404
|
for (const position of target.positions ?? all) {
|
|
1395
1405
|
const point = options.mode === "edge" ? closestPointOnSide(rect, position, pointer) : handlePoint(rect, position);
|
|
1396
1406
|
const sidePoint = closestPointOnSide(rect, position, pointer);
|
|
1397
1407
|
const rank = Math.hypot(pointer.x - sidePoint.x, pointer.y - sidePoint.y);
|
|
1398
1408
|
const distance = Math.hypot(pointer.x - point.x, pointer.y - point.y);
|
|
1399
|
-
if (!
|
|
1400
|
-
const better = !best ||
|
|
1409
|
+
if (!inside2 && rank > radius) continue;
|
|
1410
|
+
const better = !best || inside2 && !best.inside || inside2 === best.inside && rank < best.rank;
|
|
1401
1411
|
if (better) {
|
|
1402
|
-
best = { nodeId: target.id, position, side: positionToSide(position), point, distance, inside, rank };
|
|
1412
|
+
best = { nodeId: target.id, position, side: positionToSide(position), point, distance, inside: inside2, rank };
|
|
1403
1413
|
}
|
|
1404
1414
|
}
|
|
1405
1415
|
}
|
|
@@ -1662,47 +1672,1720 @@ function fitViewportToNodes(nodes, size, ids, options = {}) {
|
|
|
1662
1672
|
return bounds ? fitViewportToRect(bounds, size, options) : null;
|
|
1663
1673
|
}
|
|
1664
1674
|
|
|
1665
|
-
// src/canvas/
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1675
|
+
// src/canvas/polygonBoolean.ts
|
|
1676
|
+
function ringSignedArea(ring) {
|
|
1677
|
+
let sum = 0;
|
|
1678
|
+
const n = ring.length;
|
|
1679
|
+
for (let i = 0; i < n; i++) {
|
|
1680
|
+
const [x1, y1] = ring[i];
|
|
1681
|
+
const [x2, y2] = ring[(i + 1) % n];
|
|
1682
|
+
sum += x1 * y2 - x2 * y1;
|
|
1683
|
+
}
|
|
1684
|
+
return sum / 2;
|
|
1685
|
+
}
|
|
1686
|
+
function polygonArea(polygon) {
|
|
1687
|
+
if (polygon.length === 0) return 0;
|
|
1688
|
+
let area = Math.abs(ringSignedArea(polygon[0]));
|
|
1689
|
+
for (let i = 1; i < polygon.length; i++) area -= Math.abs(ringSignedArea(polygon[i]));
|
|
1690
|
+
return area;
|
|
1691
|
+
}
|
|
1692
|
+
function multiPolygonArea(multi) {
|
|
1693
|
+
let total = 0;
|
|
1694
|
+
for (const polygon of multi) total += polygonArea(polygon);
|
|
1695
|
+
return total;
|
|
1696
|
+
}
|
|
1697
|
+
function multiPolygonBounds(input) {
|
|
1698
|
+
let minX = Infinity;
|
|
1699
|
+
let minY = Infinity;
|
|
1700
|
+
let maxX = -Infinity;
|
|
1701
|
+
let maxY = -Infinity;
|
|
1702
|
+
const visit = (ring) => {
|
|
1703
|
+
for (const [x, y] of ring) {
|
|
1704
|
+
if (x < minX) minX = x;
|
|
1705
|
+
if (y < minY) minY = y;
|
|
1706
|
+
if (x > maxX) maxX = x;
|
|
1707
|
+
if (y > maxY) maxY = y;
|
|
1708
|
+
}
|
|
1709
|
+
};
|
|
1710
|
+
for (const item of input) {
|
|
1711
|
+
if (item.length > 0 && Array.isArray(item[0]) && Array.isArray(item[0][0])) {
|
|
1712
|
+
for (const ring of item) visit(ring);
|
|
1713
|
+
} else {
|
|
1714
|
+
visit(item);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
return minX === Infinity ? null : { minX, minY, maxX, maxY };
|
|
1718
|
+
}
|
|
1719
|
+
function pointInRings(point, rings, fillRule = "nonzero") {
|
|
1720
|
+
const [px, py] = point;
|
|
1721
|
+
let winding = 0;
|
|
1722
|
+
for (const ring of rings) {
|
|
1723
|
+
const n = ring.length;
|
|
1724
|
+
for (let i = 0; i < n; i++) {
|
|
1725
|
+
const [ax, ay] = ring[i];
|
|
1726
|
+
const [bx, by] = ring[(i + 1) % n];
|
|
1727
|
+
if (ay <= py && by > py) {
|
|
1728
|
+
if (ax + (py - ay) / (by - ay) * (bx - ax) > px) winding++;
|
|
1729
|
+
} else if (by <= py && ay > py) {
|
|
1730
|
+
if (ax + (py - ay) / (by - ay) * (bx - ax) > px) winding--;
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
return fillRule === "evenodd" ? (winding & 1) === 1 : winding !== 0;
|
|
1735
|
+
}
|
|
1736
|
+
function pointInMultiPolygon(point, multi) {
|
|
1737
|
+
return multi.some((polygon) => pointInRings(point, polygon, "evenodd"));
|
|
1738
|
+
}
|
|
1739
|
+
function cleanRing(ring) {
|
|
1740
|
+
const out = [];
|
|
1741
|
+
for (const p of ring) {
|
|
1742
|
+
if (!Number.isFinite(p[0]) || !Number.isFinite(p[1])) continue;
|
|
1743
|
+
const last = out[out.length - 1];
|
|
1744
|
+
if (last && last[0] === p[0] && last[1] === p[1]) continue;
|
|
1745
|
+
out.push([p[0], p[1]]);
|
|
1746
|
+
}
|
|
1747
|
+
while (out.length > 1) {
|
|
1748
|
+
const first = out[0];
|
|
1749
|
+
const last = out[out.length - 1];
|
|
1750
|
+
if (first[0] === last[0] && first[1] === last[1]) out.pop();
|
|
1751
|
+
else break;
|
|
1752
|
+
}
|
|
1753
|
+
return out;
|
|
1754
|
+
}
|
|
1755
|
+
function isOperand(input) {
|
|
1756
|
+
return !Array.isArray(input) && typeof input === "object" && input !== null && Array.isArray(input.rings);
|
|
1757
|
+
}
|
|
1758
|
+
function normalizeInput(input) {
|
|
1759
|
+
if (isOperand(input)) {
|
|
1760
|
+
return {
|
|
1761
|
+
rings: input.rings.map(cleanRing).filter((r) => r.length >= 3),
|
|
1762
|
+
fillRule: input.fillRule ?? "nonzero"
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
const rings = [];
|
|
1766
|
+
for (const polygon of input ?? []) {
|
|
1767
|
+
polygon.forEach((raw, index) => {
|
|
1768
|
+
const ring = cleanRing(raw);
|
|
1769
|
+
if (ring.length < 3) return;
|
|
1770
|
+
const area = ringSignedArea(ring);
|
|
1771
|
+
if (area === 0) return;
|
|
1772
|
+
const wantPositive = index === 0;
|
|
1773
|
+
rings.push(area > 0 === wantPositive ? ring : ring.reverse());
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
return { rings, fillRule: "nonzero" };
|
|
1777
|
+
}
|
|
1778
|
+
var PointPool = class {
|
|
1779
|
+
constructor(eps) {
|
|
1780
|
+
this.eps = eps;
|
|
1781
|
+
}
|
|
1782
|
+
eps;
|
|
1783
|
+
vertices = [];
|
|
1784
|
+
cells = /* @__PURE__ */ new Map();
|
|
1785
|
+
get(x, y) {
|
|
1786
|
+
const cx = Math.floor(x / this.eps);
|
|
1787
|
+
const cy = Math.floor(y / this.eps);
|
|
1788
|
+
let best = null;
|
|
1789
|
+
let bestDist = Infinity;
|
|
1790
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
1791
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
1792
|
+
const ids = this.cells.get(`${cx + dx},${cy + dy}`);
|
|
1793
|
+
if (!ids) continue;
|
|
1794
|
+
for (const id of ids) {
|
|
1795
|
+
const v = this.vertices[id];
|
|
1796
|
+
const d = Math.hypot(v.x - x, v.y - y);
|
|
1797
|
+
if (d <= this.eps && d < bestDist) {
|
|
1798
|
+
best = v;
|
|
1799
|
+
bestDist = d;
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
if (best) return best;
|
|
1805
|
+
const vertex = { id: this.vertices.length, x, y };
|
|
1806
|
+
this.vertices.push(vertex);
|
|
1807
|
+
const key = `${cx},${cy}`;
|
|
1808
|
+
const bucket = this.cells.get(key);
|
|
1809
|
+
if (bucket) bucket.push(vertex.id);
|
|
1810
|
+
else this.cells.set(key, [vertex.id]);
|
|
1811
|
+
return vertex;
|
|
1812
|
+
}
|
|
1689
1813
|
};
|
|
1690
|
-
function
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1814
|
+
function project(a, b, p) {
|
|
1815
|
+
const dx = b.x - a.x;
|
|
1816
|
+
const dy = b.y - a.y;
|
|
1817
|
+
const len2 = dx * dx + dy * dy;
|
|
1818
|
+
const t = len2 === 0 ? 0 : ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2;
|
|
1819
|
+
const cx = a.x + t * dx;
|
|
1820
|
+
const cy = a.y + t * dy;
|
|
1821
|
+
return { t, dist: Math.hypot(p.x - cx, p.y - cy) };
|
|
1822
|
+
}
|
|
1823
|
+
function addSplit(splits, t, v) {
|
|
1824
|
+
const { a, b } = splits.segment;
|
|
1825
|
+
if (v.id === a.id || v.id === b.id) return;
|
|
1826
|
+
if (!splits.points.has(v.id)) splits.points.set(v.id, { t, v });
|
|
1827
|
+
}
|
|
1828
|
+
function splitPass(segments, pool, eps) {
|
|
1829
|
+
const order = segments.map((s, i) => ({
|
|
1830
|
+
i,
|
|
1831
|
+
minX: Math.min(s.a.x, s.b.x),
|
|
1832
|
+
maxX: Math.max(s.a.x, s.b.x),
|
|
1833
|
+
minY: Math.min(s.a.y, s.b.y),
|
|
1834
|
+
maxY: Math.max(s.a.y, s.b.y)
|
|
1835
|
+
})).sort((p, q) => p.minX - q.minX);
|
|
1836
|
+
const splits = segments.map((segment) => ({ segment, points: /* @__PURE__ */ new Map() }));
|
|
1837
|
+
for (let oi = 0; oi < order.length; oi++) {
|
|
1838
|
+
const bi = order[oi];
|
|
1839
|
+
const s1 = segments[bi.i];
|
|
1840
|
+
for (let oj = oi + 1; oj < order.length; oj++) {
|
|
1841
|
+
const bj = order[oj];
|
|
1842
|
+
if (bj.minX > bi.maxX + eps) break;
|
|
1843
|
+
if (bj.minY > bi.maxY + eps || bj.maxY < bi.minY - eps) continue;
|
|
1844
|
+
const s2 = segments[bj.i];
|
|
1845
|
+
const sharedA = s1.a.id === s2.a.id || s1.a.id === s2.b.id;
|
|
1846
|
+
const sharedB = s1.b.id === s2.a.id || s1.b.id === s2.b.id;
|
|
1847
|
+
if (sharedA && sharedB) continue;
|
|
1848
|
+
let touched = false;
|
|
1849
|
+
for (const [seg, other, split] of [
|
|
1850
|
+
[s1, s2, splits[bi.i]],
|
|
1851
|
+
[s2, s1, splits[bj.i]]
|
|
1852
|
+
]) {
|
|
1853
|
+
for (const p of [other.a, other.b]) {
|
|
1854
|
+
if (p.id === seg.a.id || p.id === seg.b.id) continue;
|
|
1855
|
+
const { t: t2, dist: dist2 } = project(seg.a, seg.b, p);
|
|
1856
|
+
const len = Math.hypot(seg.b.x - seg.a.x, seg.b.y - seg.a.y);
|
|
1857
|
+
if (dist2 <= eps && t2 * len > eps && (1 - t2) * len > eps) {
|
|
1858
|
+
addSplit(split, t2, p);
|
|
1859
|
+
touched = true;
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
if (touched) continue;
|
|
1864
|
+
const d1x = s1.b.x - s1.a.x;
|
|
1865
|
+
const d1y = s1.b.y - s1.a.y;
|
|
1866
|
+
const d2x = s2.b.x - s2.a.x;
|
|
1867
|
+
const d2y = s2.b.y - s2.a.y;
|
|
1868
|
+
const denom = d1x * d2y - d1y * d2x;
|
|
1869
|
+
const len1 = Math.hypot(d1x, d1y);
|
|
1870
|
+
const len2 = Math.hypot(d2x, d2y);
|
|
1871
|
+
if (Math.abs(denom) <= 1e-12 * len1 * len2) continue;
|
|
1872
|
+
const ex = s2.a.x - s1.a.x;
|
|
1873
|
+
const ey = s2.a.y - s1.a.y;
|
|
1874
|
+
const t = (ex * d2y - ey * d2x) / denom;
|
|
1875
|
+
const u = (ex * d1y - ey * d1x) / denom;
|
|
1876
|
+
const tolT = eps / len1;
|
|
1877
|
+
const tolU = eps / len2;
|
|
1878
|
+
if (t < -tolT || t > 1 + tolT || u < -tolU || u > 1 + tolU) continue;
|
|
1879
|
+
if (sharedA || sharedB) continue;
|
|
1880
|
+
const v = pool.get(s1.a.x + t * d1x, s1.a.y + t * d1y);
|
|
1881
|
+
addSplit(splits[bi.i], project(s1.a, s1.b, v).t, v);
|
|
1882
|
+
addSplit(splits[bj.i], project(s2.a, s2.b, v).t, v);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
let changed = false;
|
|
1886
|
+
const out = [];
|
|
1887
|
+
for (const { segment, points } of splits) {
|
|
1888
|
+
if (points.size === 0) {
|
|
1889
|
+
out.push(segment);
|
|
1890
|
+
continue;
|
|
1891
|
+
}
|
|
1892
|
+
changed = true;
|
|
1893
|
+
const ordered = [...points.values()].sort((p, q) => p.t - q.t);
|
|
1894
|
+
let prev = segment.a;
|
|
1895
|
+
for (const { v } of ordered) {
|
|
1896
|
+
if (v.id !== prev.id) out.push({ a: prev, b: v, operand: segment.operand });
|
|
1897
|
+
prev = v;
|
|
1898
|
+
}
|
|
1899
|
+
if (prev.id !== segment.b.id) out.push({ a: prev, b: segment.b, operand: segment.operand });
|
|
1900
|
+
}
|
|
1901
|
+
return { segments: out, changed };
|
|
1695
1902
|
}
|
|
1696
|
-
function
|
|
1697
|
-
|
|
1698
|
-
const
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1903
|
+
function defaultEpsilon(operands) {
|
|
1904
|
+
let max = 0;
|
|
1905
|
+
for (const op of operands) {
|
|
1906
|
+
for (const ring of op.rings) {
|
|
1907
|
+
for (const [x, y] of ring) {
|
|
1908
|
+
const m = Math.max(Math.abs(x), Math.abs(y));
|
|
1909
|
+
if (m > max) max = m;
|
|
1910
|
+
}
|
|
1703
1911
|
}
|
|
1704
1912
|
}
|
|
1705
|
-
return
|
|
1913
|
+
return Math.max(1e-9, max * 1e-9);
|
|
1914
|
+
}
|
|
1915
|
+
function buildOverlay(inputs, options) {
|
|
1916
|
+
const operands = inputs.map(normalizeInput);
|
|
1917
|
+
const eps = options.epsilon && options.epsilon > 0 ? options.epsilon : defaultEpsilon(operands);
|
|
1918
|
+
const pool = new PointPool(eps);
|
|
1919
|
+
let segments = [];
|
|
1920
|
+
operands.forEach((op, operand) => {
|
|
1921
|
+
for (const ring of op.rings) {
|
|
1922
|
+
const verts = ring.map(([x, y]) => pool.get(x, y));
|
|
1923
|
+
for (let i = 0; i < verts.length; i++) {
|
|
1924
|
+
const a = verts[i];
|
|
1925
|
+
const b = verts[(i + 1) % verts.length];
|
|
1926
|
+
if (a.id !== b.id) segments.push({ a, b, operand });
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
});
|
|
1930
|
+
for (let pass = 0; pass < 8; pass++) {
|
|
1931
|
+
const result = splitPass(segments, pool, eps);
|
|
1932
|
+
segments = result.segments;
|
|
1933
|
+
if (!result.changed) break;
|
|
1934
|
+
}
|
|
1935
|
+
const count = operands.length;
|
|
1936
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1937
|
+
for (const s of segments) {
|
|
1938
|
+
const forward = s.a.id < s.b.id;
|
|
1939
|
+
const a = forward ? s.a : s.b;
|
|
1940
|
+
const b = forward ? s.b : s.a;
|
|
1941
|
+
const key = `${a.id},${b.id}`;
|
|
1942
|
+
let edge = byKey.get(key);
|
|
1943
|
+
if (!edge) {
|
|
1944
|
+
edge = { a, b, mult: new Array(count).fill(0) };
|
|
1945
|
+
byKey.set(key, edge);
|
|
1946
|
+
}
|
|
1947
|
+
edge.mult[s.operand] += forward ? 1 : -1;
|
|
1948
|
+
}
|
|
1949
|
+
const edges = [...byKey.values()].filter((e) => e.mult.some((m) => m !== 0));
|
|
1950
|
+
const right = [];
|
|
1951
|
+
const left = [];
|
|
1952
|
+
for (let i = 0; i < edges.length; i++) {
|
|
1953
|
+
const edge = edges[i];
|
|
1954
|
+
const horizontal = Math.abs(edge.b.y - edge.a.y) < Math.abs(edge.b.x - edge.a.x);
|
|
1955
|
+
const tx = (v) => horizontal ? v.y : v.x;
|
|
1956
|
+
const ty = (v) => horizontal ? -v.x : v.y;
|
|
1957
|
+
const ax = tx(edge.a);
|
|
1958
|
+
const ay = ty(edge.a);
|
|
1959
|
+
const bx = tx(edge.b);
|
|
1960
|
+
const by = ty(edge.b);
|
|
1961
|
+
const mx = (ax + bx) / 2;
|
|
1962
|
+
const my = (ay + by) / 2;
|
|
1963
|
+
const winding = new Array(count).fill(0);
|
|
1964
|
+
for (let j = 0; j < edges.length; j++) {
|
|
1965
|
+
if (j === i) continue;
|
|
1966
|
+
const other = edges[j];
|
|
1967
|
+
const oax = tx(other.a);
|
|
1968
|
+
const oay = ty(other.a);
|
|
1969
|
+
const obx = tx(other.b);
|
|
1970
|
+
const oby = ty(other.b);
|
|
1971
|
+
let dir = 0;
|
|
1972
|
+
if (oay <= my && oby > my) dir = 1;
|
|
1973
|
+
else if (oby <= my && oay > my) dir = -1;
|
|
1974
|
+
else continue;
|
|
1975
|
+
const x = oax + (my - oay) / (oby - oay) * (obx - oax);
|
|
1976
|
+
if (x <= mx) continue;
|
|
1977
|
+
for (let k = 0; k < count; k++) {
|
|
1978
|
+
if (other.mult[k] !== 0) winding[k] += dir * other.mult[k];
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
const sideIsRight = by > ay;
|
|
1982
|
+
const r = sideIsRight ? winding : winding.map((w, k) => w - edge.mult[k]);
|
|
1983
|
+
right.push(r);
|
|
1984
|
+
left.push(r.map((w, k) => w + edge.mult[k]));
|
|
1985
|
+
}
|
|
1986
|
+
return { edges, right, left, fillRules: operands.map((o) => o.fillRule), eps };
|
|
1987
|
+
}
|
|
1988
|
+
function insideFlags(windings, fillRules) {
|
|
1989
|
+
return windings.map((w, k) => fillRules[k] === "evenodd" ? (w & 1) === 1 : w !== 0);
|
|
1990
|
+
}
|
|
1991
|
+
function traceRings(directed) {
|
|
1992
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
1993
|
+
for (const e of directed) {
|
|
1994
|
+
const list = outgoing.get(e.from.id);
|
|
1995
|
+
if (list) list.push(e);
|
|
1996
|
+
else outgoing.set(e.from.id, [e]);
|
|
1997
|
+
}
|
|
1998
|
+
const TWO_PI = Math.PI * 2;
|
|
1999
|
+
const next = (incoming) => {
|
|
2000
|
+
const candidates = outgoing.get(incoming.to.id);
|
|
2001
|
+
if (!candidates) return void 0;
|
|
2002
|
+
const back = Math.atan2(incoming.from.y - incoming.to.y, incoming.from.x - incoming.to.x);
|
|
2003
|
+
let best;
|
|
2004
|
+
let bestTurn = Infinity;
|
|
2005
|
+
for (const c of candidates) {
|
|
2006
|
+
let turn = back - c.angle;
|
|
2007
|
+
turn -= Math.floor(turn / TWO_PI) * TWO_PI;
|
|
2008
|
+
if (turn <= 1e-15) turn = TWO_PI;
|
|
2009
|
+
if (turn < bestTurn) {
|
|
2010
|
+
bestTurn = turn;
|
|
2011
|
+
best = c;
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
return best;
|
|
2015
|
+
};
|
|
2016
|
+
const used = /* @__PURE__ */ new Set();
|
|
2017
|
+
const rings = [];
|
|
2018
|
+
for (const start of directed) {
|
|
2019
|
+
if (used.has(start)) continue;
|
|
2020
|
+
const ring = [];
|
|
2021
|
+
let edge = start;
|
|
2022
|
+
let guard = directed.length + 1;
|
|
2023
|
+
while (edge && !used.has(edge) && guard-- > 0) {
|
|
2024
|
+
used.add(edge);
|
|
2025
|
+
ring.push(edge.from);
|
|
2026
|
+
edge = next(edge);
|
|
2027
|
+
}
|
|
2028
|
+
if (ring.length >= 3) rings.push(ring);
|
|
2029
|
+
}
|
|
2030
|
+
return rings;
|
|
2031
|
+
}
|
|
2032
|
+
function removeCollinear(ring, eps) {
|
|
2033
|
+
let points = ring;
|
|
2034
|
+
let changed = true;
|
|
2035
|
+
while (changed && points.length > 3) {
|
|
2036
|
+
changed = false;
|
|
2037
|
+
const out = [];
|
|
2038
|
+
const n = points.length;
|
|
2039
|
+
for (let i = 0; i < n; i++) {
|
|
2040
|
+
const prev = out.length > 0 ? out[out.length - 1] : points[(i - 1 + n) % n];
|
|
2041
|
+
const cur = points[i];
|
|
2042
|
+
const nxt = points[(i + 1) % n];
|
|
2043
|
+
const dx = nxt[0] - prev[0];
|
|
2044
|
+
const dy = nxt[1] - prev[1];
|
|
2045
|
+
const len = Math.hypot(dx, dy);
|
|
2046
|
+
const cross = (cur[0] - prev[0]) * dy - (cur[1] - prev[1]) * dx;
|
|
2047
|
+
const dot2 = (cur[0] - prev[0]) * dx + (cur[1] - prev[1]) * dy;
|
|
2048
|
+
if (len > 0 && Math.abs(cross) / len <= eps && dot2 > 0 && dot2 < len * len) {
|
|
2049
|
+
changed = true;
|
|
2050
|
+
continue;
|
|
2051
|
+
}
|
|
2052
|
+
out.push(cur);
|
|
2053
|
+
}
|
|
2054
|
+
if (out.length < 3) return out;
|
|
2055
|
+
points = out;
|
|
2056
|
+
}
|
|
2057
|
+
return points;
|
|
2058
|
+
}
|
|
2059
|
+
function assemble(rings, eps) {
|
|
2060
|
+
const outers = [];
|
|
2061
|
+
const holes = [];
|
|
2062
|
+
const minArea = eps * eps;
|
|
2063
|
+
for (const vertices of rings) {
|
|
2064
|
+
const ring = removeCollinear(
|
|
2065
|
+
vertices.map((v) => [v.x, v.y]),
|
|
2066
|
+
eps
|
|
2067
|
+
);
|
|
2068
|
+
if (ring.length < 3) continue;
|
|
2069
|
+
const area = ringSignedArea(ring);
|
|
2070
|
+
if (Math.abs(area) <= minArea) continue;
|
|
2071
|
+
(area > 0 ? outers : holes).push({ ring, area: Math.abs(area) });
|
|
2072
|
+
}
|
|
2073
|
+
outers.sort((a, b) => b.area - a.area);
|
|
2074
|
+
const polygons = outers.map((o) => [o.ring]);
|
|
2075
|
+
for (const hole of holes) {
|
|
2076
|
+
const [p, q] = [hole.ring[0], hole.ring[1]];
|
|
2077
|
+
const sample = [(p[0] + q[0]) / 2, (p[1] + q[1]) / 2];
|
|
2078
|
+
let owner = -1;
|
|
2079
|
+
for (let i = outers.length - 1; i >= 0; i--) {
|
|
2080
|
+
if (outers[i].area >= hole.area && pointInRings(sample, [outers[i].ring], "evenodd")) {
|
|
2081
|
+
owner = i;
|
|
2082
|
+
break;
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
if (owner >= 0) polygons[owner].push(hole.ring);
|
|
2086
|
+
}
|
|
2087
|
+
return polygons;
|
|
2088
|
+
}
|
|
2089
|
+
function extract(overlay, isInside) {
|
|
2090
|
+
const directed = [];
|
|
2091
|
+
overlay.edges.forEach((edge, i) => {
|
|
2092
|
+
const l = isInside(insideFlags(overlay.left[i], overlay.fillRules));
|
|
2093
|
+
const r = isInside(insideFlags(overlay.right[i], overlay.fillRules));
|
|
2094
|
+
if (l === r) return;
|
|
2095
|
+
const [from, to] = l ? [edge.a, edge.b] : [edge.b, edge.a];
|
|
2096
|
+
directed.push({ from, to, angle: Math.atan2(to.y - from.y, to.x - from.x) });
|
|
2097
|
+
});
|
|
2098
|
+
return assemble(traceRings(directed), overlay.eps);
|
|
2099
|
+
}
|
|
2100
|
+
function predicate(operation) {
|
|
2101
|
+
switch (operation) {
|
|
2102
|
+
case "union":
|
|
2103
|
+
return (f) => f.some(Boolean);
|
|
2104
|
+
case "intersection":
|
|
2105
|
+
return (f) => f.length > 0 && f.every(Boolean);
|
|
2106
|
+
case "difference":
|
|
2107
|
+
return (f) => f[0] === true && !f.slice(1).some(Boolean);
|
|
2108
|
+
case "xor":
|
|
2109
|
+
return (f) => f.filter(Boolean).length % 2 === 1;
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
function polygonBoolean(operation, operands, options = {}) {
|
|
2113
|
+
if (operands.length === 0) return [];
|
|
2114
|
+
return extract(buildOverlay(operands, options), predicate(operation));
|
|
2115
|
+
}
|
|
2116
|
+
var unionPolygons = (a, b, options) => polygonBoolean("union", [a, b], options);
|
|
2117
|
+
var intersectPolygons = (a, b, options) => polygonBoolean("intersection", [a, b], options);
|
|
2118
|
+
var subtractPolygons = (a, b, options) => polygonBoolean("difference", [a, b], options);
|
|
2119
|
+
var xorPolygons = (a, b, options) => polygonBoolean("xor", [a, b], options);
|
|
2120
|
+
function dividePolygons(operands, options = {}) {
|
|
2121
|
+
if (operands.length === 0) return [];
|
|
2122
|
+
const overlay = buildOverlay(operands, options);
|
|
2123
|
+
const signatures = /* @__PURE__ */ new Map();
|
|
2124
|
+
const note = (windings) => {
|
|
2125
|
+
const flags = insideFlags(windings, overlay.fillRules);
|
|
2126
|
+
if (!flags.some(Boolean)) return;
|
|
2127
|
+
const key = flags.map((f) => f ? "1" : "0").join("");
|
|
2128
|
+
if (!signatures.has(key)) signatures.set(key, flags.flatMap((f, k) => f ? [k] : []));
|
|
2129
|
+
};
|
|
2130
|
+
overlay.edges.forEach((_, i) => {
|
|
2131
|
+
note(overlay.left[i]);
|
|
2132
|
+
note(overlay.right[i]);
|
|
2133
|
+
});
|
|
2134
|
+
const pieces = [];
|
|
2135
|
+
const keys = [...signatures.keys()].sort();
|
|
2136
|
+
for (const key of keys) {
|
|
2137
|
+
const polygons = extract(overlay, (flags) => flags.map((f) => f ? "1" : "0").join("") === key);
|
|
2138
|
+
polygons.sort((a, b) => polygonArea(b) - polygonArea(a));
|
|
2139
|
+
for (const polygon of polygons) pieces.push({ polygon, operands: signatures.get(key) });
|
|
2140
|
+
}
|
|
2141
|
+
return pieces;
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
// src/canvas/curveFit.ts
|
|
2145
|
+
function formatNumber(value, precision) {
|
|
2146
|
+
const factor = 10 ** precision;
|
|
2147
|
+
const rounded = Math.round(value * factor) / factor;
|
|
2148
|
+
return Object.is(rounded, -0) ? "0" : String(rounded);
|
|
2149
|
+
}
|
|
2150
|
+
var MAX_REPARAM = 6;
|
|
2151
|
+
var sub = (a, b) => [a[0] - b[0], a[1] - b[1]];
|
|
2152
|
+
var add = (a, v, s = 1) => [a[0] + v[0] * s, a[1] + v[1] * s];
|
|
2153
|
+
var dot = (a, b) => a[0] * b[0] + a[1] * b[1];
|
|
2154
|
+
var dist = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1]);
|
|
2155
|
+
function unit(v) {
|
|
2156
|
+
const l = Math.hypot(v[0], v[1]);
|
|
2157
|
+
return l > 1e-12 && Number.isFinite(l) ? [v[0] / l, v[1] / l] : null;
|
|
2158
|
+
}
|
|
2159
|
+
function evalCubic(c, t) {
|
|
2160
|
+
const s = 1 - t;
|
|
2161
|
+
const b0 = s * s * s;
|
|
2162
|
+
const b1 = 3 * s * s * t;
|
|
2163
|
+
const b2 = 3 * s * t * t;
|
|
2164
|
+
const b3 = t * t * t;
|
|
2165
|
+
return [
|
|
2166
|
+
b0 * c.p0[0] + b1 * c.p1[0] + b2 * c.p2[0] + b3 * c.p3[0],
|
|
2167
|
+
b0 * c.p0[1] + b1 * c.p1[1] + b2 * c.p2[1] + b3 * c.p3[1]
|
|
2168
|
+
];
|
|
2169
|
+
}
|
|
2170
|
+
function cubicPointAt(c, t) {
|
|
2171
|
+
return evalCubic(c, t);
|
|
2172
|
+
}
|
|
2173
|
+
function dedup(points) {
|
|
2174
|
+
const out = [];
|
|
2175
|
+
for (const p of points) {
|
|
2176
|
+
if (!Number.isFinite(p[0]) || !Number.isFinite(p[1])) continue;
|
|
2177
|
+
if (out.length === 0 || dist(out[out.length - 1], p) > 1e-12) out.push(p);
|
|
2178
|
+
}
|
|
2179
|
+
return out;
|
|
2180
|
+
}
|
|
2181
|
+
function chordParams(pts) {
|
|
2182
|
+
const u = [0];
|
|
2183
|
+
let acc = 0;
|
|
2184
|
+
for (let i = 1; i < pts.length; i++) {
|
|
2185
|
+
acc += dist(pts[i - 1], pts[i]);
|
|
2186
|
+
u.push(acc);
|
|
2187
|
+
}
|
|
2188
|
+
return acc > 0 ? u.map((x) => x / acc) : u;
|
|
2189
|
+
}
|
|
2190
|
+
function generate(pts, u, t0, t1) {
|
|
2191
|
+
const p0 = pts[0];
|
|
2192
|
+
const p3 = pts[pts.length - 1];
|
|
2193
|
+
const dirIn = [-t1[0], -t1[1]];
|
|
2194
|
+
let c00 = 0;
|
|
2195
|
+
let c01 = 0;
|
|
2196
|
+
let c11 = 0;
|
|
2197
|
+
let x0 = 0;
|
|
2198
|
+
let x1 = 0;
|
|
2199
|
+
for (let i = 0; i < pts.length; i++) {
|
|
2200
|
+
const t = u[i];
|
|
2201
|
+
const s = 1 - t;
|
|
2202
|
+
const b0 = s * s * s;
|
|
2203
|
+
const b1 = 3 * s * s * t;
|
|
2204
|
+
const b2 = 3 * s * t * t;
|
|
2205
|
+
const b3 = t * t * t;
|
|
2206
|
+
const a0 = [t0[0] * b1, t0[1] * b1];
|
|
2207
|
+
const a1 = [dirIn[0] * b2, dirIn[1] * b2];
|
|
2208
|
+
c00 += dot(a0, a0);
|
|
2209
|
+
c01 += dot(a0, a1);
|
|
2210
|
+
c11 += dot(a1, a1);
|
|
2211
|
+
const tmp = [
|
|
2212
|
+
pts[i][0] - (p0[0] * (b0 + b1) + p3[0] * (b2 + b3)),
|
|
2213
|
+
pts[i][1] - (p0[1] * (b0 + b1) + p3[1] * (b2 + b3))
|
|
2214
|
+
];
|
|
2215
|
+
x0 += dot(a0, tmp);
|
|
2216
|
+
x1 += dot(a1, tmp);
|
|
2217
|
+
}
|
|
2218
|
+
const det = c00 * c11 - c01 * c01;
|
|
2219
|
+
const seg = dist(p0, p3);
|
|
2220
|
+
const eps = 1e-6 * seg;
|
|
2221
|
+
let al = seg / 3;
|
|
2222
|
+
let ar = seg / 3;
|
|
2223
|
+
if (Math.abs(det) > 1e-12) {
|
|
2224
|
+
al = (x0 * c11 - x1 * c01) / det;
|
|
2225
|
+
ar = (c00 * x1 - c01 * x0) / det;
|
|
2226
|
+
}
|
|
2227
|
+
if (!Number.isFinite(al) || !Number.isFinite(ar) || al < eps || ar < eps) {
|
|
2228
|
+
al = seg / 3;
|
|
2229
|
+
ar = seg / 3;
|
|
2230
|
+
}
|
|
2231
|
+
const cap = seg * 4 + 1e-9;
|
|
2232
|
+
al = Math.min(al, cap);
|
|
2233
|
+
ar = Math.min(ar, cap);
|
|
2234
|
+
return { p0, p1: add(p0, t0, al), p2: add(p3, dirIn, ar), p3 };
|
|
2235
|
+
}
|
|
2236
|
+
function maxError(pts, c, u) {
|
|
2237
|
+
let e = 0;
|
|
2238
|
+
let idx = pts.length >> 1;
|
|
2239
|
+
for (let i = 0; i < pts.length; i++) {
|
|
2240
|
+
const d = dist(evalCubic(c, u[i]), pts[i]);
|
|
2241
|
+
if (d > e) {
|
|
2242
|
+
e = d;
|
|
2243
|
+
idx = i;
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
return [e, idx];
|
|
2247
|
+
}
|
|
2248
|
+
function newton(c, p, t) {
|
|
2249
|
+
const s = 1 - t;
|
|
2250
|
+
const q = evalCubic(c, t);
|
|
2251
|
+
const d1 = [
|
|
2252
|
+
3 * (s * s * (c.p1[0] - c.p0[0]) + 2 * s * t * (c.p2[0] - c.p1[0]) + t * t * (c.p3[0] - c.p2[0])),
|
|
2253
|
+
3 * (s * s * (c.p1[1] - c.p0[1]) + 2 * s * t * (c.p2[1] - c.p1[1]) + t * t * (c.p3[1] - c.p2[1]))
|
|
2254
|
+
];
|
|
2255
|
+
const d2 = [
|
|
2256
|
+
6 * (s * (c.p2[0] - 2 * c.p1[0] + c.p0[0]) + t * (c.p3[0] - 2 * c.p2[0] + c.p1[0])),
|
|
2257
|
+
6 * (s * (c.p2[1] - 2 * c.p1[1] + c.p0[1]) + t * (c.p3[1] - 2 * c.p2[1] + c.p1[1]))
|
|
2258
|
+
];
|
|
2259
|
+
const diff = [q[0] - p[0], q[1] - p[1]];
|
|
2260
|
+
const num = dot(diff, d1);
|
|
2261
|
+
const den = dot(d1, d1) + dot(diff, d2);
|
|
2262
|
+
if (Math.abs(den) < 1e-12) return t;
|
|
2263
|
+
const nt = t - num / den;
|
|
2264
|
+
return Number.isFinite(nt) ? Math.min(1, Math.max(0, nt)) : t;
|
|
2265
|
+
}
|
|
2266
|
+
function fitSingleFrom(pts, u, t0, t1) {
|
|
2267
|
+
let best = generate(pts, u, t0, t1);
|
|
2268
|
+
let [bestErr, bestSplit] = maxError(pts, best, u);
|
|
2269
|
+
const params = u.slice();
|
|
2270
|
+
for (let round = 0; round < MAX_REPARAM; round++) {
|
|
2271
|
+
for (let i = 0; i < params.length; i++) params[i] = newton(best, pts[i], params[i]);
|
|
2272
|
+
const c = generate(pts, params, t0, t1);
|
|
2273
|
+
const [e, s] = maxError(pts, c, params);
|
|
2274
|
+
if (e < bestErr) {
|
|
2275
|
+
best = c;
|
|
2276
|
+
bestErr = e;
|
|
2277
|
+
bestSplit = s;
|
|
2278
|
+
} else break;
|
|
2279
|
+
}
|
|
2280
|
+
return [best, bestErr, bestSplit];
|
|
2281
|
+
}
|
|
2282
|
+
function fitRec(pts, t0, t1, tol, out, depth) {
|
|
2283
|
+
const n = pts.length;
|
|
2284
|
+
if (n === 2) {
|
|
2285
|
+
const d = dist(pts[0], pts[1]) / 3;
|
|
2286
|
+
out.push({ p0: pts[0], p1: add(pts[0], t0, d), p2: add(pts[1], t1, -d), p3: pts[1] });
|
|
2287
|
+
return;
|
|
2288
|
+
}
|
|
2289
|
+
const [c, err, splitAt] = fitSingleFrom(pts, chordParams(pts), t0, t1);
|
|
2290
|
+
if (err <= tol || depth > 40) {
|
|
2291
|
+
out.push(c);
|
|
2292
|
+
return;
|
|
2293
|
+
}
|
|
2294
|
+
const split = Math.min(n - 2, Math.max(1, splitAt));
|
|
2295
|
+
const tc = unit(sub(pts[split + 1], pts[split - 1])) ?? t0;
|
|
2296
|
+
fitRec(pts.slice(0, split + 1), t0, tc, tol, out, depth + 1);
|
|
2297
|
+
fitRec(pts.slice(split), tc, t1, tol, out, depth + 1);
|
|
2298
|
+
}
|
|
2299
|
+
function tangentFrom(a, b, c) {
|
|
2300
|
+
const chord = unit(sub(b, a));
|
|
2301
|
+
if (!c) return chord;
|
|
2302
|
+
const d1 = dist(a, b);
|
|
2303
|
+
const d2 = dist(b, c);
|
|
2304
|
+
if (!(d1 > 0) || !(d2 > 0)) return chord;
|
|
2305
|
+
const ka = -(2 * d1 + d2) / (d1 * (d1 + d2));
|
|
2306
|
+
const kb = (d1 + d2) / (d1 * d2);
|
|
2307
|
+
const kc = -d1 / (d2 * (d1 + d2));
|
|
2308
|
+
const est = unit([ka * a[0] + kb * b[0] + kc * c[0], ka * a[1] + kb * b[1] + kc * c[1]]);
|
|
2309
|
+
return est && chord && dot(est, chord) > Math.cos(Math.PI / 4) ? est : chord;
|
|
2310
|
+
}
|
|
2311
|
+
function fitCubicRun(points, tolerance, t0, t1) {
|
|
2312
|
+
const pts = dedup(points);
|
|
2313
|
+
const out = [];
|
|
2314
|
+
if (pts.length < 2) return out;
|
|
2315
|
+
const n = pts.length;
|
|
2316
|
+
const start = (t0 && unit([t0[0], t0[1]])) ?? tangentFrom(pts[0], pts[1], pts[2]) ?? [1, 0];
|
|
2317
|
+
const back = tangentFrom(pts[n - 1], pts[n - 2], pts[n - 3]);
|
|
2318
|
+
const end = (t1 && unit([t1[0], t1[1]])) ?? (back ? [-back[0], -back[1]] : [1, 0]);
|
|
2319
|
+
fitRec(pts, start, end, Math.max(tolerance, 1e-9), out, 0);
|
|
2320
|
+
return out;
|
|
2321
|
+
}
|
|
2322
|
+
function turnDeg(a, b, c) {
|
|
2323
|
+
const u = unit(sub(b, a));
|
|
2324
|
+
const v = unit(sub(c, b));
|
|
2325
|
+
if (!u || !v) return 0;
|
|
2326
|
+
return Math.acos(Math.min(1, Math.max(-1, dot(u, v)))) * 180 / Math.PI;
|
|
2327
|
+
}
|
|
2328
|
+
function findCorners(pts, closed, cornerAngle, span) {
|
|
2329
|
+
const n = pts.length;
|
|
2330
|
+
const at = (i) => pts[closed ? (i % n + n) % n : i];
|
|
2331
|
+
const reach = (i, step) => {
|
|
2332
|
+
for (let k = 1; k < (closed ? n : Infinity); k++) {
|
|
2333
|
+
const j = i + step * k;
|
|
2334
|
+
if (!closed && (j < 0 || j >= n)) return k > 1 ? j - step : null;
|
|
2335
|
+
if (dist(at(j), pts[i]) >= span) return j;
|
|
2336
|
+
}
|
|
2337
|
+
return null;
|
|
2338
|
+
};
|
|
2339
|
+
const turns = new Float64Array(n);
|
|
2340
|
+
const windows = [];
|
|
2341
|
+
for (let i = 0; i < n; i++) {
|
|
2342
|
+
windows.push([i, i]);
|
|
2343
|
+
if (!closed && (i === 0 || i === n - 1)) continue;
|
|
2344
|
+
const prev = reach(i, -1);
|
|
2345
|
+
const next = reach(i, 1);
|
|
2346
|
+
if (prev === null || next === null) continue;
|
|
2347
|
+
windows[i] = [prev, next];
|
|
2348
|
+
turns[i] = turnDeg(at(prev), pts[i], at(next));
|
|
2349
|
+
}
|
|
2350
|
+
const corners = [];
|
|
2351
|
+
for (let i = 0; i < n; i++) {
|
|
2352
|
+
if (!(turns[i] > cornerAngle)) continue;
|
|
2353
|
+
const [lo, hi] = windows[i];
|
|
2354
|
+
let sharpest = true;
|
|
2355
|
+
for (let j = lo; j <= hi && sharpest; j++) {
|
|
2356
|
+
const k = closed ? (j % n + n) % n : j;
|
|
2357
|
+
if (k === i || dist(pts[k], pts[i]) >= span) continue;
|
|
2358
|
+
if (turns[k] > turns[i] || turns[k] === turns[i] && k < i) sharpest = false;
|
|
2359
|
+
}
|
|
2360
|
+
if (sharpest) corners.push(i);
|
|
2361
|
+
}
|
|
2362
|
+
return corners;
|
|
2363
|
+
}
|
|
2364
|
+
function densify(run) {
|
|
2365
|
+
if (run.length < 3) return run;
|
|
2366
|
+
const lengths = [];
|
|
2367
|
+
for (let i = 1; i < run.length; i++) lengths.push(dist(run[i - 1], run[i]));
|
|
2368
|
+
const sorted = [...lengths].sort((a, b) => a - b);
|
|
2369
|
+
const median = sorted[sorted.length >> 1];
|
|
2370
|
+
if (!(median > 0)) return run;
|
|
2371
|
+
const out = [run[0]];
|
|
2372
|
+
for (let i = 1; i < run.length; i++) {
|
|
2373
|
+
const len = lengths[i - 1];
|
|
2374
|
+
if (len > 2 * median) {
|
|
2375
|
+
const steps = Math.min(64, Math.ceil(len / median));
|
|
2376
|
+
const a = run[i - 1];
|
|
2377
|
+
const b = run[i];
|
|
2378
|
+
for (let k = 1; k < steps; k++) {
|
|
2379
|
+
const t = k / steps;
|
|
2380
|
+
out.push([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]);
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
out.push(run[i]);
|
|
2384
|
+
}
|
|
2385
|
+
return out;
|
|
2386
|
+
}
|
|
2387
|
+
function fitCubics(points, tolerance, options = {}) {
|
|
2388
|
+
let pts = dedup(points);
|
|
2389
|
+
const closed = options.closed === true;
|
|
2390
|
+
if (closed && pts.length > 1 && dist(pts[0], pts[pts.length - 1]) <= 1e-12) pts = pts.slice(0, -1);
|
|
2391
|
+
if (pts.length < 2) return [];
|
|
2392
|
+
const tol = Math.max(tolerance, 1e-6);
|
|
2393
|
+
const cornerAngle = options.cornerAngleDeg ?? 30;
|
|
2394
|
+
const n = pts.length;
|
|
2395
|
+
const span = 4 * tol;
|
|
2396
|
+
if (!closed || n < 3) {
|
|
2397
|
+
const corners2 = [0, ...n > 2 ? findCorners(pts, false, cornerAngle, span) : [], n - 1];
|
|
2398
|
+
const out2 = [];
|
|
2399
|
+
for (let k = 1; k < corners2.length; k++) {
|
|
2400
|
+
const run = pts.slice(corners2[k - 1], corners2[k] + 1);
|
|
2401
|
+
out2.push(...fitCubicRun(densify(run), tol));
|
|
2402
|
+
}
|
|
2403
|
+
if (closed && n === 2) out2.push(...fitCubicRun([pts[1], pts[0]], tol));
|
|
2404
|
+
return out2;
|
|
2405
|
+
}
|
|
2406
|
+
const corners = findCorners(pts, true, cornerAngle, span);
|
|
2407
|
+
if (corners.length === 0) {
|
|
2408
|
+
const ring = [...pts, pts[0]];
|
|
2409
|
+
const t = unit(sub(pts[1], pts[n - 1])) ?? [1, 0];
|
|
2410
|
+
return fitCubicRun(densify(ring), tol, t, t);
|
|
2411
|
+
}
|
|
2412
|
+
const out = [];
|
|
2413
|
+
for (let k = 0; k < corners.length; k++) {
|
|
2414
|
+
const from = corners[k];
|
|
2415
|
+
const to = corners[(k + 1) % corners.length];
|
|
2416
|
+
const run = [];
|
|
2417
|
+
for (let i = from; ; i = (i + 1) % n) {
|
|
2418
|
+
run.push(pts[i]);
|
|
2419
|
+
if (run.length > 1 && i === to) break;
|
|
2420
|
+
}
|
|
2421
|
+
out.push(...fitCubicRun(densify(run), tol));
|
|
2422
|
+
}
|
|
2423
|
+
return out;
|
|
2424
|
+
}
|
|
2425
|
+
function isStraightCubic(c, tol) {
|
|
2426
|
+
const chord = sub(c.p3, c.p0);
|
|
2427
|
+
const len = Math.hypot(chord[0], chord[1]);
|
|
2428
|
+
if (len < 1e-12) return dist(c.p1, c.p0) <= tol && dist(c.p2, c.p0) <= tol;
|
|
2429
|
+
const dir = [chord[0] / len, chord[1] / len];
|
|
2430
|
+
return [c.p1, c.p2].every((h) => {
|
|
2431
|
+
const v = sub(h, c.p0);
|
|
2432
|
+
const along = dot(v, dir);
|
|
2433
|
+
const across = Math.abs(v[0] * dir[1] - v[1] * dir[0]);
|
|
2434
|
+
return across <= tol && along >= -tol && along <= len + tol;
|
|
2435
|
+
});
|
|
2436
|
+
}
|
|
2437
|
+
function cubicsToPathData(segments, options = {}) {
|
|
2438
|
+
if (segments.length === 0) return "";
|
|
2439
|
+
const f = (n) => formatNumber(n, options.precision ?? 3);
|
|
2440
|
+
const dx = options.offset?.x ?? 0;
|
|
2441
|
+
const dy = options.offset?.y ?? 0;
|
|
2442
|
+
const pt = (p) => `${f(p[0] + dx)} ${f(p[1] + dy)}`;
|
|
2443
|
+
const straight = options.straightTolerance ?? 1e-3;
|
|
2444
|
+
let d = `M${pt(segments[0].p0)}`;
|
|
2445
|
+
segments.forEach((c, i) => {
|
|
2446
|
+
const last = options.closed && i === segments.length - 1;
|
|
2447
|
+
if (isStraightCubic(c, straight)) {
|
|
2448
|
+
if (!last) d += `L${pt(c.p3)}`;
|
|
2449
|
+
} else d += `C${pt(c.p1)} ${pt(c.p2)} ${pt(c.p3)}`;
|
|
2450
|
+
});
|
|
2451
|
+
return options.closed ? `${d}Z` : d;
|
|
2452
|
+
}
|
|
2453
|
+
function polylineToPathData(points, closed, options) {
|
|
2454
|
+
if (points.length < 2) return "";
|
|
2455
|
+
const f = (n) => formatNumber(n, options.precision ?? 3);
|
|
2456
|
+
const dx = options.offset?.x ?? 0;
|
|
2457
|
+
const dy = options.offset?.y ?? 0;
|
|
2458
|
+
let d = `M${f(points[0][0] + dx)} ${f(points[0][1] + dy)}`;
|
|
2459
|
+
for (let i = 1; i < points.length; i++) d += `L${f(points[i][0] + dx)} ${f(points[i][1] + dy)}`;
|
|
2460
|
+
return closed ? `${d}Z` : d;
|
|
2461
|
+
}
|
|
2462
|
+
function fitPathData(ring, options = {}) {
|
|
2463
|
+
const closed = options.closed ?? true;
|
|
2464
|
+
const tolerance = options.tolerance ?? 0.5;
|
|
2465
|
+
const pts = dedup(ring);
|
|
2466
|
+
const segments = fitCubics(pts, tolerance, { ...options, closed });
|
|
2467
|
+
if (segments.length === 0) return "";
|
|
2468
|
+
const edges = closed ? pts.length : pts.length - 1;
|
|
2469
|
+
if (options.noWorse !== false) {
|
|
2470
|
+
const straight = segments.filter((c) => isStraightCubic(c, tolerance * 0.02)).length;
|
|
2471
|
+
if (segments.length >= edges && straight === segments.length) return polylineToPathData(pts, closed, options);
|
|
2472
|
+
if (segments.length > edges) return polylineToPathData(pts, closed, options);
|
|
2473
|
+
}
|
|
2474
|
+
return cubicsToPathData(segments, {
|
|
2475
|
+
precision: options.precision,
|
|
2476
|
+
offset: options.offset,
|
|
2477
|
+
closed,
|
|
2478
|
+
straightTolerance: tolerance * 0.02
|
|
2479
|
+
});
|
|
2480
|
+
}
|
|
2481
|
+
function fitFreehandPath(points, options = {}) {
|
|
2482
|
+
return fitPathData(points, { tolerance: 1, ...options, closed: false });
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
// src/canvas/shapeGeometry.ts
|
|
2486
|
+
var SHAPE_KINDS = ["rect", "ellipse", "polygon", "star", "path"];
|
|
2487
|
+
function isShapeDescription(value) {
|
|
2488
|
+
if (!value || typeof value !== "object") return false;
|
|
2489
|
+
const kind = value.kind;
|
|
2490
|
+
if (!SHAPE_KINDS.includes(kind)) return false;
|
|
2491
|
+
return kind !== "path" || typeof value.d === "string";
|
|
2492
|
+
}
|
|
2493
|
+
var PARAM_COUNTS = { m: 2, l: 2, h: 1, v: 1, c: 6, s: 4, q: 4, t: 2, a: 7, z: 0 };
|
|
2494
|
+
var PathScanner = class {
|
|
2495
|
+
constructor(text) {
|
|
2496
|
+
this.text = text;
|
|
2497
|
+
}
|
|
2498
|
+
text;
|
|
2499
|
+
pos = 0;
|
|
2500
|
+
skip() {
|
|
2501
|
+
while (this.pos < this.text.length && /[\s,]/.test(this.text[this.pos])) this.pos++;
|
|
2502
|
+
}
|
|
2503
|
+
/** The next command letter, or `null` when a number (or the end) follows. */
|
|
2504
|
+
command() {
|
|
2505
|
+
this.skip();
|
|
2506
|
+
const ch = this.text[this.pos];
|
|
2507
|
+
if (ch && /[MmLlHhVvCcSsQqTtAaZz]/.test(ch)) {
|
|
2508
|
+
this.pos++;
|
|
2509
|
+
return ch;
|
|
2510
|
+
}
|
|
2511
|
+
return null;
|
|
2512
|
+
}
|
|
2513
|
+
done() {
|
|
2514
|
+
this.skip();
|
|
2515
|
+
return this.pos >= this.text.length;
|
|
2516
|
+
}
|
|
2517
|
+
number() {
|
|
2518
|
+
this.skip();
|
|
2519
|
+
const match = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/.exec(this.text.slice(this.pos, this.pos + 64));
|
|
2520
|
+
if (!match) return null;
|
|
2521
|
+
this.pos += match[0].length;
|
|
2522
|
+
const value = Number(match[0]);
|
|
2523
|
+
return Number.isFinite(value) ? value : null;
|
|
2524
|
+
}
|
|
2525
|
+
flag() {
|
|
2526
|
+
this.skip();
|
|
2527
|
+
const ch = this.text[this.pos];
|
|
2528
|
+
if (ch === "0" || ch === "1") {
|
|
2529
|
+
this.pos++;
|
|
2530
|
+
return ch === "1" ? 1 : 0;
|
|
2531
|
+
}
|
|
2532
|
+
return null;
|
|
2533
|
+
}
|
|
2534
|
+
};
|
|
2535
|
+
function parseSvgPath(d) {
|
|
2536
|
+
const scanner = new PathScanner(d ?? "");
|
|
2537
|
+
const commands = [];
|
|
2538
|
+
let cx = 0;
|
|
2539
|
+
let cy = 0;
|
|
2540
|
+
let startX = 0;
|
|
2541
|
+
let startY = 0;
|
|
2542
|
+
let lastControl = null;
|
|
2543
|
+
let command = "";
|
|
2544
|
+
while (!scanner.done()) {
|
|
2545
|
+
const letter = scanner.command();
|
|
2546
|
+
if (letter) {
|
|
2547
|
+
command = letter;
|
|
2548
|
+
if (command === "z" || command === "Z") {
|
|
2549
|
+
commands.push({ type: "Z" });
|
|
2550
|
+
cx = startX;
|
|
2551
|
+
cy = startY;
|
|
2552
|
+
lastControl = null;
|
|
2553
|
+
continue;
|
|
2554
|
+
}
|
|
2555
|
+
} else if (!command || command === "z" || command === "Z") {
|
|
2556
|
+
break;
|
|
2557
|
+
}
|
|
2558
|
+
const lower = command.toLowerCase();
|
|
2559
|
+
const count = PARAM_COUNTS[lower];
|
|
2560
|
+
const args = [];
|
|
2561
|
+
for (let k = 0; k < count; k++) {
|
|
2562
|
+
const value = lower === "a" && (k === 3 || k === 4) ? scanner.flag() : scanner.number();
|
|
2563
|
+
if (value === null) return commands;
|
|
2564
|
+
args.push(value);
|
|
2565
|
+
}
|
|
2566
|
+
const rel = command !== command.toUpperCase();
|
|
2567
|
+
const ox = rel ? cx : 0;
|
|
2568
|
+
const oy = rel ? cy : 0;
|
|
2569
|
+
switch (lower) {
|
|
2570
|
+
case "m": {
|
|
2571
|
+
cx = args[0] + ox;
|
|
2572
|
+
cy = args[1] + oy;
|
|
2573
|
+
startX = cx;
|
|
2574
|
+
startY = cy;
|
|
2575
|
+
commands.push({ type: "M", x: cx, y: cy });
|
|
2576
|
+
lastControl = null;
|
|
2577
|
+
command = rel ? "l" : "L";
|
|
2578
|
+
break;
|
|
2579
|
+
}
|
|
2580
|
+
case "l":
|
|
2581
|
+
cx = args[0] + ox;
|
|
2582
|
+
cy = args[1] + oy;
|
|
2583
|
+
commands.push({ type: "L", x: cx, y: cy });
|
|
2584
|
+
lastControl = null;
|
|
2585
|
+
break;
|
|
2586
|
+
case "h":
|
|
2587
|
+
cx = args[0] + (rel ? cx : 0);
|
|
2588
|
+
commands.push({ type: "L", x: cx, y: cy });
|
|
2589
|
+
lastControl = null;
|
|
2590
|
+
break;
|
|
2591
|
+
case "v":
|
|
2592
|
+
cy = args[0] + (rel ? cy : 0);
|
|
2593
|
+
commands.push({ type: "L", x: cx, y: cy });
|
|
2594
|
+
lastControl = null;
|
|
2595
|
+
break;
|
|
2596
|
+
case "c": {
|
|
2597
|
+
const c = {
|
|
2598
|
+
type: "C",
|
|
2599
|
+
x1: args[0] + ox,
|
|
2600
|
+
y1: args[1] + oy,
|
|
2601
|
+
x2: args[2] + ox,
|
|
2602
|
+
y2: args[3] + oy,
|
|
2603
|
+
x: args[4] + ox,
|
|
2604
|
+
y: args[5] + oy
|
|
2605
|
+
};
|
|
2606
|
+
commands.push(c);
|
|
2607
|
+
lastControl = { x: c.x2, y: c.y2, type: "C" };
|
|
2608
|
+
cx = c.x;
|
|
2609
|
+
cy = c.y;
|
|
2610
|
+
break;
|
|
2611
|
+
}
|
|
2612
|
+
case "s": {
|
|
2613
|
+
const x1 = lastControl?.type === "C" ? 2 * cx - lastControl.x : cx;
|
|
2614
|
+
const y1 = lastControl?.type === "C" ? 2 * cy - lastControl.y : cy;
|
|
2615
|
+
const c = {
|
|
2616
|
+
type: "C",
|
|
2617
|
+
x1,
|
|
2618
|
+
y1,
|
|
2619
|
+
x2: args[0] + ox,
|
|
2620
|
+
y2: args[1] + oy,
|
|
2621
|
+
x: args[2] + ox,
|
|
2622
|
+
y: args[3] + oy
|
|
2623
|
+
};
|
|
2624
|
+
commands.push(c);
|
|
2625
|
+
lastControl = { x: c.x2, y: c.y2, type: "C" };
|
|
2626
|
+
cx = c.x;
|
|
2627
|
+
cy = c.y;
|
|
2628
|
+
break;
|
|
2629
|
+
}
|
|
2630
|
+
case "q": {
|
|
2631
|
+
const c = { type: "Q", x1: args[0] + ox, y1: args[1] + oy, x: args[2] + ox, y: args[3] + oy };
|
|
2632
|
+
commands.push(c);
|
|
2633
|
+
lastControl = { x: c.x1, y: c.y1, type: "Q" };
|
|
2634
|
+
cx = c.x;
|
|
2635
|
+
cy = c.y;
|
|
2636
|
+
break;
|
|
2637
|
+
}
|
|
2638
|
+
case "t": {
|
|
2639
|
+
const x1 = lastControl?.type === "Q" ? 2 * cx - lastControl.x : cx;
|
|
2640
|
+
const y1 = lastControl?.type === "Q" ? 2 * cy - lastControl.y : cy;
|
|
2641
|
+
const c = { type: "Q", x1, y1, x: args[0] + ox, y: args[1] + oy };
|
|
2642
|
+
commands.push(c);
|
|
2643
|
+
lastControl = { x: x1, y: y1, type: "Q" };
|
|
2644
|
+
cx = c.x;
|
|
2645
|
+
cy = c.y;
|
|
2646
|
+
break;
|
|
2647
|
+
}
|
|
2648
|
+
case "a": {
|
|
2649
|
+
const c = {
|
|
2650
|
+
type: "A",
|
|
2651
|
+
rx: Math.abs(args[0]),
|
|
2652
|
+
ry: Math.abs(args[1]),
|
|
2653
|
+
rotation: args[2],
|
|
2654
|
+
largeArc: args[3] !== 0,
|
|
2655
|
+
sweep: args[4] !== 0,
|
|
2656
|
+
x: args[5] + ox,
|
|
2657
|
+
y: args[6] + oy
|
|
2658
|
+
};
|
|
2659
|
+
commands.push(c);
|
|
2660
|
+
lastControl = null;
|
|
2661
|
+
cx = c.x;
|
|
2662
|
+
cy = c.y;
|
|
2663
|
+
break;
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
return commands;
|
|
2668
|
+
}
|
|
2669
|
+
function segmentCount(curvature, tolerance, max) {
|
|
2670
|
+
if (!(curvature > 0)) return 1;
|
|
2671
|
+
return Math.max(1, Math.min(max, Math.ceil(Math.sqrt(curvature / tolerance))));
|
|
2672
|
+
}
|
|
2673
|
+
function flattenCubic(out, p0, x1, y1, x2, y2, x, y, tolerance, max) {
|
|
2674
|
+
const dd = Math.max(
|
|
2675
|
+
Math.hypot(p0[0] - 2 * x1 + x2, p0[1] - 2 * y1 + y2),
|
|
2676
|
+
Math.hypot(x1 - 2 * x2 + x, y1 - 2 * y2 + y)
|
|
2677
|
+
);
|
|
2678
|
+
const n = segmentCount(6 * dd / 8, tolerance, max);
|
|
2679
|
+
for (let k = 1; k <= n; k++) {
|
|
2680
|
+
const t = k / n;
|
|
2681
|
+
const mt = 1 - t;
|
|
2682
|
+
const a = mt * mt * mt;
|
|
2683
|
+
const b = 3 * mt * mt * t;
|
|
2684
|
+
const c = 3 * mt * t * t;
|
|
2685
|
+
const e = t * t * t;
|
|
2686
|
+
out.push([a * p0[0] + b * x1 + c * x2 + e * x, a * p0[1] + b * y1 + c * y2 + e * y]);
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
function flattenQuadratic(out, p0, x1, y1, x, y, tolerance, max) {
|
|
2690
|
+
const dd = Math.hypot(p0[0] - 2 * x1 + x, p0[1] - 2 * y1 + y);
|
|
2691
|
+
const n = segmentCount(2 * dd / 8, tolerance, max);
|
|
2692
|
+
for (let k = 1; k <= n; k++) {
|
|
2693
|
+
const t = k / n;
|
|
2694
|
+
const mt = 1 - t;
|
|
2695
|
+
out.push([mt * mt * p0[0] + 2 * mt * t * x1 + t * t * x, mt * mt * p0[1] + 2 * mt * t * y1 + t * t * y]);
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
function arcSteps(radius, sweep, tolerance, max) {
|
|
2699
|
+
if (radius <= tolerance) return Math.max(1, Math.ceil(Math.abs(sweep) / (Math.PI / 2)));
|
|
2700
|
+
const step = 2 * Math.acos(1 - tolerance / radius);
|
|
2701
|
+
return Math.max(1, Math.min(max, Math.ceil(Math.abs(sweep) / step)));
|
|
2702
|
+
}
|
|
2703
|
+
function flattenArc(out, p0, cmd, tolerance, max) {
|
|
2704
|
+
const [x0, y0] = p0;
|
|
2705
|
+
let { rx, ry } = cmd;
|
|
2706
|
+
const { x, y } = cmd;
|
|
2707
|
+
if (x0 === x && y0 === y) return;
|
|
2708
|
+
if (rx === 0 || ry === 0) {
|
|
2709
|
+
out.push([x, y]);
|
|
2710
|
+
return;
|
|
2711
|
+
}
|
|
2712
|
+
const phi = cmd.rotation * Math.PI / 180;
|
|
2713
|
+
const cos = Math.cos(phi);
|
|
2714
|
+
const sin = Math.sin(phi);
|
|
2715
|
+
const dx = (x0 - x) / 2;
|
|
2716
|
+
const dy = (y0 - y) / 2;
|
|
2717
|
+
const x1p = cos * dx + sin * dy;
|
|
2718
|
+
const y1p = -sin * dx + cos * dy;
|
|
2719
|
+
const lambda = x1p * x1p / (rx * rx) + y1p * y1p / (ry * ry);
|
|
2720
|
+
if (lambda > 1) {
|
|
2721
|
+
const s = Math.sqrt(lambda);
|
|
2722
|
+
rx *= s;
|
|
2723
|
+
ry *= s;
|
|
2724
|
+
}
|
|
2725
|
+
const num = rx * rx * ry * ry - rx * rx * y1p * y1p - ry * ry * x1p * x1p;
|
|
2726
|
+
const den = rx * rx * y1p * y1p + ry * ry * x1p * x1p;
|
|
2727
|
+
let coef = den === 0 ? 0 : Math.sqrt(Math.max(0, num / den));
|
|
2728
|
+
if (cmd.largeArc === cmd.sweep) coef = -coef;
|
|
2729
|
+
const cxp = coef * rx * y1p / ry;
|
|
2730
|
+
const cyp = -coef * ry * x1p / rx;
|
|
2731
|
+
const cx = cos * cxp - sin * cyp + (x0 + x) / 2;
|
|
2732
|
+
const cy = sin * cxp + cos * cyp + (y0 + y) / 2;
|
|
2733
|
+
const angle = (ux, uy, vx, vy) => {
|
|
2734
|
+
const a = Math.atan2(ux * vy - uy * vx, ux * vx + uy * vy);
|
|
2735
|
+
return a;
|
|
2736
|
+
};
|
|
2737
|
+
const theta1 = angle(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry);
|
|
2738
|
+
let delta = angle((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, (-y1p - cyp) / ry);
|
|
2739
|
+
if (!cmd.sweep && delta > 0) delta -= 2 * Math.PI;
|
|
2740
|
+
else if (cmd.sweep && delta < 0) delta += 2 * Math.PI;
|
|
2741
|
+
const n = arcSteps(Math.max(rx, ry), delta, tolerance, max);
|
|
2742
|
+
for (let k = 1; k < n; k++) {
|
|
2743
|
+
const t = theta1 + delta * k / n;
|
|
2744
|
+
const ex = rx * Math.cos(t);
|
|
2745
|
+
const ey = ry * Math.sin(t);
|
|
2746
|
+
out.push([cos * ex - sin * ey + cx, sin * ex + cos * ey + cy]);
|
|
2747
|
+
}
|
|
2748
|
+
out.push([x, y]);
|
|
2749
|
+
}
|
|
2750
|
+
function flattenSvgPath(d, options = {}) {
|
|
2751
|
+
const tolerance = options.tolerance && options.tolerance > 0 ? options.tolerance : 0.25;
|
|
2752
|
+
const max = options.maxSegments ?? 512;
|
|
2753
|
+
const commands = typeof d === "string" ? parseSvgPath(d) : d;
|
|
2754
|
+
const rings = [];
|
|
2755
|
+
let current = [];
|
|
2756
|
+
let start = [0, 0];
|
|
2757
|
+
const flush = () => {
|
|
2758
|
+
if (current.length >= 3) rings.push(current);
|
|
2759
|
+
current = [];
|
|
2760
|
+
};
|
|
2761
|
+
const last = () => current[current.length - 1] ?? start;
|
|
2762
|
+
for (const cmd of commands) {
|
|
2763
|
+
switch (cmd.type) {
|
|
2764
|
+
case "M":
|
|
2765
|
+
flush();
|
|
2766
|
+
start = [cmd.x, cmd.y];
|
|
2767
|
+
current = [start];
|
|
2768
|
+
break;
|
|
2769
|
+
case "L":
|
|
2770
|
+
if (current.length === 0) current = [start];
|
|
2771
|
+
current.push([cmd.x, cmd.y]);
|
|
2772
|
+
break;
|
|
2773
|
+
case "C":
|
|
2774
|
+
if (current.length === 0) current = [start];
|
|
2775
|
+
flattenCubic(current, last(), cmd.x1, cmd.y1, cmd.x2, cmd.y2, cmd.x, cmd.y, tolerance, max);
|
|
2776
|
+
break;
|
|
2777
|
+
case "Q":
|
|
2778
|
+
if (current.length === 0) current = [start];
|
|
2779
|
+
flattenQuadratic(current, last(), cmd.x1, cmd.y1, cmd.x, cmd.y, tolerance, max);
|
|
2780
|
+
break;
|
|
2781
|
+
case "A":
|
|
2782
|
+
if (current.length === 0) current = [start];
|
|
2783
|
+
flattenArc(current, last(), cmd, tolerance, max);
|
|
2784
|
+
break;
|
|
2785
|
+
case "Z":
|
|
2786
|
+
flush();
|
|
2787
|
+
break;
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
flush();
|
|
2791
|
+
return rings.map(dedupeRing).filter((r) => r.length >= 3);
|
|
2792
|
+
}
|
|
2793
|
+
function dedupeRing(ring) {
|
|
2794
|
+
const out = [];
|
|
2795
|
+
for (const p of ring) {
|
|
2796
|
+
const prev = out[out.length - 1];
|
|
2797
|
+
if (!prev || prev[0] !== p[0] || prev[1] !== p[1]) out.push(p);
|
|
2798
|
+
}
|
|
2799
|
+
while (out.length > 1 && out[0][0] === out[out.length - 1][0] && out[0][1] === out[out.length - 1][1]) out.pop();
|
|
2800
|
+
return out;
|
|
2801
|
+
}
|
|
2802
|
+
function cornerRadii(shape, width, height) {
|
|
2803
|
+
const raw = shape.cornerRadius ?? 0;
|
|
2804
|
+
let radii = (typeof raw === "number" ? [raw, raw, raw, raw] : [...raw]).map(
|
|
2805
|
+
(r) => Number.isFinite(r) && r > 0 ? r : 0
|
|
2806
|
+
);
|
|
2807
|
+
const factor = Math.min(
|
|
2808
|
+
1,
|
|
2809
|
+
width / Math.max(radii[0] + radii[1], 1e-12),
|
|
2810
|
+
height / Math.max(radii[1] + radii[2], 1e-12),
|
|
2811
|
+
width / Math.max(radii[2] + radii[3], 1e-12),
|
|
2812
|
+
height / Math.max(radii[3] + radii[0], 1e-12)
|
|
2813
|
+
);
|
|
2814
|
+
if (factor < 1) radii = radii.map((r) => r * factor);
|
|
2815
|
+
return radii;
|
|
2816
|
+
}
|
|
2817
|
+
function polygonVertices(count, inner, width, height) {
|
|
2818
|
+
const raw = [];
|
|
2819
|
+
const total = inner === null ? count : count * 2;
|
|
2820
|
+
for (let k = 0; k < total; k++) {
|
|
2821
|
+
const angle = -Math.PI / 2 + k * 2 * Math.PI / total;
|
|
2822
|
+
const r = inner !== null && k % 2 === 1 ? inner : 1;
|
|
2823
|
+
raw.push([r * Math.cos(angle), r * Math.sin(angle)]);
|
|
2824
|
+
}
|
|
2825
|
+
let minX = Infinity;
|
|
2826
|
+
let minY = Infinity;
|
|
2827
|
+
let maxX = -Infinity;
|
|
2828
|
+
let maxY = -Infinity;
|
|
2829
|
+
for (const [x, y] of raw) {
|
|
2830
|
+
minX = Math.min(minX, x);
|
|
2831
|
+
minY = Math.min(minY, y);
|
|
2832
|
+
maxX = Math.max(maxX, x);
|
|
2833
|
+
maxY = Math.max(maxY, y);
|
|
2834
|
+
}
|
|
2835
|
+
const sx = width / (maxX - minX || 1);
|
|
2836
|
+
const sy = height / (maxY - minY || 1);
|
|
2837
|
+
return raw.map(([x, y]) => [(x - minX) * sx, (y - minY) * sy]);
|
|
2838
|
+
}
|
|
2839
|
+
function rotationTransform(shape, size) {
|
|
2840
|
+
const deg = shape.rotation ?? 0;
|
|
2841
|
+
if (!deg || !Number.isFinite(deg)) return null;
|
|
2842
|
+
const rad = deg * Math.PI / 180;
|
|
2843
|
+
const cos = Math.cos(rad);
|
|
2844
|
+
const sin = Math.sin(rad);
|
|
2845
|
+
const cx = size.width / 2;
|
|
2846
|
+
const cy = size.height / 2;
|
|
2847
|
+
return ([x, y]) => [cx + (x - cx) * cos - (y - cy) * sin, cy + (x - cx) * sin + (y - cy) * cos];
|
|
2848
|
+
}
|
|
2849
|
+
function getShapeSvg(shape, size) {
|
|
2850
|
+
const { width: w, height: h } = size;
|
|
2851
|
+
const f = (n) => formatNumber2(n, 3);
|
|
2852
|
+
const transforms = [];
|
|
2853
|
+
if (shape.rotation) transforms.push(`rotate(${f(shape.rotation)} ${f(w / 2)} ${f(h / 2)})`);
|
|
2854
|
+
let d = "";
|
|
2855
|
+
let fillRule = "nonzero";
|
|
2856
|
+
switch (shape.kind) {
|
|
2857
|
+
case "rect": {
|
|
2858
|
+
const [tl, tr, br, bl] = cornerRadii(shape, w, h);
|
|
2859
|
+
if (tl + tr + br + bl === 0) {
|
|
2860
|
+
d = `M0 0H${f(w)}V${f(h)}H0Z`;
|
|
2861
|
+
} else {
|
|
2862
|
+
const arc = (r, x, y) => r > 0 ? `A${f(r)} ${f(r)} 0 0 1 ${f(x)} ${f(y)}` : "";
|
|
2863
|
+
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`;
|
|
2864
|
+
}
|
|
2865
|
+
break;
|
|
2866
|
+
}
|
|
2867
|
+
case "ellipse": {
|
|
2868
|
+
const rx = w / 2;
|
|
2869
|
+
const ry = h / 2;
|
|
2870
|
+
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`;
|
|
2871
|
+
break;
|
|
2872
|
+
}
|
|
2873
|
+
case "polygon":
|
|
2874
|
+
case "star": {
|
|
2875
|
+
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);
|
|
2876
|
+
d = ringsToPath([points], f);
|
|
2877
|
+
break;
|
|
2878
|
+
}
|
|
2879
|
+
case "path": {
|
|
2880
|
+
d = shape.d;
|
|
2881
|
+
fillRule = shape.fillRule ?? "nonzero";
|
|
2882
|
+
const vb = validViewBox(shape.viewBox);
|
|
2883
|
+
if (vb) {
|
|
2884
|
+
transforms.push(
|
|
2885
|
+
`scale(${f(w / vb[2])} ${f(h / vb[3])})`,
|
|
2886
|
+
`translate(${f(-vb[0])} ${f(-vb[1])})`
|
|
2887
|
+
);
|
|
2888
|
+
}
|
|
2889
|
+
break;
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
return transforms.length > 0 ? { d, transform: transforms.join(" "), fillRule } : { d, fillRule };
|
|
2893
|
+
}
|
|
2894
|
+
function getShapeRenderData(shape, size = {}) {
|
|
2895
|
+
const vb = shape.kind === "path" ? validViewBox(shape.viewBox) : null;
|
|
2896
|
+
const width = size.width && size.width > 0 ? size.width : vb ? vb[2] : 100;
|
|
2897
|
+
const height = size.height && size.height > 0 ? size.height : vb ? vb[3] : 100;
|
|
2898
|
+
const svg = getShapeSvg(shape, { width, height });
|
|
2899
|
+
const data = {
|
|
2900
|
+
viewBox: `0 0 ${formatNumber2(width, 3)} ${formatNumber2(height, 3)}`,
|
|
2901
|
+
width,
|
|
2902
|
+
height,
|
|
2903
|
+
d: svg.d,
|
|
2904
|
+
fillRule: svg.fillRule,
|
|
2905
|
+
fill: shape.fill ?? "currentColor",
|
|
2906
|
+
stroke: shape.stroke ?? "none",
|
|
2907
|
+
strokeWidth: shape.strokeWidth ?? 1
|
|
2908
|
+
};
|
|
2909
|
+
if (svg.transform) data.transform = svg.transform;
|
|
2910
|
+
if (shape.opacity !== void 0) data.opacity = shape.opacity;
|
|
2911
|
+
return data;
|
|
2912
|
+
}
|
|
2913
|
+
function clampRatio(value) {
|
|
2914
|
+
if (value === void 0 || !Number.isFinite(value)) return 0.5;
|
|
2915
|
+
return Math.min(1, Math.max(0.01, value));
|
|
2916
|
+
}
|
|
2917
|
+
function validViewBox(vb) {
|
|
2918
|
+
if (!vb || vb.length !== 4 || !vb.every(Number.isFinite) || vb[2] <= 0 || vb[3] <= 0) return null;
|
|
2919
|
+
return vb;
|
|
2920
|
+
}
|
|
2921
|
+
function shapeToPolygon(shape, size, options = {}) {
|
|
2922
|
+
const { width: w, height: h } = size;
|
|
2923
|
+
if (!(w > 0) || !(h > 0)) return { rings: [], fillRule: "nonzero" };
|
|
2924
|
+
let rings;
|
|
2925
|
+
let fillRule = "nonzero";
|
|
2926
|
+
if (shape.kind === "path") {
|
|
2927
|
+
fillRule = shape.fillRule ?? "nonzero";
|
|
2928
|
+
const vb = validViewBox(shape.viewBox);
|
|
2929
|
+
if (vb) {
|
|
2930
|
+
const sx = w / vb[2];
|
|
2931
|
+
const sy = h / vb[3];
|
|
2932
|
+
const tolerance = (options.tolerance ?? 0.25) / Math.max(sx, sy);
|
|
2933
|
+
rings = flattenSvgPath(shape.d, { ...options, tolerance }).map(
|
|
2934
|
+
(ring) => ring.map(([x, y]) => [(x - vb[0]) * sx, (y - vb[1]) * sy])
|
|
2935
|
+
);
|
|
2936
|
+
} else {
|
|
2937
|
+
rings = flattenSvgPath(shape.d, options);
|
|
2938
|
+
}
|
|
2939
|
+
} else {
|
|
2940
|
+
const { d } = getShapeSvg({ ...shape, rotation: 0 }, size);
|
|
2941
|
+
rings = flattenSvgPath(d, options);
|
|
2942
|
+
}
|
|
2943
|
+
const rotate = rotationTransform(shape, size);
|
|
2944
|
+
if (rotate) rings = rings.map((ring) => ring.map(rotate));
|
|
2945
|
+
return { rings, fillRule };
|
|
2946
|
+
}
|
|
2947
|
+
function formatNumber2(value, precision = 3) {
|
|
2948
|
+
const factor = 10 ** precision;
|
|
2949
|
+
const rounded = Math.round(value * factor) / factor;
|
|
2950
|
+
return Object.is(rounded, -0) ? "0" : String(rounded);
|
|
2951
|
+
}
|
|
2952
|
+
function ringsToPath(rings, f) {
|
|
2953
|
+
let d = "";
|
|
2954
|
+
for (const ring of rings) {
|
|
2955
|
+
if (ring.length < 2) continue;
|
|
2956
|
+
d += `M${f(ring[0][0])} ${f(ring[0][1])}`;
|
|
2957
|
+
for (let i = 1; i < ring.length; i++) d += `L${f(ring[i][0])} ${f(ring[i][1])}`;
|
|
2958
|
+
d += "Z";
|
|
2959
|
+
}
|
|
2960
|
+
return d;
|
|
2961
|
+
}
|
|
2962
|
+
function segmentDistance(p, a, b) {
|
|
2963
|
+
const dx = b[0] - a[0];
|
|
2964
|
+
const dy = b[1] - a[1];
|
|
2965
|
+
const len2 = dx * dx + dy * dy;
|
|
2966
|
+
if (len2 === 0) return Math.hypot(p[0] - a[0], p[1] - a[1]);
|
|
2967
|
+
const t = Math.max(0, Math.min(1, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / len2));
|
|
2968
|
+
return Math.hypot(p[0] - (a[0] + t * dx), p[1] - (a[1] + t * dy));
|
|
2969
|
+
}
|
|
2970
|
+
function rdp(points, tolerance) {
|
|
2971
|
+
if (points.length <= 2) return points;
|
|
2972
|
+
const keep = new Uint8Array(points.length);
|
|
2973
|
+
keep[0] = 1;
|
|
2974
|
+
keep[points.length - 1] = 1;
|
|
2975
|
+
const stack = [[0, points.length - 1]];
|
|
2976
|
+
while (stack.length) {
|
|
2977
|
+
const [s, e] = stack.pop();
|
|
2978
|
+
let maxDist = -1;
|
|
2979
|
+
let index = -1;
|
|
2980
|
+
for (let i = s + 1; i < e; i++) {
|
|
2981
|
+
const dist2 = segmentDistance(points[i], points[s], points[e]);
|
|
2982
|
+
if (dist2 > maxDist) {
|
|
2983
|
+
maxDist = dist2;
|
|
2984
|
+
index = i;
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
if (index !== -1 && maxDist > tolerance) {
|
|
2988
|
+
keep[index] = 1;
|
|
2989
|
+
stack.push([s, index], [index, e]);
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
return points.filter((_, i) => keep[i]);
|
|
2993
|
+
}
|
|
2994
|
+
function simplifyRing(ring, tolerance) {
|
|
2995
|
+
if (ring.length <= 3 || !(tolerance > 0)) return [...ring];
|
|
2996
|
+
let far = 0;
|
|
2997
|
+
let farDist = -1;
|
|
2998
|
+
for (let i = 1; i < ring.length; i++) {
|
|
2999
|
+
const dist2 = Math.hypot(ring[i][0] - ring[0][0], ring[i][1] - ring[0][1]);
|
|
3000
|
+
if (dist2 > farDist) {
|
|
3001
|
+
farDist = dist2;
|
|
3002
|
+
far = i;
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
const first = rdp(ring.slice(0, far + 1), tolerance);
|
|
3006
|
+
const second = rdp([...ring.slice(far), ring[0]], tolerance);
|
|
3007
|
+
const out = [...first.slice(0, -1), ...second.slice(0, -1)];
|
|
3008
|
+
return out.length >= 3 ? out : [];
|
|
3009
|
+
}
|
|
3010
|
+
function polygonToSvgPath(input, options = {}) {
|
|
3011
|
+
const precision = options.precision ?? 3;
|
|
3012
|
+
const f = (n) => formatNumber2(n, precision);
|
|
3013
|
+
const dx = options.offset?.x ?? 0;
|
|
3014
|
+
const dy = options.offset?.y ?? 0;
|
|
3015
|
+
const rings = [];
|
|
3016
|
+
const fit = options.curves === "fit";
|
|
3017
|
+
let fitted = "";
|
|
3018
|
+
for (const item of input) {
|
|
3019
|
+
const list = item.length > 0 && Array.isArray(item[0]) && Array.isArray(item[0][0]) ? item : [item];
|
|
3020
|
+
for (const ring of list) {
|
|
3021
|
+
let r = ring;
|
|
3022
|
+
if (options.simplify && options.simplify > 0) r = simplifyRing(r, options.simplify);
|
|
3023
|
+
if (r.length < 3) continue;
|
|
3024
|
+
if (fit) {
|
|
3025
|
+
fitted += fitPathData(r, {
|
|
3026
|
+
tolerance: options.curveTolerance ?? 0.25,
|
|
3027
|
+
cornerAngleDeg: options.cornerAngleDeg,
|
|
3028
|
+
precision,
|
|
3029
|
+
offset: { x: dx, y: dy }
|
|
3030
|
+
});
|
|
3031
|
+
continue;
|
|
3032
|
+
}
|
|
3033
|
+
rings.push(dx || dy ? r.map(([x, y]) => [x + dx, y + dy]) : r);
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
return fit ? fitted : ringsToPath(rings, f);
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
// src/canvas/shapeNodes.ts
|
|
3040
|
+
var SHAPE_DATA_KEY = "shape";
|
|
3041
|
+
var SHAPE_NODE_TYPE = "shape";
|
|
3042
|
+
var SHAPE_OPERATIONS = ["unite", "intersect", "subtract", "exclude", "divide"];
|
|
3043
|
+
var BOOLEAN_FOR = {
|
|
3044
|
+
unite: "union",
|
|
3045
|
+
intersect: "intersection",
|
|
3046
|
+
subtract: "difference",
|
|
3047
|
+
exclude: "xor"
|
|
3048
|
+
};
|
|
3049
|
+
function isShapeNode(node) {
|
|
3050
|
+
return !!node && isShapeDescription(node.data?.[SHAPE_DATA_KEY]);
|
|
3051
|
+
}
|
|
3052
|
+
function getNodeShape(node) {
|
|
3053
|
+
const shape = node?.data?.[SHAPE_DATA_KEY];
|
|
3054
|
+
return isShapeDescription(shape) ? shape : null;
|
|
3055
|
+
}
|
|
3056
|
+
function shapeNodeToPolygon(node, lookup, options = {}) {
|
|
3057
|
+
const shape = getNodeShape(node);
|
|
3058
|
+
if (!shape) return null;
|
|
3059
|
+
const size = getNodeSize(node, options);
|
|
3060
|
+
const origin = node.parentId && lookup ? getAbsolutePosition(node, lookup) : node.position;
|
|
3061
|
+
const { rings, fillRule } = shapeToPolygon(shape, size, { tolerance: options.tolerance });
|
|
3062
|
+
return {
|
|
3063
|
+
rings: rings.map((ring) => ring.map(([x, y]) => [x + origin.x, y + origin.y])),
|
|
3064
|
+
fillRule
|
|
3065
|
+
};
|
|
3066
|
+
}
|
|
3067
|
+
var idCounter = 0;
|
|
3068
|
+
function defaultId2(index) {
|
|
3069
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
3070
|
+
idCounter += 1;
|
|
3071
|
+
return `shape-${Date.now().toString(36)}-${idCounter}-${index}`;
|
|
3072
|
+
}
|
|
3073
|
+
function stackOrder(nodes, ids) {
|
|
3074
|
+
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);
|
|
3075
|
+
}
|
|
3076
|
+
function canCombineShapeNodes(nodes, ids) {
|
|
3077
|
+
const idSet = new Set(ids);
|
|
3078
|
+
const shapes = nodes.filter((n) => idSet.has(n.id) && isShapeNode(n));
|
|
3079
|
+
if (shapes.some((n) => isLocked(n))) return { ok: false, reason: "locked" };
|
|
3080
|
+
if (shapes.length < 2) return { ok: false, reason: "too-few" };
|
|
3081
|
+
return { ok: true, shapeIds: shapes.map((n) => n.id) };
|
|
3082
|
+
}
|
|
3083
|
+
function combineShapeNodes(nodes, ids, operation, options = {}) {
|
|
3084
|
+
const idList = [...ids];
|
|
3085
|
+
const check = canCombineShapeNodes(nodes, idList);
|
|
3086
|
+
if (!check.ok) return { ok: false, operation, reason: check.reason };
|
|
3087
|
+
const lookup = createNodeLookup(nodes);
|
|
3088
|
+
const shapeIds = new Set(check.shapeIds);
|
|
3089
|
+
const ordered = stackOrder(nodes, shapeIds);
|
|
3090
|
+
const operands = ordered.map((node) => shapeNodeToPolygon(node, lookup, options));
|
|
3091
|
+
const parentIds = new Set(ordered.map((n) => n.parentId ?? ""));
|
|
3092
|
+
const sharedParent = parentIds.size === 1 ? ordered[0].parentId : void 0;
|
|
3093
|
+
const parentOrigin = sharedParent && lookup.get(sharedParent) ? getAbsolutePosition(lookup.get(sharedParent), lookup) : { x: 0, y: 0 };
|
|
3094
|
+
const pickSource = () => {
|
|
3095
|
+
const style = options.styleFrom ?? "bottom";
|
|
3096
|
+
if (style === "top") return ordered[ordered.length - 1];
|
|
3097
|
+
if (style === "first") {
|
|
3098
|
+
const first = idList.find((id) => shapeIds.has(id));
|
|
3099
|
+
return first && lookup.get(first) || ordered[0];
|
|
3100
|
+
}
|
|
3101
|
+
if (typeof style === "object") return shapeIds.has(style.id) && lookup.get(style.id) || ordered[0];
|
|
3102
|
+
return ordered[0];
|
|
3103
|
+
};
|
|
3104
|
+
const createId = options.createId ?? defaultId2;
|
|
3105
|
+
const build = (polygon, source, index) => {
|
|
3106
|
+
const bounds = multiPolygonBounds(polygon);
|
|
3107
|
+
if (!bounds) return null;
|
|
3108
|
+
const width = bounds.maxX - bounds.minX;
|
|
3109
|
+
const height = bounds.maxY - bounds.minY;
|
|
3110
|
+
if (!(width > 0) || !(height > 0)) return null;
|
|
3111
|
+
const d = polygonToSvgPath(polygon, {
|
|
3112
|
+
precision: options.precision,
|
|
3113
|
+
simplify: options.simplify,
|
|
3114
|
+
offset: { x: -bounds.minX, y: -bounds.minY },
|
|
3115
|
+
curves: options.curves ?? "fit",
|
|
3116
|
+
curveTolerance: options.curveTolerance ?? options.tolerance ?? 0.25,
|
|
3117
|
+
cornerAngleDeg: options.cornerAngleDeg
|
|
3118
|
+
});
|
|
3119
|
+
if (!d) return null;
|
|
3120
|
+
const sourceShape = getNodeShape(source);
|
|
3121
|
+
const shape = {
|
|
3122
|
+
kind: "path",
|
|
3123
|
+
d,
|
|
3124
|
+
viewBox: [0, 0, roundSize(width), roundSize(height)],
|
|
3125
|
+
fillRule: "nonzero"
|
|
3126
|
+
};
|
|
3127
|
+
for (const key of ["fill", "stroke", "strokeWidth", "opacity"]) {
|
|
3128
|
+
if (sourceShape[key] !== void 0) shape[key] = sourceShape[key];
|
|
3129
|
+
}
|
|
3130
|
+
const {
|
|
3131
|
+
[LOCKED_DATA_KEY]: _locked,
|
|
3132
|
+
[LOCKED_PREVIOUS_FLAGS_KEY]: _previous,
|
|
3133
|
+
...data
|
|
3134
|
+
} = source.data ?? {};
|
|
3135
|
+
const {
|
|
3136
|
+
measured: _measured,
|
|
3137
|
+
selected: _selected,
|
|
3138
|
+
dragging: _dragging,
|
|
3139
|
+
positionAbsolute: _positionAbsolute,
|
|
3140
|
+
internals: _internals,
|
|
3141
|
+
parentId: _parentId,
|
|
3142
|
+
...rest
|
|
3143
|
+
} = source;
|
|
3144
|
+
const node = {
|
|
3145
|
+
...rest,
|
|
3146
|
+
id: createId(index),
|
|
3147
|
+
position: { x: bounds.minX - parentOrigin.x, y: bounds.minY - parentOrigin.y },
|
|
3148
|
+
width,
|
|
3149
|
+
height,
|
|
3150
|
+
data: { ...data, [SHAPE_DATA_KEY]: shape }
|
|
3151
|
+
};
|
|
3152
|
+
if (sharedParent) node.parentId = sharedParent;
|
|
3153
|
+
if (options.nodeType !== void 0) node.type = options.nodeType;
|
|
3154
|
+
if (source.style) node.style = withStyleSize(source.style, { width, height });
|
|
3155
|
+
if (options.select !== false) node.selected = true;
|
|
3156
|
+
return node;
|
|
3157
|
+
};
|
|
3158
|
+
const created = [];
|
|
3159
|
+
if (operation === "divide") {
|
|
3160
|
+
const pieces = dividePolygons(operands, { epsilon: options.epsilon });
|
|
3161
|
+
pieces.forEach((piece) => {
|
|
3162
|
+
const source = ordered[piece.operands[piece.operands.length - 1]];
|
|
3163
|
+
const node = build([piece.polygon], source, created.length);
|
|
3164
|
+
if (node) created.push(node);
|
|
3165
|
+
});
|
|
3166
|
+
} else {
|
|
3167
|
+
const result = polygonBoolean(BOOLEAN_FOR[operation], operands, { epsilon: options.epsilon });
|
|
3168
|
+
const node = build(result, pickSource(), 0);
|
|
3169
|
+
if (node) created.push(node);
|
|
3170
|
+
}
|
|
3171
|
+
if (created.length === 0) return { ok: false, operation, reason: "empty" };
|
|
3172
|
+
return { ok: true, operation, removeIds: ordered.map((n) => n.id), nodes: created };
|
|
3173
|
+
}
|
|
3174
|
+
function roundSize(value) {
|
|
3175
|
+
return Math.round(value * 1e3) / 1e3;
|
|
3176
|
+
}
|
|
3177
|
+
function applyShapeCombination(nodes, change, options = {}) {
|
|
3178
|
+
const remove = new Set(change.removeIds);
|
|
3179
|
+
const insertAt = nodes.findIndex((n) => remove.has(n.id));
|
|
3180
|
+
const deselect = options.deselect !== false && change.nodes.some((n) => n.selected);
|
|
3181
|
+
const out = [];
|
|
3182
|
+
nodes.forEach((node, index) => {
|
|
3183
|
+
if (index === insertAt) out.push(...change.nodes);
|
|
3184
|
+
if (remove.has(node.id)) return;
|
|
3185
|
+
out.push(deselect && node.selected ? { ...node, selected: false } : node);
|
|
3186
|
+
});
|
|
3187
|
+
if (insertAt === -1) out.push(...change.nodes);
|
|
3188
|
+
return out;
|
|
3189
|
+
}
|
|
3190
|
+
function revertShapeCombination(nodes, change, previous) {
|
|
3191
|
+
const created = new Set(change.nodes.map((n) => n.id));
|
|
3192
|
+
const removed = new Set(change.removeIds);
|
|
3193
|
+
const out = nodes.filter((n) => !created.has(n.id));
|
|
3194
|
+
previous.forEach((node, index) => {
|
|
3195
|
+
if (!removed.has(node.id) || out.some((n) => n.id === node.id)) return;
|
|
3196
|
+
out.splice(Math.min(index, out.length), 0, node);
|
|
3197
|
+
});
|
|
3198
|
+
return out;
|
|
3199
|
+
}
|
|
3200
|
+
|
|
3201
|
+
// src/canvas/resizeSnap.ts
|
|
3202
|
+
var RESIZE_HANDLES = [
|
|
3203
|
+
"top-left",
|
|
3204
|
+
"top",
|
|
3205
|
+
"top-right",
|
|
3206
|
+
"right",
|
|
3207
|
+
"bottom-right",
|
|
3208
|
+
"bottom",
|
|
3209
|
+
"bottom-left",
|
|
3210
|
+
"left"
|
|
3211
|
+
];
|
|
3212
|
+
function resizeHandleAxes(handle) {
|
|
3213
|
+
const x = handle.includes("left") ? -1 : handle.includes("right") ? 1 : 0;
|
|
3214
|
+
const y = handle.includes("top") ? -1 : handle.includes("bottom") ? 1 : 0;
|
|
3215
|
+
return { x, y };
|
|
3216
|
+
}
|
|
3217
|
+
function inferResizeHandle(start, next, eps = 1e-6) {
|
|
3218
|
+
const moved = (a, b) => Math.abs(a - b) > eps;
|
|
3219
|
+
const left = moved(start.x, next.x);
|
|
3220
|
+
const right = moved(start.x + start.width, next.x + next.width);
|
|
3221
|
+
const top = moved(start.y, next.y);
|
|
3222
|
+
const bottom = moved(start.y + start.height, next.y + next.height);
|
|
3223
|
+
const h = left ? "left" : right ? "right" : "";
|
|
3224
|
+
const v = top ? "top" : bottom ? "bottom" : "";
|
|
3225
|
+
if (h && v) return `${v}-${h}`;
|
|
3226
|
+
return h || v || null;
|
|
3227
|
+
}
|
|
3228
|
+
function within(axis, v, tolerance) {
|
|
3229
|
+
const { values } = axis;
|
|
3230
|
+
let lo = 0;
|
|
3231
|
+
let hi = values.length;
|
|
3232
|
+
while (lo < hi) {
|
|
3233
|
+
const mid = lo + hi >>> 1;
|
|
3234
|
+
if (values[mid] < v - tolerance) lo = mid + 1;
|
|
3235
|
+
else hi = mid;
|
|
3236
|
+
}
|
|
3237
|
+
const out = [];
|
|
3238
|
+
for (let i = lo; i < values.length && values[i] <= v + tolerance; i++) out.push(values[i]);
|
|
3239
|
+
return out;
|
|
3240
|
+
}
|
|
3241
|
+
function matching(axis, v, eps) {
|
|
3242
|
+
const { values, rects } = axis;
|
|
3243
|
+
const out = [];
|
|
3244
|
+
for (let i = 0; i < values.length; i++) if (Math.abs(values[i] - v) <= eps) out.push(rects[i]);
|
|
3245
|
+
return out;
|
|
3246
|
+
}
|
|
3247
|
+
function snapResize(rect, handle, targets, options = {}) {
|
|
3248
|
+
const index = Array.isArray(targets) ? createGuideIndex(targets) : targets;
|
|
3249
|
+
const zoom = options.zoom && options.zoom > 0 ? options.zoom : 1;
|
|
3250
|
+
const tolerance = (options.threshold ?? 6) / zoom;
|
|
3251
|
+
const eps = 0.5 / zoom;
|
|
3252
|
+
const useEdges = options.edges !== false;
|
|
3253
|
+
const useCentres = options.centres !== false;
|
|
3254
|
+
const grid = options.gridSize ?? 0;
|
|
3255
|
+
const axes = resizeHandleAxes(handle);
|
|
3256
|
+
const unchanged = { rect: { ...rect }, guides: [], snappedX: false, snappedY: false, changed: false };
|
|
3257
|
+
if (![rect.x, rect.y, rect.width, rect.height].every(Number.isFinite)) return unchanged;
|
|
3258
|
+
const x0 = rect.x;
|
|
3259
|
+
const x1 = rect.x + rect.width;
|
|
3260
|
+
const y0 = rect.y;
|
|
3261
|
+
const y1 = rect.y + rect.height;
|
|
3262
|
+
const bestHit = (horizontal) => {
|
|
3263
|
+
const dir = horizontal ? axes.x : axes.y;
|
|
3264
|
+
if (dir === 0) return null;
|
|
3265
|
+
const moving = horizontal ? dir < 0 ? x0 : x1 : dir < 0 ? y0 : y1;
|
|
3266
|
+
const fixed = horizontal ? dir < 0 ? x1 : x0 : dir < 0 ? y1 : y0;
|
|
3267
|
+
const min = (horizontal ? options.minWidth : options.minHeight) ?? 0;
|
|
3268
|
+
const valid = (t) => {
|
|
3269
|
+
const size = dir < 0 ? fixed - t : t - fixed;
|
|
3270
|
+
return Number.isFinite(size) && size > 1e-9 && size >= min;
|
|
3271
|
+
};
|
|
3272
|
+
let best = null;
|
|
3273
|
+
const consider = (values, kind) => {
|
|
3274
|
+
for (const t of values) {
|
|
3275
|
+
if (!valid(t)) continue;
|
|
3276
|
+
if (!best || Math.abs(t - moving) < Math.abs(best.value - moving)) best = { value: t, kind };
|
|
3277
|
+
}
|
|
3278
|
+
};
|
|
3279
|
+
if (useEdges) consider(within(horizontal ? index.edgeX : index.edgeY, moving, tolerance), "edge");
|
|
3280
|
+
if (useCentres) consider(within(horizontal ? index.centreX : index.centreY, moving, tolerance), "centre");
|
|
3281
|
+
if (!best && grid > 0) {
|
|
3282
|
+
const origin = horizontal ? options.gridOrigin?.x ?? 0 : options.gridOrigin?.y ?? 0;
|
|
3283
|
+
consider([snapToGrid(moving, grid, origin)], "grid");
|
|
3284
|
+
}
|
|
3285
|
+
return best;
|
|
3286
|
+
};
|
|
3287
|
+
let nx0 = x0;
|
|
3288
|
+
let nx1 = x1;
|
|
3289
|
+
let ny0 = y0;
|
|
3290
|
+
let ny1 = y1;
|
|
3291
|
+
const setEdge = (horizontal, value) => {
|
|
3292
|
+
const dir = horizontal ? axes.x : axes.y;
|
|
3293
|
+
if (horizontal) {
|
|
3294
|
+
if (dir < 0) nx0 = value;
|
|
3295
|
+
else nx1 = value;
|
|
3296
|
+
} else if (dir < 0) ny0 = value;
|
|
3297
|
+
else ny1 = value;
|
|
3298
|
+
};
|
|
3299
|
+
let hitX = bestHit(true);
|
|
3300
|
+
let hitY = bestHit(false);
|
|
3301
|
+
const ratio = rect.height > 0 ? rect.width / rect.height : 0;
|
|
3302
|
+
if (options.keepAspectRatio && ratio > 0 && (hitX || hitY)) {
|
|
3303
|
+
if (axes.x !== 0 && axes.y !== 0) {
|
|
3304
|
+
const dist2 = (h, horizontal) => h ? Math.abs(h.value - (horizontal ? axes.x < 0 ? x0 : x1 : axes.y < 0 ? y0 : y1)) : Infinity;
|
|
3305
|
+
const useX = dist2(hitX, true) <= dist2(hitY, false);
|
|
3306
|
+
if (useX && hitX) {
|
|
3307
|
+
setEdge(true, hitX.value);
|
|
3308
|
+
const height = (nx1 - nx0) / ratio;
|
|
3309
|
+
if (axes.y < 0) ny0 = ny1 - height;
|
|
3310
|
+
else ny1 = ny0 + height;
|
|
3311
|
+
hitY = null;
|
|
3312
|
+
} else if (hitY) {
|
|
3313
|
+
setEdge(false, hitY.value);
|
|
3314
|
+
const width = (ny1 - ny0) * ratio;
|
|
3315
|
+
if (axes.x < 0) nx0 = nx1 - width;
|
|
3316
|
+
else nx1 = nx0 + width;
|
|
3317
|
+
hitX = null;
|
|
3318
|
+
}
|
|
3319
|
+
const min = (v) => v ?? 0;
|
|
3320
|
+
if (nx1 - nx0 < min(options.minWidth) || ny1 - ny0 < min(options.minHeight) || !(nx1 > nx0 && ny1 > ny0)) {
|
|
3321
|
+
return unchanged;
|
|
3322
|
+
}
|
|
3323
|
+
} else if (axes.x !== 0 && hitX) {
|
|
3324
|
+
setEdge(true, hitX.value);
|
|
3325
|
+
const height = (nx1 - nx0) / ratio;
|
|
3326
|
+
const cy = (y0 + y1) / 2;
|
|
3327
|
+
ny0 = cy - height / 2;
|
|
3328
|
+
ny1 = cy + height / 2;
|
|
3329
|
+
} else if (axes.y !== 0 && hitY) {
|
|
3330
|
+
setEdge(false, hitY.value);
|
|
3331
|
+
const width = (ny1 - ny0) * ratio;
|
|
3332
|
+
const cx = (x0 + x1) / 2;
|
|
3333
|
+
nx0 = cx - width / 2;
|
|
3334
|
+
nx1 = cx + width / 2;
|
|
3335
|
+
}
|
|
3336
|
+
} else {
|
|
3337
|
+
if (hitX) setEdge(true, hitX.value);
|
|
3338
|
+
if (hitY) setEdge(false, hitY.value);
|
|
3339
|
+
}
|
|
3340
|
+
const out = { x: nx0, y: ny0, width: nx1 - nx0, height: ny1 - ny0 };
|
|
3341
|
+
const guides = [];
|
|
3342
|
+
const guideFor = (horizontal, hit) => {
|
|
3343
|
+
if (!hit || hit.kind === "grid") return;
|
|
3344
|
+
const axis = hit.kind === "edge" ? horizontal ? index.edgeX : index.edgeY : horizontal ? index.centreX : index.centreY;
|
|
3345
|
+
const aligned = matching(axis, hit.value, eps);
|
|
3346
|
+
const span = boundsOf([out, ...aligned]);
|
|
3347
|
+
guides.push(
|
|
3348
|
+
horizontal ? { orientation: "vertical", kind: hit.kind, x1: hit.value, y1: span.y, x2: hit.value, y2: span.y + span.height } : { orientation: "horizontal", kind: hit.kind, x1: span.x, y1: hit.value, x2: span.x + span.width, y2: hit.value }
|
|
3349
|
+
);
|
|
3350
|
+
};
|
|
3351
|
+
guideFor(true, hitX);
|
|
3352
|
+
guideFor(false, hitY);
|
|
3353
|
+
const changed = out.x !== rect.x || out.y !== rect.y || out.width !== rect.width || out.height !== rect.height;
|
|
3354
|
+
return {
|
|
3355
|
+
rect: out,
|
|
3356
|
+
guides,
|
|
3357
|
+
snappedX: !!hitX && hitX.kind !== "grid",
|
|
3358
|
+
snappedY: !!hitY && hitY.kind !== "grid",
|
|
3359
|
+
changed
|
|
3360
|
+
};
|
|
3361
|
+
}
|
|
3362
|
+
function snapNodeResize(nodes, id, rect, handle, options = {}) {
|
|
3363
|
+
const lookup = createNodeLookup(nodes);
|
|
3364
|
+
const node = lookup.get(id);
|
|
3365
|
+
if (!node || isLocked(node)) {
|
|
3366
|
+
return { rect: { ...rect }, guides: [], snappedX: false, snappedY: false, changed: false, updates: [] };
|
|
3367
|
+
}
|
|
3368
|
+
const index = options.index ?? createNodeGuideIndex(nodes, [id], options);
|
|
3369
|
+
const result = snapResize(rect, handle, index, options);
|
|
3370
|
+
if (!result.changed) return { ...result, updates: [] };
|
|
3371
|
+
const parent = node.parentId ? lookup.get(node.parentId) : void 0;
|
|
3372
|
+
const origin = parent ? getAbsolutePosition(parent, lookup) : { x: 0, y: 0 };
|
|
3373
|
+
return {
|
|
3374
|
+
...result,
|
|
3375
|
+
updates: [
|
|
3376
|
+
{
|
|
3377
|
+
id,
|
|
3378
|
+
position: { x: result.rect.x - origin.x, y: result.rect.y - origin.y },
|
|
3379
|
+
width: result.rect.width,
|
|
3380
|
+
height: result.rect.height
|
|
3381
|
+
}
|
|
3382
|
+
]
|
|
3383
|
+
};
|
|
3384
|
+
}
|
|
3385
|
+
function getResizingNodeRect(node, lookup, options = {}) {
|
|
3386
|
+
const { x, y } = node.parentId ? getAbsolutePosition(node, lookup) : node.position;
|
|
3387
|
+
const size = getNodeSize(node, options);
|
|
3388
|
+
return { x, y, width: node.width ?? size.width, height: node.height ?? size.height };
|
|
1706
3389
|
}
|
|
1707
3390
|
|
|
1708
3391
|
// src/canvas/arrangeController.ts
|
|
@@ -1710,6 +3393,11 @@ function createCanvasArrangeController(options) {
|
|
|
1710
3393
|
let guides = [];
|
|
1711
3394
|
let dragIndex = null;
|
|
1712
3395
|
let dragKey = "";
|
|
3396
|
+
let lastSnapped = /* @__PURE__ */ new Map();
|
|
3397
|
+
let resizeId = null;
|
|
3398
|
+
let resizeStart = null;
|
|
3399
|
+
let resizeIndex = null;
|
|
3400
|
+
let lastResized = null;
|
|
1713
3401
|
const selectedIds = (ids) => ids ? [...ids] : options.getSelectedIds?.() ?? options.getNodes().filter((n) => n.selected).map((n) => n.id);
|
|
1714
3402
|
const setGuides = (next) => {
|
|
1715
3403
|
if (next.length === 0 && guides.length === 0) return;
|
|
@@ -1777,6 +3465,30 @@ function createCanvasArrangeController(options) {
|
|
|
1777
3465
|
return anyUnlocked ? controller.lock(list) : controller.unlock(list);
|
|
1778
3466
|
},
|
|
1779
3467
|
isLocked: (id) => isLocked(options.getNodes().find((n) => n.id === id)),
|
|
3468
|
+
/**
|
|
3469
|
+
* Freeform shape operation (Unite / Intersect / Subtract / Exclude / Divide) on the
|
|
3470
|
+
* shape nodes among `ids` (default: the selection). Replaces them with the result as
|
|
3471
|
+
* one undoable step. Refuses without changing anything when a selected shape is
|
|
3472
|
+
* locked, fewer than two shapes are selected, or the result is empty.
|
|
3473
|
+
*/
|
|
3474
|
+
combineShapes(operation, ids, combineOptions = {}) {
|
|
3475
|
+
const before = options.getNodes();
|
|
3476
|
+
const result = combineShapeNodes(before, selectedIds(ids), operation, { ...rect(), ...combineOptions });
|
|
3477
|
+
if (!result.ok) return result;
|
|
3478
|
+
const name = `${operation[0].toUpperCase()}${operation.slice(1)} Shapes`;
|
|
3479
|
+
const run = () => options.setNodes(applyShapeCombination(options.getNodes(), result));
|
|
3480
|
+
const undo = () => options.setNodes(revertShapeCombination(options.getNodes(), result, before));
|
|
3481
|
+
if (options.history) options.history.execute({ name, execute: run, undo });
|
|
3482
|
+
else run();
|
|
3483
|
+
return result;
|
|
3484
|
+
},
|
|
3485
|
+
unite: (ids, o) => controller.combineShapes("unite", ids, o),
|
|
3486
|
+
intersect: (ids, o) => controller.combineShapes("intersect", ids, o),
|
|
3487
|
+
subtract: (ids, o) => controller.combineShapes("subtract", ids, o),
|
|
3488
|
+
exclude: (ids, o) => controller.combineShapes("exclude", ids, o),
|
|
3489
|
+
divide: (ids, o) => controller.combineShapes("divide", ids, o),
|
|
3490
|
+
/** Whether the shape operations can run on `ids` (default: the selection). */
|
|
3491
|
+
canCombineShapes: (ids) => canCombineShapeNodes(options.getNodes(), selectedIds(ids)).ok,
|
|
1780
3492
|
/**
|
|
1781
3493
|
* Runs a shortcut action. Scene actions are not handled here (see the scenes store);
|
|
1782
3494
|
* returns `false` for them and for unknown actions.
|
|
@@ -1851,8 +3563,10 @@ function createCanvasArrangeController(options) {
|
|
|
1851
3563
|
zoom: options.getZoom?.() ?? 1,
|
|
1852
3564
|
index: dragIndex
|
|
1853
3565
|
});
|
|
3566
|
+
lastSnapped = /* @__PURE__ */ new Map();
|
|
1854
3567
|
if (result.updates.length > 0) {
|
|
1855
3568
|
options.setNodes(applyNodeUpdates(options.getNodes(), result.updates));
|
|
3569
|
+
for (const u of result.updates) if (u.position) lastSnapped.set(u.id, u.position);
|
|
1856
3570
|
}
|
|
1857
3571
|
setGuides(result.guides);
|
|
1858
3572
|
return result;
|
|
@@ -1863,10 +3577,17 @@ function createCanvasArrangeController(options) {
|
|
|
1863
3577
|
* A batch without a dragging position change ends the drag and clears the guides.
|
|
1864
3578
|
*/
|
|
1865
3579
|
snapPositionChanges(changes) {
|
|
1866
|
-
const dragging = changes.filter(
|
|
3580
|
+
const dragging = changes.filter(
|
|
3581
|
+
(c) => c.type === "position" && typeof c.id === "string" && !!c.position && !!c.dragging
|
|
3582
|
+
);
|
|
1867
3583
|
if (dragging.length === 0) {
|
|
1868
|
-
if (changes.some((c) => c.type === "position" && c.dragging === false))
|
|
1869
|
-
|
|
3584
|
+
if (!changes.some((c) => c.type === "position" && c.dragging === false)) return changes;
|
|
3585
|
+
const snapped = lastSnapped;
|
|
3586
|
+
lastSnapped = /* @__PURE__ */ new Map();
|
|
3587
|
+
controller.endDrag();
|
|
3588
|
+
const keep = (c) => c.type === "position" && c.dragging === false && !!c.position && c.id !== void 0 && snapped.has(c.id);
|
|
3589
|
+
if (!changes.some(keep)) return changes;
|
|
3590
|
+
return changes.map((c) => keep(c) ? { ...c, position: snapped.get(c.id) } : c);
|
|
1870
3591
|
}
|
|
1871
3592
|
if (options.snapping === false) return changes;
|
|
1872
3593
|
const ids = dragging.map((c) => c.id);
|
|
@@ -1882,22 +3603,302 @@ function createCanvasArrangeController(options) {
|
|
|
1882
3603
|
});
|
|
1883
3604
|
setGuides(result.guides);
|
|
1884
3605
|
const { x: dx, y: dy } = result.offset;
|
|
3606
|
+
lastSnapped = /* @__PURE__ */ new Map();
|
|
1885
3607
|
if (dx === 0 && dy === 0) return changes;
|
|
1886
3608
|
const parents = new Map(nodes.map((n) => [n.id, n.parentId]));
|
|
1887
3609
|
const shift = new Set(ids.filter((id) => !proposed.has(parents.get(id) ?? "")));
|
|
1888
|
-
return changes.map(
|
|
1889
|
-
(c
|
|
3610
|
+
return changes.map((c) => {
|
|
3611
|
+
if (!(c.type === "position" && c.position && c.dragging && c.id !== void 0 && shift.has(c.id))) return c;
|
|
3612
|
+
const position = { x: c.position.x + dx, y: c.position.y + dy };
|
|
3613
|
+
lastSnapped.set(c.id, position);
|
|
3614
|
+
return { ...c, position };
|
|
3615
|
+
});
|
|
3616
|
+
},
|
|
3617
|
+
/**
|
|
3618
|
+
* Starts resize snapping for node `id`: records its box (to tell which handle is
|
|
3619
|
+
* dragged) and caches the snap targets. Optional: `resize` starts lazily.
|
|
3620
|
+
*/
|
|
3621
|
+
beginResize(id) {
|
|
3622
|
+
const nodes = options.getNodes();
|
|
3623
|
+
const lookup = createNodeLookup(nodes);
|
|
3624
|
+
const node = lookup.get(id);
|
|
3625
|
+
resizeId = id;
|
|
3626
|
+
resizeStart = node ? getResizingNodeRect(node, lookup, rect()) : null;
|
|
3627
|
+
resizeIndex = createNodeGuideIndex(nodes, [id], rect());
|
|
3628
|
+
lastResized = null;
|
|
3629
|
+
},
|
|
3630
|
+
/**
|
|
3631
|
+
* Snaps node `id` as currently sized in `getNodes()` (call it after the flow wrote the
|
|
3632
|
+
* new size, e.g. SvelteFlow's `NodeResizer` `onResize`, deferred by a microtask) and
|
|
3633
|
+
* writes the snapped box back through `setNodes` (not recorded in history). `handle`
|
|
3634
|
+
* defaults to the one inferred from the edges that moved since `beginResize`. A locked
|
|
3635
|
+
* node is restored to its starting box.
|
|
3636
|
+
*/
|
|
3637
|
+
resize(id, handle) {
|
|
3638
|
+
if (resizeId !== id) controller.beginResize(id);
|
|
3639
|
+
const nodes = options.getNodes();
|
|
3640
|
+
const lookup = createNodeLookup(nodes);
|
|
3641
|
+
const node = lookup.get(id);
|
|
3642
|
+
if (!node || !resizeStart) return null;
|
|
3643
|
+
const proposed = getResizingNodeRect(node, lookup, rect());
|
|
3644
|
+
if (isLocked(node)) {
|
|
3645
|
+
const start = resizeStart;
|
|
3646
|
+
const parent = node.parentId ? lookup.get(node.parentId) : void 0;
|
|
3647
|
+
const origin = parent ? getResizingNodeRect(parent, lookup, rect()) : { x: 0, y: 0 };
|
|
3648
|
+
if (proposed.x !== start.x || proposed.y !== start.y || proposed.width !== start.width || proposed.height !== start.height) {
|
|
3649
|
+
options.setNodes(
|
|
3650
|
+
applyNodeUpdates(nodes, [
|
|
3651
|
+
{ id, position: { x: start.x - origin.x, y: start.y - origin.y }, width: start.width, height: start.height }
|
|
3652
|
+
])
|
|
3653
|
+
);
|
|
3654
|
+
}
|
|
3655
|
+
return null;
|
|
3656
|
+
}
|
|
3657
|
+
if ((options.resizeSnapping ?? options.snapping) === false) return null;
|
|
3658
|
+
const h = handle ?? inferResizeHandle(resizeStart, proposed);
|
|
3659
|
+
if (!h) {
|
|
3660
|
+
setGuides([]);
|
|
3661
|
+
return null;
|
|
3662
|
+
}
|
|
3663
|
+
const result = snapNodeResize(nodes, id, proposed, h, {
|
|
3664
|
+
...rect(),
|
|
3665
|
+
...options.guides,
|
|
3666
|
+
...options.resize,
|
|
3667
|
+
zoom: options.getZoom?.() ?? 1,
|
|
3668
|
+
index: resizeIndex
|
|
3669
|
+
});
|
|
3670
|
+
lastResized = result.updates.length > 0 ? result : null;
|
|
3671
|
+
if (result.updates.length > 0) options.setNodes(applyNodeUpdates(nodes, result.updates));
|
|
3672
|
+
setGuides(result.guides);
|
|
3673
|
+
return result;
|
|
3674
|
+
},
|
|
3675
|
+
/**
|
|
3676
|
+
* Ends resize snapping: clears guides and re-applies the last snapped box if the flow
|
|
3677
|
+
* wrote the raw pointer size afterwards.
|
|
3678
|
+
*/
|
|
3679
|
+
endResize() {
|
|
3680
|
+
const last = lastResized;
|
|
3681
|
+
resizeId = null;
|
|
3682
|
+
resizeStart = null;
|
|
3683
|
+
resizeIndex = null;
|
|
3684
|
+
lastResized = null;
|
|
3685
|
+
setGuides([]);
|
|
3686
|
+
const update = last?.updates[0];
|
|
3687
|
+
if (!update) return;
|
|
3688
|
+
const node = options.getNodes().find((n) => n.id === update.id);
|
|
3689
|
+
if (!node) return;
|
|
3690
|
+
const drift = node.position.x !== update.position.x || node.position.y !== update.position.y || node.width !== update.width || node.height !== update.height;
|
|
3691
|
+
if (drift) options.setNodes(applyNodeUpdates(options.getNodes(), [update]));
|
|
3692
|
+
},
|
|
3693
|
+
/**
|
|
3694
|
+
* React Flow style: snaps the `dimensions` (and matching `position`) changes a
|
|
3695
|
+
* `NodeResizer` emits in an `onNodesChange` batch and updates `guides`. The resize-end
|
|
3696
|
+
* change (`resizing: false`) carries the raw pointer size, so the last snapped size is
|
|
3697
|
+
* kept. Resize changes of locked nodes are dropped. Returns the (possibly new) changes.
|
|
3698
|
+
*/
|
|
3699
|
+
snapResizeChanges(changes) {
|
|
3700
|
+
const dims = changes.find(
|
|
3701
|
+
(c) => c.type === "dimensions" && typeof c.id === "string" && !!c.dimensions && typeof c.resizing === "boolean"
|
|
1890
3702
|
);
|
|
3703
|
+
if (!dims) return changes;
|
|
3704
|
+
const id = dims.id;
|
|
3705
|
+
const nodes = options.getNodes();
|
|
3706
|
+
const lookup = createNodeLookup(nodes);
|
|
3707
|
+
const node = lookup.get(id);
|
|
3708
|
+
const isResizeChange = (c) => c.id === id && (c.type === "dimensions" || c.type === "position");
|
|
3709
|
+
if (node && isLocked(node)) {
|
|
3710
|
+
if (dims.resizing === false) controller.endResize();
|
|
3711
|
+
return changes.filter((c) => !isResizeChange(c));
|
|
3712
|
+
}
|
|
3713
|
+
if (dims.resizing === false) {
|
|
3714
|
+
const last = lastResized?.updates[0];
|
|
3715
|
+
lastResized = null;
|
|
3716
|
+
controller.endResize();
|
|
3717
|
+
if (!last || last.id !== id) return changes;
|
|
3718
|
+
return changes.map(
|
|
3719
|
+
(c) => c === dims ? { ...c, dimensions: { width: last.width, height: last.height } } : c
|
|
3720
|
+
);
|
|
3721
|
+
}
|
|
3722
|
+
if (!node || (options.resizeSnapping ?? options.snapping) === false) return changes;
|
|
3723
|
+
if (resizeId !== id) controller.beginResize(id);
|
|
3724
|
+
if (!resizeStart) return changes;
|
|
3725
|
+
const posChange = changes.find((c) => c.id === id && c.type === "position" && !!c.position);
|
|
3726
|
+
const parent = node.parentId ? lookup.get(node.parentId) : void 0;
|
|
3727
|
+
const origin = parent ? getResizingNodeRect(parent, lookup, rect()) : { x: 0, y: 0 };
|
|
3728
|
+
const position = posChange?.position ?? node.position;
|
|
3729
|
+
const proposed = {
|
|
3730
|
+
x: position.x + origin.x,
|
|
3731
|
+
y: position.y + origin.y,
|
|
3732
|
+
width: dims.dimensions.width,
|
|
3733
|
+
height: dims.dimensions.height
|
|
3734
|
+
};
|
|
3735
|
+
const handle = inferResizeHandle(resizeStart, proposed);
|
|
3736
|
+
if (!handle) {
|
|
3737
|
+
setGuides([]);
|
|
3738
|
+
return changes;
|
|
3739
|
+
}
|
|
3740
|
+
const result = snapNodeResize(nodes, id, proposed, handle, {
|
|
3741
|
+
...rect(),
|
|
3742
|
+
...options.guides,
|
|
3743
|
+
...options.resize,
|
|
3744
|
+
zoom: options.getZoom?.() ?? 1,
|
|
3745
|
+
index: resizeIndex
|
|
3746
|
+
});
|
|
3747
|
+
setGuides(result.guides);
|
|
3748
|
+
const update = result.updates[0];
|
|
3749
|
+
lastResized = update ? result : null;
|
|
3750
|
+
if (!update) return changes;
|
|
3751
|
+
const next = changes.map((c) => {
|
|
3752
|
+
if (c === dims) return { ...c, dimensions: { width: update.width, height: update.height } };
|
|
3753
|
+
if (c === posChange) return { ...c, position: update.position };
|
|
3754
|
+
return c;
|
|
3755
|
+
});
|
|
3756
|
+
if (!posChange && (update.position.x !== node.position.x || update.position.y !== node.position.y)) {
|
|
3757
|
+
next.push({ id, type: "position", position: update.position });
|
|
3758
|
+
}
|
|
3759
|
+
return next;
|
|
1891
3760
|
},
|
|
1892
|
-
/**
|
|
3761
|
+
/**
|
|
3762
|
+
* Clears guides and the cached snap targets. Call it from the flow's drag-stop
|
|
3763
|
+
* handler: flow libraries write the raw pointer position when a drag ends, so the
|
|
3764
|
+
* last snapped positions from `drag` are written back here.
|
|
3765
|
+
*/
|
|
1893
3766
|
endDrag() {
|
|
3767
|
+
const snapped = lastSnapped;
|
|
1894
3768
|
dragIndex = null;
|
|
1895
3769
|
dragKey = "";
|
|
3770
|
+
lastSnapped = /* @__PURE__ */ new Map();
|
|
1896
3771
|
setGuides([]);
|
|
3772
|
+
if (snapped.size === 0) return;
|
|
3773
|
+
const nodes = options.getNodes();
|
|
3774
|
+
const drift = nodes.some((n) => {
|
|
3775
|
+
const p = snapped.get(n.id);
|
|
3776
|
+
return !!p && (n.position.x !== p.x || n.position.y !== p.y);
|
|
3777
|
+
});
|
|
3778
|
+
if (drift) {
|
|
3779
|
+
options.setNodes(applyNodeUpdates(nodes, [...snapped].map(([id, position]) => ({ id, position }))));
|
|
3780
|
+
}
|
|
1897
3781
|
}
|
|
1898
3782
|
};
|
|
1899
3783
|
return controller;
|
|
1900
3784
|
}
|
|
3785
|
+
|
|
3786
|
+
// src/canvas/measure.ts
|
|
3787
|
+
var EPS = 1e-6;
|
|
3788
|
+
function measureLabel(distance) {
|
|
3789
|
+
const v = Math.round(Math.abs(distance) * 100) / 100;
|
|
3790
|
+
return Object.is(v, -0) ? "0" : String(v);
|
|
3791
|
+
}
|
|
3792
|
+
var toBox = (r) => ({ x0: r.x, y0: r.y, x1: r.x + r.width, y1: r.y + r.height });
|
|
3793
|
+
var finiteBox = (b) => [b.x0, b.y0, b.x1, b.y1].every(Number.isFinite);
|
|
3794
|
+
function line(kind, x1, y1, x2, y2) {
|
|
3795
|
+
return { orientation: x1 === x2 ? "vertical" : "horizontal", kind, x1, y1, x2, y2 };
|
|
3796
|
+
}
|
|
3797
|
+
function measured(out, x1, y1, x2, y2) {
|
|
3798
|
+
const d = Math.hypot(x2 - x1, y2 - y1);
|
|
3799
|
+
if (d > EPS && Number.isFinite(d)) out.push({ ...line("measure", x1, y1, x2, y2), label: measureLabel(d) });
|
|
3800
|
+
}
|
|
3801
|
+
function extension(out, x1, y1, x2, y2) {
|
|
3802
|
+
if (Math.hypot(x2 - x1, y2 - y1) > EPS) out.push(line("extension", x1, y1, x2, y2));
|
|
3803
|
+
}
|
|
3804
|
+
function contains(outer, inner) {
|
|
3805
|
+
return outer.x0 <= inner.x0 + EPS && outer.y0 <= inner.y0 + EPS && outer.x1 >= inner.x1 - EPS && outer.y1 >= inner.y1 - EPS;
|
|
3806
|
+
}
|
|
3807
|
+
function overlapMid(a0, a1, b0, b1) {
|
|
3808
|
+
const lo = Math.max(a0, b0);
|
|
3809
|
+
const hi = Math.min(a1, b1);
|
|
3810
|
+
return lo <= hi ? (lo + hi) / 2 : null;
|
|
3811
|
+
}
|
|
3812
|
+
function inside(out, inner, outer) {
|
|
3813
|
+
const cx = (inner.x0 + inner.x1) / 2;
|
|
3814
|
+
const cy = (inner.y0 + inner.y1) / 2;
|
|
3815
|
+
measured(out, outer.x0, cy, inner.x0, cy);
|
|
3816
|
+
measured(out, inner.x1, cy, outer.x1, cy);
|
|
3817
|
+
measured(out, cx, outer.y0, cx, inner.y0);
|
|
3818
|
+
measured(out, cx, inner.y1, cx, outer.y1);
|
|
3819
|
+
}
|
|
3820
|
+
function measureBetween(a, b) {
|
|
3821
|
+
const out = [];
|
|
3822
|
+
const A = toBox(a);
|
|
3823
|
+
const B = toBox(b);
|
|
3824
|
+
if (!finiteBox(A) || !finiteBox(B)) return out;
|
|
3825
|
+
if (contains(B, A)) {
|
|
3826
|
+
inside(out, A, B);
|
|
3827
|
+
return out;
|
|
3828
|
+
}
|
|
3829
|
+
if (contains(A, B)) {
|
|
3830
|
+
inside(out, B, A);
|
|
3831
|
+
return out;
|
|
3832
|
+
}
|
|
3833
|
+
const ox = overlapMid(A.x0, A.x1, B.x0, B.x1);
|
|
3834
|
+
const oy = overlapMid(A.y0, A.y1, B.y0, B.y1);
|
|
3835
|
+
const overlapping = A.x0 < B.x1 && B.x0 < A.x1 && A.y0 < B.y1 && B.y0 < A.y1;
|
|
3836
|
+
if (overlapping && ox !== null && oy !== null) {
|
|
3837
|
+
measured(out, A.x0, oy, B.x0, oy);
|
|
3838
|
+
measured(out, A.x1, oy, B.x1, oy);
|
|
3839
|
+
measured(out, ox, A.y0, ox, B.y0);
|
|
3840
|
+
measured(out, ox, A.y1, ox, B.y1);
|
|
3841
|
+
return out;
|
|
3842
|
+
}
|
|
3843
|
+
const cx = (A.x0 + A.x1) / 2;
|
|
3844
|
+
const cy = (A.y0 + A.y1) / 2;
|
|
3845
|
+
const gapX = B.x0 >= A.x1 ? [A.x1, B.x0] : B.x1 <= A.x0 ? [A.x0, B.x1] : null;
|
|
3846
|
+
if (gapX) {
|
|
3847
|
+
const [from, to] = gapX;
|
|
3848
|
+
const y = oy ?? cy;
|
|
3849
|
+
measured(out, from, y, to, y);
|
|
3850
|
+
if (oy === null) extension(out, to, y, to, y < B.y0 ? B.y0 : B.y1);
|
|
3851
|
+
}
|
|
3852
|
+
const gapY = B.y0 >= A.y1 ? [A.y1, B.y0] : B.y1 <= A.y0 ? [A.y0, B.y1] : null;
|
|
3853
|
+
if (gapY) {
|
|
3854
|
+
const [from, to] = gapY;
|
|
3855
|
+
const x = ox ?? cx;
|
|
3856
|
+
measured(out, x, from, x, to);
|
|
3857
|
+
if (ox === null) extension(out, x, to, x < B.x0 ? B.x0 : B.x1, to);
|
|
3858
|
+
}
|
|
3859
|
+
return out;
|
|
3860
|
+
}
|
|
3861
|
+
function measureToLine(a, orientation, at) {
|
|
3862
|
+
const out = [];
|
|
3863
|
+
const A = toBox(a);
|
|
3864
|
+
if (!finiteBox(A) || !Number.isFinite(at)) return out;
|
|
3865
|
+
const vertical = orientation === "vertical";
|
|
3866
|
+
const cx = (A.x0 + A.x1) / 2;
|
|
3867
|
+
const cy = (A.y0 + A.y1) / 2;
|
|
3868
|
+
const [lo, hi] = vertical ? [A.x0, A.x1] : [A.y0, A.y1];
|
|
3869
|
+
const seg = (from, to) => vertical ? measured(out, from, cy, to, cy) : measured(out, cx, from, cx, to);
|
|
3870
|
+
if (at >= hi) seg(hi, at);
|
|
3871
|
+
else if (at <= lo) seg(at, lo);
|
|
3872
|
+
else {
|
|
3873
|
+
seg(lo, at);
|
|
3874
|
+
seg(at, hi);
|
|
3875
|
+
}
|
|
3876
|
+
return out;
|
|
3877
|
+
}
|
|
3878
|
+
function measureSelection(nodes, selectedIds, hoveredId, options = {}) {
|
|
3879
|
+
const none = (selection2 = null) => ({ guides: [], target: null, selection: selection2 });
|
|
3880
|
+
const lookup = createNodeLookup(nodes);
|
|
3881
|
+
const selected = [...new Set(selectedIds)].map((id) => lookup.get(id)).filter((n) => !!n && n.hidden !== true);
|
|
3882
|
+
if (selected.length === 0) return none();
|
|
3883
|
+
const selection = boundsOf(selected.map((n) => getNodeRect(n, lookup, options)));
|
|
3884
|
+
if (!selection) return none();
|
|
3885
|
+
const selectedSet = new Set(selected.map((n) => n.id));
|
|
3886
|
+
let targetId = hoveredId ?? null;
|
|
3887
|
+
if (targetId === null || selectedSet.has(targetId)) {
|
|
3888
|
+
const parentId = selected[0].parentId;
|
|
3889
|
+
const sameParent = selected.every((n) => n.parentId === parentId);
|
|
3890
|
+
targetId = parentId && sameParent && !selectedSet.has(parentId) ? parentId : null;
|
|
3891
|
+
}
|
|
3892
|
+
const target = targetId !== null ? lookup.get(targetId) : void 0;
|
|
3893
|
+
if (!target || target.hidden === true) return none(selection);
|
|
3894
|
+
const rect = getNodeRect(target, lookup, options);
|
|
3895
|
+
return { guides: measureBetween(selection, rect), target: rect, selection };
|
|
3896
|
+
}
|
|
3897
|
+
function isMeasureModifierHeld(event) {
|
|
3898
|
+
if (!event) return false;
|
|
3899
|
+
if (event.type === "keyup" && event.key === "Alt") return false;
|
|
3900
|
+
return event.altKey === true || event.type === "keydown" && event.key === "Alt";
|
|
3901
|
+
}
|
|
1901
3902
|
export {
|
|
1902
3903
|
CONNECTION_LINE_TYPES,
|
|
1903
3904
|
CanvasScenesStore,
|
|
@@ -1908,17 +3909,25 @@ export {
|
|
|
1908
3909
|
LOCKED_DATA_KEY,
|
|
1909
3910
|
LOCKED_PREVIOUS_FLAGS_KEY,
|
|
1910
3911
|
ManualClock,
|
|
3912
|
+
RESIZE_HANDLES,
|
|
3913
|
+
SHAPE_DATA_KEY,
|
|
3914
|
+
SHAPE_KINDS,
|
|
3915
|
+
SHAPE_NODE_TYPE,
|
|
3916
|
+
SHAPE_OPERATIONS,
|
|
1911
3917
|
StreamingLayoutManager,
|
|
1912
3918
|
alignNodes,
|
|
1913
3919
|
applyNodeUpdates,
|
|
3920
|
+
applyShapeCombination,
|
|
1914
3921
|
blendLayout,
|
|
1915
3922
|
boundsOf,
|
|
1916
3923
|
calculateDagreReflowDisplacement,
|
|
1917
3924
|
calculateDirectionalDisplacement,
|
|
3925
|
+
canCombineShapeNodes,
|
|
1918
3926
|
canvasHeight,
|
|
1919
3927
|
centerNodes,
|
|
1920
3928
|
centre,
|
|
1921
3929
|
collectDescendantIds,
|
|
3930
|
+
combineShapeNodes,
|
|
1922
3931
|
computeNodesBoundingBox,
|
|
1923
3932
|
computeSnapGuides,
|
|
1924
3933
|
connectedOnly,
|
|
@@ -1929,24 +3938,43 @@ export {
|
|
|
1929
3938
|
createGuideIndex,
|
|
1930
3939
|
createNodeGuideIndex,
|
|
1931
3940
|
createNodeLookup,
|
|
3941
|
+
cubicPointAt,
|
|
3942
|
+
cubicsToPathData,
|
|
1932
3943
|
defaultEasing,
|
|
1933
3944
|
distributeNodes,
|
|
3945
|
+
dividePolygons,
|
|
1934
3946
|
ensureLayout,
|
|
1935
3947
|
estimateCanvasHeight,
|
|
1936
3948
|
estimateNodeSize,
|
|
1937
3949
|
findConnectorSnap,
|
|
3950
|
+
fitCubicRun,
|
|
3951
|
+
fitCubics,
|
|
3952
|
+
fitFreehandPath,
|
|
3953
|
+
fitPathData,
|
|
1938
3954
|
fitViewportToNodes,
|
|
1939
3955
|
fitViewportToRect,
|
|
3956
|
+
flattenSvgPath,
|
|
1940
3957
|
flowSide,
|
|
3958
|
+
formatNumber2 as formatNumber,
|
|
1941
3959
|
getAbsolutePosition,
|
|
1942
3960
|
getCenteredNodePosition,
|
|
1943
3961
|
getLayoutedNodes,
|
|
1944
3962
|
getNodeRect,
|
|
3963
|
+
getNodeShape,
|
|
1945
3964
|
getNodeSize,
|
|
3965
|
+
getResizingNodeRect,
|
|
3966
|
+
getShapeRenderData,
|
|
3967
|
+
getShapeSvg,
|
|
1946
3968
|
handlePoint,
|
|
3969
|
+
inferResizeHandle,
|
|
3970
|
+
intersectPolygons,
|
|
1947
3971
|
invertNodeUpdates,
|
|
1948
3972
|
isEventFromTextInput,
|
|
1949
3973
|
isLocked,
|
|
3974
|
+
isMeasureModifierHeld,
|
|
3975
|
+
isShapeDescription,
|
|
3976
|
+
isShapeNode,
|
|
3977
|
+
isStraightCubic,
|
|
1950
3978
|
isValidCanvasEdge,
|
|
1951
3979
|
isValidCanvasNode,
|
|
1952
3980
|
isValidCanvasScene,
|
|
@@ -1957,26 +3985,50 @@ export {
|
|
|
1957
3985
|
lockNodes,
|
|
1958
3986
|
matchArrangeShortcut,
|
|
1959
3987
|
matchSize,
|
|
3988
|
+
measureBetween,
|
|
3989
|
+
measureLabel,
|
|
3990
|
+
measureSelection,
|
|
3991
|
+
measureToLine,
|
|
3992
|
+
multiPolygonArea,
|
|
3993
|
+
multiPolygonBounds,
|
|
1960
3994
|
nearestHandlePositions,
|
|
1961
3995
|
oppositePosition,
|
|
1962
3996
|
overlaps,
|
|
1963
3997
|
pageHidden,
|
|
1964
3998
|
parseCanvasClipboardData,
|
|
1965
3999
|
parseCanvasScenes,
|
|
4000
|
+
parseSvgPath,
|
|
1966
4001
|
placeBeside,
|
|
4002
|
+
pointInMultiPolygon,
|
|
4003
|
+
pointInRings,
|
|
4004
|
+
polygonArea,
|
|
4005
|
+
polygonBoolean,
|
|
4006
|
+
polygonToSvgPath,
|
|
1967
4007
|
positionToSide,
|
|
1968
4008
|
readStyleSize,
|
|
1969
4009
|
realClock,
|
|
1970
4010
|
remapPastedNodes,
|
|
1971
4011
|
reorderNodes,
|
|
1972
4012
|
repositionNodesTo,
|
|
4013
|
+
resizeHandleAxes,
|
|
4014
|
+
revertShapeCombination,
|
|
4015
|
+
ringSignedArea,
|
|
1973
4016
|
runMultiNodeTransition,
|
|
1974
4017
|
serializeCanvasNodes,
|
|
4018
|
+
shapeNodeToPolygon,
|
|
4019
|
+
shapeToPolygon,
|
|
1975
4020
|
sideToPosition,
|
|
4021
|
+
simplifyRing,
|
|
1976
4022
|
snapNodeDrag,
|
|
4023
|
+
snapNodeResize,
|
|
4024
|
+
snapResize,
|
|
4025
|
+
snapToGrid,
|
|
4026
|
+
subtractPolygons,
|
|
1977
4027
|
toggleLockNodes,
|
|
4028
|
+
unionPolygons,
|
|
1978
4029
|
unlockNodes,
|
|
1979
4030
|
withStyleOpacity,
|
|
1980
|
-
withStyleSize
|
|
4031
|
+
withStyleSize,
|
|
4032
|
+
xorPolygons
|
|
1981
4033
|
};
|
|
1982
4034
|
//# sourceMappingURL=index.js.map
|