@pixerate/editor 0.34.0 → 0.35.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/THIRD_PARTY_NOTICES.md +7 -0
- package/dist/canvas/index.cjs +178 -623
- package/dist/canvas/index.cjs.map +1 -1
- package/dist/canvas/index.d.cts +10 -1
- package/dist/canvas/index.d.ts +10 -1
- package/dist/canvas/index.js +35 -480
- package/dist/canvas/index.js.map +1 -1
- package/dist/{chunk-UG5DVXJK.cjs → chunk-BGNPT5PE.cjs} +446 -6
- package/dist/chunk-BGNPT5PE.cjs.map +1 -0
- package/dist/{chunk-RK6HUKDI.js → chunk-EGPIVUEF.js} +451 -11
- package/dist/chunk-EGPIVUEF.js.map +1 -0
- package/dist/vector/index.cjs +1801 -116
- package/dist/vector/index.cjs.map +1 -1
- package/dist/vector/index.d.cts +373 -7
- package/dist/vector/index.d.ts +373 -7
- package/dist/vector/index.js +1721 -36
- package/dist/vector/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-RK6HUKDI.js.map +0 -1
- package/dist/chunk-UG5DVXJK.cjs.map +0 -1
package/dist/vector/index.js
CHANGED
|
@@ -4,23 +4,33 @@ import {
|
|
|
4
4
|
import {
|
|
5
5
|
bezierPathBounds,
|
|
6
6
|
bezierPathToSvg,
|
|
7
|
+
closeContour,
|
|
8
|
+
contourSignedArea,
|
|
9
|
+
contoursToPath,
|
|
7
10
|
distanceToBezierPath,
|
|
11
|
+
fitCubicWithTangents,
|
|
8
12
|
fitCubics,
|
|
9
13
|
formatNumber,
|
|
10
14
|
gradientToSvgDef,
|
|
15
|
+
normalizeBezierPath,
|
|
16
|
+
outlineStroke,
|
|
11
17
|
pathBoolean,
|
|
18
|
+
pathToContours,
|
|
12
19
|
pointInBezierPath,
|
|
20
|
+
segmentBounds,
|
|
13
21
|
segmentDerivative,
|
|
14
22
|
segmentIntersections,
|
|
15
23
|
segmentNearest,
|
|
16
24
|
segmentPoint,
|
|
17
25
|
splitSegment,
|
|
26
|
+
subSegment,
|
|
18
27
|
toBezierPath,
|
|
19
|
-
transformBezierPath
|
|
20
|
-
|
|
28
|
+
transformBezierPath,
|
|
29
|
+
windingNumber
|
|
30
|
+
} from "../chunk-EGPIVUEF.js";
|
|
21
31
|
|
|
22
32
|
// src/vector/types.ts
|
|
23
|
-
var VECTOR_TOOLS = ["select", "direct", "pen", "pencil", "rect", "ellipse", "polygon", "star", "line"];
|
|
33
|
+
var VECTOR_TOOLS = ["select", "direct", "pen", "pencil", "rect", "ellipse", "polygon", "star", "line", "scissors", "knife", "eraser"];
|
|
24
34
|
var VECTOR_TRANSFORM_HANDLES = ["nw", "n", "ne", "e", "se", "s", "sw", "w", "rotate"];
|
|
25
35
|
|
|
26
36
|
// src/vector/geometry.ts
|
|
@@ -1029,13 +1039,13 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1029
1039
|
return budget;
|
|
1030
1040
|
}
|
|
1031
1041
|
pushRect(r, kind, centre) {
|
|
1032
|
-
const
|
|
1042
|
+
const corners2 = [
|
|
1033
1043
|
{ x: r.x, y: r.y },
|
|
1034
1044
|
{ x: r.x + r.width, y: r.y },
|
|
1035
1045
|
{ x: r.x + r.width, y: r.y + r.height },
|
|
1036
1046
|
{ x: r.x, y: r.y + r.height }
|
|
1037
1047
|
];
|
|
1038
|
-
for (const p of
|
|
1048
|
+
for (const p of corners2) {
|
|
1039
1049
|
this.xs.push({ v: p.x, from: p, kind });
|
|
1040
1050
|
this.ys.push({ v: p.y, from: p, kind });
|
|
1041
1051
|
}
|
|
@@ -1136,14 +1146,14 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1136
1146
|
}
|
|
1137
1147
|
const point = this.nearestPoint(p, tol);
|
|
1138
1148
|
if (point) return at(point.p, point.kind);
|
|
1139
|
-
const
|
|
1140
|
-
const crossing = this.crossing(
|
|
1149
|
+
const near2 = this.segmentsNear(p, tol);
|
|
1150
|
+
const crossing = this.crossing(near2, p, tol);
|
|
1141
1151
|
if (crossing) return at(crossing, "intersect");
|
|
1142
1152
|
if (leave && leave.curves !== false) {
|
|
1143
|
-
const foot = this.curvePoint(
|
|
1153
|
+
const foot = this.curvePoint(near2, leave.at, p, tol);
|
|
1144
1154
|
if (foot) return at(foot.point, foot.kind, [line("construction", leave.at, foot.point)]);
|
|
1145
1155
|
}
|
|
1146
|
-
const on = this.onSegment(
|
|
1156
|
+
const on = this.onSegment(near2);
|
|
1147
1157
|
if (on) return at(on, "path");
|
|
1148
1158
|
if (leave?.angles?.length) {
|
|
1149
1159
|
const c = construction(leave, p, tol);
|
|
@@ -1226,12 +1236,12 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1226
1236
|
}
|
|
1227
1237
|
return out.sort((a, b) => a.distance - b.distance);
|
|
1228
1238
|
}
|
|
1229
|
-
onSegment(
|
|
1230
|
-
return
|
|
1239
|
+
onSegment(near2) {
|
|
1240
|
+
return near2.length ? toVec(segmentPoint(near2[0].seg, near2[0].t)) : null;
|
|
1231
1241
|
}
|
|
1232
1242
|
/** Where two of the target segments near `p` cross, the crossing nearest `p` within `tol`. */
|
|
1233
|
-
crossing(
|
|
1234
|
-
const list =
|
|
1243
|
+
crossing(near2, p, tol) {
|
|
1244
|
+
const list = near2.slice(0, CROSSING_PAIRS);
|
|
1235
1245
|
let best = null;
|
|
1236
1246
|
let bestD = Infinity;
|
|
1237
1247
|
for (let i = 0; i < list.length; i++) {
|
|
@@ -1253,9 +1263,9 @@ var VectorSnapTargets = class _VectorSnapTargets {
|
|
|
1253
1263
|
* (within `tol` of it): roots of `(B(t) − base) · B′(t)` and `(B(t) − base) × B′(t)`
|
|
1254
1264
|
* bracketed by sign changes of samples and narrowed by bisection.
|
|
1255
1265
|
*/
|
|
1256
|
-
curvePoint(
|
|
1266
|
+
curvePoint(near2, base, p, tol) {
|
|
1257
1267
|
let best = null;
|
|
1258
|
-
for (const { seg } of
|
|
1268
|
+
for (const { seg } of near2) {
|
|
1259
1269
|
for (const kind of ["perpendicular", "tangent"]) {
|
|
1260
1270
|
if (kind === "tangent" && seg.length === 2) continue;
|
|
1261
1271
|
for (const q of solveOnSegment(seg, base, kind === "perpendicular" ? dotCondition : crossCondition)) {
|
|
@@ -1603,8 +1613,968 @@ function solveOnSegment(seg, base, cond) {
|
|
|
1603
1613
|
return out;
|
|
1604
1614
|
}
|
|
1605
1615
|
|
|
1616
|
+
// src/vector/pathEdit.ts
|
|
1617
|
+
var EPS2 = 1e-9;
|
|
1618
|
+
var unit = (x, y) => {
|
|
1619
|
+
const l = Math.hypot(x, y);
|
|
1620
|
+
return l > 1e-12 && Number.isFinite(l) ? [x / l, y / l] : null;
|
|
1621
|
+
};
|
|
1622
|
+
function cubicOf(sp, i) {
|
|
1623
|
+
const s = subpathSegment(sp, i);
|
|
1624
|
+
if (s.length === 4) return s;
|
|
1625
|
+
const [a, b] = s;
|
|
1626
|
+
return [a, [a[0] + (b[0] - a[0]) / 3, a[1] + (b[1] - a[1]) / 3], [a[0] + 2 * (b[0] - a[0]) / 3, a[1] + 2 * (b[1] - a[1]) / 3], b];
|
|
1627
|
+
}
|
|
1628
|
+
var isLine = (sp, i) => subpathSegment(sp, i).length === 2;
|
|
1629
|
+
function startTangent(c) {
|
|
1630
|
+
for (let k = 1; k < 4; k++) {
|
|
1631
|
+
const u = unit(c[k][0] - c[0][0], c[k][1] - c[0][1]);
|
|
1632
|
+
if (u) return u;
|
|
1633
|
+
}
|
|
1634
|
+
return [1, 0];
|
|
1635
|
+
}
|
|
1636
|
+
function endTangent(c) {
|
|
1637
|
+
for (let k = 2; k >= 0; k--) {
|
|
1638
|
+
const u = unit(c[3][0] - c[k][0], c[3][1] - c[k][1]);
|
|
1639
|
+
if (u) return u;
|
|
1640
|
+
}
|
|
1641
|
+
return [1, 0];
|
|
1642
|
+
}
|
|
1643
|
+
function sample(c, n, out, withStart) {
|
|
1644
|
+
for (let i = withStart ? 0 : 1; i <= n; i++) out.push(segmentPoint(c, i / n));
|
|
1645
|
+
}
|
|
1646
|
+
var onAnchor = (a, h) => !h || Math.abs(h.x - a.x) < EPS2 && Math.abs(h.y - a.y) < EPS2;
|
|
1647
|
+
function tidy(a, keepType = true) {
|
|
1648
|
+
const next = cloneAnchor(a);
|
|
1649
|
+
if (onAnchor(next, next.in)) delete next.in;
|
|
1650
|
+
if (onAnchor(next, next.out)) delete next.out;
|
|
1651
|
+
const shown = inferAnchorType(next);
|
|
1652
|
+
const type = keepType && a.type ? a.type === "symmetric" && shown !== "symmetric" ? shown === "corner" ? "corner" : "smooth" : a.type === "smooth" && shown === "corner" ? "corner" : a.type : shown;
|
|
1653
|
+
if (type === "corner") delete next.type;
|
|
1654
|
+
else next.type = type;
|
|
1655
|
+
return next;
|
|
1656
|
+
}
|
|
1657
|
+
function neighbours(sp, i) {
|
|
1658
|
+
const n = sp.anchors.length;
|
|
1659
|
+
const wrap = sp.closed && n > 1;
|
|
1660
|
+
return [i > 0 ? i - 1 : wrap ? n - 1 : -1, i < n - 1 ? i + 1 : wrap ? 0 : -1];
|
|
1661
|
+
}
|
|
1662
|
+
function goldenMin(f, a, b) {
|
|
1663
|
+
const R = 0.618033988749895;
|
|
1664
|
+
let x1 = b - R * (b - a);
|
|
1665
|
+
let x2 = a + R * (b - a);
|
|
1666
|
+
let f1 = f(x1);
|
|
1667
|
+
let f2 = f(x2);
|
|
1668
|
+
for (let i = 0; i < 40; i++) {
|
|
1669
|
+
if (f1.error < f2.error || Number.isNaN(f2.error)) {
|
|
1670
|
+
b = x2;
|
|
1671
|
+
x2 = x1;
|
|
1672
|
+
f2 = f1;
|
|
1673
|
+
x1 = b - R * (b - a);
|
|
1674
|
+
f1 = f(x1);
|
|
1675
|
+
} else {
|
|
1676
|
+
a = x1;
|
|
1677
|
+
x1 = x2;
|
|
1678
|
+
f1 = f2;
|
|
1679
|
+
x2 = a + R * (b - a);
|
|
1680
|
+
f2 = f(x2);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
return f1.error <= f2.error ? f1 : f2;
|
|
1684
|
+
}
|
|
1685
|
+
function fitAcross(pts, N, t0, t1) {
|
|
1686
|
+
const fit = (k) => {
|
|
1687
|
+
const u = [];
|
|
1688
|
+
for (let i = 0; i <= N; i++) u.push(k * i / N);
|
|
1689
|
+
for (let i = 1; i <= N; i++) u.push(k + (1 - k) * i / N);
|
|
1690
|
+
return fitCubicWithTangents(pts, u, t0, t1);
|
|
1691
|
+
};
|
|
1692
|
+
return goldenMin(fit, 0, 1);
|
|
1693
|
+
}
|
|
1694
|
+
function removeAnchorKeepingShape(sp, index) {
|
|
1695
|
+
const anchors = sp.anchors.map(cloneAnchor);
|
|
1696
|
+
const n = anchors.length;
|
|
1697
|
+
if (index < 0 || index >= n) return { anchors, closed: sp.closed };
|
|
1698
|
+
if ((sp.closed || index > 0 && index < n - 1) && n >= 3) {
|
|
1699
|
+
const pi = (index + n - 1) % n;
|
|
1700
|
+
const ni = (index + 1) % n;
|
|
1701
|
+
if (!(isLine(sp, pi) && isLine(sp, index))) {
|
|
1702
|
+
const left = cubicOf(sp, pi);
|
|
1703
|
+
const right = cubicOf(sp, index);
|
|
1704
|
+
const N = 16;
|
|
1705
|
+
const pts = [];
|
|
1706
|
+
sample(left, N, pts, true);
|
|
1707
|
+
sample(right, N, pts, false);
|
|
1708
|
+
const t0 = startTangent(left);
|
|
1709
|
+
const t1 = endTangent(right);
|
|
1710
|
+
const { curve } = fitAcross(pts, N, t0, t1);
|
|
1711
|
+
if ([curve.p1, curve.p2].every((p) => Number.isFinite(p[0]) && Number.isFinite(p[1]))) {
|
|
1712
|
+
anchors[pi] = tidy({ ...anchors[pi], out: { x: curve.p1[0], y: curve.p1[1] } });
|
|
1713
|
+
anchors[ni] = tidy({ ...anchors[ni], in: { x: curve.p2[0], y: curve.p2[1] } });
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
anchors.splice(index, 1);
|
|
1718
|
+
let closed = sp.closed;
|
|
1719
|
+
if (!closed && anchors.length) {
|
|
1720
|
+
anchors[0] = tidy({ ...anchors[0], in: null });
|
|
1721
|
+
const last = anchors.length - 1;
|
|
1722
|
+
anchors[last] = tidy({ ...anchors[last], out: null });
|
|
1723
|
+
}
|
|
1724
|
+
if (anchors.length < 3) closed = closed && anchors.length === 2 && anchors.some((a) => a.in || a.out);
|
|
1725
|
+
return { anchors, closed };
|
|
1726
|
+
}
|
|
1727
|
+
function removeAnchorsKeepingShape(sp, indices) {
|
|
1728
|
+
const order = [...new Set(indices)].filter((i) => i >= 0 && i < sp.anchors.length).sort((a, b) => b - a);
|
|
1729
|
+
return order.reduce((acc, i) => removeAnchorKeepingShape(acc, i), { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed });
|
|
1730
|
+
}
|
|
1731
|
+
function bendSegment(sp, segment, t, p) {
|
|
1732
|
+
const anchors = sp.anchors.map(cloneAnchor);
|
|
1733
|
+
const n = anchors.length;
|
|
1734
|
+
if (segment < 0 || segment >= subpathSegmentCount(sp) || !Number.isFinite(t) || !Number.isFinite(p.x) || !Number.isFinite(p.y)) return { anchors, closed: sp.closed };
|
|
1735
|
+
const tc = Math.min(0.95, Math.max(0.05, t));
|
|
1736
|
+
const ia = segment;
|
|
1737
|
+
const ib = (segment + 1) % n;
|
|
1738
|
+
const a = anchors[ia];
|
|
1739
|
+
const b = anchors[ib];
|
|
1740
|
+
const k = 3 * tc * (1 - tc);
|
|
1741
|
+
const ox = (p.x - (a.x + (b.x - a.x) * tc)) / k;
|
|
1742
|
+
const oy = (p.y - (a.y + (b.y - a.y) * tc)) / k;
|
|
1743
|
+
const c1 = { x: a.x + (b.x - a.x) / 3 + ox, y: a.y + (b.y - a.y) / 3 + oy };
|
|
1744
|
+
const c2 = { x: b.x + (a.x - b.x) / 3 + ox, y: b.y + (a.y - b.y) / 3 + oy };
|
|
1745
|
+
anchors[ia] = moveAnchorHandle(a, "out", c1);
|
|
1746
|
+
anchors[ib] = moveAnchorHandle(b, "in", c2);
|
|
1747
|
+
return { anchors, closed: sp.closed };
|
|
1748
|
+
}
|
|
1749
|
+
function subpathPointAt(sp, segment, t) {
|
|
1750
|
+
const [x, y] = segmentPoint(subpathSegment(sp, segment), t);
|
|
1751
|
+
return { x, y };
|
|
1752
|
+
}
|
|
1753
|
+
function curvatureMoveAnchor(sp, index, q) {
|
|
1754
|
+
const anchors = sp.anchors.map(cloneAnchor);
|
|
1755
|
+
const a = sp.anchors[index];
|
|
1756
|
+
if (!a) return { anchors, closed: sp.closed };
|
|
1757
|
+
const own = [
|
|
1758
|
+
a.in && !onAnchor(a, a.in) ? { x: a.in.x - a.x, y: a.in.y - a.y } : null,
|
|
1759
|
+
a.out && !onAnchor(a, a.out) ? { x: a.out.x - a.x, y: a.out.y - a.y } : null
|
|
1760
|
+
];
|
|
1761
|
+
const ends = [null, null];
|
|
1762
|
+
neighbours(sp, index).forEach((j, side) => {
|
|
1763
|
+
if (j < 0 || j === index) return;
|
|
1764
|
+
const b = anchors[j];
|
|
1765
|
+
const old = Math.hypot(a.x - b.x, a.y - b.y);
|
|
1766
|
+
const s = old > 1e-9 ? Math.hypot(q.x - b.x, q.y - b.y) / old : 1;
|
|
1767
|
+
const key = side === 0 ? "out" : "in";
|
|
1768
|
+
const h = b[key];
|
|
1769
|
+
if (h) b[key] = { x: b.x + (h.x - b.x) * s, y: b.y + (h.y - b.y) * s };
|
|
1770
|
+
const o = own[side];
|
|
1771
|
+
if (o) own[side] = { x: o.x * s, y: o.y * s };
|
|
1772
|
+
ends[side] = { x: b.x, y: b.y };
|
|
1773
|
+
});
|
|
1774
|
+
const moved = { x: q.x, y: q.y };
|
|
1775
|
+
if (own[0]) moved.in = { x: q.x + own[0].x, y: q.y + own[0].y };
|
|
1776
|
+
if (own[1]) moved.out = { x: q.x + own[1].x, y: q.y + own[1].y };
|
|
1777
|
+
if (a.type) moved.type = a.type;
|
|
1778
|
+
const [prev, next] = ends;
|
|
1779
|
+
if (a.type && a.type !== "corner" && prev && next) {
|
|
1780
|
+
const dx = next.x - prev.x;
|
|
1781
|
+
const dy = next.y - prev.y;
|
|
1782
|
+
const l = Math.hypot(dx, dy);
|
|
1783
|
+
if (l > 1e-9) {
|
|
1784
|
+
const len = (h, to) => h ? Math.hypot(h.x, h.y) : Math.hypot(q.x - to.x, q.y - to.y) / 3;
|
|
1785
|
+
let li = len(own[0], prev);
|
|
1786
|
+
let lo = len(own[1], next);
|
|
1787
|
+
if (a.type === "symmetric") li = lo = (li + lo) / 2;
|
|
1788
|
+
moved.in = { x: q.x - dx * li / l, y: q.y - dy * li / l };
|
|
1789
|
+
moved.out = { x: q.x + dx * lo / l, y: q.y + dy * lo / l };
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
anchors[index] = moved;
|
|
1793
|
+
return { anchors, closed: sp.closed };
|
|
1794
|
+
}
|
|
1795
|
+
function cutSubpathAt(sp, at) {
|
|
1796
|
+
const n = sp.anchors.length;
|
|
1797
|
+
const cuts = [...new Set(at)].filter((i) => Number.isInteger(i) && i >= 0 && i < n && (sp.closed || i > 0 && i < n - 1)).sort((a, b) => a - b);
|
|
1798
|
+
if (!cuts.length || n < 2) return [{ anchors: sp.anchors.map(cloneAnchor), closed: sp.closed }];
|
|
1799
|
+
const first = cuts[0];
|
|
1800
|
+
const run = sp.closed ? Array.from({ length: n + 1 }, (_, i) => sp.anchors[(first + i) % n]) : sp.anchors;
|
|
1801
|
+
const cutSet = new Set(sp.closed ? [...cuts.map((c) => (c - first + n) % n), n] : cuts);
|
|
1802
|
+
const pieces = [];
|
|
1803
|
+
let cur = [];
|
|
1804
|
+
run.forEach((a, i) => {
|
|
1805
|
+
if (!cutSet.has(i)) {
|
|
1806
|
+
cur.push(cloneAnchor(a));
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
if (cur.length) {
|
|
1810
|
+
cur.push(tidy({ ...a, out: null }, false));
|
|
1811
|
+
pieces.push({ anchors: cur, closed: false });
|
|
1812
|
+
cur = [];
|
|
1813
|
+
}
|
|
1814
|
+
if (i < n) cur.push(tidy({ ...a, in: null }, false));
|
|
1815
|
+
});
|
|
1816
|
+
if (cur.length > 1) pieces.push({ anchors: cur, closed: false });
|
|
1817
|
+
return pieces;
|
|
1818
|
+
}
|
|
1819
|
+
function splitSubpathAt(sp, segment, t, endTolerance = 1e-6) {
|
|
1820
|
+
const n = sp.anchors.length;
|
|
1821
|
+
if (segment < 0 || segment >= subpathSegmentCount(sp)) return [{ anchors: sp.anchors.map(cloneAnchor), closed: sp.closed }];
|
|
1822
|
+
if (t <= endTolerance) return cutSubpathAt(sp, [segment]);
|
|
1823
|
+
if (t >= 1 - endTolerance) return cutSubpathAt(sp, [(segment + 1) % n]);
|
|
1824
|
+
const { subpath, index } = insertAnchor(sp, segment, t);
|
|
1825
|
+
return cutSubpathAt(subpath, [index]);
|
|
1826
|
+
}
|
|
1827
|
+
var endOf = (sp, last) => sp.anchors[last ? sp.anchors.length - 1 : 0];
|
|
1828
|
+
var dist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
|
|
1829
|
+
function reverse(sp) {
|
|
1830
|
+
return {
|
|
1831
|
+
closed: sp.closed,
|
|
1832
|
+
anchors: sp.anchors.slice().reverse().map((a) => {
|
|
1833
|
+
const next = { x: a.x, y: a.y };
|
|
1834
|
+
if (a.out) next.in = { ...a.out };
|
|
1835
|
+
if (a.in) next.out = { ...a.in };
|
|
1836
|
+
if (a.type) next.type = a.type;
|
|
1837
|
+
return next;
|
|
1838
|
+
})
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
function joinSubpaths(subpaths, tolerance) {
|
|
1842
|
+
const closed = [];
|
|
1843
|
+
let open = [];
|
|
1844
|
+
for (const sp of subpaths) {
|
|
1845
|
+
const copy = { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed };
|
|
1846
|
+
if (sp.closed || sp.anchors.length < 2) {
|
|
1847
|
+
if (sp.anchors.length) closed.push(copy);
|
|
1848
|
+
} else open.push(copy);
|
|
1849
|
+
}
|
|
1850
|
+
if (open.length === 1) {
|
|
1851
|
+
const sp = open[0];
|
|
1852
|
+
const first = sp.anchors[0];
|
|
1853
|
+
const last = sp.anchors[sp.anchors.length - 1];
|
|
1854
|
+
if (sp.anchors.length > 2 && dist(first, last) <= tolerance) {
|
|
1855
|
+
sp.anchors.pop();
|
|
1856
|
+
sp.anchors[0] = tidy({ ...first, in: last.in ?? null }, false);
|
|
1857
|
+
} else {
|
|
1858
|
+
sp.anchors[0] = tidy({ ...first, in: null }, false);
|
|
1859
|
+
sp.anchors[sp.anchors.length - 1] = tidy({ ...sp.anchors[sp.anchors.length - 1], out: null }, false);
|
|
1860
|
+
}
|
|
1861
|
+
sp.closed = true;
|
|
1862
|
+
return [...closed, sp];
|
|
1863
|
+
}
|
|
1864
|
+
while (open.length > 1) {
|
|
1865
|
+
let best = { d: Infinity, i: 0, j: 1, ei: true, ej: false };
|
|
1866
|
+
for (let i = 0; i < open.length; i++) {
|
|
1867
|
+
for (let j = i + 1; j < open.length; j++) {
|
|
1868
|
+
for (const ei of [false, true]) {
|
|
1869
|
+
for (const ej of [false, true]) {
|
|
1870
|
+
const d = dist(endOf(open[i], ei), endOf(open[j], ej));
|
|
1871
|
+
if (d < best.d) best = { d, i, j, ei, ej };
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
let a = open[best.i];
|
|
1877
|
+
let b = open[best.j];
|
|
1878
|
+
open = open.filter((_, k) => k !== best.j);
|
|
1879
|
+
if (!best.ei) a = reverse(a);
|
|
1880
|
+
if (best.ej) b = reverse(b);
|
|
1881
|
+
const anchors = a.anchors;
|
|
1882
|
+
if (best.d <= tolerance) {
|
|
1883
|
+
const [head, ...rest] = b.anchors;
|
|
1884
|
+
const l = anchors[anchors.length - 1];
|
|
1885
|
+
anchors[anchors.length - 1] = tidy({ ...l, out: head.out ?? null }, false);
|
|
1886
|
+
anchors.push(...rest);
|
|
1887
|
+
} else {
|
|
1888
|
+
const l = anchors[anchors.length - 1];
|
|
1889
|
+
anchors[anchors.length - 1] = tidy({ ...l, out: null }, false);
|
|
1890
|
+
anchors.push(tidy({ ...b.anchors[0], in: null }, false), ...b.anchors.slice(1));
|
|
1891
|
+
}
|
|
1892
|
+
open[best.i > best.j ? best.i - 1 : best.i] = { anchors, closed: false };
|
|
1893
|
+
}
|
|
1894
|
+
return [...closed, ...open];
|
|
1895
|
+
}
|
|
1896
|
+
function smoothSubpath(sp, amount = 1, only) {
|
|
1897
|
+
const t = Math.min(1, Math.max(0, amount));
|
|
1898
|
+
const orig = sp.anchors;
|
|
1899
|
+
const n = orig.length;
|
|
1900
|
+
const anchors = orig.map(cloneAnchor);
|
|
1901
|
+
if (n < 3 || t === 0) return { anchors, closed: sp.closed };
|
|
1902
|
+
for (let i = 0; i < n; i++) {
|
|
1903
|
+
if (!sp.closed && (i === 0 || i === n - 1)) continue;
|
|
1904
|
+
if (only && !only.has(i)) continue;
|
|
1905
|
+
const prev = orig[(i + n - 1) % n];
|
|
1906
|
+
const next = orig[(i + 1) % n];
|
|
1907
|
+
const p = orig[i];
|
|
1908
|
+
const dir = unit(next.x - prev.x, next.y - prev.y);
|
|
1909
|
+
if (!dir) continue;
|
|
1910
|
+
const li = dist(p, prev) / 3;
|
|
1911
|
+
const lo = dist(p, next) / 3;
|
|
1912
|
+
const hin = p.in ?? p;
|
|
1913
|
+
const hout = p.out ?? p;
|
|
1914
|
+
const target = (h, x, y) => ({ x: h.x + (x - h.x) * t, y: h.y + (y - h.y) * t });
|
|
1915
|
+
anchors[i] = tidy(
|
|
1916
|
+
{
|
|
1917
|
+
x: p.x,
|
|
1918
|
+
y: p.y,
|
|
1919
|
+
in: target(hin, p.x - dir[0] * li, p.y - dir[1] * li),
|
|
1920
|
+
out: target(hout, p.x + dir[0] * lo, p.y + dir[1] * lo)
|
|
1921
|
+
},
|
|
1922
|
+
false
|
|
1923
|
+
);
|
|
1924
|
+
}
|
|
1925
|
+
return { anchors, closed: sp.closed };
|
|
1926
|
+
}
|
|
1927
|
+
function segsOf(sp) {
|
|
1928
|
+
const out = [];
|
|
1929
|
+
for (let i = 0; i < subpathSegmentCount(sp); i++) {
|
|
1930
|
+
const s = subpathSegment(sp, i);
|
|
1931
|
+
out.push(s.length === 2 ? { c: s, line: true } : { c: s, line: false });
|
|
1932
|
+
}
|
|
1933
|
+
return out;
|
|
1934
|
+
}
|
|
1935
|
+
function segsToSubpath(segs, closed, types) {
|
|
1936
|
+
if (!segs.length) return null;
|
|
1937
|
+
const anchors = [];
|
|
1938
|
+
const at = (p) => ({ x: p[0], y: p[1] });
|
|
1939
|
+
anchors.push(at(segs[0].c[0]));
|
|
1940
|
+
for (const s of segs) {
|
|
1941
|
+
const end = s.c[s.c.length - 1];
|
|
1942
|
+
if (!s.line) anchors[anchors.length - 1].out = { x: s.c[1][0], y: s.c[1][1] };
|
|
1943
|
+
const next = at(end);
|
|
1944
|
+
if (!s.line) next.in = { x: s.c[2][0], y: s.c[2][1] };
|
|
1945
|
+
anchors.push(next);
|
|
1946
|
+
}
|
|
1947
|
+
if (closed) {
|
|
1948
|
+
const last = anchors.pop();
|
|
1949
|
+
if (last.in) anchors[0].in = last.in;
|
|
1950
|
+
}
|
|
1951
|
+
const typed = anchors.map((a) => {
|
|
1952
|
+
const type = types.get(`${a.x},${a.y}`);
|
|
1953
|
+
return tidy(type ? { ...a, type } : a, !!type);
|
|
1954
|
+
});
|
|
1955
|
+
return { anchors: typed, closed };
|
|
1956
|
+
}
|
|
1957
|
+
var isZeroSeg = (s) => s.c.every((p) => Math.abs(p[0] - s.c[0][0]) < 1e-9 && Math.abs(p[1] - s.c[0][1]) < 1e-9);
|
|
1958
|
+
function mergePair(a, b, tol) {
|
|
1959
|
+
const a0 = a.c[0];
|
|
1960
|
+
const a3 = a.c[a.c.length - 1];
|
|
1961
|
+
const b3 = b.c[b.c.length - 1];
|
|
1962
|
+
if (a.line && b.line) {
|
|
1963
|
+
const cx = b3[0] - a0[0];
|
|
1964
|
+
const cy = b3[1] - a0[1];
|
|
1965
|
+
const len = Math.hypot(cx, cy);
|
|
1966
|
+
if (len < 1e-12) return null;
|
|
1967
|
+
const mx = a3[0] - a0[0];
|
|
1968
|
+
const my = a3[1] - a0[1];
|
|
1969
|
+
const along = (mx * cx + my * cy) / len;
|
|
1970
|
+
return Math.abs((mx * cy - my * cx) / len) <= tol && along >= 0 && along <= len ? { c: [a0, b3], line: true } : null;
|
|
1971
|
+
}
|
|
1972
|
+
const ca = a.line ? [a0, a0, a3, a3] : a.c;
|
|
1973
|
+
const cb = b.line ? [a3, a3, b3, b3] : b.c;
|
|
1974
|
+
const ta = endTangent(ca);
|
|
1975
|
+
const tb = startTangent(cb);
|
|
1976
|
+
if (ta[0] * tb[0] + ta[1] * tb[1] < 0.99) return null;
|
|
1977
|
+
const pts = [];
|
|
1978
|
+
sample(ca, 16, pts, true);
|
|
1979
|
+
sample(cb, 16, pts, false);
|
|
1980
|
+
const { curve, error } = fitAcross(pts, 16, startTangent(ca), endTangent(cb));
|
|
1981
|
+
return error <= tol ? { c: [curve.p0, curve.p1, curve.p2, curve.p3], line: false } : null;
|
|
1982
|
+
}
|
|
1983
|
+
function removeRedundantPoints(sp, tolerance) {
|
|
1984
|
+
const tol = Math.max(tolerance, 1e-9);
|
|
1985
|
+
const types = new Map(sp.anchors.filter((a) => a.type).map((a) => [`${a.x},${a.y}`, a.type]));
|
|
1986
|
+
const segs = segsOf(sp).filter((s) => !isZeroSeg(s));
|
|
1987
|
+
if (!segs.length) return { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed };
|
|
1988
|
+
const minSegs = sp.closed ? 2 : 1;
|
|
1989
|
+
let changed = true;
|
|
1990
|
+
while (changed && segs.length > minSegs) {
|
|
1991
|
+
changed = false;
|
|
1992
|
+
const n = segs.length;
|
|
1993
|
+
const joints = sp.closed ? n : n - 1;
|
|
1994
|
+
for (let j = 0; j < joints; j++) {
|
|
1995
|
+
const [i0, i1] = sp.closed ? [(j + n - 1) % n, j] : [j, j + 1];
|
|
1996
|
+
const m = mergePair(segs[i0], segs[i1], tol);
|
|
1997
|
+
if (!m) continue;
|
|
1998
|
+
segs[i0] = m;
|
|
1999
|
+
segs.splice(i1, 1);
|
|
2000
|
+
if (sp.closed && i1 === 0) segs.unshift(segs.pop());
|
|
2001
|
+
changed = true;
|
|
2002
|
+
break;
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
return segsToSubpath(segs, sp.closed, types) ?? { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed };
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
// src/vector/pathCut.ts
|
|
2009
|
+
var loopArea = (l) => contourSignedArea(l.segs);
|
|
2010
|
+
function loopSample(l) {
|
|
2011
|
+
const k = l.chord.findIndex((c) => !c);
|
|
2012
|
+
return segmentPoint(l.segs[k >= 0 ? k : 0], 0.5);
|
|
2013
|
+
}
|
|
2014
|
+
var contains2 = (l, p) => windingNumber([l.segs], p) !== 0;
|
|
2015
|
+
function groupFaces(loops) {
|
|
2016
|
+
const usable = loops.filter((l) => l.segs.length && Math.abs(loopArea(l)) > 1e-12);
|
|
2017
|
+
if (!usable.length) return [];
|
|
2018
|
+
const largest = usable.reduce((a, b) => Math.abs(loopArea(b)) > Math.abs(loopArea(a)) ? b : a);
|
|
2019
|
+
const sign = Math.sign(loopArea(largest));
|
|
2020
|
+
const outers = usable.filter((l) => Math.sign(loopArea(l)) === sign).sort((a, b) => Math.abs(loopArea(a)) - Math.abs(loopArea(b)));
|
|
2021
|
+
const faces = new Map(outers.map((o) => [o, { loops: [o] }]));
|
|
2022
|
+
for (const hole of usable.filter((l) => Math.sign(loopArea(l)) !== sign)) {
|
|
2023
|
+
const p = loopSample(hole);
|
|
2024
|
+
const parent = outers.find((o) => Math.abs(loopArea(o)) > Math.abs(loopArea(hole)) && contains2(o, p));
|
|
2025
|
+
if (parent) faces.get(parent).loops.push(hole);
|
|
2026
|
+
}
|
|
2027
|
+
return outers.reverse().map((o) => faces.get(o));
|
|
2028
|
+
}
|
|
2029
|
+
function faceToPath(face) {
|
|
2030
|
+
return contoursToPath(face.loops.map((l) => ({ segments: l.segs, closed: true })));
|
|
2031
|
+
}
|
|
2032
|
+
function facesOf(path, fillRule) {
|
|
2033
|
+
const normalized = normalizeBezierPath(path, fillRule);
|
|
2034
|
+
const loops = pathToContours(normalized).map((c) => {
|
|
2035
|
+
const segs = closeContour(c, true).map((s) => s.slice());
|
|
2036
|
+
return { segs, chord: segs.map(() => false) };
|
|
2037
|
+
});
|
|
2038
|
+
return groupFaces(loops);
|
|
2039
|
+
}
|
|
2040
|
+
function splitIntoFaces(path, fillRule = "nonzero") {
|
|
2041
|
+
return facesOf(path, fillRule).map(faceToPath);
|
|
2042
|
+
}
|
|
2043
|
+
function rootsInUnit(a, b, c, d, scale) {
|
|
2044
|
+
const f = (t) => ((a * t + b) * t + c) * t + d;
|
|
2045
|
+
const tol = 1e-12 * Math.max(1, scale);
|
|
2046
|
+
const cuts = [0];
|
|
2047
|
+
const qa = 3 * a;
|
|
2048
|
+
const qb = 2 * b;
|
|
2049
|
+
if (Math.abs(qa) > 1e-18) {
|
|
2050
|
+
const disc = qb * qb - 4 * qa * c;
|
|
2051
|
+
if (disc >= 0) {
|
|
2052
|
+
const r = Math.sqrt(disc);
|
|
2053
|
+
for (const t of [(-qb - r) / (2 * qa), (-qb + r) / (2 * qa)].sort((x, y) => x - y)) if (t > 0 && t < 1) cuts.push(t);
|
|
2054
|
+
}
|
|
2055
|
+
} else if (Math.abs(qb) > 1e-18) {
|
|
2056
|
+
const t = -c / qb;
|
|
2057
|
+
if (t > 0 && t < 1) cuts.push(t);
|
|
2058
|
+
}
|
|
2059
|
+
cuts.push(1);
|
|
2060
|
+
const roots = [];
|
|
2061
|
+
const push = (t) => {
|
|
2062
|
+
if (!roots.some((r) => Math.abs(r - t) < 1e-9)) roots.push(t);
|
|
2063
|
+
};
|
|
2064
|
+
for (let i = 0; i < cuts.length - 1; i++) {
|
|
2065
|
+
let lo = cuts[i];
|
|
2066
|
+
let hi = cuts[i + 1];
|
|
2067
|
+
let flo = f(lo);
|
|
2068
|
+
const fhi = f(hi);
|
|
2069
|
+
if (Math.abs(flo) <= tol) push(lo);
|
|
2070
|
+
if (Math.abs(fhi) <= tol) push(hi);
|
|
2071
|
+
if (flo * fhi >= 0) continue;
|
|
2072
|
+
for (let k = 0; k < 80; k++) {
|
|
2073
|
+
const m = (lo + hi) / 2;
|
|
2074
|
+
const fm = f(m);
|
|
2075
|
+
if (fm === 0) {
|
|
2076
|
+
lo = hi = m;
|
|
2077
|
+
break;
|
|
2078
|
+
}
|
|
2079
|
+
if (fm * flo < 0) hi = m;
|
|
2080
|
+
else {
|
|
2081
|
+
lo = m;
|
|
2082
|
+
flo = fm;
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
push((lo + hi) / 2);
|
|
2086
|
+
}
|
|
2087
|
+
return roots.sort((x, y) => x - y);
|
|
2088
|
+
}
|
|
2089
|
+
function lineHits(p, q, seg) {
|
|
2090
|
+
const dx = q[0] - p[0];
|
|
2091
|
+
const dy = q[1] - p[1];
|
|
2092
|
+
const len2 = dx * dx + dy * dy;
|
|
2093
|
+
if (len2 < 1e-24) return [];
|
|
2094
|
+
const h = (pt) => (pt[0] - p[0]) * -dy + (pt[1] - p[1]) * dx;
|
|
2095
|
+
let coeffs;
|
|
2096
|
+
if (seg.length === 2) coeffs = [0, 0, h(seg[1]) - h(seg[0]), h(seg[0])];
|
|
2097
|
+
else {
|
|
2098
|
+
const [h0, h1, h2, h3] = seg.map(h);
|
|
2099
|
+
coeffs = [-h0 + 3 * h1 - 3 * h2 + h3, 3 * h0 - 6 * h1 + 3 * h2, -3 * h0 + 3 * h1, h0];
|
|
2100
|
+
}
|
|
2101
|
+
if (coeffs.every((c) => Math.abs(c) < 1e-12 * Math.max(1, len2))) return [];
|
|
2102
|
+
const scale = Math.sqrt(len2) * Math.max(1, ...seg.map((pt) => Math.abs(pt[0]) + Math.abs(pt[1])));
|
|
2103
|
+
const out = [];
|
|
2104
|
+
for (const t of rootsInUnit(...coeffs, scale)) {
|
|
2105
|
+
const point = segmentPoint(seg, t);
|
|
2106
|
+
const s = ((point[0] - p[0]) * dx + (point[1] - p[1]) * dy) / len2;
|
|
2107
|
+
if (s >= -1e-9 && s <= 1 + 1e-9) out.push({ s: Math.min(1, Math.max(0, s)), t, point });
|
|
2108
|
+
}
|
|
2109
|
+
return out;
|
|
2110
|
+
}
|
|
2111
|
+
var near = (a, b, eps) => Math.abs(a[0] - b[0]) <= eps && Math.abs(a[1] - b[1]) <= eps;
|
|
2112
|
+
var degenerate = (seg, eps) => seg.every((p) => near(p, seg[0], eps));
|
|
2113
|
+
function arc(l, a, b, eps) {
|
|
2114
|
+
const n = l.segs.length;
|
|
2115
|
+
const segs = [];
|
|
2116
|
+
const chord = [];
|
|
2117
|
+
const push = (k, t0, t1) => {
|
|
2118
|
+
const piece = t0 <= 0 && t1 >= 1 ? l.segs[k].slice() : subSegment(l.segs[k], t0, t1);
|
|
2119
|
+
if (!degenerate(piece, eps)) {
|
|
2120
|
+
segs.push(piece);
|
|
2121
|
+
chord.push(l.chord[k]);
|
|
2122
|
+
}
|
|
2123
|
+
};
|
|
2124
|
+
if (a.seg === b.seg && b.t >= a.t + 1e-12) {
|
|
2125
|
+
push(a.seg, a.t, b.t);
|
|
2126
|
+
return { segs, chord };
|
|
2127
|
+
}
|
|
2128
|
+
push(a.seg, a.t, 1);
|
|
2129
|
+
for (let k = (a.seg + 1) % n; k !== b.seg; k = (k + 1) % n) push(k, 0, 1);
|
|
2130
|
+
push(b.seg, 0, b.t);
|
|
2131
|
+
return { segs, chord };
|
|
2132
|
+
}
|
|
2133
|
+
function polySegs(points, eps) {
|
|
2134
|
+
const out = [];
|
|
2135
|
+
for (let i = 1; i < points.length; i++) if (!near(points[i - 1], points[i], eps)) out.push([points[i - 1], points[i]]);
|
|
2136
|
+
return out;
|
|
2137
|
+
}
|
|
2138
|
+
function knifePath(path, fillRule, knife) {
|
|
2139
|
+
const pts = knife.filter((p) => Number.isFinite(p[0]) && Number.isFinite(p[1]));
|
|
2140
|
+
if (pts.length < 2) return null;
|
|
2141
|
+
let faces = facesOf(path, fillRule);
|
|
2142
|
+
if (!faces.length) return null;
|
|
2143
|
+
const b = bezierPathBounds(path);
|
|
2144
|
+
const size = b ? Math.max(b.maxX - b.minX, b.maxY - b.minY, 1) : 1;
|
|
2145
|
+
const eps = size * 1e-9;
|
|
2146
|
+
let cursor = -Infinity;
|
|
2147
|
+
let cuts = 0;
|
|
2148
|
+
for (let guard = 0; guard < 512; guard++) {
|
|
2149
|
+
let best = null;
|
|
2150
|
+
faces.forEach((face2, fi2) => {
|
|
2151
|
+
const hits = [];
|
|
2152
|
+
for (let m = 0; m < pts.length - 1; m++) {
|
|
2153
|
+
if (m + 1 < cursor - 1e-9) continue;
|
|
2154
|
+
const kb = { minX: Math.min(pts[m][0], pts[m + 1][0]), maxX: Math.max(pts[m][0], pts[m + 1][0]), minY: Math.min(pts[m][1], pts[m + 1][1]), maxY: Math.max(pts[m][1], pts[m + 1][1]) };
|
|
2155
|
+
face2.loops.forEach(
|
|
2156
|
+
(l, li) => l.segs.forEach((seg, si) => {
|
|
2157
|
+
const sb = segmentBounds(seg);
|
|
2158
|
+
if (sb.maxX < kb.minX - eps || sb.minX > kb.maxX + eps || sb.maxY < kb.minY - eps || sb.minY > kb.maxY + eps) return;
|
|
2159
|
+
for (const h of lineHits(pts[m], pts[m + 1], seg)) {
|
|
2160
|
+
const s = m + h.s;
|
|
2161
|
+
if (s >= cursor - 1e-9) hits.push({ s, loop: li, seg: si, t: h.t, point: h.point });
|
|
2162
|
+
}
|
|
2163
|
+
})
|
|
2164
|
+
);
|
|
2165
|
+
}
|
|
2166
|
+
hits.sort((x, y) => x.s - y.s);
|
|
2167
|
+
const unique = hits.filter((h, i) => i === 0 || h.s - hits[i - 1].s > 1e-9 || !near(h.point, hits[i - 1].point, eps * 10));
|
|
2168
|
+
const inside = (s) => {
|
|
2169
|
+
const m = Math.min(pts.length - 2, Math.floor(s));
|
|
2170
|
+
const u = s - m;
|
|
2171
|
+
const q = [pts[m][0] + (pts[m + 1][0] - pts[m][0]) * u, pts[m][1] + (pts[m + 1][1] - pts[m][1]) * u];
|
|
2172
|
+
return windingNumber(face2.loops.map((l) => l.segs), q) !== 0;
|
|
2173
|
+
};
|
|
2174
|
+
for (let i = 0; i < unique.length - 1; i++) {
|
|
2175
|
+
const from2 = unique[i];
|
|
2176
|
+
const to2 = unique[i + 1];
|
|
2177
|
+
if (near(from2.point, to2.point, eps * 10) || !inside((from2.s + to2.s) / 2)) continue;
|
|
2178
|
+
if (!best || from2.s < best.from.s) best = { face: fi2, from: from2, to: to2 };
|
|
2179
|
+
break;
|
|
2180
|
+
}
|
|
2181
|
+
});
|
|
2182
|
+
if (!best) break;
|
|
2183
|
+
const { face: fi, from, to } = best;
|
|
2184
|
+
const face = faces[fi];
|
|
2185
|
+
const corners2 = [from.point];
|
|
2186
|
+
for (let m = Math.floor(from.s) + 1; m <= to.s - 1e-12 && m < pts.length; m++) if (m > from.s) corners2.push(pts[m]);
|
|
2187
|
+
corners2.push(to.point);
|
|
2188
|
+
const forward = polySegs(corners2, eps);
|
|
2189
|
+
const back = polySegs(corners2.slice().reverse(), eps);
|
|
2190
|
+
const la = face.loops[from.loop];
|
|
2191
|
+
const loops = face.loops.filter((_, i) => i !== from.loop && i !== to.loop);
|
|
2192
|
+
if (from.loop === to.loop) {
|
|
2193
|
+
const ab = arc(la, from, to, eps);
|
|
2194
|
+
const ba = arc(la, to, from, eps);
|
|
2195
|
+
loops.push(
|
|
2196
|
+
{ segs: [...ab.segs, ...back], chord: [...ab.chord, ...back.map(() => true)] },
|
|
2197
|
+
{ segs: [...ba.segs, ...forward], chord: [...ba.chord, ...forward.map(() => true)] }
|
|
2198
|
+
);
|
|
2199
|
+
} else {
|
|
2200
|
+
const lb = face.loops[to.loop];
|
|
2201
|
+
const roundA = arc(la, from, from, eps);
|
|
2202
|
+
const roundB = arc(lb, to, to, eps);
|
|
2203
|
+
loops.push({
|
|
2204
|
+
segs: [...roundA.segs, ...forward, ...roundB.segs, ...back],
|
|
2205
|
+
chord: [...roundA.chord, ...forward.map(() => true), ...roundB.chord, ...back.map(() => true)]
|
|
2206
|
+
});
|
|
2207
|
+
}
|
|
2208
|
+
faces = [...faces.slice(0, fi), ...groupFaces(loops), ...faces.slice(fi + 1)];
|
|
2209
|
+
cursor = to.s;
|
|
2210
|
+
cuts++;
|
|
2211
|
+
}
|
|
2212
|
+
return cuts ? faces.map(faceToPath) : null;
|
|
2213
|
+
}
|
|
2214
|
+
function eraserRegion(points, width) {
|
|
2215
|
+
const pts = points.filter((p) => Number.isFinite(p[0]) && Number.isFinite(p[1]));
|
|
2216
|
+
const r = width / 2;
|
|
2217
|
+
if (!pts.length || !(r > 0)) return [];
|
|
2218
|
+
const moved = pts.some((p) => !near(p, pts[0], 1e-9));
|
|
2219
|
+
if (!moved) {
|
|
2220
|
+
const [cx, cy] = pts[0];
|
|
2221
|
+
const k = r * 0.5522847498307936;
|
|
2222
|
+
return [
|
|
2223
|
+
{ type: "M", x: cx, y: cy - r },
|
|
2224
|
+
{ type: "C", x1: cx + k, y1: cy - r, x2: cx + r, y2: cy - k, x: cx + r, y: cy },
|
|
2225
|
+
{ type: "C", x1: cx + r, y1: cy + k, x2: cx + k, y2: cy + r, x: cx, y: cy + r },
|
|
2226
|
+
{ type: "C", x1: cx - k, y1: cy + r, x2: cx - r, y2: cy + k, x: cx - r, y: cy },
|
|
2227
|
+
{ type: "C", x1: cx - r, y1: cy - k, x2: cx - k, y2: cy - r, x: cx, y: cy - r },
|
|
2228
|
+
{ type: "Z" }
|
|
2229
|
+
];
|
|
2230
|
+
}
|
|
2231
|
+
const line2 = [{ type: "M", x: pts[0][0], y: pts[0][1] }, ...pts.slice(1).map((p) => ({ type: "L", x: p[0], y: p[1] }))];
|
|
2232
|
+
return outlineStroke(line2, { width, cap: "round", join: "round" });
|
|
2233
|
+
}
|
|
2234
|
+
function eraseFromFill(path, fillRule, region) {
|
|
2235
|
+
const overlap = pathBoolean("intersection", [{ path, fillRule }, { path: region }]).path;
|
|
2236
|
+
if (!pathToContours(overlap).some((c) => Math.abs(contourSignedArea(closeContour(c, true))) > 1e-9)) return null;
|
|
2237
|
+
const rest = pathBoolean("difference", [{ path, fillRule }, { path: region }]).path;
|
|
2238
|
+
return splitIntoFaces(rest);
|
|
2239
|
+
}
|
|
2240
|
+
function eraseFromOutline(sp, region) {
|
|
2241
|
+
const regionSegs = pathToContours(region).flatMap((c) => closeContour(c, true));
|
|
2242
|
+
if (!regionSegs.length) return null;
|
|
2243
|
+
const rb = bezierPathBounds(region);
|
|
2244
|
+
const count = subpathSegmentCount(sp);
|
|
2245
|
+
if (!rb || count === 0) return null;
|
|
2246
|
+
const removed = (p) => pointInBezierPath(region, p, "nonzero");
|
|
2247
|
+
const runs = [];
|
|
2248
|
+
let cur = [];
|
|
2249
|
+
let any = false;
|
|
2250
|
+
for (let i = 0; i < count; i++) {
|
|
2251
|
+
const seg = subpathSegment(sp, i);
|
|
2252
|
+
const sb = segmentBounds(seg);
|
|
2253
|
+
const ts = [0, 1];
|
|
2254
|
+
if (!(sb.maxX < rb.minX || sb.minX > rb.maxX || sb.maxY < rb.minY || sb.minY > rb.maxY)) {
|
|
2255
|
+
for (const r of regionSegs) {
|
|
2256
|
+
const b = segmentBounds(r);
|
|
2257
|
+
if (b.maxX < sb.minX || b.minX > sb.maxX || b.maxY < sb.minY || b.minY > sb.maxY) continue;
|
|
2258
|
+
for (const hit of segmentIntersections(seg, r)) if (hit.ta > 1e-9 && hit.ta < 1 - 1e-9) ts.push(hit.ta);
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
ts.sort((a, b) => a - b);
|
|
2262
|
+
for (let k = 0; k < ts.length - 1; k++) {
|
|
2263
|
+
const t0 = ts[k];
|
|
2264
|
+
const t1 = ts[k + 1];
|
|
2265
|
+
if (t1 - t0 < 1e-9) continue;
|
|
2266
|
+
if (removed(segmentPoint(seg, (t0 + t1) / 2))) {
|
|
2267
|
+
any = true;
|
|
2268
|
+
if (cur.length) runs.push(cur);
|
|
2269
|
+
cur = [];
|
|
2270
|
+
} else cur.push({ seg: i, t0, t1 });
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
if (cur.length) runs.push(cur);
|
|
2274
|
+
if (!any) return null;
|
|
2275
|
+
if (sp.closed && runs.length >= 2) {
|
|
2276
|
+
const first = runs[0][0];
|
|
2277
|
+
const lastRun = runs[runs.length - 1];
|
|
2278
|
+
const last = lastRun[lastRun.length - 1];
|
|
2279
|
+
if (first.seg === 0 && first.t0 === 0 && last.seg === count - 1 && last.t1 === 1) lastRun.push(...runs.shift());
|
|
2280
|
+
}
|
|
2281
|
+
const out = [];
|
|
2282
|
+
for (const run of runs) {
|
|
2283
|
+
const anchors = [];
|
|
2284
|
+
for (const { seg, t0, t1 } of run) {
|
|
2285
|
+
const c = subSegment(subpathSegment(sp, seg), t0, t1);
|
|
2286
|
+
const line2 = c.length === 2;
|
|
2287
|
+
const start = c[0];
|
|
2288
|
+
const end = c[c.length - 1];
|
|
2289
|
+
if (!anchors.length) anchors.push({ x: start[0], y: start[1] });
|
|
2290
|
+
if (!line2) anchors[anchors.length - 1].out = { x: c[1][0], y: c[1][1] };
|
|
2291
|
+
const next = { x: end[0], y: end[1] };
|
|
2292
|
+
if (!line2) next.in = { x: c[2][0], y: c[2][1] };
|
|
2293
|
+
anchors.push(next);
|
|
2294
|
+
}
|
|
2295
|
+
if (anchors.length < 2) continue;
|
|
2296
|
+
for (const a of anchors) {
|
|
2297
|
+
const type = inferAnchorType(a);
|
|
2298
|
+
if (type !== "corner") a.type = type;
|
|
2299
|
+
}
|
|
2300
|
+
out.push({ anchors, closed: false });
|
|
2301
|
+
}
|
|
2302
|
+
return out;
|
|
2303
|
+
}
|
|
2304
|
+
function polylineBounds(points, pad = 0) {
|
|
2305
|
+
if (!points.length) return null;
|
|
2306
|
+
let minX = Infinity;
|
|
2307
|
+
let minY = Infinity;
|
|
2308
|
+
let maxX = -Infinity;
|
|
2309
|
+
let maxY = -Infinity;
|
|
2310
|
+
for (const [x, y] of points) {
|
|
2311
|
+
minX = Math.min(minX, x);
|
|
2312
|
+
minY = Math.min(minY, y);
|
|
2313
|
+
maxX = Math.max(maxX, x);
|
|
2314
|
+
maxY = Math.max(maxY, y);
|
|
2315
|
+
}
|
|
2316
|
+
return { x: minX - pad, y: minY - pad, width: maxX - minX + 2 * pad, height: maxY - minY + 2 * pad };
|
|
2317
|
+
}
|
|
2318
|
+
function simplifyPolyline(points, tolerance) {
|
|
2319
|
+
const n = points.length;
|
|
2320
|
+
if (n < 3) return points.slice();
|
|
2321
|
+
const keep = new Array(n).fill(false);
|
|
2322
|
+
keep[0] = keep[n - 1] = true;
|
|
2323
|
+
const stack = [[0, n - 1]];
|
|
2324
|
+
while (stack.length) {
|
|
2325
|
+
const [a, b] = stack.pop();
|
|
2326
|
+
const [ax, ay] = points[a];
|
|
2327
|
+
const dx = points[b][0] - ax;
|
|
2328
|
+
const dy = points[b][1] - ay;
|
|
2329
|
+
const len = Math.hypot(dx, dy);
|
|
2330
|
+
let far = -1;
|
|
2331
|
+
let farD = 0;
|
|
2332
|
+
for (let i = a + 1; i < b; i++) {
|
|
2333
|
+
const [px, py] = points[i];
|
|
2334
|
+
const d = len < 1e-12 ? Math.hypot(px - ax, py - ay) : Math.abs((px - ax) * dy - (py - ay) * dx) / len;
|
|
2335
|
+
if (d > farD) {
|
|
2336
|
+
farD = d;
|
|
2337
|
+
far = i;
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
if (far >= 0 && farD > tolerance) {
|
|
2341
|
+
keep[far] = true;
|
|
2342
|
+
stack.push([a, far], [far, b]);
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
return points.filter((_, i) => keep[i]);
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
// src/vector/ink.ts
|
|
2349
|
+
var LazyMouse = class {
|
|
2350
|
+
/** `length` is the string length in the units of the points (0 turns smoothing off). */
|
|
2351
|
+
constructor(length2) {
|
|
2352
|
+
this.length = length2;
|
|
2353
|
+
}
|
|
2354
|
+
length;
|
|
2355
|
+
pos = null;
|
|
2356
|
+
last = null;
|
|
2357
|
+
push(p) {
|
|
2358
|
+
this.last = p;
|
|
2359
|
+
if (!(this.length > 0)) return [p];
|
|
2360
|
+
const pos = this.pos;
|
|
2361
|
+
if (!pos) {
|
|
2362
|
+
this.pos = p;
|
|
2363
|
+
return [p];
|
|
2364
|
+
}
|
|
2365
|
+
const dx = p[0] - pos[0];
|
|
2366
|
+
const dy = p[1] - pos[1];
|
|
2367
|
+
const d = Math.hypot(dx, dy);
|
|
2368
|
+
if (d <= this.length) return [];
|
|
2369
|
+
const k = (d - this.length) / d;
|
|
2370
|
+
const next = [pos[0] + dx * k, pos[1] + dy * k];
|
|
2371
|
+
this.pos = next;
|
|
2372
|
+
return [next];
|
|
2373
|
+
}
|
|
2374
|
+
/** The string from the pen to the pointer while they are apart, else null. */
|
|
2375
|
+
string() {
|
|
2376
|
+
const { pos, last } = this;
|
|
2377
|
+
if (!(this.length > 0) || !pos || !last || pos[0] === last[0] && pos[1] === last[1]) return null;
|
|
2378
|
+
return [pos, last];
|
|
2379
|
+
}
|
|
2380
|
+
/** End of stroke: the pen catches up with the pointer (the point to add, if it lagged). */
|
|
2381
|
+
finish() {
|
|
2382
|
+
const { pos, last } = this;
|
|
2383
|
+
if (!(this.length > 0) || !pos || !last || pos[0] === last[0] && pos[1] === last[1]) return [];
|
|
2384
|
+
this.pos = last;
|
|
2385
|
+
return [last];
|
|
2386
|
+
}
|
|
2387
|
+
};
|
|
2388
|
+
var VECTOR_INK_MAX_CORNERS = 8;
|
|
2389
|
+
var MAX_POINTS = 1e5;
|
|
2390
|
+
function vectorInkShapeName(shape) {
|
|
2391
|
+
switch (shape.kind) {
|
|
2392
|
+
case "line":
|
|
2393
|
+
return "line";
|
|
2394
|
+
case "rect":
|
|
2395
|
+
return shape.width === shape.height ? "square" : "rectangle";
|
|
2396
|
+
case "ellipse":
|
|
2397
|
+
return shape.width === shape.height ? "circle" : "ellipse";
|
|
2398
|
+
case "polygon":
|
|
2399
|
+
return shape.points.length === 3 ? "triangle" : "polygon";
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
var dist2 = (a, b) => Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
2403
|
+
function bbox(pts) {
|
|
2404
|
+
if (!pts.length) return [0, 0, 0, 0];
|
|
2405
|
+
let [x0, y0] = pts[0];
|
|
2406
|
+
let [x1, y1] = pts[0];
|
|
2407
|
+
for (const [x, y] of pts) {
|
|
2408
|
+
x0 = Math.min(x0, x);
|
|
2409
|
+
y0 = Math.min(y0, y);
|
|
2410
|
+
x1 = Math.max(x1, x);
|
|
2411
|
+
y1 = Math.max(y1, y);
|
|
2412
|
+
}
|
|
2413
|
+
return [x0, y0, x1 - x0, y1 - y0];
|
|
2414
|
+
}
|
|
2415
|
+
function segDistance(a, b, p) {
|
|
2416
|
+
const dx = b[0] - a[0];
|
|
2417
|
+
const dy = b[1] - a[1];
|
|
2418
|
+
const len2 = dx * dx + dy * dy;
|
|
2419
|
+
const t = len2 > 0 ? Math.min(1, Math.max(0, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / len2)) : 0;
|
|
2420
|
+
return dist2([a[0] + t * dx, a[1] + t * dy], p);
|
|
2421
|
+
}
|
|
2422
|
+
function polygonDistance(c, p) {
|
|
2423
|
+
let best = Infinity;
|
|
2424
|
+
for (let i = 0; i < c.length; i++) best = Math.min(best, segDistance(c[i], c[(i + 1) % c.length], p));
|
|
2425
|
+
return best;
|
|
2426
|
+
}
|
|
2427
|
+
function ellipseRms(p, x0, y0, w, h) {
|
|
2428
|
+
const rx = w / 2;
|
|
2429
|
+
const ry = h / 2;
|
|
2430
|
+
if (rx <= 0 || ry <= 0 || !p.length) return Infinity;
|
|
2431
|
+
const cx = x0 + rx;
|
|
2432
|
+
const cy = y0 + ry;
|
|
2433
|
+
let sum = 0;
|
|
2434
|
+
for (const [x, y] of p) sum += (Math.hypot((x - cx) / rx, (y - cy) / ry) - 1) ** 2;
|
|
2435
|
+
return Math.sqrt(sum / p.length);
|
|
2436
|
+
}
|
|
2437
|
+
function snapSquare(x, y, w, h) {
|
|
2438
|
+
if (Math.abs(w - h) <= 0.12 * Math.max(w, h)) {
|
|
2439
|
+
const s = (w + h) / 2;
|
|
2440
|
+
return [x + (w - s) / 2, y + (h - s) / 2, s, s];
|
|
2441
|
+
}
|
|
2442
|
+
return [x, y, w, h];
|
|
2443
|
+
}
|
|
2444
|
+
function turning(a, b, c) {
|
|
2445
|
+
const u = [b[0] - a[0], b[1] - a[1]];
|
|
2446
|
+
const v = [c[0] - b[0], c[1] - b[1]];
|
|
2447
|
+
const lu = Math.hypot(u[0], u[1]);
|
|
2448
|
+
const lv = Math.hypot(v[0], v[1]);
|
|
2449
|
+
if (lu <= 0 || lv <= 0) return 0;
|
|
2450
|
+
return Math.acos(Math.min(1, Math.max(-1, (u[0] * v[0] + u[1] * v[1]) / (lu * lv)))) * 180 / Math.PI;
|
|
2451
|
+
}
|
|
2452
|
+
function rdp(p, eps) {
|
|
2453
|
+
const n = p.length;
|
|
2454
|
+
if (n < 3) return p.slice();
|
|
2455
|
+
const keep = new Array(n).fill(false);
|
|
2456
|
+
keep[0] = keep[n - 1] = true;
|
|
2457
|
+
const stack = [[0, n - 1]];
|
|
2458
|
+
while (stack.length) {
|
|
2459
|
+
const [a, b] = stack.pop();
|
|
2460
|
+
let far = a;
|
|
2461
|
+
let farD = 0;
|
|
2462
|
+
for (let i = a + 1; i < b; i++) {
|
|
2463
|
+
const d = segDistance(p[a], p[b], p[i]);
|
|
2464
|
+
if (d > farD) {
|
|
2465
|
+
farD = d;
|
|
2466
|
+
far = i;
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
if (farD > eps) {
|
|
2470
|
+
keep[far] = true;
|
|
2471
|
+
stack.push([a, far], [far, b]);
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
return p.filter((_, i) => keep[i]);
|
|
2475
|
+
}
|
|
2476
|
+
function corners(p, diag) {
|
|
2477
|
+
const eps = 0.05 * diag;
|
|
2478
|
+
const start = p[0];
|
|
2479
|
+
let far = 0;
|
|
2480
|
+
let farD = 0;
|
|
2481
|
+
p.forEach((q, i) => {
|
|
2482
|
+
const d = dist2(start, q);
|
|
2483
|
+
if (d > farD) {
|
|
2484
|
+
farD = d;
|
|
2485
|
+
far = i;
|
|
2486
|
+
}
|
|
2487
|
+
});
|
|
2488
|
+
const a = p.slice(0, far);
|
|
2489
|
+
const b = p.slice(far);
|
|
2490
|
+
const ring = rdp([...a, ...b.slice(0, 1)], eps);
|
|
2491
|
+
const back = rdp([...b, start], eps);
|
|
2492
|
+
ring.pop();
|
|
2493
|
+
back.pop();
|
|
2494
|
+
ring.push(...back);
|
|
2495
|
+
while (ring.length > 3) {
|
|
2496
|
+
const n = ring.length;
|
|
2497
|
+
const turn = (i) => turning(ring[(i + n - 1) % n], ring[i], ring[(i + 1) % n]);
|
|
2498
|
+
let flat = 0;
|
|
2499
|
+
let flatT = Infinity;
|
|
2500
|
+
for (let i = 0; i < n; i++) {
|
|
2501
|
+
const t = turn(i);
|
|
2502
|
+
if (t < flatT) {
|
|
2503
|
+
flatT = t;
|
|
2504
|
+
flat = i;
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
let short = -1;
|
|
2508
|
+
for (let j = 0; j < n; j++) {
|
|
2509
|
+
if (dist2(ring[j], ring[(j + 1) % n]) < 0.1 * diag) {
|
|
2510
|
+
short = j;
|
|
2511
|
+
break;
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
if (flatT < 30) ring.splice(flat, 1);
|
|
2515
|
+
else if (short >= 0) {
|
|
2516
|
+
const k = turn(short) < turn((short + 1) % n) ? short : (short + 1) % n;
|
|
2517
|
+
ring.splice(k, 1);
|
|
2518
|
+
} else break;
|
|
2519
|
+
}
|
|
2520
|
+
return ring;
|
|
2521
|
+
}
|
|
2522
|
+
function upright(c) {
|
|
2523
|
+
const n = c.length;
|
|
2524
|
+
const dirs = c.map((a, i) => {
|
|
2525
|
+
const b = c[(i + 1) % n];
|
|
2526
|
+
const deg = (Math.atan2(b[1] - a[1], b[0] - a[0]) * 180 / Math.PI % 180 + 180) % 180;
|
|
2527
|
+
if (deg <= 15 || deg >= 165) return "across";
|
|
2528
|
+
if (deg >= 75 && deg <= 105) return "down";
|
|
2529
|
+
return null;
|
|
2530
|
+
});
|
|
2531
|
+
return dirs.every((d) => d !== null) && dirs.every((d, i) => d !== dirs[(i + 1) % n]);
|
|
2532
|
+
}
|
|
2533
|
+
function recognizeInkShape(points, options = {}) {
|
|
2534
|
+
const minSize = options.minSize ?? 6;
|
|
2535
|
+
const p = [];
|
|
2536
|
+
for (const q of points.slice(0, MAX_POINTS)) {
|
|
2537
|
+
if (!Number.isFinite(q[0]) || !Number.isFinite(q[1]) || Math.abs(q[0]) >= 1e9 || Math.abs(q[1]) >= 1e9) continue;
|
|
2538
|
+
const last2 = p[p.length - 1];
|
|
2539
|
+
if (!last2 || last2[0] !== q[0] || last2[1] !== q[1]) p.push([q[0], q[1]]);
|
|
2540
|
+
}
|
|
2541
|
+
if (p.length < 2) return null;
|
|
2542
|
+
const first = p[0];
|
|
2543
|
+
const last = p[p.length - 1];
|
|
2544
|
+
const [x0, y0, w, h] = bbox(p);
|
|
2545
|
+
const size = Math.max(w, h);
|
|
2546
|
+
const diag = Math.hypot(w, h);
|
|
2547
|
+
if (size < minSize) return null;
|
|
2548
|
+
let length2 = 0;
|
|
2549
|
+
for (let i = 1; i < p.length; i++) length2 += dist2(p[i - 1], p[i]);
|
|
2550
|
+
const gap = dist2(first, last);
|
|
2551
|
+
if (gap > 0.85 * length2 && gap >= minSize) {
|
|
2552
|
+
const off = Math.max(...p.map((q) => segDistance(first, last, q)));
|
|
2553
|
+
if (off <= Math.max(0.06 * gap, 1.5 * minSize / 6)) return { kind: "line", a: first, b: last };
|
|
2554
|
+
}
|
|
2555
|
+
if (gap > 0.2 * size || length2 < 2 * size) return null;
|
|
2556
|
+
const rms = ellipseRms(p, x0, y0, w, h);
|
|
2557
|
+
const roundish = w > 0.2 * size && h > 0.2 * size;
|
|
2558
|
+
const ellipse = () => {
|
|
2559
|
+
const [x, y, sw, sh] = snapSquare(x0, y0, w, h);
|
|
2560
|
+
return { kind: "ellipse", x, y, width: sw, height: sh };
|
|
2561
|
+
};
|
|
2562
|
+
if (roundish && rms < 0.06) return ellipse();
|
|
2563
|
+
const c = corners(p, diag);
|
|
2564
|
+
const fits = c.length >= 3 && p.every((q) => polygonDistance(c, q) <= 0.08 * diag);
|
|
2565
|
+
if (fits && c.length === 4 && upright(c)) {
|
|
2566
|
+
const [bx, by, bw, bh] = bbox(c);
|
|
2567
|
+
const [x, y, sw, sh] = snapSquare(bx, by, bw, bh);
|
|
2568
|
+
return { kind: "rect", x, y, width: sw, height: sh };
|
|
2569
|
+
}
|
|
2570
|
+
if (fits && c.length <= VECTOR_INK_MAX_CORNERS) return { kind: "polygon", points: c };
|
|
2571
|
+
return roundish && rms < 0.1 ? ellipse() : null;
|
|
2572
|
+
}
|
|
2573
|
+
|
|
1606
2574
|
// src/vector/controller.ts
|
|
1607
2575
|
var DEFAULT_VECTOR_SNAP = { grid: 0, objects: true, guides: true, threshold: 5, angles: VECTOR_CONSTRUCTION_ANGLES };
|
|
2576
|
+
var DEFAULT_VECTOR_PATH_EDIT = { keepShapeOnDelete: true, curvatureMoves: false, pencilSmoothing: 0, recognizeShapes: false, eraserSize: 10 };
|
|
2577
|
+
var VECTOR_PENCIL_STRING_PX = 100;
|
|
1608
2578
|
var VECTOR_CLIPBOARD_TYPE = "pixerate/vector";
|
|
1609
2579
|
var DEFAULT_DEFAULTS = {
|
|
1610
2580
|
fill: "#d4d4d8",
|
|
@@ -1723,14 +2693,14 @@ function getVectorSelectionBoxGeometry(frame, viewport) {
|
|
|
1723
2693
|
const rot = rotateMatrix(frame.angle, { x: viewport.x, y: viewport.y });
|
|
1724
2694
|
const toScreen2 = (p) => frame.angle ? applyMatrix(rot, p) : { x: p.x, y: p.y };
|
|
1725
2695
|
const handles = getTransformHandlePositions(box).map((h) => ({ ...h, screen: toScreen2(h) }));
|
|
1726
|
-
const
|
|
2696
|
+
const corners2 = [
|
|
1727
2697
|
{ x: box.x, y: box.y },
|
|
1728
2698
|
{ x: box.x + box.width, y: box.y },
|
|
1729
2699
|
{ x: box.x + box.width, y: box.y + box.height },
|
|
1730
2700
|
{ x: box.x, y: box.y + box.height }
|
|
1731
2701
|
].map(toScreen2);
|
|
1732
2702
|
const r = (n) => Math.round(n * 1e3) / 1e3;
|
|
1733
|
-
return { box, angle: frame.angle, transform: frame.angle ? `rotate(${r(frame.angle)} ${r(viewport.x)} ${r(viewport.y)})` : "", handles, corners };
|
|
2703
|
+
return { box, angle: frame.angle, transform: frame.angle ? `rotate(${r(frame.angle)} ${r(viewport.x)} ${r(viewport.y)})` : "", handles, corners: corners2 };
|
|
1734
2704
|
}
|
|
1735
2705
|
var RESIZE_CURSOR_ORDER = ["ew-resize", "nwse-resize", "ns-resize", "nesw-resize"];
|
|
1736
2706
|
var HANDLE_ANGLES = { e: 0, se: 45, s: 90, sw: 135, w: 180, nw: 225, n: 270, ne: 315 };
|
|
@@ -1748,9 +2718,12 @@ var TOOL_LABELS = {
|
|
|
1748
2718
|
ellipse: "Ellipse",
|
|
1749
2719
|
polygon: "Polygon",
|
|
1750
2720
|
star: "Star",
|
|
1751
|
-
line: "Line"
|
|
2721
|
+
line: "Line",
|
|
2722
|
+
scissors: "Scissors",
|
|
2723
|
+
knife: "Knife",
|
|
2724
|
+
eraser: "Eraser"
|
|
1752
2725
|
};
|
|
1753
|
-
var TOOL_KEYS = { v: "select", a: "direct", p: "pen", n: "pencil", r: "rect", o: "ellipse",
|
|
2726
|
+
var TOOL_KEYS = { v: "select", a: "direct", p: "pen", n: "pencil", r: "rect", o: "ellipse", y: "polygon", s: "star", l: "line", c: "scissors", k: "knife", e: "eraser" };
|
|
1754
2727
|
function vectorToolLabel(tool) {
|
|
1755
2728
|
return TOOL_LABELS[tool];
|
|
1756
2729
|
}
|
|
@@ -1763,7 +2736,10 @@ var VECTOR_TOOL_SHORTCUTS = {
|
|
|
1763
2736
|
ellipse: "O",
|
|
1764
2737
|
polygon: "Y",
|
|
1765
2738
|
star: "S",
|
|
1766
|
-
line: "L"
|
|
2739
|
+
line: "L",
|
|
2740
|
+
scissors: "C",
|
|
2741
|
+
knife: "K",
|
|
2742
|
+
eraser: "E"
|
|
1767
2743
|
};
|
|
1768
2744
|
var VECTOR_TOOL_GLYPHS = {
|
|
1769
2745
|
select: "\u2196",
|
|
@@ -1774,8 +2750,12 @@ var VECTOR_TOOL_GLYPHS = {
|
|
|
1774
2750
|
ellipse: "\u25EF",
|
|
1775
2751
|
polygon: "\u2B21",
|
|
1776
2752
|
star: "\u2606",
|
|
1777
|
-
line: "\u2571"
|
|
2753
|
+
line: "\u2571",
|
|
2754
|
+
scissors: "\u2702",
|
|
2755
|
+
knife: "\u27CB",
|
|
2756
|
+
eraser: "\u232B"
|
|
1778
2757
|
};
|
|
2758
|
+
var SHAPE_TOOLS = /* @__PURE__ */ new Set(["rect", "ellipse", "polygon", "star", "line"]);
|
|
1779
2759
|
var BOOLEAN_OPS = {
|
|
1780
2760
|
union: "union",
|
|
1781
2761
|
subtract: "difference",
|
|
@@ -1797,6 +2777,28 @@ function mapTree(nodes, fn) {
|
|
|
1797
2777
|
}
|
|
1798
2778
|
return changed ? out : nodes;
|
|
1799
2779
|
}
|
|
2780
|
+
function spliceTree(nodes, id, replacements) {
|
|
2781
|
+
let changed = false;
|
|
2782
|
+
const out = [];
|
|
2783
|
+
for (const node of nodes) {
|
|
2784
|
+
if (node.id === id) {
|
|
2785
|
+
changed = true;
|
|
2786
|
+
out.push(...replacements);
|
|
2787
|
+
continue;
|
|
2788
|
+
}
|
|
2789
|
+
if (node.type === "group") {
|
|
2790
|
+
const children = spliceTree(node.children, id, replacements);
|
|
2791
|
+
if (children !== node.children) {
|
|
2792
|
+
changed = true;
|
|
2793
|
+
out.push({ ...node, children });
|
|
2794
|
+
continue;
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
out.push(node);
|
|
2798
|
+
}
|
|
2799
|
+
return changed ? out : nodes;
|
|
2800
|
+
}
|
|
2801
|
+
var toPoint2 = (p) => [p.x, p.y];
|
|
1800
2802
|
var isRoot = (m) => m === IDENTITY_MATRIX || m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0;
|
|
1801
2803
|
var idCounter = 0;
|
|
1802
2804
|
var defaultCreateId = () => `vn-${Date.now().toString(36)}${(++idCounter).toString(36)}`;
|
|
@@ -1811,6 +2813,7 @@ var VectorEditorController = class {
|
|
|
1811
2813
|
defaults;
|
|
1812
2814
|
viewport;
|
|
1813
2815
|
snap;
|
|
2816
|
+
editing;
|
|
1814
2817
|
hitRadius;
|
|
1815
2818
|
hover = null;
|
|
1816
2819
|
pointer = null;
|
|
@@ -1839,6 +2842,7 @@ var VectorEditorController = class {
|
|
|
1839
2842
|
this.defaults = { ...DEFAULT_DEFAULTS, ...options.defaults ?? {} };
|
|
1840
2843
|
this.viewport = { x: 0, y: 0, zoom: 1, ...options.viewport ?? {} };
|
|
1841
2844
|
this.snap = { ...DEFAULT_VECTOR_SNAP, ...options.snap ?? {} };
|
|
2845
|
+
this.editing = { ...DEFAULT_VECTOR_PATH_EDIT, ...options.editing ?? {} };
|
|
1842
2846
|
this.hitRadius = options.hitRadius ?? 6;
|
|
1843
2847
|
this.createId = options.createId ?? defaultCreateId;
|
|
1844
2848
|
this.onChange = options.onChange;
|
|
@@ -1887,6 +2891,7 @@ var VectorEditorController = class {
|
|
|
1887
2891
|
viewport: this.viewport,
|
|
1888
2892
|
defaults: this.defaults,
|
|
1889
2893
|
snap: this.snap,
|
|
2894
|
+
editing: this.editing,
|
|
1890
2895
|
guides: this.guides,
|
|
1891
2896
|
hitRadius: this.hitRadius,
|
|
1892
2897
|
hover: this.hover,
|
|
@@ -2088,6 +3093,14 @@ var VectorEditorController = class {
|
|
|
2088
3093
|
if (!this.snap.guides || !this.snap.objects) this.guides = null;
|
|
2089
3094
|
this.notify();
|
|
2090
3095
|
}
|
|
3096
|
+
/** Changes how deleting, dragging anchors, the pencil and the eraser behave. */
|
|
3097
|
+
setEditing(editing) {
|
|
3098
|
+
const next = { ...this.editing, ...editing };
|
|
3099
|
+
next.pencilSmoothing = Math.min(1, Math.max(0, Number.isFinite(next.pencilSmoothing) ? next.pencilSmoothing : 0));
|
|
3100
|
+
if (!(next.eraserSize > 0)) next.eraserSize = this.editing.eraserSize;
|
|
3101
|
+
this.editing = next;
|
|
3102
|
+
this.notify();
|
|
3103
|
+
}
|
|
2091
3104
|
/** Turns object snapping on or off (the toolbar's snapping toggle). */
|
|
2092
3105
|
toggleSnapping(on = !this.snap.objects) {
|
|
2093
3106
|
this.setSnap({ objects: on });
|
|
@@ -2150,7 +3163,7 @@ var VectorEditorController = class {
|
|
|
2150
3163
|
const leave = last ? { at: { x: last.x, y: last.y }, constrain: input.shift ? "angle" : null } : null;
|
|
2151
3164
|
return this.snapDrawn(p, leave, input, () => this.drawingTargets(this.pen?.nodeId ?? null));
|
|
2152
3165
|
}
|
|
2153
|
-
if (
|
|
3166
|
+
if (SHAPE_TOOLS.has(this.tool)) return this.snapDrawn(p, null, input, () => this.drawingTargets(null));
|
|
2154
3167
|
this.setGuides(null);
|
|
2155
3168
|
return p;
|
|
2156
3169
|
}
|
|
@@ -2753,12 +3766,47 @@ var VectorEditorController = class {
|
|
|
2753
3766
|
return translateAnchor(a, d.x, d.y);
|
|
2754
3767
|
});
|
|
2755
3768
|
}
|
|
2756
|
-
/**
|
|
2757
|
-
|
|
3769
|
+
/**
|
|
3770
|
+
* Moves one anchor to a document position. Its handles follow; with `curvature` (default:
|
|
3771
|
+
* the `curvatureMoves` setting) its two segments bend instead, as the Curvature tool does.
|
|
3772
|
+
*/
|
|
3773
|
+
moveAnchor(ref, to, options = {}) {
|
|
2758
3774
|
const a = getAnchorWorld(this.doc, ref);
|
|
2759
3775
|
if (!a) return { ok: false, message: "There is no such anchor." };
|
|
3776
|
+
const curvature = options.curvature ?? this.editing.curvatureMoves;
|
|
2760
3777
|
this.change("Move Point", () => {
|
|
2761
|
-
this.doc = this.movedAnchors(this.doc, [ref], to.x - a.x, to.y - a.y);
|
|
3778
|
+
this.doc = curvature ? this.curvatureMoved(this.doc, ref, to) : this.movedAnchors(this.doc, [ref], to.x - a.x, to.y - a.y);
|
|
3779
|
+
});
|
|
3780
|
+
return { ok: true };
|
|
3781
|
+
}
|
|
3782
|
+
/** `doc` with anchor `ref` moved to document point `to` by a Curvature-tool move. */
|
|
3783
|
+
curvatureMoved(doc, ref, to) {
|
|
3784
|
+
const node = findVectorNode(doc.nodes, ref.nodeId);
|
|
3785
|
+
if (node?.type !== "path" || node.locked || !node.subpaths[ref.subpath]) return doc;
|
|
3786
|
+
const q = this.toLocal(ref.nodeId, to, doc);
|
|
3787
|
+
const subpaths = node.subpaths.map((sp, i) => i === ref.subpath ? curvatureMoveAnchor(sp, ref.index, q) : sp);
|
|
3788
|
+
const nodes = mapTree(doc.nodes, (n) => n.id === ref.nodeId ? { ...node, subpaths } : n);
|
|
3789
|
+
return { ...doc, nodes };
|
|
3790
|
+
}
|
|
3791
|
+
/** `doc` with segment `ref` bent through document point `p` at parameter `t`. */
|
|
3792
|
+
bent(doc, ref, t, p) {
|
|
3793
|
+
const node = findVectorNode(doc.nodes, ref.nodeId);
|
|
3794
|
+
if (node?.type !== "path" || node.locked || !node.subpaths[ref.subpath]) return doc;
|
|
3795
|
+
const q = this.toLocal(ref.nodeId, p, doc);
|
|
3796
|
+
const subpaths = node.subpaths.map((sp, i) => i === ref.subpath ? bendSegment(sp, ref.segment, t, q) : sp);
|
|
3797
|
+
return { ...doc, nodes: mapTree(doc.nodes, (n) => n.id === ref.nodeId ? { ...node, subpaths } : n) };
|
|
3798
|
+
}
|
|
3799
|
+
/**
|
|
3800
|
+
* Bends a segment so the curve passes through document point `to` at parameter `t`
|
|
3801
|
+
* (default 0.5, clamped to 0.05–0.95); see `bendSegment` in `pathEdit`.
|
|
3802
|
+
*/
|
|
3803
|
+
bendSegment(ref, to, t = 0.5) {
|
|
3804
|
+
const node = this.getNode(ref.nodeId);
|
|
3805
|
+
const sp = node?.type === "path" ? node.subpaths[ref.subpath] : void 0;
|
|
3806
|
+
if (!sp || ref.segment < 0 || ref.segment >= (sp.closed ? sp.anchors.length : sp.anchors.length - 1) || sp.anchors.length < 2) return { ok: false, message: "There is no such segment." };
|
|
3807
|
+
if (node.locked) return { ok: false, message: "The path is locked." };
|
|
3808
|
+
this.change("Bend Segment", () => {
|
|
3809
|
+
this.doc = this.bent(this.doc, ref, t, to);
|
|
2762
3810
|
});
|
|
2763
3811
|
return { ok: true };
|
|
2764
3812
|
}
|
|
@@ -2801,15 +3849,24 @@ var VectorEditorController = class {
|
|
|
2801
3849
|
});
|
|
2802
3850
|
return ref;
|
|
2803
3851
|
}
|
|
2804
|
-
/**
|
|
2805
|
-
|
|
3852
|
+
/**
|
|
3853
|
+
* Deletes the selected (or given) anchors; subpaths left with one anchor and empty paths go
|
|
3854
|
+
* too. With `keepShape` (default: the `keepShapeOnDelete` setting, on) the neighbours'
|
|
3855
|
+
* handles are refitted so the path keeps its shape; off, the neighbours are joined as they are.
|
|
3856
|
+
*/
|
|
3857
|
+
deleteAnchors(refs = this.anchors, options = {}) {
|
|
2806
3858
|
if (!refs.length) return { ok: false, message: "Select points to delete." };
|
|
3859
|
+
const keepShape = options.keepShape ?? this.editing.keepShapeOnDelete;
|
|
2807
3860
|
this.change("Delete Points", () => {
|
|
2808
3861
|
const nodes = mapTree(this.doc.nodes, (n) => {
|
|
2809
|
-
if (n.type !== "path") return n;
|
|
3862
|
+
if (n.type !== "path" || n.locked) return n;
|
|
2810
3863
|
const mine = refs.filter((r) => r.nodeId === n.id);
|
|
2811
3864
|
if (!mine.length) return n;
|
|
2812
|
-
const subpaths = n.subpaths.map((sp, s) =>
|
|
3865
|
+
const subpaths = n.subpaths.map((sp, s) => {
|
|
3866
|
+
const indices = mine.filter((r) => r.subpath === s).map((r) => r.index);
|
|
3867
|
+
if (!indices.length) return sp;
|
|
3868
|
+
return keepShape ? removeAnchorsKeepingShape(sp, indices) : removeAnchors(sp, indices);
|
|
3869
|
+
}).filter((sp) => sp.anchors.length > 1);
|
|
2813
3870
|
return subpaths.length ? { ...n, subpaths } : null;
|
|
2814
3871
|
});
|
|
2815
3872
|
this.setDoc(nodes);
|
|
@@ -2847,6 +3904,269 @@ var VectorEditorController = class {
|
|
|
2847
3904
|
});
|
|
2848
3905
|
return { ok: true };
|
|
2849
3906
|
}
|
|
3907
|
+
// -------------------------------------------------------------------------
|
|
3908
|
+
// Path clean-up, Scissors, Knife and Eraser
|
|
3909
|
+
// -------------------------------------------------------------------------
|
|
3910
|
+
/** Unlocked path nodes in the given roots (default: the paths being edited, else the selection). */
|
|
3911
|
+
pathTargets(ids) {
|
|
3912
|
+
const roots = ids ?? (this.tool === "direct" && this.anchors.length ? [...new Set(this.anchors.map((r) => r.nodeId))] : this.selection);
|
|
3913
|
+
const out = /* @__PURE__ */ new Map();
|
|
3914
|
+
for (const id of roots) {
|
|
3915
|
+
const node = this.getNode(id);
|
|
3916
|
+
if (node && !node.locked) {
|
|
3917
|
+
for (const p of collectPathNodes(node)) if (!p.locked) out.set(p.id, p);
|
|
3918
|
+
}
|
|
3919
|
+
}
|
|
3920
|
+
const order = [];
|
|
3921
|
+
walkVectorNodes(this.doc.nodes, (n) => {
|
|
3922
|
+
if (out.has(n.id)) order.push(n.id);
|
|
3923
|
+
});
|
|
3924
|
+
return order.map((id) => out.get(id));
|
|
3925
|
+
}
|
|
3926
|
+
/** Subpaths of node `from` mapped into node `to`'s own space. */
|
|
3927
|
+
subpathsIn(from, to) {
|
|
3928
|
+
const m = multiplyMatrices(this.worldOf(from.id), invertMatrix(this.worldOf(to)));
|
|
3929
|
+
if (isRoot(m)) return from.subpaths;
|
|
3930
|
+
const map = (p) => applyMatrix(m, p);
|
|
3931
|
+
return from.subpaths.map((sp) => ({
|
|
3932
|
+
closed: sp.closed,
|
|
3933
|
+
anchors: sp.anchors.map((a) => ({ ...map(a), ...a.in ? { in: map(a.in) } : {}, ...a.out ? { out: map(a.out) } : {}, ...a.type ? { type: a.type } : {} }))
|
|
3934
|
+
}));
|
|
3935
|
+
}
|
|
3936
|
+
/**
|
|
3937
|
+
* Replaces a path with pieces: each entry becomes one path (the first keeps the node's id and
|
|
3938
|
+
* place, the others go just above it with the same style). No pieces removes the path.
|
|
3939
|
+
* Returns the ids of the paths now standing in for it.
|
|
3940
|
+
*/
|
|
3941
|
+
replaceWithPieces(node, pieces) {
|
|
3942
|
+
const nodes = pieces.filter((p) => p.length).map((subpaths, i) => ({ ...node, id: i === 0 ? node.id : this.newId(), subpaths }));
|
|
3943
|
+
this.setDoc(spliceTree(this.doc.nodes, node.id, nodes));
|
|
3944
|
+
return nodes.map((n) => n.id);
|
|
3945
|
+
}
|
|
3946
|
+
/** Clears anchor selections, editing and pen state that no longer point at anything. */
|
|
3947
|
+
dropStaleRefs() {
|
|
3948
|
+
this.selection = this.selection.filter((id) => this.getNode(id));
|
|
3949
|
+
this.anchors = this.anchors.filter((r) => getAnchor(this.doc, r));
|
|
3950
|
+
if (this.editingId && !this.getNode(this.editingId)) this.editingId = null;
|
|
3951
|
+
if (this.pen && !this.getNode(this.pen.nodeId)) this.pen = null;
|
|
3952
|
+
}
|
|
3953
|
+
/**
|
|
3954
|
+
* Join: the open subpaths of the selected (or given) paths become one open path, joined
|
|
3955
|
+
* nearest endpoints first (ends within `tolerance` document units merge into one anchor,
|
|
3956
|
+
* others get a straight segment); a single open subpath is closed. The result replaces the
|
|
3957
|
+
* lowest path, in its space and style. Default tolerance: the pick distance.
|
|
3958
|
+
*/
|
|
3959
|
+
joinPaths(options = {}) {
|
|
3960
|
+
const paths = this.pathTargets(options.ids);
|
|
3961
|
+
if (!paths.length) return { ok: false, message: "Select paths to join." };
|
|
3962
|
+
const base = paths[0];
|
|
3963
|
+
const all = paths.flatMap((p) => this.subpathsIn(p, base.id));
|
|
3964
|
+
if (!all.some((sp) => !sp.closed && sp.anchors.length > 1)) return { ok: false, message: "There are no open paths to join." };
|
|
3965
|
+
const subpaths = joinSubpaths(all, options.tolerance ?? this.tolerance());
|
|
3966
|
+
const drop = new Set(paths.slice(1).map((p) => p.id));
|
|
3967
|
+
this.change("Join", () => {
|
|
3968
|
+
this.setDoc(mapTree(this.doc.nodes, (n) => n.id === base.id ? { ...base, subpaths } : drop.has(n.id) ? null : n));
|
|
3969
|
+
this.selection = [base.id];
|
|
3970
|
+
this.anchors = [];
|
|
3971
|
+
if (this.editingId && drop.has(this.editingId)) this.editingId = base.id;
|
|
3972
|
+
this.dropStaleRefs();
|
|
3973
|
+
});
|
|
3974
|
+
return { ok: true };
|
|
3975
|
+
}
|
|
3976
|
+
/** Average: moves the selected (or given) anchors onto their average position along `axis`, in document space. */
|
|
3977
|
+
averageAnchors(axis = "both", refs = this.anchors) {
|
|
3978
|
+
const points = refs.map((r) => ({ ref: r, at: getAnchorWorld(this.doc, r) })).filter((p) => !!p.at);
|
|
3979
|
+
if (points.length < 2) return { ok: false, message: "Select at least two points to average." };
|
|
3980
|
+
const cx = points.reduce((s, p) => s + p.at.x, 0) / points.length;
|
|
3981
|
+
const cy = points.reduce((s, p) => s + p.at.y, 0) / points.length;
|
|
3982
|
+
this.change("Average", () => {
|
|
3983
|
+
for (const { ref, at } of points) {
|
|
3984
|
+
const dx = axis === "horizontal" ? 0 : cx - at.x;
|
|
3985
|
+
const dy = axis === "vertical" ? 0 : cy - at.y;
|
|
3986
|
+
if (dx || dy) this.doc = this.movedAnchors(this.doc, [ref], dx, dy);
|
|
3987
|
+
}
|
|
3988
|
+
});
|
|
3989
|
+
return { ok: true };
|
|
3990
|
+
}
|
|
3991
|
+
/** Applies `fn` to every subpath of the target paths as one undo step. */
|
|
3992
|
+
editSubpaths(label, ids, fn) {
|
|
3993
|
+
const paths = this.pathTargets(ids);
|
|
3994
|
+
if (!paths.length) return { ok: false, message: "Select a path." };
|
|
3995
|
+
const set = new Set(paths.map((p) => p.id));
|
|
3996
|
+
this.change(label, () => {
|
|
3997
|
+
this.setDoc(mapTree(this.doc.nodes, (n) => n.type === "path" && set.has(n.id) ? { ...n, subpaths: n.subpaths.map((sp, i) => fn(sp, n, i)) } : n));
|
|
3998
|
+
this.dropStaleRefs();
|
|
3999
|
+
});
|
|
4000
|
+
return { ok: true };
|
|
4001
|
+
}
|
|
4002
|
+
/**
|
|
4003
|
+
* Remove Redundant Points: drops anchors of the selected (or given) paths that can go
|
|
4004
|
+
* without moving the outline more than `tolerance` document units (default 0.5).
|
|
4005
|
+
*/
|
|
4006
|
+
removeRedundantPoints(options = {}) {
|
|
4007
|
+
const tolerance = options.tolerance ?? 0.5;
|
|
4008
|
+
return this.editSubpaths("Remove Redundant Points", options.ids, (sp) => removeRedundantPoints(sp, tolerance));
|
|
4009
|
+
}
|
|
4010
|
+
/**
|
|
4011
|
+
* Smooth: pulls handles towards smooth tangents by `amount` (0–1, default 1). With the direct
|
|
4012
|
+
* tool and points selected, only those points change.
|
|
4013
|
+
*/
|
|
4014
|
+
smoothPaths(options = {}) {
|
|
4015
|
+
const refs = options.refs ?? (!options.ids && this.tool === "direct" && this.anchors.length ? this.anchors : null);
|
|
4016
|
+
const ids = options.ids ?? (refs ? [...new Set(refs.map((r) => r.nodeId))] : void 0);
|
|
4017
|
+
return this.editSubpaths("Smooth", ids, (sp, node, i) => {
|
|
4018
|
+
const only = refs ? new Set(refs.filter((r) => r.nodeId === node.id && r.subpath === i).map((r) => r.index)) : void 0;
|
|
4019
|
+
return only && !only.size ? sp : smoothSubpath(sp, options.amount ?? 1, only);
|
|
4020
|
+
});
|
|
4021
|
+
}
|
|
4022
|
+
/** Splits a cut subpath into pieces: separate paths for a one-subpath node, subpaths in place for a compound one. */
|
|
4023
|
+
applyCuts(node, cut) {
|
|
4024
|
+
const results = node.subpaths.map((sp, i) => cut(sp, i));
|
|
4025
|
+
if (results.every((r) => !r)) return null;
|
|
4026
|
+
const subpaths = node.subpaths.flatMap((sp, i) => results[i] ?? [sp]);
|
|
4027
|
+
if (node.subpaths.length === 1) return this.replaceWithPieces(node, subpaths.map((sp) => [sp]));
|
|
4028
|
+
this.replaceNode(node.id, { ...node, subpaths });
|
|
4029
|
+
return [node.id];
|
|
4030
|
+
}
|
|
4031
|
+
/**
|
|
4032
|
+
* Scissors: cuts path `nodeId` at the outline point nearest document point `point` (at an
|
|
4033
|
+
* anchor when within the pick distance of one). A closed subpath opens there; an open one
|
|
4034
|
+
* splits in two (a one-subpath path becomes two paths). Returns the ids of the paths.
|
|
4035
|
+
*/
|
|
4036
|
+
cutPathAt(nodeId, point) {
|
|
4037
|
+
const node = this.getNode(nodeId);
|
|
4038
|
+
if (node?.type !== "path" || node.locked) return null;
|
|
4039
|
+
const local = this.toLocal(nodeId, point);
|
|
4040
|
+
const hit = nearestSegment(node, local);
|
|
4041
|
+
if (!hit) return null;
|
|
4042
|
+
const sp = node.subpaths[hit.subpath];
|
|
4043
|
+
const n = sp.anchors.length;
|
|
4044
|
+
const near2 = [hit.segment, (hit.segment + 1) % n].find((i) => Math.hypot(sp.anchors[i].x - hit.point.x, sp.anchors[i].y - hit.point.y) <= this.tolerance());
|
|
4045
|
+
if (!sp.closed && n > 1 && (near2 === 0 || near2 === n - 1) && !(hit.t > 1e-6 && hit.t < 1 - 1e-6)) return null;
|
|
4046
|
+
let ids = null;
|
|
4047
|
+
this.change("Cut Path", () => {
|
|
4048
|
+
ids = this.applyCuts(node, (s, i) => i !== hit.subpath ? null : near2 !== void 0 ? cutSubpathAt(s, [near2]) : splitSubpathAt(s, hit.segment, hit.t));
|
|
4049
|
+
if (ids) {
|
|
4050
|
+
this.selection = ids;
|
|
4051
|
+
this.anchors = [];
|
|
4052
|
+
if (this.editingId === nodeId) this.editingId = null;
|
|
4053
|
+
}
|
|
4054
|
+
this.dropStaleRefs();
|
|
4055
|
+
});
|
|
4056
|
+
return ids;
|
|
4057
|
+
}
|
|
4058
|
+
/** Scissors on the selected (or given) anchors: each cuts its subpath there (see `cutPathAt`). */
|
|
4059
|
+
cutAtAnchors(refs = this.anchors) {
|
|
4060
|
+
const byNode = /* @__PURE__ */ new Map();
|
|
4061
|
+
for (const r of refs) if (getAnchor(this.doc, r)) byNode.set(r.nodeId, [...byNode.get(r.nodeId) ?? [], r]);
|
|
4062
|
+
if (!byNode.size) return { ok: false, message: "Select points to cut at." };
|
|
4063
|
+
const created = [];
|
|
4064
|
+
this.change("Cut Path", () => {
|
|
4065
|
+
for (const [id, list] of byNode) {
|
|
4066
|
+
const node = this.getNode(id);
|
|
4067
|
+
if (node.locked) continue;
|
|
4068
|
+
const ids = this.applyCuts(node, (sp, i) => {
|
|
4069
|
+
const at = list.filter((r) => r.subpath === i).map((r) => r.index);
|
|
4070
|
+
const pieces = at.length ? cutSubpathAt(sp, at) : null;
|
|
4071
|
+
return pieces && !(pieces.length === 1 && pieces[0].closed === sp.closed) ? pieces : null;
|
|
4072
|
+
});
|
|
4073
|
+
if (ids) created.push(...ids);
|
|
4074
|
+
}
|
|
4075
|
+
if (created.length) this.selection = created;
|
|
4076
|
+
this.anchors = [];
|
|
4077
|
+
this.dropStaleRefs();
|
|
4078
|
+
});
|
|
4079
|
+
return created.length ? { ok: true } : { ok: false, message: "Points at the ends of open paths do not cut." };
|
|
4080
|
+
}
|
|
4081
|
+
/** Visible, unlocked paths a stroke over `points` can reach: in `ids`, the selection, or (with neither) any whose bounds it crosses. */
|
|
4082
|
+
strokeTargets(points, ids, pad) {
|
|
4083
|
+
const blocked = /* @__PURE__ */ new Set();
|
|
4084
|
+
walkVectorNodes(this.doc.nodes, (n, parent) => {
|
|
4085
|
+
if (n.visible === false || n.locked || parent && blocked.has(parent.id)) blocked.add(n.id);
|
|
4086
|
+
});
|
|
4087
|
+
const visible = (p) => !blocked.has(p.id);
|
|
4088
|
+
if (ids?.length || this.selection.length) return this.pathTargets(ids ?? this.selection).filter(visible);
|
|
4089
|
+
const box = polylineBounds(points.map(toPoint2), pad);
|
|
4090
|
+
if (!box) return [];
|
|
4091
|
+
const out = [];
|
|
4092
|
+
walkVectorNodes(this.doc.nodes, (n) => {
|
|
4093
|
+
if (n.type !== "path" || !visible(n)) return;
|
|
4094
|
+
const b = vectorNodeBounds(n, vectorParentMatrix(this.doc.nodes, n.id) ?? IDENTITY_MATRIX);
|
|
4095
|
+
if (b && boundsIntersect(b, box)) out.push(n);
|
|
4096
|
+
});
|
|
4097
|
+
return out;
|
|
4098
|
+
}
|
|
4099
|
+
/** A stroke's points in node `id`'s own space. */
|
|
4100
|
+
localPoints(id, points) {
|
|
4101
|
+
const inv = invertMatrix(this.worldOf(id));
|
|
4102
|
+
return points.map((p) => toPoint2(applyMatrix(inv, p)));
|
|
4103
|
+
}
|
|
4104
|
+
/**
|
|
4105
|
+
* Knife: cuts closed paths along the polyline `points` (document space) wherever it crosses
|
|
4106
|
+
* them from edge to edge, with no gap between the pieces. It cuts the given paths, the
|
|
4107
|
+
* selection, or (with neither) every path it crosses. Returns the ids of the pieces (empty
|
|
4108
|
+
* when nothing was cut).
|
|
4109
|
+
*/
|
|
4110
|
+
knife(points, options = {}) {
|
|
4111
|
+
const pts = simplifyPolyline(points.map(toPoint2), 0.25 / this.viewport.zoom).map(([x, y]) => ({ x, y }));
|
|
4112
|
+
if (pts.length < 2) return [];
|
|
4113
|
+
const plan = [];
|
|
4114
|
+
for (const node of this.strokeTargets(pts, options.ids, 0)) {
|
|
4115
|
+
const closed = node.subpaths.filter((sp) => sp.anchors.length > 1);
|
|
4116
|
+
if (!closed.length || closed.some((sp) => !sp.closed)) continue;
|
|
4117
|
+
const result = knifePath(subpathsToBezier(closed), node.fillRule ?? "nonzero", this.localPoints(node.id, pts));
|
|
4118
|
+
if (result) plan.push({ node, pieces: result.map((path) => bezierToSubpaths(path)) });
|
|
4119
|
+
}
|
|
4120
|
+
if (!plan.length) return [];
|
|
4121
|
+
const created = [];
|
|
4122
|
+
this.change("Knife", () => {
|
|
4123
|
+
for (const { node, pieces } of plan) created.push(...this.replaceWithPieces(node, pieces));
|
|
4124
|
+
this.selection = created;
|
|
4125
|
+
this.anchors = [];
|
|
4126
|
+
this.dropStaleRefs();
|
|
4127
|
+
});
|
|
4128
|
+
return created;
|
|
4129
|
+
}
|
|
4130
|
+
/**
|
|
4131
|
+
* Eraser: rubs out a round stroke of diameter `size` (default: the `eraserSize` setting)
|
|
4132
|
+
* along `points` (document space). Filled closed paths lose that area (pieces that come
|
|
4133
|
+
* apart become separate paths); outlines are cut where they pass under it. It erases from
|
|
4134
|
+
* the given paths, the selection, or (with neither) every path it reaches. Returns the ids
|
|
4135
|
+
* of the paths it changed or created.
|
|
4136
|
+
*/
|
|
4137
|
+
erase(points, options = {}) {
|
|
4138
|
+
const width = options.size ?? this.editing.eraserSize;
|
|
4139
|
+
if (!(width > 0) || !points.length) return [];
|
|
4140
|
+
const pts = simplifyPolyline(points.map(toPoint2), Math.min(width * 0.05, 0.25 / this.viewport.zoom));
|
|
4141
|
+
const region = eraserRegion(pts, width);
|
|
4142
|
+
if (!region.length) return [];
|
|
4143
|
+
const plan = [];
|
|
4144
|
+
const rb = bezierPathBounds(region);
|
|
4145
|
+
const reach = rb ? [{ x: rb.minX, y: rb.minY }, { x: rb.maxX, y: rb.maxY }] : points;
|
|
4146
|
+
for (const node of this.strokeTargets(reach, options.ids, 0)) {
|
|
4147
|
+
const local = transformBezierPath(region, invertMatrix(this.worldOf(node.id)));
|
|
4148
|
+
const live = node.subpaths.filter((sp) => sp.anchors.length > 1);
|
|
4149
|
+
if (!live.length) continue;
|
|
4150
|
+
if (hasPaint(node.fill) && live.every((sp) => sp.closed)) {
|
|
4151
|
+
const faces = eraseFromFill(subpathsToBezier(live), node.fillRule ?? "nonzero", local);
|
|
4152
|
+
if (faces) plan.push({ node, pieces: faces.map((f) => bezierToSubpaths(f)) });
|
|
4153
|
+
continue;
|
|
4154
|
+
}
|
|
4155
|
+
const results = live.map((sp) => eraseFromOutline(sp, local));
|
|
4156
|
+
if (results.every((r) => !r)) continue;
|
|
4157
|
+
const subpaths = live.flatMap((sp, i) => results[i] ?? [sp]);
|
|
4158
|
+
plan.push({ node, pieces: live.length === 1 ? subpaths.map((sp) => [sp]) : [subpaths] });
|
|
4159
|
+
}
|
|
4160
|
+
if (!plan.length) return [];
|
|
4161
|
+
const changed = [];
|
|
4162
|
+
this.change("Erase", () => {
|
|
4163
|
+
for (const { node, pieces } of plan) changed.push(...this.replaceWithPieces(node, pieces));
|
|
4164
|
+
this.anchors = [];
|
|
4165
|
+
this.dropStaleRefs();
|
|
4166
|
+
if (this.selection.length || options.ids) this.selection = [.../* @__PURE__ */ new Set([...this.selection, ...changed.filter((id) => this.getNode(id))])];
|
|
4167
|
+
});
|
|
4168
|
+
return changed;
|
|
4169
|
+
}
|
|
2850
4170
|
/** Ends the pen path being drawn (a lone anchor is removed). */
|
|
2851
4171
|
finishPath() {
|
|
2852
4172
|
if (!this.pen) return;
|
|
@@ -3039,10 +4359,38 @@ var VectorEditorController = class {
|
|
|
3039
4359
|
const anchor = this.hitAnchor(screen, refs);
|
|
3040
4360
|
return anchor ? { nodeId: anchor.nodeId, anchor } : null;
|
|
3041
4361
|
}
|
|
4362
|
+
case "scissors": {
|
|
4363
|
+
const seg = this.nearestOutline(p);
|
|
4364
|
+
return seg ? { nodeId: seg.nodeId, segment: seg } : null;
|
|
4365
|
+
}
|
|
3042
4366
|
default:
|
|
3043
4367
|
return null;
|
|
3044
4368
|
}
|
|
3045
4369
|
}
|
|
4370
|
+
/** The nearest outline point of any visible, unlocked path within the pick distance of document point `p`. */
|
|
4371
|
+
nearestOutline(p) {
|
|
4372
|
+
let best = null;
|
|
4373
|
+
const tol = this.tolerance();
|
|
4374
|
+
const visit = (list, parent) => {
|
|
4375
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
4376
|
+
const n = list[i];
|
|
4377
|
+
if (n.visible === false || n.locked) continue;
|
|
4378
|
+
const m = multiplyMatrices(nodeMatrix(n), parent);
|
|
4379
|
+
if (n.type === "group") {
|
|
4380
|
+
visit(n.children, m);
|
|
4381
|
+
continue;
|
|
4382
|
+
}
|
|
4383
|
+
const hit = nearestSegment(n, applyMatrix(invertMatrix(m), p));
|
|
4384
|
+
if (hit && hit.distance <= tol && (!best || hit.distance < best.distance - 1e-9)) {
|
|
4385
|
+
best = { nodeId: n.id, subpath: hit.subpath, segment: hit.segment, t: hit.t, point: applyMatrix(m, hit.point), distance: hit.distance };
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
};
|
|
4389
|
+
visit(this.doc.nodes, IDENTITY_MATRIX);
|
|
4390
|
+
if (!best) return null;
|
|
4391
|
+
const { distance: _d, ...seg } = best;
|
|
4392
|
+
return seg;
|
|
4393
|
+
}
|
|
3046
4394
|
sameHover(a, b) {
|
|
3047
4395
|
return JSON.stringify(a) === JSON.stringify(b);
|
|
3048
4396
|
}
|
|
@@ -3067,6 +4415,17 @@ var VectorEditorController = class {
|
|
|
3067
4415
|
return this.downPen(screen, p, input);
|
|
3068
4416
|
case "pencil":
|
|
3069
4417
|
return this.downPencil(p);
|
|
4418
|
+
case "scissors": {
|
|
4419
|
+
const seg = this.nearestOutline(p);
|
|
4420
|
+
if (seg) this.cutPathAt(seg.nodeId, seg.point);
|
|
4421
|
+
return;
|
|
4422
|
+
}
|
|
4423
|
+
case "knife":
|
|
4424
|
+
case "eraser":
|
|
4425
|
+
this.drag = { kind: "stroke", tool: this.tool, points: [p], before: this.snapshot() };
|
|
4426
|
+
this.interaction = { type: "stroke", tool: this.tool, points: [p], width: this.tool === "eraser" ? this.editing.eraserSize : 0 };
|
|
4427
|
+
this.notify();
|
|
4428
|
+
return;
|
|
3070
4429
|
default:
|
|
3071
4430
|
return this.downShape(p, input);
|
|
3072
4431
|
}
|
|
@@ -3161,7 +4520,7 @@ var VectorEditorController = class {
|
|
|
3161
4520
|
const b = { nodeId: id, subpath: seg.subpath, index: (seg.segment + 1) % sp.anchors.length };
|
|
3162
4521
|
this.anchors = input.shift ? [...this.anchors.filter((r) => !sameRef(r, a) && !sameRef(r, b)), a, b] : [a, b];
|
|
3163
4522
|
this.editingId = id;
|
|
3164
|
-
this.drag = { kind: "
|
|
4523
|
+
this.drag = { kind: "bend", ref: { nodeId: id, subpath: seg.subpath, segment: seg.segment }, t: seg.t, start: p, before: this.snapshot(), base: this.doc, moved: false };
|
|
3165
4524
|
this.notify();
|
|
3166
4525
|
return;
|
|
3167
4526
|
}
|
|
@@ -3253,7 +4612,9 @@ var VectorEditorController = class {
|
|
|
3253
4612
|
subpaths: [{ anchors: [{ x: p.x, y: p.y }], closed: false }]
|
|
3254
4613
|
};
|
|
3255
4614
|
this.setDoc([...this.doc.nodes, node]);
|
|
3256
|
-
|
|
4615
|
+
const lazy = new LazyMouse(this.editing.pencilSmoothing * VECTOR_PENCIL_STRING_PX / this.viewport.zoom);
|
|
4616
|
+
lazy.push([p.x, p.y]);
|
|
4617
|
+
this.drag = { kind: "pencil", nodeId: node.id, points: [[p.x, p.y]], before, lazy };
|
|
3257
4618
|
this.interaction = { type: "draw", tool: "pencil", nodeId: node.id, bounds: { x: p.x, y: p.y, width: 0, height: 0 } };
|
|
3258
4619
|
this.notify(true);
|
|
3259
4620
|
}
|
|
@@ -3365,7 +4726,10 @@ var VectorEditorController = class {
|
|
|
3365
4726
|
dx = q.x - lead.x;
|
|
3366
4727
|
dy = q.y - lead.y;
|
|
3367
4728
|
} else if (input.shift) Math.abs(dx) > Math.abs(dy) ? dy = 0 : dx = 0;
|
|
3368
|
-
|
|
4729
|
+
const single = this.anchors.length === 1 ? this.anchors[0] : null;
|
|
4730
|
+
const from = single ? getAnchorWorld(drag.base, single) : void 0;
|
|
4731
|
+
if (single && from && this.editing.curvatureMoves !== !!input.alt) this.doc = this.curvatureMoved(drag.base, single, { x: from.x + dx, y: from.y + dy });
|
|
4732
|
+
else this.doc = this.movedAnchors(drag.base, this.anchors, dx, dy);
|
|
3369
4733
|
this.interaction = { type: "anchors", dx, dy };
|
|
3370
4734
|
this.notify(true);
|
|
3371
4735
|
return;
|
|
@@ -3380,6 +4744,22 @@ var VectorEditorController = class {
|
|
|
3380
4744
|
this.notify(true);
|
|
3381
4745
|
return;
|
|
3382
4746
|
}
|
|
4747
|
+
case "bend": {
|
|
4748
|
+
if (!drag.moved && Math.hypot(p.x - drag.start.x, p.y - drag.start.y) * this.viewport.zoom < 2) return;
|
|
4749
|
+
drag.moved = true;
|
|
4750
|
+
this.doc = this.bent(drag.base, drag.ref, drag.t, p);
|
|
4751
|
+
this.interaction = { type: "bend", ref: drag.ref };
|
|
4752
|
+
this.notify(true);
|
|
4753
|
+
return;
|
|
4754
|
+
}
|
|
4755
|
+
case "stroke": {
|
|
4756
|
+
const last = drag.points[drag.points.length - 1];
|
|
4757
|
+
if (Math.hypot(p.x - last.x, p.y - last.y) * this.viewport.zoom < 1.5) return;
|
|
4758
|
+
drag.points.push(p);
|
|
4759
|
+
this.interaction = { type: "stroke", tool: drag.tool, points: drag.points.slice(), width: drag.tool === "eraser" ? this.editing.eraserSize : 0 };
|
|
4760
|
+
this.notify();
|
|
4761
|
+
return;
|
|
4762
|
+
}
|
|
3383
4763
|
case "pull": {
|
|
3384
4764
|
const anchor = getAnchorWorld(drag.base, drag.ref);
|
|
3385
4765
|
if (!anchor) return;
|
|
@@ -3433,10 +4813,22 @@ var VectorEditorController = class {
|
|
|
3433
4813
|
}
|
|
3434
4814
|
case "pencil": {
|
|
3435
4815
|
const last = drag.points[drag.points.length - 1];
|
|
3436
|
-
|
|
3437
|
-
drag.
|
|
4816
|
+
let moved = false;
|
|
4817
|
+
for (const q of drag.lazy.push([p.x, p.y])) {
|
|
4818
|
+
if (Math.hypot(q[0] - last[0], q[1] - last[1]) * this.viewport.zoom < 1.5) continue;
|
|
4819
|
+
drag.points.push(q);
|
|
4820
|
+
moved = true;
|
|
4821
|
+
}
|
|
4822
|
+
const string = drag.lazy.string();
|
|
3438
4823
|
const node = this.getNode(drag.nodeId);
|
|
3439
4824
|
if (node?.type !== "path") return;
|
|
4825
|
+
if (!moved) {
|
|
4826
|
+
if (string && this.interaction?.type === "draw") {
|
|
4827
|
+
this.interaction = { ...this.interaction, string: [{ x: string[0][0], y: string[0][1] }, { x: string[1][0], y: string[1][1] }] };
|
|
4828
|
+
this.notify();
|
|
4829
|
+
}
|
|
4830
|
+
return;
|
|
4831
|
+
}
|
|
3440
4832
|
this.replaceNode(drag.nodeId, { ...node, subpaths: [{ anchors: drag.points.map(([x, y]) => ({ x, y })), closed: false }] });
|
|
3441
4833
|
const xs = drag.points.map((q) => q[0]);
|
|
3442
4834
|
const ys = drag.points.map((q) => q[1]);
|
|
@@ -3444,7 +4836,8 @@ var VectorEditorController = class {
|
|
|
3444
4836
|
type: "draw",
|
|
3445
4837
|
tool: "pencil",
|
|
3446
4838
|
nodeId: drag.nodeId,
|
|
3447
|
-
bounds: { x: Math.min(...xs), y: Math.min(...ys), width: Math.max(...xs) - Math.min(...xs), height: Math.max(...ys) - Math.min(...ys) }
|
|
4839
|
+
bounds: { x: Math.min(...xs), y: Math.min(...ys), width: Math.max(...xs) - Math.min(...xs), height: Math.max(...ys) - Math.min(...ys) },
|
|
4840
|
+
...string ? { string: [{ x: string[0][0], y: string[0][1] }, { x: string[1][0], y: string[1][1] }] } : {}
|
|
3448
4841
|
};
|
|
3449
4842
|
this.notify(true);
|
|
3450
4843
|
return;
|
|
@@ -3536,6 +4929,18 @@ var VectorEditorController = class {
|
|
|
3536
4929
|
return this.commit(drag.before.doc !== drag.base ? "Add Point" : "Move Points", drag.before);
|
|
3537
4930
|
case "handle":
|
|
3538
4931
|
return this.commit("Move Handle", drag.before);
|
|
4932
|
+
case "bend":
|
|
4933
|
+
if (!drag.moved) {
|
|
4934
|
+
this.notify();
|
|
4935
|
+
return;
|
|
4936
|
+
}
|
|
4937
|
+
return this.commit("Bend Segment", drag.before);
|
|
4938
|
+
case "stroke": {
|
|
4939
|
+
this.notify();
|
|
4940
|
+
if (drag.tool === "knife") this.knife(drag.points);
|
|
4941
|
+
else this.erase(drag.points);
|
|
4942
|
+
return;
|
|
4943
|
+
}
|
|
3539
4944
|
case "pull":
|
|
3540
4945
|
if (drag.closing) this.pen = null;
|
|
3541
4946
|
return this.commit(drag.label, drag.before);
|
|
@@ -3560,6 +4965,13 @@ var VectorEditorController = class {
|
|
|
3560
4965
|
this.notify(true);
|
|
3561
4966
|
return;
|
|
3562
4967
|
}
|
|
4968
|
+
for (const q of drag.lazy.finish()) drag.points.push(q);
|
|
4969
|
+
const shape = this.editing.recognizeShapes ? recognizeInkShape(drag.points, { minSize: 6 / this.viewport.zoom }) : null;
|
|
4970
|
+
if (shape) {
|
|
4971
|
+
this.replaceNode(drag.nodeId, { ...node, name: inkShapeLabel(shape), subpaths: [inkShapeSubpath(shape)] });
|
|
4972
|
+
this.selection = [drag.nodeId];
|
|
4973
|
+
return this.commit(`Draw ${inkShapeLabel(shape)}`, drag.before);
|
|
4974
|
+
}
|
|
3563
4975
|
const fitted = fitCubics(drag.points, 1.5 / this.viewport.zoom, { cornerAngleDeg: 60 });
|
|
3564
4976
|
const anchors = [];
|
|
3565
4977
|
for (const c of fitted) {
|
|
@@ -3666,6 +5078,8 @@ var VectorEditorController = class {
|
|
|
3666
5078
|
case "g":
|
|
3667
5079
|
input.shift ? this.ungroup() : this.group();
|
|
3668
5080
|
return true;
|
|
5081
|
+
case "j":
|
|
5082
|
+
return input.alt ? this.averageAnchors("both").ok : this.joinPaths().ok;
|
|
3669
5083
|
case "]":
|
|
3670
5084
|
this.arrange(input.alt ? "front" : "forward");
|
|
3671
5085
|
return true;
|
|
@@ -3746,6 +5160,22 @@ var VectorEditorController = class {
|
|
|
3746
5160
|
}
|
|
3747
5161
|
}
|
|
3748
5162
|
};
|
|
5163
|
+
function inkShapeLabel(shape) {
|
|
5164
|
+
const name = vectorInkShapeName(shape);
|
|
5165
|
+
return name[0].toUpperCase() + name.slice(1);
|
|
5166
|
+
}
|
|
5167
|
+
function inkShapeSubpath(shape) {
|
|
5168
|
+
switch (shape.kind) {
|
|
5169
|
+
case "line":
|
|
5170
|
+
return lineSubpath({ x: shape.a[0], y: shape.a[1] }, { x: shape.b[0], y: shape.b[1] });
|
|
5171
|
+
case "rect":
|
|
5172
|
+
return rectSubpath(shape);
|
|
5173
|
+
case "ellipse":
|
|
5174
|
+
return ellipseSubpath(shape);
|
|
5175
|
+
case "polygon":
|
|
5176
|
+
return { anchors: shape.points.map(([x, y]) => ({ x, y })), closed: true };
|
|
5177
|
+
}
|
|
5178
|
+
}
|
|
3749
5179
|
function constrain45(origin, p) {
|
|
3750
5180
|
const dx = p.x - origin.x;
|
|
3751
5181
|
const dy = p.y - origin.y;
|
|
@@ -3917,7 +5347,20 @@ function getVectorOverlayRenderData(state) {
|
|
|
3917
5347
|
guides.labels.push({ kind: l.kind, x: fmt(p.x), y: fmt(p.y), text: l.text });
|
|
3918
5348
|
}
|
|
3919
5349
|
}
|
|
3920
|
-
|
|
5350
|
+
let toolStroke = null;
|
|
5351
|
+
if (it?.type === "stroke") {
|
|
5352
|
+
const pts = it.points.map((q) => toScreen(state, q));
|
|
5353
|
+
const d = pts.map((q, i) => `${i ? "L" : "M"}${fmt(q.x)} ${fmt(q.y)}`).join("") + (pts.length === 1 ? `L${fmt(pts[0].x)} ${fmt(pts[0].y)}` : "");
|
|
5354
|
+
toolStroke = { tool: it.tool, d, width: fmt(it.width * z) };
|
|
5355
|
+
}
|
|
5356
|
+
let pencilString = null;
|
|
5357
|
+
if (it?.type === "draw" && it.string) {
|
|
5358
|
+
const a = toScreen(state, it.string[0]);
|
|
5359
|
+
const b = toScreen(state, it.string[1]);
|
|
5360
|
+
pencilString = { x1: fmt(a.x), y1: fmt(a.y), x2: fmt(b.x), y2: fmt(b.y) };
|
|
5361
|
+
}
|
|
5362
|
+
const cutPoint = state.tool === "scissors" && state.hover?.segment && !it ? (({ x, y }) => ({ x: fmt(x), y: fmt(y) }))(toScreen(state, state.hover.segment.point)) : null;
|
|
5363
|
+
return { outlines, selectionBox, anchors, handles, marquee, penPreview, artboard, guides, status, toolStroke, pencilString, cutPoint };
|
|
3921
5364
|
}
|
|
3922
5365
|
var VECTOR_OVERLAY_THEME = {
|
|
3923
5366
|
accent: "#0ea5e9",
|
|
@@ -3925,6 +5368,8 @@ var VECTOR_OVERLAY_THEME = {
|
|
|
3925
5368
|
guide: "#ff3dfc",
|
|
3926
5369
|
handleFill: "#ffffff",
|
|
3927
5370
|
marqueeFill: "rgba(14, 165, 233, 0.08)",
|
|
5371
|
+
/** The eraser's swath while it is dragged. */
|
|
5372
|
+
eraserFill: "rgba(239, 68, 68, 0.25)",
|
|
3928
5373
|
transformHandleSize: 8,
|
|
3929
5374
|
anchorSize: 7,
|
|
3930
5375
|
handleRadius: 3.5
|
|
@@ -3970,12 +5415,39 @@ function vectorPaintToHex(paint) {
|
|
|
3970
5415
|
const hex = m[1].length === 3 ? m[1].replace(/./g, (c) => c + c) : m[1];
|
|
3971
5416
|
return `#${hex.toLowerCase()}`;
|
|
3972
5417
|
}
|
|
5418
|
+
var VECTOR_PATH_ACTIONS = [
|
|
5419
|
+
{ id: "join", label: "\u22C8", title: "Join", shortcut: "J", enabled: (s, p) => s + p > 0, run: (c) => c.joinPaths() },
|
|
5420
|
+
{ id: "average", label: "\u22B9", title: "Average Points", shortcut: "Alt+J", enabled: (_s, p) => p > 1, run: (c) => c.averageAnchors("both") },
|
|
5421
|
+
{ id: "cut", label: "\u2702\xB7", title: "Cut at Points", enabled: (_s, p) => p > 0, run: (c) => c.cutAtAnchors() },
|
|
5422
|
+
{ id: "clean", label: "\u2301", title: "Remove Redundant Points", enabled: (s, p) => s + p > 0, run: (c) => c.removeRedundantPoints() },
|
|
5423
|
+
{ id: "smooth", label: "\u223F", title: "Smooth", enabled: (s, p) => s + p > 0, run: (c) => c.smoothPaths() }
|
|
5424
|
+
];
|
|
5425
|
+
var VECTOR_EDIT_TOGGLES = [
|
|
5426
|
+
{ key: "keepShapeOnDelete", label: "Keep", title: "Keep shape when deleting points" },
|
|
5427
|
+
{ key: "curvatureMoves", label: "Curv", title: "Curvature point moves (Alt flips while dragging)" },
|
|
5428
|
+
{ key: "recognizeShapes", label: "Shapes", title: "Turn pencil scribbles into shapes" }
|
|
5429
|
+
];
|
|
3973
5430
|
|
|
3974
5431
|
// src/vector/commands.ts
|
|
3975
5432
|
var fail = (message) => {
|
|
3976
5433
|
throw new Error(message);
|
|
3977
5434
|
};
|
|
3978
5435
|
var IDS = { type: "array", items: { type: "string" }, description: "Node ids (default: the selection)." };
|
|
5436
|
+
var ANCHORS = {
|
|
5437
|
+
type: "array",
|
|
5438
|
+
items: {
|
|
5439
|
+
type: "object",
|
|
5440
|
+
properties: { nodeId: { type: "string" }, subpath: { type: "integer", minimum: 0 }, index: { type: "integer", minimum: 0 } },
|
|
5441
|
+
required: ["nodeId", "subpath", "index"]
|
|
5442
|
+
},
|
|
5443
|
+
description: "Anchors as { nodeId, subpath, index } (default: the selected points)."
|
|
5444
|
+
};
|
|
5445
|
+
var POINTS = {
|
|
5446
|
+
type: "array",
|
|
5447
|
+
items: { type: "array", items: { type: "number" }, minItems: 2, maxItems: 2 },
|
|
5448
|
+
minItems: 1,
|
|
5449
|
+
description: "Polyline [[x, y], \u2026] in document units."
|
|
5450
|
+
};
|
|
3979
5451
|
var PAINT = {
|
|
3980
5452
|
description: 'A CSS colour, `null` for none, or a gradient `{ type: "linear", angle, stops: [{ offset, color }] }`.'
|
|
3981
5453
|
};
|
|
@@ -4008,6 +5480,14 @@ function createVectorCommands(controller, options = {}) {
|
|
|
4008
5480
|
return list2;
|
|
4009
5481
|
};
|
|
4010
5482
|
const enabledFor = (_, params) => params?.ids?.length ? true : hasSelection();
|
|
5483
|
+
const anchorsOf = (refs) => {
|
|
5484
|
+
if (!refs) return [...controller.getState().selectedAnchors];
|
|
5485
|
+
for (const r of refs) {
|
|
5486
|
+
const node = controller.getNode(r.nodeId);
|
|
5487
|
+
if (node?.type !== "path" || !node.subpaths[r.subpath]?.anchors[r.index]) fail(`There is no anchor ${r.index} on subpath ${r.subpath} of "${r.nodeId}".`);
|
|
5488
|
+
}
|
|
5489
|
+
return refs;
|
|
5490
|
+
};
|
|
4011
5491
|
const list = () => {
|
|
4012
5492
|
const state = controller.getState();
|
|
4013
5493
|
const out = [];
|
|
@@ -4246,6 +5726,186 @@ function createVectorCommands(controller, options = {}) {
|
|
|
4246
5726
|
return { selection: controller.getState().selection };
|
|
4247
5727
|
}
|
|
4248
5728
|
},
|
|
5729
|
+
{
|
|
5730
|
+
...base,
|
|
5731
|
+
id: `${prefix}deleteAnchors`,
|
|
5732
|
+
label: "Delete Points",
|
|
5733
|
+
keywords: ["remove", "anchor", "point", "node"],
|
|
5734
|
+
description: "Deletes anchors. With `keepShape` (default: the editor setting, on) the neighbouring handles are refitted so the outline keeps its shape; off, the neighbours are joined as they are.",
|
|
5735
|
+
params: { type: "object", properties: { anchors: ANCHORS, keepShape: { type: "boolean" } } },
|
|
5736
|
+
undoable: true,
|
|
5737
|
+
run: (_ctx, params) => {
|
|
5738
|
+
check(controller.deleteAnchors(anchorsOf(params?.anchors), { keepShape: params?.keepShape }));
|
|
5739
|
+
return { ok: true };
|
|
5740
|
+
}
|
|
5741
|
+
},
|
|
5742
|
+
{
|
|
5743
|
+
...base,
|
|
5744
|
+
id: `${prefix}moveAnchor`,
|
|
5745
|
+
label: "Move Point\u2026",
|
|
5746
|
+
keywords: ["anchor", "drag", "curvature"],
|
|
5747
|
+
description: "Moves one anchor to (x, y) in document units. `curvature: true` bends its two segments like the Curvature tool instead of carrying its handles along.",
|
|
5748
|
+
params: {
|
|
5749
|
+
type: "object",
|
|
5750
|
+
properties: { nodeId: { type: "string" }, subpath: { type: "integer", minimum: 0 }, index: { type: "integer", minimum: 0 }, x: { type: "number" }, y: { type: "number" }, curvature: { type: "boolean" } },
|
|
5751
|
+
required: ["nodeId", "subpath", "index", "x", "y"]
|
|
5752
|
+
},
|
|
5753
|
+
undoable: true,
|
|
5754
|
+
run: (_ctx, params) => {
|
|
5755
|
+
check(controller.moveAnchor({ nodeId: params.nodeId, subpath: params.subpath, index: params.index }, { x: params.x, y: params.y }, { curvature: params.curvature }));
|
|
5756
|
+
return { ok: true };
|
|
5757
|
+
}
|
|
5758
|
+
},
|
|
5759
|
+
{
|
|
5760
|
+
...base,
|
|
5761
|
+
id: `${prefix}bendSegment`,
|
|
5762
|
+
label: "Bend Segment\u2026",
|
|
5763
|
+
keywords: ["curve", "segment", "drag"],
|
|
5764
|
+
description: "Bends segment `segment` (from anchor `segment` to the next) so the curve passes through (x, y) at parameter `t` (default 0.5).",
|
|
5765
|
+
params: {
|
|
5766
|
+
type: "object",
|
|
5767
|
+
properties: {
|
|
5768
|
+
nodeId: { type: "string" },
|
|
5769
|
+
subpath: { type: "integer", minimum: 0 },
|
|
5770
|
+
segment: { type: "integer", minimum: 0 },
|
|
5771
|
+
x: { type: "number" },
|
|
5772
|
+
y: { type: "number" },
|
|
5773
|
+
t: { type: "number", minimum: 0, maximum: 1 }
|
|
5774
|
+
},
|
|
5775
|
+
required: ["nodeId", "subpath", "segment", "x", "y"]
|
|
5776
|
+
},
|
|
5777
|
+
undoable: true,
|
|
5778
|
+
run: (_ctx, params) => {
|
|
5779
|
+
check(controller.bendSegment({ nodeId: params.nodeId, subpath: params.subpath, segment: params.segment }, { x: params.x, y: params.y }, params.t));
|
|
5780
|
+
return { ok: true };
|
|
5781
|
+
}
|
|
5782
|
+
},
|
|
5783
|
+
{
|
|
5784
|
+
...base,
|
|
5785
|
+
id: `${prefix}join`,
|
|
5786
|
+
label: "Join",
|
|
5787
|
+
shortcut: "Mod+J",
|
|
5788
|
+
keywords: ["connect", "close", "merge", "endpoints"],
|
|
5789
|
+
description: "Joins the open subpaths of paths nearest endpoints first (ends within `tolerance` merge); a single open path is closed.",
|
|
5790
|
+
params: { type: "object", properties: { ids: IDS, tolerance: { type: "number", minimum: 0 } } },
|
|
5791
|
+
undoable: true,
|
|
5792
|
+
run: (_ctx, params) => {
|
|
5793
|
+
check(controller.joinPaths({ ids: params?.ids ? ids(params) : void 0, tolerance: params?.tolerance }));
|
|
5794
|
+
return { selection: controller.getState().selection };
|
|
5795
|
+
}
|
|
5796
|
+
},
|
|
5797
|
+
{
|
|
5798
|
+
...base,
|
|
5799
|
+
id: `${prefix}average`,
|
|
5800
|
+
label: "Average Points\u2026",
|
|
5801
|
+
shortcut: "Mod+Alt+J",
|
|
5802
|
+
keywords: ["align", "points", "anchors", "collapse"],
|
|
5803
|
+
description: "Moves anchors onto their average position: the same y (`horizontal`), the same x (`vertical`) or the same point (`both`, default).",
|
|
5804
|
+
params: { type: "object", properties: { axis: { type: "string", enum: ["horizontal", "vertical", "both"] }, anchors: ANCHORS } },
|
|
5805
|
+
undoable: true,
|
|
5806
|
+
run: (_ctx, params) => {
|
|
5807
|
+
check(controller.averageAnchors(params?.axis ?? "both", anchorsOf(params?.anchors)));
|
|
5808
|
+
return { ok: true };
|
|
5809
|
+
}
|
|
5810
|
+
},
|
|
5811
|
+
{
|
|
5812
|
+
...base,
|
|
5813
|
+
id: `${prefix}removeRedundantPoints`,
|
|
5814
|
+
label: "Remove Redundant Points",
|
|
5815
|
+
keywords: ["simplify", "clean up", "anchors", "collinear"],
|
|
5816
|
+
description: "Drops anchors that can go without moving the outline more than `tolerance` document units (default 0.5).",
|
|
5817
|
+
params: { type: "object", properties: { ids: IDS, tolerance: { type: "number", minimum: 0 } } },
|
|
5818
|
+
enabled: enabledFor,
|
|
5819
|
+
undoable: true,
|
|
5820
|
+
run: (_ctx, params) => {
|
|
5821
|
+
check(controller.removeRedundantPoints({ ids: params?.ids ? ids(params) : void 0, tolerance: params?.tolerance }));
|
|
5822
|
+
return { ok: true };
|
|
5823
|
+
}
|
|
5824
|
+
},
|
|
5825
|
+
{
|
|
5826
|
+
...base,
|
|
5827
|
+
id: `${prefix}smooth`,
|
|
5828
|
+
label: "Smooth Path",
|
|
5829
|
+
keywords: ["round", "soften", "handles", "curves"],
|
|
5830
|
+
description: "Pulls the handles of paths (or of the selected points) towards smooth tangents by `amount` (0\u20131, default 1).",
|
|
5831
|
+
params: { type: "object", properties: { ids: IDS, amount: { type: "number", minimum: 0, maximum: 1 }, anchors: ANCHORS } },
|
|
5832
|
+
undoable: true,
|
|
5833
|
+
run: (_ctx, params) => {
|
|
5834
|
+
check(controller.smoothPaths({ ids: params?.ids ? ids(params) : void 0, amount: params?.amount, refs: params?.anchors }));
|
|
5835
|
+
return { ok: true };
|
|
5836
|
+
}
|
|
5837
|
+
},
|
|
5838
|
+
{
|
|
5839
|
+
...base,
|
|
5840
|
+
id: `${prefix}cut`,
|
|
5841
|
+
label: "Cut Path (Scissors)\u2026",
|
|
5842
|
+
keywords: ["scissors", "split", "open", "break"],
|
|
5843
|
+
description: "Scissors: cuts path `nodeId` at the outline point nearest (x, y), or cuts at `anchors`. A closed path opens; an open one splits in two.",
|
|
5844
|
+
params: { type: "object", properties: { nodeId: { type: "string" }, x: { type: "number" }, y: { type: "number" }, anchors: ANCHORS } },
|
|
5845
|
+
undoable: true,
|
|
5846
|
+
run: (_ctx, params) => {
|
|
5847
|
+
if (params?.nodeId !== void 0) {
|
|
5848
|
+
if (!controller.getNode(params.nodeId)) fail(`There is no node "${params.nodeId}".`);
|
|
5849
|
+
if (params.x === void 0 || params.y === void 0) fail("Pass x and y with nodeId.");
|
|
5850
|
+
const created = controller.cutPathAt(params.nodeId, { x: params.x, y: params.y });
|
|
5851
|
+
if (!created) fail("Nothing to cut there (the end of an open path, or a locked path).");
|
|
5852
|
+
return { ids: created };
|
|
5853
|
+
}
|
|
5854
|
+
check(controller.cutAtAnchors(anchorsOf(params?.anchors)));
|
|
5855
|
+
return { ids: controller.getState().selection };
|
|
5856
|
+
}
|
|
5857
|
+
},
|
|
5858
|
+
{
|
|
5859
|
+
...base,
|
|
5860
|
+
id: `${prefix}knife`,
|
|
5861
|
+
label: "Knife\u2026",
|
|
5862
|
+
keywords: ["slice", "cut", "split", "divide"],
|
|
5863
|
+
description: "Cuts closed paths along a polyline wherever it crosses them edge to edge (no gap between pieces). Cuts `ids`, else the selection, else every path the line crosses.",
|
|
5864
|
+
params: { type: "object", properties: { points: { ...POINTS, minItems: 2 }, ids: IDS }, required: ["points"] },
|
|
5865
|
+
undoable: true,
|
|
5866
|
+
run: (_ctx, params) => {
|
|
5867
|
+
const created = controller.knife(params.points.map(([x, y]) => ({ x, y })), { ids: params.ids ? ids(params) : void 0 });
|
|
5868
|
+
if (!created.length) fail("The knife did not cross any closed path from edge to edge.");
|
|
5869
|
+
return { ids: created };
|
|
5870
|
+
}
|
|
5871
|
+
},
|
|
5872
|
+
{
|
|
5873
|
+
...base,
|
|
5874
|
+
id: `${prefix}erase`,
|
|
5875
|
+
label: "Erase\u2026",
|
|
5876
|
+
keywords: ["eraser", "rub out", "remove"],
|
|
5877
|
+
description: "Erases a round stroke of diameter `size` (default: the eraser size setting) along a polyline: filled shapes lose the area, outlines are cut under it. Erases from `ids`, else the selection, else every path it reaches.",
|
|
5878
|
+
params: { type: "object", properties: { points: POINTS, size: { type: "number", exclusiveMinimum: 0 }, ids: IDS }, required: ["points"] },
|
|
5879
|
+
undoable: true,
|
|
5880
|
+
run: (_ctx, params) => {
|
|
5881
|
+
const changed = controller.erase(params.points.map(([x, y]) => ({ x, y })), { size: params.size, ids: params.ids ? ids(params) : void 0 });
|
|
5882
|
+
if (!changed.length) fail("The eraser did not touch any path.");
|
|
5883
|
+
return { ids: changed.filter((id) => controller.getNode(id)) };
|
|
5884
|
+
}
|
|
5885
|
+
},
|
|
5886
|
+
{
|
|
5887
|
+
...base,
|
|
5888
|
+
id: `${prefix}setEditing`,
|
|
5889
|
+
label: "Path Editing Options\u2026",
|
|
5890
|
+
keywords: ["settings", "curvature", "pencil", "smoothing", "lazy mouse", "shapes", "eraser"],
|
|
5891
|
+
description: "Sets how editing behaves: `keepShapeOnDelete`, `curvatureMoves` (anchor drags bend like the Curvature tool), `pencilSmoothing` (lazy mouse, 0\u20131), `recognizeShapes` (pencil scribbles become lines, rectangles, ellipses, polygons) and `eraserSize`.",
|
|
5892
|
+
params: {
|
|
5893
|
+
type: "object",
|
|
5894
|
+
properties: {
|
|
5895
|
+
keepShapeOnDelete: { type: "boolean" },
|
|
5896
|
+
curvatureMoves: { type: "boolean" },
|
|
5897
|
+
pencilSmoothing: { type: "number", minimum: 0, maximum: 1 },
|
|
5898
|
+
recognizeShapes: { type: "boolean" },
|
|
5899
|
+
eraserSize: { type: "number", exclusiveMinimum: 0 }
|
|
5900
|
+
}
|
|
5901
|
+
},
|
|
5902
|
+
run: (_ctx, params) => {
|
|
5903
|
+
const patch = {};
|
|
5904
|
+
for (const key of ["keepShapeOnDelete", "curvatureMoves", "pencilSmoothing", "recognizeShapes", "eraserSize"]) if (params?.[key] !== void 0) patch[key] = params[key];
|
|
5905
|
+
controller.setEditing(patch);
|
|
5906
|
+
return { editing: controller.getState().editing };
|
|
5907
|
+
}
|
|
5908
|
+
},
|
|
4249
5909
|
{
|
|
4250
5910
|
...base,
|
|
4251
5911
|
id: `${prefix}setTool`,
|
|
@@ -4284,12 +5944,18 @@ function createVectorCommands(controller, options = {}) {
|
|
|
4284
5944
|
];
|
|
4285
5945
|
}
|
|
4286
5946
|
export {
|
|
5947
|
+
DEFAULT_VECTOR_PATH_EDIT,
|
|
4287
5948
|
DEFAULT_VECTOR_SNAP,
|
|
4288
5949
|
IDENTITY_MATRIX,
|
|
4289
5950
|
KAPPA,
|
|
5951
|
+
LazyMouse,
|
|
4290
5952
|
VECTOR_CLIPBOARD_TYPE,
|
|
4291
5953
|
VECTOR_CONSTRUCTION_ANGLES,
|
|
5954
|
+
VECTOR_EDIT_TOGGLES,
|
|
5955
|
+
VECTOR_INK_MAX_CORNERS,
|
|
4292
5956
|
VECTOR_OVERLAY_THEME,
|
|
5957
|
+
VECTOR_PATH_ACTIONS,
|
|
5958
|
+
VECTOR_PENCIL_STRING_PX,
|
|
4293
5959
|
VECTOR_ROTATE_HANDLE_OFFSET,
|
|
4294
5960
|
VECTOR_TOOLS,
|
|
4295
5961
|
VECTOR_TOOL_GLYPHS,
|
|
@@ -4299,6 +5965,7 @@ export {
|
|
|
4299
5965
|
VectorSnapTargets,
|
|
4300
5966
|
applyMatrix,
|
|
4301
5967
|
applyMatrixLinear,
|
|
5968
|
+
bendSegment,
|
|
4302
5969
|
bezierToSubpaths,
|
|
4303
5970
|
boundsFromPoints,
|
|
4304
5971
|
boundsIntersect,
|
|
@@ -4310,8 +5977,13 @@ export {
|
|
|
4310
5977
|
constrainToLeave,
|
|
4311
5978
|
convertAnchor,
|
|
4312
5979
|
createVectorCommands,
|
|
5980
|
+
curvatureMoveAnchor,
|
|
5981
|
+
cutSubpathAt,
|
|
4313
5982
|
decomposeRigid,
|
|
4314
5983
|
ellipseSubpath,
|
|
5984
|
+
eraseFromFill,
|
|
5985
|
+
eraseFromOutline,
|
|
5986
|
+
eraserRegion,
|
|
4315
5987
|
findVectorNode,
|
|
4316
5988
|
getAnchor,
|
|
4317
5989
|
getAnchorWorld,
|
|
@@ -4332,6 +6004,8 @@ export {
|
|
|
4332
6004
|
invertMatrix,
|
|
4333
6005
|
isIdentityMatrix,
|
|
4334
6006
|
isVectorTextTarget,
|
|
6007
|
+
joinSubpaths,
|
|
6008
|
+
knifePath,
|
|
4335
6009
|
lineSubpath,
|
|
4336
6010
|
locateVectorNode,
|
|
4337
6011
|
matrixRotation,
|
|
@@ -4342,14 +6016,24 @@ export {
|
|
|
4342
6016
|
parseSvgTransform,
|
|
4343
6017
|
parseVectorSvg,
|
|
4344
6018
|
polygonSubpath,
|
|
6019
|
+
polylineBounds,
|
|
6020
|
+
recognizeInkShape,
|
|
4345
6021
|
rectSubpath,
|
|
6022
|
+
removeAnchorKeepingShape,
|
|
4346
6023
|
removeAnchors,
|
|
6024
|
+
removeAnchorsKeepingShape,
|
|
6025
|
+
removeRedundantPoints,
|
|
4347
6026
|
reverseSubpath,
|
|
4348
6027
|
rotateMatrix,
|
|
4349
6028
|
roundSubpaths,
|
|
4350
6029
|
scaleFor,
|
|
4351
6030
|
scaleMatrix,
|
|
6031
|
+
simplifyPolyline,
|
|
6032
|
+
smoothSubpath,
|
|
6033
|
+
splitIntoFaces,
|
|
6034
|
+
splitSubpathAt,
|
|
4352
6035
|
starSubpath,
|
|
6036
|
+
subpathPointAt,
|
|
4353
6037
|
subpathSegment,
|
|
4354
6038
|
subpathSegmentCount,
|
|
4355
6039
|
subpathsToBezier,
|
|
@@ -4361,6 +6045,7 @@ export {
|
|
|
4361
6045
|
translateMatrix,
|
|
4362
6046
|
unionBounds,
|
|
4363
6047
|
vectorDocumentToSvg,
|
|
6048
|
+
vectorInkShapeName,
|
|
4364
6049
|
vectorNodeBounds,
|
|
4365
6050
|
vectorNodeRotation,
|
|
4366
6051
|
vectorNodeToBezier,
|