@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.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2;
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3;
|
|
2
2
|
|
|
3
3
|
var _chunkQKLRAA4Tcjs = require('../chunk-QKLRAA4T.cjs');
|
|
4
4
|
|
|
@@ -17,10 +17,20 @@ var _chunkQKLRAA4Tcjs = require('../chunk-QKLRAA4T.cjs');
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
var _chunkBGNPT5PEcjs = require('../chunk-BGNPT5PE.cjs');
|
|
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
|
|
@@ -84,7 +94,7 @@ function vectorNodeToBezier(node) {
|
|
|
84
94
|
}
|
|
85
95
|
function vectorPathData(input, precision = 3) {
|
|
86
96
|
const subpaths = Array.isArray(input) ? input : input.subpaths;
|
|
87
|
-
return
|
|
97
|
+
return _chunkBGNPT5PEcjs.bezierPathToSvg.call(void 0, subpathsToBezier(subpaths), { precision });
|
|
88
98
|
}
|
|
89
99
|
function inferAnchorType(a) {
|
|
90
100
|
if (isOnAnchor(a, a.in) || isOnAnchor(a, a.out)) return "corner";
|
|
@@ -100,7 +110,7 @@ function inferAnchorType(a) {
|
|
|
100
110
|
return Math.abs(li - lo) <= 1e-3 * Math.max(li, lo) ? "symmetric" : "smooth";
|
|
101
111
|
}
|
|
102
112
|
function bezierToSubpaths(input) {
|
|
103
|
-
const path =
|
|
113
|
+
const path = _chunkBGNPT5PEcjs.toBezierPath.call(void 0, input);
|
|
104
114
|
const out = [];
|
|
105
115
|
let current = null;
|
|
106
116
|
const last = () => current.anchors[current.anchors.length - 1];
|
|
@@ -197,14 +207,14 @@ function nodeMatrix(node) {
|
|
|
197
207
|
function vectorPathWorldBezier(node, parent = IDENTITY_MATRIX) {
|
|
198
208
|
const m = multiplyMatrices(nodeMatrix(node), parent);
|
|
199
209
|
const path = subpathsToBezier(node.subpaths);
|
|
200
|
-
return isIdentityMatrix(m) ? path :
|
|
210
|
+
return isIdentityMatrix(m) ? path : _chunkBGNPT5PEcjs.transformBezierPath.call(void 0, path, m);
|
|
201
211
|
}
|
|
202
212
|
function vectorNodeBounds(node, parent = IDENTITY_MATRIX) {
|
|
203
213
|
if (node.type === "group") {
|
|
204
214
|
const m = multiplyMatrices(nodeMatrix(node), parent);
|
|
205
215
|
return unionBounds(node.children.map((c) => vectorNodeBounds(c, m)));
|
|
206
216
|
}
|
|
207
|
-
const b =
|
|
217
|
+
const b = _chunkBGNPT5PEcjs.bezierPathBounds.call(void 0, vectorPathWorldBezier(node, parent));
|
|
208
218
|
if (!b) return null;
|
|
209
219
|
return { x: b.minX, y: b.minY, width: b.maxX - b.minX, height: b.maxY - b.minY };
|
|
210
220
|
}
|
|
@@ -238,9 +248,9 @@ function hitTestVectorNode(node, p, tolerance = 0, parent = IDENTITY_MATRIX) {
|
|
|
238
248
|
const path = vectorPathWorldBezier(node, parent);
|
|
239
249
|
if (path.length === 0) return false;
|
|
240
250
|
const point = [p.x, p.y];
|
|
241
|
-
if (hasPaint(node.fill) &&
|
|
251
|
+
if (hasPaint(node.fill) && _chunkBGNPT5PEcjs.pointInBezierPath.call(void 0, path, point, _nullishCoalesce(node.fillRule, () => ( "nonzero")))) return true;
|
|
242
252
|
const reach = (hasPaint(node.stroke) ? (_nullishCoalesce(node.strokeWidth, () => ( 1))) / 2 : 0) + tolerance;
|
|
243
|
-
return
|
|
253
|
+
return _chunkBGNPT5PEcjs.distanceToBezierPath.call(void 0, path, point) <= reach;
|
|
244
254
|
}
|
|
245
255
|
function hitTestVectorNodes(nodes, p, tolerance = 0) {
|
|
246
256
|
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
@@ -256,9 +266,9 @@ function nearestSegment(node, p) {
|
|
|
256
266
|
const count = subpathSegmentCount(sp);
|
|
257
267
|
for (let i = 0; i < count; i++) {
|
|
258
268
|
const seg = subpathSegment(sp, i);
|
|
259
|
-
const hit =
|
|
269
|
+
const hit = _chunkBGNPT5PEcjs.segmentNearest.call(void 0, seg, [p.x, p.y]);
|
|
260
270
|
if (!best || hit.distance < best.distance) {
|
|
261
|
-
const pt =
|
|
271
|
+
const pt = _chunkBGNPT5PEcjs.segmentPoint.call(void 0, seg, hit.t);
|
|
262
272
|
best = { subpath: s, segment: i, t: hit.t, point: { x: pt[0], y: pt[1] }, distance: hit.distance };
|
|
263
273
|
}
|
|
264
274
|
}
|
|
@@ -429,7 +439,7 @@ function insertAnchor(sp, segment, t) {
|
|
|
429
439
|
const ia = segment;
|
|
430
440
|
const ib = (segment + 1) % n;
|
|
431
441
|
const seg = subpathSegment(sp, segment);
|
|
432
|
-
const [left, right] =
|
|
442
|
+
const [left, right] = _chunkBGNPT5PEcjs.splitSegment.call(void 0, seg, t);
|
|
433
443
|
const mid = left[left.length - 1];
|
|
434
444
|
const anchor = { x: mid[0], y: mid[1] };
|
|
435
445
|
if (seg.length === 4) {
|
|
@@ -537,7 +547,7 @@ var escapeAttr = (value) => value.replace(/&/g, "&").replace(/"/g, """)
|
|
|
537
547
|
function vectorPaintToSvg(paint, gradientId) {
|
|
538
548
|
if (!hasPaint(paint)) return { value: "none", gradient: null };
|
|
539
549
|
if (typeof paint === "string") return { value: paint, gradient: null };
|
|
540
|
-
return { value: `url(#${gradientId})`, gradient:
|
|
550
|
+
return { value: `url(#${gradientId})`, gradient: _chunkBGNPT5PEcjs.gradientToSvgDef.call(void 0, paint, gradientId) };
|
|
541
551
|
}
|
|
542
552
|
function gradientMarkup(def) {
|
|
543
553
|
const tag = def.type === "radial" ? "radialGradient" : "linearGradient";
|
|
@@ -551,11 +561,11 @@ function vectorDocumentToSvg(doc, options = {}) {
|
|
|
551
561
|
const pretty = options.pretty !== false;
|
|
552
562
|
const defs = [];
|
|
553
563
|
const lines = [];
|
|
554
|
-
const num = (n) =>
|
|
564
|
+
const num = (n) => _chunkBGNPT5PEcjs.formatNumber.call(void 0, n, precision);
|
|
555
565
|
const indent = (depth) => pretty ? " ".repeat(depth) : "";
|
|
556
566
|
const common = (node) => {
|
|
557
567
|
let out2 = ` id="${escapeAttr(node.id)}"`;
|
|
558
|
-
if (node.transform) out2 += ` transform="matrix(${node.transform.map((n) =>
|
|
568
|
+
if (node.transform) out2 += ` transform="matrix(${node.transform.map((n) => _chunkBGNPT5PEcjs.formatNumber.call(void 0, n, 6)).join(" ")})"`;
|
|
559
569
|
if (node.opacity !== void 0 && node.opacity < 1) out2 += ` opacity="${num(node.opacity)}"`;
|
|
560
570
|
if (node.visible === false) out2 += ` display="none"`;
|
|
561
571
|
return out2;
|
|
@@ -1029,13 +1039,13 @@ var VectorSnapTargets = (_class = 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 = 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 (_optionalChain([leave, 'optionalAccess', _15 => _15.angles, 'optionalAccess', _16 => _16.length])) {
|
|
1149
1159
|
const c = construction(leave, p, tol);
|
|
@@ -1221,22 +1231,22 @@ var VectorSnapTargets = (_class = class _VectorSnapTargets {
|
|
|
1221
1231
|
const pt = toPoint(p);
|
|
1222
1232
|
for (const s of this.segments) {
|
|
1223
1233
|
if (p.x < s.minX - tol || p.x > s.maxX + tol || p.y < s.minY - tol || p.y > s.maxY + tol) continue;
|
|
1224
|
-
const hit =
|
|
1234
|
+
const hit = _chunkBGNPT5PEcjs.segmentNearest.call(void 0, s.seg, pt);
|
|
1225
1235
|
if (hit.distance <= tol) out.push({ seg: s.seg, t: hit.t, distance: hit.distance });
|
|
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(_chunkBGNPT5PEcjs.segmentPoint.call(void 0, 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++) {
|
|
1238
1248
|
for (let j = i + 1; j < list.length; j++) {
|
|
1239
|
-
for (const hit of
|
|
1249
|
+
for (const hit of _chunkBGNPT5PEcjs.segmentIntersections.call(void 0, list[i].seg, list[j].seg)) {
|
|
1240
1250
|
const q = toVec(hit.point);
|
|
1241
1251
|
const d = distance(q, p);
|
|
1242
1252
|
if (d <= tol && d < bestD) {
|
|
@@ -1253,9 +1263,9 @@ var VectorSnapTargets = (_class = 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)) {
|
|
@@ -1558,15 +1568,15 @@ var crossCondition = (r, d) => r[0] * d[1] - r[1] * d[0];
|
|
|
1558
1568
|
function solveOnSegment(seg, base, cond) {
|
|
1559
1569
|
const b = [base.x, base.y];
|
|
1560
1570
|
const g = (t) => {
|
|
1561
|
-
const p =
|
|
1562
|
-
return cond([p[0] - b[0], p[1] - b[1]],
|
|
1571
|
+
const p = _chunkBGNPT5PEcjs.segmentPoint.call(void 0, seg, t);
|
|
1572
|
+
return cond([p[0] - b[0], p[1] - b[1]], _chunkBGNPT5PEcjs.segmentDerivative.call(void 0, seg, t));
|
|
1563
1573
|
};
|
|
1564
1574
|
const vals = [];
|
|
1565
1575
|
let scale = 0;
|
|
1566
1576
|
for (let i = 0; i <= CURVE_SAMPLES; i++) {
|
|
1567
1577
|
const t = i / CURVE_SAMPLES;
|
|
1568
|
-
const p =
|
|
1569
|
-
const d =
|
|
1578
|
+
const p = _chunkBGNPT5PEcjs.segmentPoint.call(void 0, seg, t);
|
|
1579
|
+
const d = _chunkBGNPT5PEcjs.segmentDerivative.call(void 0, seg, t);
|
|
1570
1580
|
const r = [p[0] - b[0], p[1] - b[1]];
|
|
1571
1581
|
vals.push(cond(r, d));
|
|
1572
1582
|
scale = Math.max(scale, Math.hypot(r[0], r[1]) * Math.hypot(d[0], d[1]));
|
|
@@ -1593,8 +1603,8 @@ function solveOnSegment(seg, base, cond) {
|
|
|
1593
1603
|
} else hi = m;
|
|
1594
1604
|
}
|
|
1595
1605
|
const t = (lo + hi) / 2;
|
|
1596
|
-
const p =
|
|
1597
|
-
const dv =
|
|
1606
|
+
const p = _chunkBGNPT5PEcjs.segmentPoint.call(void 0, seg, t);
|
|
1607
|
+
const dv = _chunkBGNPT5PEcjs.segmentDerivative.call(void 0, seg, t);
|
|
1598
1608
|
const r = Math.hypot(p[0] - b[0], p[1] - b[1]);
|
|
1599
1609
|
const dl = Math.hypot(dv[0], dv[1]);
|
|
1600
1610
|
if (!(r > 1e-9 && dl > 0) || Math.abs(cond([p[0] - b[0], p[1] - b[1]], dv)) > 1e-6 * r * dl) continue;
|
|
@@ -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(_chunkBGNPT5PEcjs.segmentPoint.call(void 0, 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 _chunkBGNPT5PEcjs.fitCubicWithTangents.call(void 0, 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] = _chunkBGNPT5PEcjs.segmentPoint.call(void 0, 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: _nullishCoalesce(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: _nullishCoalesce(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 = _nullishCoalesce(p.in, () => ( p));
|
|
1913
|
+
const hout = _nullishCoalesce(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 _nullishCoalesce(segsToSubpath(segs, sp.closed, types), () => ( { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed }));
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
// src/vector/pathCut.ts
|
|
2009
|
+
var loopArea = (l) => _chunkBGNPT5PEcjs.contourSignedArea.call(void 0, l.segs);
|
|
2010
|
+
function loopSample(l) {
|
|
2011
|
+
const k = l.chord.findIndex((c) => !c);
|
|
2012
|
+
return _chunkBGNPT5PEcjs.segmentPoint.call(void 0, l.segs[k >= 0 ? k : 0], 0.5);
|
|
2013
|
+
}
|
|
2014
|
+
var contains2 = (l, p) => _chunkBGNPT5PEcjs.windingNumber.call(void 0, [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 _chunkBGNPT5PEcjs.contoursToPath.call(void 0, face.loops.map((l) => ({ segments: l.segs, closed: true })));
|
|
2031
|
+
}
|
|
2032
|
+
function facesOf(path, fillRule) {
|
|
2033
|
+
const normalized = _chunkBGNPT5PEcjs.normalizeBezierPath.call(void 0, path, fillRule);
|
|
2034
|
+
const loops = _chunkBGNPT5PEcjs.pathToContours.call(void 0, normalized).map((c) => {
|
|
2035
|
+
const segs = _chunkBGNPT5PEcjs.closeContour.call(void 0, 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 = _chunkBGNPT5PEcjs.segmentPoint.call(void 0, 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() : _chunkBGNPT5PEcjs.subSegment.call(void 0, 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 = _chunkBGNPT5PEcjs.bezierPathBounds.call(void 0, 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 = _chunkBGNPT5PEcjs.segmentBounds.call(void 0, 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 _chunkBGNPT5PEcjs.windingNumber.call(void 0, 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 _chunkBGNPT5PEcjs.outlineStroke.call(void 0, line2, { width, cap: "round", join: "round" });
|
|
2233
|
+
}
|
|
2234
|
+
function eraseFromFill(path, fillRule, region) {
|
|
2235
|
+
const overlap = _chunkBGNPT5PEcjs.pathBoolean.call(void 0, "intersection", [{ path, fillRule }, { path: region }]).path;
|
|
2236
|
+
if (!_chunkBGNPT5PEcjs.pathToContours.call(void 0, overlap).some((c) => Math.abs(_chunkBGNPT5PEcjs.contourSignedArea.call(void 0, _chunkBGNPT5PEcjs.closeContour.call(void 0, c, true))) > 1e-9)) return null;
|
|
2237
|
+
const rest = _chunkBGNPT5PEcjs.pathBoolean.call(void 0, "difference", [{ path, fillRule }, { path: region }]).path;
|
|
2238
|
+
return splitIntoFaces(rest);
|
|
2239
|
+
}
|
|
2240
|
+
function eraseFromOutline(sp, region) {
|
|
2241
|
+
const regionSegs = _chunkBGNPT5PEcjs.pathToContours.call(void 0, region).flatMap((c) => _chunkBGNPT5PEcjs.closeContour.call(void 0, c, true));
|
|
2242
|
+
if (!regionSegs.length) return null;
|
|
2243
|
+
const rb = _chunkBGNPT5PEcjs.bezierPathBounds.call(void 0, region);
|
|
2244
|
+
const count = subpathSegmentCount(sp);
|
|
2245
|
+
if (!rb || count === 0) return null;
|
|
2246
|
+
const removed = (p) => _chunkBGNPT5PEcjs.pointInBezierPath.call(void 0, 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 = _chunkBGNPT5PEcjs.segmentBounds.call(void 0, 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 = _chunkBGNPT5PEcjs.segmentBounds.call(void 0, 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 _chunkBGNPT5PEcjs.segmentIntersections.call(void 0, 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(_chunkBGNPT5PEcjs.segmentPoint.call(void 0, 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 = _chunkBGNPT5PEcjs.subSegment.call(void 0, 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 = (_class2 = class {
|
|
2350
|
+
/** `length` is the string length in the units of the points (0 turns smoothing off). */
|
|
2351
|
+
constructor(length2) {;_class2.prototype.__init7.call(this);_class2.prototype.__init8.call(this);
|
|
2352
|
+
this.length = length2;
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
__init7() {this.pos = null}
|
|
2356
|
+
__init8() {this.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
|
+
}, _class2);
|
|
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 = _nullishCoalesce(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,48 +2777,72 @@ 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)}`;
|
|
1803
|
-
var VectorEditorController = (
|
|
2805
|
+
var VectorEditorController = (_class3 = class {
|
|
1804
2806
|
|
|
1805
2807
|
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
2808
|
+
__init9() {this.selection = []}
|
|
2809
|
+
__init10() {this.anchors = []}
|
|
2810
|
+
__init11() {this.editingId = null}
|
|
2811
|
+
__init12() {this.pen = null}
|
|
1810
2812
|
|
|
1811
2813
|
|
|
1812
2814
|
|
|
1813
2815
|
|
|
1814
2816
|
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
2817
|
+
|
|
2818
|
+
__init13() {this.hover = null}
|
|
2819
|
+
__init14() {this.pointer = null}
|
|
2820
|
+
__init15() {this.drag = null}
|
|
2821
|
+
__init16() {this.interaction = null}
|
|
2822
|
+
__init17() {this.guides = null}
|
|
1820
2823
|
/** Whether the last `snapDrawn` landed on an object (guides may be hidden). */
|
|
1821
|
-
|
|
2824
|
+
__init18() {this.snappedToObject = false}
|
|
1822
2825
|
/** Drawing targets for hovering the pen and shape tools, kept while the document and the excluded path stay the same. */
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
2826
|
+
__init19() {this.hoverTargets = null}
|
|
2827
|
+
__init20() {this.spaceHeld = false}
|
|
2828
|
+
__init21() {this.clipboard = null}
|
|
1826
2829
|
|
|
1827
2830
|
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
2831
|
+
__init22() {this.listeners = /* @__PURE__ */ new Set()}
|
|
2832
|
+
__init23() {this.cleanups = []}
|
|
2833
|
+
__init24() {this.applying = false}
|
|
1831
2834
|
/** Set while a commit records a change that was already applied, so `restore` still reports it. */
|
|
1832
|
-
|
|
1833
|
-
|
|
2835
|
+
__init25() {this.committing = false}
|
|
2836
|
+
__init26() {this.revision = 0}
|
|
1834
2837
|
|
|
1835
|
-
constructor(options = {}) {;
|
|
2838
|
+
constructor(options = {}) {;_class3.prototype.__init9.call(this);_class3.prototype.__init10.call(this);_class3.prototype.__init11.call(this);_class3.prototype.__init12.call(this);_class3.prototype.__init13.call(this);_class3.prototype.__init14.call(this);_class3.prototype.__init15.call(this);_class3.prototype.__init16.call(this);_class3.prototype.__init17.call(this);_class3.prototype.__init18.call(this);_class3.prototype.__init19.call(this);_class3.prototype.__init20.call(this);_class3.prototype.__init21.call(this);_class3.prototype.__init22.call(this);_class3.prototype.__init23.call(this);_class3.prototype.__init24.call(this);_class3.prototype.__init25.call(this);_class3.prototype.__init26.call(this);_class3.prototype.__init27.call(this);_class3.prototype.__init28.call(this);
|
|
1836
2839
|
this.history = _nullishCoalesce(options.history, () => ( new (0, _chunkQKLRAA4Tcjs.HistoryManager)()));
|
|
1837
2840
|
this.doc = options.document ? cloneVectorDocument(options.document) : { width: 800, height: 600, nodes: [] };
|
|
1838
2841
|
this.tool = _nullishCoalesce(options.tool, () => ( "select"));
|
|
1839
2842
|
this.defaults = { ...DEFAULT_DEFAULTS, ..._nullishCoalesce(options.defaults, () => ( {})) };
|
|
1840
2843
|
this.viewport = { x: 0, y: 0, zoom: 1, ..._nullishCoalesce(options.viewport, () => ( {})) };
|
|
1841
2844
|
this.snap = { ...DEFAULT_VECTOR_SNAP, ..._nullishCoalesce(options.snap, () => ( {})) };
|
|
2845
|
+
this.editing = { ...DEFAULT_VECTOR_PATH_EDIT, ..._nullishCoalesce(options.editing, () => ( {})) };
|
|
1842
2846
|
this.hitRadius = _nullishCoalesce(options.hitRadius, () => ( 6));
|
|
1843
2847
|
this.createId = _nullishCoalesce(options.createId, () => ( defaultCreateId));
|
|
1844
2848
|
this.onChange = options.onChange;
|
|
@@ -1860,9 +2864,9 @@ var VectorEditorController = (_class2 = class {
|
|
|
1860
2864
|
// State
|
|
1861
2865
|
// -------------------------------------------------------------------------
|
|
1862
2866
|
/** The current snapshot (stable until the next change). */
|
|
1863
|
-
|
|
2867
|
+
__init27() {this.getState = () => this.state}
|
|
1864
2868
|
/** Calls `listener` after every change. Returns an unsubscribe function. */
|
|
1865
|
-
|
|
2869
|
+
__init28() {this.subscribe = (listener) => {
|
|
1866
2870
|
this.listeners.add(listener);
|
|
1867
2871
|
return () => {
|
|
1868
2872
|
this.listeners.delete(listener);
|
|
@@ -1887,6 +2891,7 @@ var VectorEditorController = (_class2 = 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 = (_class2 = 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 = (_class2 = 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(_nullishCoalesce(_optionalChain([this, 'access', _67 => _67.pen, 'optionalAccess', _68 => _68.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
|
}
|
|
@@ -2653,7 +3666,7 @@ var VectorEditorController = (_class2 = class {
|
|
|
2653
3666
|
});
|
|
2654
3667
|
const paths = [...new Set(order)].map((id2) => this.getNode(id2));
|
|
2655
3668
|
if (paths.length < 2) return null;
|
|
2656
|
-
const result =
|
|
3669
|
+
const result = _chunkBGNPT5PEcjs.pathBoolean.call(void 0,
|
|
2657
3670
|
BOOLEAN_OPS[op],
|
|
2658
3671
|
paths.map((p) => ({
|
|
2659
3672
|
path: vectorPathWorldBezier(
|
|
@@ -2703,7 +3716,7 @@ var VectorEditorController = (_class2 = class {
|
|
|
2703
3716
|
/** A document-space path mapped into the parent space of node `id`. */
|
|
2704
3717
|
intoParentSpace(id, path) {
|
|
2705
3718
|
const parent = _nullishCoalesce(vectorParentMatrix(this.doc.nodes, id), () => ( IDENTITY_MATRIX));
|
|
2706
|
-
return isRoot(parent) ? path :
|
|
3719
|
+
return isRoot(parent) ? path : _chunkBGNPT5PEcjs.transformBezierPath.call(void 0, path, invertMatrix(parent));
|
|
2707
3720
|
}
|
|
2708
3721
|
/** Splits selected compound paths into one path per subpath. */
|
|
2709
3722
|
breakApart(options = {}) {
|
|
@@ -2753,12 +3766,47 @@ var VectorEditorController = (_class2 = 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 = _nullishCoalesce(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 (_optionalChain([node, 'optionalAccess', _79 => _79.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 (_optionalChain([node, 'optionalAccess', _80 => _80.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 = _optionalChain([node, 'optionalAccess', _81 => _81.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
|
}
|
|
@@ -2789,7 +3837,7 @@ var VectorEditorController = (_class2 = class {
|
|
|
2789
3837
|
/** Adds an anchor on the nearest segment of `nodeId` to document point `point` (shape unchanged). */
|
|
2790
3838
|
insertAnchorAt(nodeId, point) {
|
|
2791
3839
|
const node = this.getNode(nodeId);
|
|
2792
|
-
if (_optionalChain([node, 'optionalAccess',
|
|
3840
|
+
if (_optionalChain([node, 'optionalAccess', _82 => _82.type]) !== "path") return null;
|
|
2793
3841
|
const hit = nearestSegment(node, this.toLocal(nodeId, point));
|
|
2794
3842
|
if (!hit) return null;
|
|
2795
3843
|
const { subpath, index } = insertAnchor(node.subpaths[hit.subpath], hit.segment, hit.t);
|
|
@@ -2801,15 +3849,24 @@ var VectorEditorController = (_class2 = 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 = _nullishCoalesce(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);
|
|
@@ -2824,7 +3881,7 @@ var VectorEditorController = (_class2 = class {
|
|
|
2824
3881
|
setClosed(closed, nodeId) {
|
|
2825
3882
|
const targets = nodeId ? (() => {
|
|
2826
3883
|
const n = this.getNode(nodeId);
|
|
2827
|
-
return _optionalChain([n, 'optionalAccess',
|
|
3884
|
+
return _optionalChain([n, 'optionalAccess', _83 => _83.type]) === "path" ? n.subpaths.map((_, s) => ({ nodeId, subpath: s })) : [];
|
|
2828
3885
|
})() : this.anchors.map((r) => ({ nodeId: r.nodeId, subpath: r.subpath }));
|
|
2829
3886
|
if (!targets.length) return { ok: false, message: "Select a path or points." };
|
|
2830
3887
|
this.change(closed ? "Close Path" : "Open Path", () => {
|
|
@@ -2847,11 +3904,274 @@ var VectorEditorController = (_class2 = 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 = _nullishCoalesce(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, _nullishCoalesce(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 = _nullishCoalesce(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 = _nullishCoalesce(options.refs, () => ( (!options.ids && this.tool === "direct" && this.anchors.length ? this.anchors : null)));
|
|
4016
|
+
const ids = _nullishCoalesce(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, _nullishCoalesce(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) => _nullishCoalesce(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 (_optionalChain([node, 'optionalAccess', _84 => _84.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, [..._nullishCoalesce(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 (_optionalChain([ids, 'optionalAccess', _85 => _85.length]) || this.selection.length) return this.pathTargets(_nullishCoalesce(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, _nullishCoalesce(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), _nullishCoalesce(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 = _nullishCoalesce(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 = _chunkBGNPT5PEcjs.bezierPathBounds.call(void 0, 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 = _chunkBGNPT5PEcjs.transformBezierPath.call(void 0, 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), _nullishCoalesce(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) => _nullishCoalesce(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;
|
|
2853
4173
|
const node = this.getNode(this.pen.nodeId);
|
|
2854
|
-
const sp = _optionalChain([node, 'optionalAccess',
|
|
4174
|
+
const sp = _optionalChain([node, 'optionalAccess', _86 => _86.subpaths, 'access', _87 => _87[this.pen.subpath]]);
|
|
2855
4175
|
const pen = this.pen;
|
|
2856
4176
|
this.pen = null;
|
|
2857
4177
|
this.pointer = null;
|
|
@@ -2895,7 +4215,7 @@ var VectorEditorController = (_class2 = class {
|
|
|
2895
4215
|
if (trimmed.startsWith("{")) {
|
|
2896
4216
|
try {
|
|
2897
4217
|
const data = JSON.parse(trimmed);
|
|
2898
|
-
if (_optionalChain([data, 'optionalAccess',
|
|
4218
|
+
if (_optionalChain([data, 'optionalAccess', _88 => _88.type]) === VECTOR_CLIPBOARD_TYPE && Array.isArray(data.nodes)) nodes = data.nodes;
|
|
2899
4219
|
} catch (e2) {
|
|
2900
4220
|
nodes = null;
|
|
2901
4221
|
}
|
|
@@ -2907,7 +4227,7 @@ var VectorEditorController = (_class2 = class {
|
|
|
2907
4227
|
}
|
|
2908
4228
|
}
|
|
2909
4229
|
} else nodes = this.clipboard;
|
|
2910
|
-
if (!_optionalChain([nodes, 'optionalAccess',
|
|
4230
|
+
if (!_optionalChain([nodes, 'optionalAccess', _89 => _89.length])) return [];
|
|
2911
4231
|
const offset = _nullishCoalesce(options.offset, () => ( (text && !text.trim().startsWith("{") ? { x: 0, y: 0 } : { x: 10, y: 10 })));
|
|
2912
4232
|
const copies = nodes.map((n) => transformVectorNode(this.freshIds(n), translateMatrix(offset.x, offset.y)));
|
|
2913
4233
|
this.change("Paste", () => {
|
|
@@ -2958,7 +4278,7 @@ var VectorEditorController = (_class2 = class {
|
|
|
2958
4278
|
hitHandle(screen) {
|
|
2959
4279
|
for (const ref of getVisibleHandles(this.view()).reverse()) {
|
|
2960
4280
|
const a = getAnchorWorld(this.doc, ref);
|
|
2961
|
-
const h = _optionalChain([a, 'optionalAccess',
|
|
4281
|
+
const h = _optionalChain([a, 'optionalAccess', _90 => _90[ref.handle]]);
|
|
2962
4282
|
if (!h) continue;
|
|
2963
4283
|
const s = this.documentToScreen(h);
|
|
2964
4284
|
if (Math.hypot(s.x - screen.x, s.y - screen.y) <= this.hitRadius) return ref;
|
|
@@ -3039,10 +4359,38 @@ var VectorEditorController = (_class2 = 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 = (_class2 = 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 = (_class2 = 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
|
}
|
|
@@ -3184,7 +4543,7 @@ var VectorEditorController = (_class2 = class {
|
|
|
3184
4543
|
}
|
|
3185
4544
|
downPen(screen, p, input) {
|
|
3186
4545
|
const before = this.snapshot();
|
|
3187
|
-
const sp = this.pen ? _optionalChain([this, 'access',
|
|
4546
|
+
const sp = this.pen ? _optionalChain([this, 'access', _91 => _91.getNode, 'call', _92 => _92(this.pen.nodeId), 'optionalAccess', _93 => _93.subpaths, 'access', _94 => _94[this.pen.subpath]]) : void 0;
|
|
3188
4547
|
if (this.pen && sp) {
|
|
3189
4548
|
const pen = this.pen;
|
|
3190
4549
|
const first = { nodeId: pen.nodeId, subpath: pen.subpath, index: 0 };
|
|
@@ -3253,7 +4612,9 @@ var VectorEditorController = (_class2 = 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 = (_class2 = 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 = (_class2 = 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;
|
|
@@ -3402,7 +4782,7 @@ var VectorEditorController = (_class2 = class {
|
|
|
3402
4782
|
}
|
|
3403
4783
|
case "shape": {
|
|
3404
4784
|
const node = this.getNode(drag.nodeId);
|
|
3405
|
-
if (_optionalChain([node, 'optionalAccess',
|
|
4785
|
+
if (_optionalChain([node, 'optionalAccess', _95 => _95.type]) !== "path") return;
|
|
3406
4786
|
const leave = { at: drag.start, constrain: input.shift ? drag.tool === "line" ? "angle" : "square" : null, curves: false };
|
|
3407
4787
|
let end = this.snapDrawn(p, leave, input, () => drag.targets ??= this.drawingTargets(null));
|
|
3408
4788
|
if (!this.snappedToObject && drag.tool !== "line" && !input.shift && !input.mod && this.snap.objects && drag.targets) end = this.matchShapeSize(drag.targets, drag.start, end, input.alt);
|
|
@@ -3433,10 +4813,22 @@ var VectorEditorController = (_class2 = 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
|
-
if (_optionalChain([node, 'optionalAccess',
|
|
4824
|
+
if (_optionalChain([node, 'optionalAccess', _96 => _96.type]) !== "path") return;
|
|
4825
|
+
if (!moved) {
|
|
4826
|
+
if (string && _optionalChain([this, 'access', _97 => _97.interaction, 'optionalAccess', _98 => _98.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 = (_class2 = 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;
|
|
@@ -3466,8 +4859,8 @@ var VectorEditorController = (_class2 = class {
|
|
|
3466
4859
|
}
|
|
3467
4860
|
this.setGuides(res);
|
|
3468
4861
|
return {
|
|
3469
|
-
dx: _optionalChain([res, 'optionalAccess',
|
|
3470
|
-
dy: _optionalChain([res, 'optionalAccess',
|
|
4862
|
+
dx: _optionalChain([res, 'optionalAccess', _99 => _99.x]) ? dx + res.dx : axes.x ? this.gridValue(origin.x + dx) - origin.x : dx,
|
|
4863
|
+
dy: _optionalChain([res, 'optionalAccess', _100 => _100.y]) ? dy + res.dy : axes.y ? this.gridValue(origin.y + dy) - origin.y : dy
|
|
3471
4864
|
};
|
|
3472
4865
|
}
|
|
3473
4866
|
/** The anchor a point drag snaps by, where it started: the grabbed one, else the moving one nearest the press. */
|
|
@@ -3536,6 +4929,18 @@ var VectorEditorController = (_class2 = 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);
|
|
@@ -3555,12 +4960,19 @@ var VectorEditorController = (_class2 = class {
|
|
|
3555
4960
|
}
|
|
3556
4961
|
case "pencil": {
|
|
3557
4962
|
const node = this.getNode(drag.nodeId);
|
|
3558
|
-
if (_optionalChain([node, 'optionalAccess',
|
|
4963
|
+
if (_optionalChain([node, 'optionalAccess', _101 => _101.type]) !== "path" || drag.points.length < 2) {
|
|
3559
4964
|
this.doc = drag.before.doc;
|
|
3560
4965
|
this.notify(true);
|
|
3561
4966
|
return;
|
|
3562
4967
|
}
|
|
3563
|
-
const
|
|
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
|
+
}
|
|
4975
|
+
const fitted = _chunkBGNPT5PEcjs.fitCubics.call(void 0, drag.points, 1.5 / this.viewport.zoom, { cornerAngleDeg: 60 });
|
|
3564
4976
|
const anchors = [];
|
|
3565
4977
|
for (const c of fitted) {
|
|
3566
4978
|
if (!anchors.length) anchors.push({ x: c.p0[0], y: c.p0[1] });
|
|
@@ -3622,7 +5034,7 @@ var VectorEditorController = (_class2 = class {
|
|
|
3622
5034
|
this.convertAnchors(a.in || a.out ? "corner" : "smooth", [anchor]);
|
|
3623
5035
|
return;
|
|
3624
5036
|
}
|
|
3625
|
-
const id = _nullishCoalesce(_optionalChain([this, 'access',
|
|
5037
|
+
const id = _nullishCoalesce(_optionalChain([this, 'access', _102 => _102.hover, 'optionalAccess', _103 => _103.segment, 'optionalAccess', _104 => _104.nodeId]), () => ( this.editingId));
|
|
3626
5038
|
if (id) {
|
|
3627
5039
|
const node = this.getNode(id);
|
|
3628
5040
|
const seg = node ? nearestSegment(node, this.toLocal(id, p)) : null;
|
|
@@ -3666,6 +5078,8 @@ var VectorEditorController = (_class2 = 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;
|
|
@@ -3700,7 +5114,7 @@ var VectorEditorController = (_class2 = class {
|
|
|
3700
5114
|
this.finishPath();
|
|
3701
5115
|
return true;
|
|
3702
5116
|
}
|
|
3703
|
-
if (this.tool === "select" && this.selection.length === 1 && _optionalChain([this, 'access',
|
|
5117
|
+
if (this.tool === "select" && this.selection.length === 1 && _optionalChain([this, 'access', _105 => _105.getNode, 'call', _106 => _106(this.selection[0]), 'optionalAccess', _107 => _107.type]) === "path") {
|
|
3704
5118
|
this.tool = "direct";
|
|
3705
5119
|
this.editingId = this.selection[0];
|
|
3706
5120
|
this.notify();
|
|
@@ -3745,7 +5159,23 @@ var VectorEditorController = (_class2 = class {
|
|
|
3745
5159
|
this.notify();
|
|
3746
5160
|
}
|
|
3747
5161
|
}
|
|
3748
|
-
},
|
|
5162
|
+
}, _class3);
|
|
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;
|
|
@@ -3798,7 +5228,7 @@ function renderNode(node, defs, idPrefix) {
|
|
|
3798
5228
|
strokeWidth: _nullishCoalesce(node.strokeWidth, () => ( 1)),
|
|
3799
5229
|
...node.strokeLinecap ? { strokeLinecap: node.strokeLinecap } : {},
|
|
3800
5230
|
...node.strokeLinejoin ? { strokeLinejoin: node.strokeLinejoin } : {},
|
|
3801
|
-
..._optionalChain([node, 'access',
|
|
5231
|
+
..._optionalChain([node, 'access', _108 => _108.strokeDasharray, 'optionalAccess', _109 => _109.length]) ? { strokeDasharray: node.strokeDasharray.join(" ") } : {},
|
|
3802
5232
|
fillRule: _nullishCoalesce(node.fillRule, () => ( "nonzero")),
|
|
3803
5233
|
opacity: _nullishCoalesce(node.opacity, () => ( 1))
|
|
3804
5234
|
};
|
|
@@ -3822,7 +5252,7 @@ function screenOutline(state, node) {
|
|
|
3822
5252
|
const v = state.viewport;
|
|
3823
5253
|
const view = [v.zoom, 0, 0, v.zoom, v.x, v.y];
|
|
3824
5254
|
const parent = _nullishCoalesce(vectorParentMatrix(state.document.nodes, node.id), () => ( IDENTITY_MATRIX));
|
|
3825
|
-
return collectWithMatrix(node, parent).map(({ path, parent: m }) =>
|
|
5255
|
+
return collectWithMatrix(node, parent).map(({ path, parent: m }) => _chunkBGNPT5PEcjs.bezierPathToSvg.call(void 0, _chunkBGNPT5PEcjs.transformBezierPath.call(void 0, vectorPathWorldBezier(path, m), view), { precision: 2 })).join(" ");
|
|
3826
5256
|
}
|
|
3827
5257
|
function collectWithMatrix(node, parent) {
|
|
3828
5258
|
if (node.type === "path") return [{ path: node, parent }];
|
|
@@ -3847,9 +5277,9 @@ function getVectorOverlayRenderData(state) {
|
|
|
3847
5277
|
};
|
|
3848
5278
|
for (const id of state.selection) outline(id, true);
|
|
3849
5279
|
if (state.editingId) outline(state.editingId, true);
|
|
3850
|
-
if (_optionalChain([state, 'access',
|
|
5280
|
+
if (_optionalChain([state, 'access', _110 => _110.hover, 'optionalAccess', _111 => _111.nodeId]) && !state.interaction) outline(state.hover.nodeId, false);
|
|
3851
5281
|
let selectionBox = null;
|
|
3852
|
-
if (state.tool === "select" && state.selectionFrame && _optionalChain([state, 'access',
|
|
5282
|
+
if (state.tool === "select" && state.selectionFrame && _optionalChain([state, 'access', _112 => _112.interaction, 'optionalAccess', _113 => _113.type]) !== "marquee") {
|
|
3853
5283
|
const g = getVectorSelectionBoxGeometry(state.selectionFrame, state.viewport);
|
|
3854
5284
|
selectionBox = { ...g.box, angle: g.angle, transform: g.transform, corners: g.corners, handles: g.handles };
|
|
3855
5285
|
}
|
|
@@ -3859,9 +5289,9 @@ function getVectorOverlayRenderData(state) {
|
|
|
3859
5289
|
const a = getAnchorWorld(doc, ref);
|
|
3860
5290
|
const s = toScreen(state, a);
|
|
3861
5291
|
const closes = !!state.pen && state.pen.nodeId === ref.nodeId && state.pen.subpath === ref.subpath && ref.index === 0;
|
|
3862
|
-
return { ref, x: s.x, y: s.y, type: _nullishCoalesce(a.type, () => ( inferAnchorType(a))), selected: selected(ref), hovered: sameAnchor(_optionalChain([state, 'access',
|
|
5292
|
+
return { ref, x: s.x, y: s.y, type: _nullishCoalesce(a.type, () => ( inferAnchorType(a))), selected: selected(ref), hovered: sameAnchor(_optionalChain([state, 'access', _114 => _114.hover, 'optionalAccess', _115 => _115.anchor]), ref), closes };
|
|
3863
5293
|
});
|
|
3864
|
-
if (state.tool === "pen" && !state.pen && _optionalChain([state, 'access',
|
|
5294
|
+
if (state.tool === "pen" && !state.pen && _optionalChain([state, 'access', _116 => _116.hover, 'optionalAccess', _117 => _117.anchor])) {
|
|
3865
5295
|
const a = getAnchorWorld(doc, state.hover.anchor);
|
|
3866
5296
|
if (a) {
|
|
3867
5297
|
const s = toScreen(state, a);
|
|
@@ -3870,15 +5300,15 @@ function getVectorOverlayRenderData(state) {
|
|
|
3870
5300
|
}
|
|
3871
5301
|
const handles = getVisibleHandles(state).flatMap((ref) => {
|
|
3872
5302
|
const a = getAnchorWorld(doc, ref);
|
|
3873
|
-
const h = _optionalChain([a, 'optionalAccess',
|
|
5303
|
+
const h = _optionalChain([a, 'optionalAccess', _118 => _118[ref.handle]]);
|
|
3874
5304
|
if (!a || !h) return [];
|
|
3875
5305
|
const s = toScreen(state, h);
|
|
3876
5306
|
const sa = toScreen(state, a);
|
|
3877
|
-
const hovered = !!_optionalChain([state, 'access',
|
|
5307
|
+
const hovered = !!_optionalChain([state, 'access', _119 => _119.hover, 'optionalAccess', _120 => _120.handle]) && sameAnchor(state.hover.handle, ref) && state.hover.handle.handle === ref.handle;
|
|
3878
5308
|
return [{ ref, x: s.x, y: s.y, anchorX: sa.x, anchorY: sa.y, hovered }];
|
|
3879
5309
|
});
|
|
3880
5310
|
let marquee = null;
|
|
3881
|
-
if (_optionalChain([state, 'access',
|
|
5311
|
+
if (_optionalChain([state, 'access', _121 => _121.interaction, 'optionalAccess', _122 => _122.type]) === "marquee") {
|
|
3882
5312
|
const b = state.interaction.bounds;
|
|
3883
5313
|
const tl = toScreen(state, b);
|
|
3884
5314
|
marquee = { x: tl.x, y: tl.y, width: b.width * z, height: b.height * z };
|
|
@@ -3886,7 +5316,7 @@ function getVectorOverlayRenderData(state) {
|
|
|
3886
5316
|
let penPreview = null;
|
|
3887
5317
|
if (state.tool === "pen" && state.pen && state.pointer && !state.interaction) {
|
|
3888
5318
|
const node = findVectorNode(doc.nodes, state.pen.nodeId);
|
|
3889
|
-
const sp = _optionalChain([node, 'optionalAccess',
|
|
5319
|
+
const sp = _optionalChain([node, 'optionalAccess', _123 => _123.type]) === "path" ? node.subpaths[state.pen.subpath] : void 0;
|
|
3890
5320
|
const last = sp ? getAnchorWorld(doc, { nodeId: state.pen.nodeId, subpath: state.pen.subpath, index: sp.anchors.length - 1 }) : void 0;
|
|
3891
5321
|
if (last) {
|
|
3892
5322
|
const a = toScreen(state, last);
|
|
@@ -3899,9 +5329,9 @@ function getVectorOverlayRenderData(state) {
|
|
|
3899
5329
|
const artboard = { x: origin.x, y: origin.y, width: doc.width * z, height: doc.height * z };
|
|
3900
5330
|
let status = null;
|
|
3901
5331
|
const it = state.interaction;
|
|
3902
|
-
if (_optionalChain([it, 'optionalAccess',
|
|
3903
|
-
else if (_optionalChain([it, 'optionalAccess',
|
|
3904
|
-
else if (_optionalChain([it, 'optionalAccess',
|
|
5332
|
+
if (_optionalChain([it, 'optionalAccess', _124 => _124.type]) === "rotate") status = `${Math.round(it.angle)}\xB0`;
|
|
5333
|
+
else if (_optionalChain([it, 'optionalAccess', _125 => _125.type]) === "move" || _optionalChain([it, 'optionalAccess', _126 => _126.type]) === "anchors") status = `\u0394 ${fmt(it.dx)}, ${fmt(it.dy)}`;
|
|
5334
|
+
else if (_optionalChain([it, 'optionalAccess', _127 => _127.type]) === "scale" || _optionalChain([it, 'optionalAccess', _128 => _128.type]) === "draw") {
|
|
3905
5335
|
const b = it.type === "draw" ? findVectorNode(doc.nodes, it.nodeId) ? vectorNodeBounds(findVectorNode(doc.nodes, it.nodeId)) : null : state.selectionFrame;
|
|
3906
5336
|
if (b) status = `W ${fmt(b.width)} \xD7 H ${fmt(b.height)}`;
|
|
3907
5337
|
}
|
|
@@ -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 (_optionalChain([it, 'optionalAccess', _129 => _129.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 (_optionalChain([it, 'optionalAccess', _130 => _130.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" && _optionalChain([state, 'access', _131 => _131.hover, 'optionalAccess', _132 => _132.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
|
|
@@ -3943,7 +5388,7 @@ function isVectorTextTarget(target) {
|
|
|
3943
5388
|
if (!el || typeof el !== "object") return false;
|
|
3944
5389
|
const tag = (_nullishCoalesce(el.tagName, () => ( ""))).toUpperCase();
|
|
3945
5390
|
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
|
|
3946
|
-
return !!el.isContentEditable || _optionalChain([el, 'access',
|
|
5391
|
+
return !!el.isContentEditable || _optionalChain([el, 'access', _133 => _133.getAttribute, 'optionalCall', _134 => _134("contenteditable")]) === "true";
|
|
3947
5392
|
}
|
|
3948
5393
|
function getVectorSelectionStyle(state) {
|
|
3949
5394
|
const paths = [];
|
|
@@ -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
|
};
|
|
@@ -4002,12 +5474,20 @@ function createVectorCommands(controller, options = {}) {
|
|
|
4002
5474
|
const base = { group, ...options.scope ? { scope: options.scope } : {} };
|
|
4003
5475
|
const hasSelection = () => controller.getState().selection.length ? true : "Nothing is selected.";
|
|
4004
5476
|
const ids = (params) => {
|
|
4005
|
-
const list2 = _nullishCoalesce(_optionalChain([params, 'optionalAccess',
|
|
5477
|
+
const list2 = _nullishCoalesce(_optionalChain([params, 'optionalAccess', _135 => _135.ids]), () => ( controller.getState().selection));
|
|
4006
5478
|
for (const id of list2) if (!controller.getNode(id)) fail(`There is no node "${id}".`);
|
|
4007
5479
|
if (!list2.length) fail("Nothing is selected; pass ids.");
|
|
4008
5480
|
return list2;
|
|
4009
5481
|
};
|
|
4010
|
-
const enabledFor = (_, params) => _optionalChain([params, 'optionalAccess',
|
|
5482
|
+
const enabledFor = (_, params) => _optionalChain([params, 'optionalAccess', _136 => _136.ids, 'optionalAccess', _137 => _137.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 (_optionalChain([node, 'optionalAccess', _138 => _138.type]) !== "path" || !_optionalChain([node, 'access', _139 => _139.subpaths, 'access', _140 => _140[r.subpath], 'optionalAccess', _141 => _141.anchors, 'access', _142 => _142[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 = [];
|
|
@@ -4094,7 +5574,7 @@ function createVectorCommands(controller, options = {}) {
|
|
|
4094
5574
|
run: (_ctx, params) => {
|
|
4095
5575
|
const id = controller.addPath(params.d, { ...pickStyle(params), ...params.name ? { name: params.name } : {} });
|
|
4096
5576
|
const node = controller.getNode(id);
|
|
4097
|
-
if (_optionalChain([node, 'optionalAccess',
|
|
5577
|
+
if (_optionalChain([node, 'optionalAccess', _143 => _143.type]) === "path" && !node.subpaths.length) {
|
|
4098
5578
|
controller.undo();
|
|
4099
5579
|
fail("The path data has no points.");
|
|
4100
5580
|
}
|
|
@@ -4227,7 +5707,7 @@ function createVectorCommands(controller, options = {}) {
|
|
|
4227
5707
|
enabled: enabledFor,
|
|
4228
5708
|
undoable: true,
|
|
4229
5709
|
run: (_ctx, params) => {
|
|
4230
|
-
const id = controller.group({ ids: ids(params), name: _optionalChain([params, 'optionalAccess',
|
|
5710
|
+
const id = controller.group({ ids: ids(params), name: _optionalChain([params, 'optionalAccess', _144 => _144.name]) });
|
|
4231
5711
|
if (!id) fail("Only nodes that share a parent can be grouped.");
|
|
4232
5712
|
return { id };
|
|
4233
5713
|
}
|
|
@@ -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(_optionalChain([params, 'optionalAccess', _145 => _145.anchors])), { keepShape: _optionalChain([params, 'optionalAccess', _146 => _146.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: _optionalChain([params, 'optionalAccess', _147 => _147.ids]) ? ids(params) : void 0, tolerance: _optionalChain([params, 'optionalAccess', _148 => _148.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(_nullishCoalesce(_optionalChain([params, 'optionalAccess', _149 => _149.axis]), () => ( "both")), anchorsOf(_optionalChain([params, 'optionalAccess', _150 => _150.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: _optionalChain([params, 'optionalAccess', _151 => _151.ids]) ? ids(params) : void 0, tolerance: _optionalChain([params, 'optionalAccess', _152 => _152.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: _optionalChain([params, 'optionalAccess', _153 => _153.ids]) ? ids(params) : void 0, amount: _optionalChain([params, 'optionalAccess', _154 => _154.amount]), refs: _optionalChain([params, 'optionalAccess', _155 => _155.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 (_optionalChain([params, 'optionalAccess', _156 => _156.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(_optionalChain([params, 'optionalAccess', _157 => _157.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 (_optionalChain([params, 'optionalAccess', _158 => _158[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`,
|
|
@@ -4266,7 +5926,7 @@ function createVectorCommands(controller, options = {}) {
|
|
|
4266
5926
|
keywords: ["download", "save", "markup"],
|
|
4267
5927
|
description: "Returns the document as SVG markup.",
|
|
4268
5928
|
params: { type: "object", properties: { precision: { type: "integer", minimum: 0, maximum: 8 } } },
|
|
4269
|
-
run: (_ctx, params) => ({ svg: controller.toSvg({ precision: _optionalChain([params, 'optionalAccess',
|
|
5929
|
+
run: (_ctx, params) => ({ svg: controller.toSvg({ precision: _optionalChain([params, 'optionalAccess', _159 => _159.precision]) }) })
|
|
4270
5930
|
},
|
|
4271
5931
|
{
|
|
4272
5932
|
...base,
|
|
@@ -4373,5 +6033,30 @@ function createVectorCommands(controller, options = {}) {
|
|
|
4373
6033
|
|
|
4374
6034
|
|
|
4375
6035
|
|
|
4376
|
-
|
|
6036
|
+
|
|
6037
|
+
|
|
6038
|
+
|
|
6039
|
+
|
|
6040
|
+
|
|
6041
|
+
|
|
6042
|
+
|
|
6043
|
+
|
|
6044
|
+
|
|
6045
|
+
|
|
6046
|
+
|
|
6047
|
+
|
|
6048
|
+
|
|
6049
|
+
|
|
6050
|
+
|
|
6051
|
+
|
|
6052
|
+
|
|
6053
|
+
|
|
6054
|
+
|
|
6055
|
+
|
|
6056
|
+
|
|
6057
|
+
|
|
6058
|
+
|
|
6059
|
+
|
|
6060
|
+
|
|
6061
|
+
exports.DEFAULT_VECTOR_PATH_EDIT = DEFAULT_VECTOR_PATH_EDIT; exports.DEFAULT_VECTOR_SNAP = DEFAULT_VECTOR_SNAP; exports.IDENTITY_MATRIX = IDENTITY_MATRIX; exports.KAPPA = KAPPA; exports.LazyMouse = LazyMouse; exports.VECTOR_CLIPBOARD_TYPE = VECTOR_CLIPBOARD_TYPE; exports.VECTOR_CONSTRUCTION_ANGLES = VECTOR_CONSTRUCTION_ANGLES; exports.VECTOR_EDIT_TOGGLES = VECTOR_EDIT_TOGGLES; exports.VECTOR_INK_MAX_CORNERS = VECTOR_INK_MAX_CORNERS; exports.VECTOR_OVERLAY_THEME = VECTOR_OVERLAY_THEME; exports.VECTOR_PATH_ACTIONS = VECTOR_PATH_ACTIONS; exports.VECTOR_PENCIL_STRING_PX = VECTOR_PENCIL_STRING_PX; exports.VECTOR_ROTATE_HANDLE_OFFSET = VECTOR_ROTATE_HANDLE_OFFSET; exports.VECTOR_TOOLS = VECTOR_TOOLS; exports.VECTOR_TOOL_GLYPHS = VECTOR_TOOL_GLYPHS; exports.VECTOR_TOOL_SHORTCUTS = VECTOR_TOOL_SHORTCUTS; exports.VECTOR_TRANSFORM_HANDLES = VECTOR_TRANSFORM_HANDLES; exports.VectorEditorController = VectorEditorController; exports.VectorSnapTargets = VectorSnapTargets; exports.applyMatrix = applyMatrix; exports.applyMatrixLinear = applyMatrixLinear; exports.bendSegment = bendSegment; exports.bezierToSubpaths = bezierToSubpaths; exports.boundsFromPoints = boundsFromPoints; exports.boundsIntersect = boundsIntersect; exports.cloneAnchor = cloneAnchor; exports.cloneSubpath = cloneSubpath; exports.cloneVectorDocument = cloneVectorDocument; exports.cloneVectorNode = cloneVectorNode; exports.collectPathNodes = collectPathNodes; exports.constrainToLeave = constrainToLeave; exports.convertAnchor = convertAnchor; exports.createVectorCommands = createVectorCommands; exports.curvatureMoveAnchor = curvatureMoveAnchor; exports.cutSubpathAt = cutSubpathAt; exports.decomposeRigid = decomposeRigid; exports.ellipseSubpath = ellipseSubpath; exports.eraseFromFill = eraseFromFill; exports.eraseFromOutline = eraseFromOutline; exports.eraserRegion = eraserRegion; exports.findVectorNode = findVectorNode; exports.getAnchor = getAnchor; exports.getAnchorWorld = getAnchorWorld; exports.getEditablePathIds = getEditablePathIds; exports.getTransformHandlePositions = getTransformHandlePositions; exports.getVectorOverlayRenderData = getVectorOverlayRenderData; exports.getVectorSceneRenderData = getVectorSceneRenderData; exports.getVectorSelectionBoxGeometry = getVectorSelectionBoxGeometry; exports.getVectorSelectionFrame = getVectorSelectionFrame; exports.getVectorSelectionStyle = getVectorSelectionStyle; exports.getVisibleAnchors = getVisibleAnchors; exports.getVisibleHandles = getVisibleHandles; exports.hasPaint = hasPaint; exports.hitTestVectorNode = hitTestVectorNode; exports.hitTestVectorNodes = hitTestVectorNodes; exports.inferAnchorType = inferAnchorType; exports.insertAnchor = insertAnchor; exports.invertMatrix = invertMatrix; exports.isIdentityMatrix = isIdentityMatrix; exports.isVectorTextTarget = isVectorTextTarget; exports.joinSubpaths = joinSubpaths; exports.knifePath = knifePath; exports.lineSubpath = lineSubpath; exports.locateVectorNode = locateVectorNode; exports.matrixRotation = matrixRotation; exports.moveAnchorHandle = moveAnchorHandle; exports.multiplyMatrices = multiplyMatrices; exports.nearestSegment = nearestSegment; exports.nodeMatrix = nodeMatrix; exports.parseSvgTransform = parseSvgTransform; exports.parseVectorSvg = parseVectorSvg; exports.polygonSubpath = polygonSubpath; exports.polylineBounds = polylineBounds; exports.recognizeInkShape = recognizeInkShape; exports.rectSubpath = rectSubpath; exports.removeAnchorKeepingShape = removeAnchorKeepingShape; exports.removeAnchors = removeAnchors; exports.removeAnchorsKeepingShape = removeAnchorsKeepingShape; exports.removeRedundantPoints = removeRedundantPoints; exports.reverseSubpath = reverseSubpath; exports.rotateMatrix = rotateMatrix; exports.roundSubpaths = roundSubpaths; exports.scaleFor = scaleFor; exports.scaleMatrix = scaleMatrix; exports.simplifyPolyline = simplifyPolyline; exports.smoothSubpath = smoothSubpath; exports.splitIntoFaces = splitIntoFaces; exports.splitSubpathAt = splitSubpathAt; exports.starSubpath = starSubpath; exports.subpathPointAt = subpathPointAt; exports.subpathSegment = subpathSegment; exports.subpathSegmentCount = subpathSegmentCount; exports.subpathsToBezier = subpathsToBezier; exports.toVectorKeyInput = toVectorKeyInput; exports.toVectorModifiers = toVectorModifiers; exports.toVectorPointerInput = toVectorPointerInput; exports.transformVectorNode = transformVectorNode; exports.translateAnchor = translateAnchor; exports.translateMatrix = translateMatrix; exports.unionBounds = unionBounds; exports.vectorDocumentToSvg = vectorDocumentToSvg; exports.vectorInkShapeName = vectorInkShapeName; exports.vectorNodeBounds = vectorNodeBounds; exports.vectorNodeRotation = vectorNodeRotation; exports.vectorNodeToBezier = vectorNodeToBezier; exports.vectorPaintToHex = vectorPaintToHex; exports.vectorPaintToSvg = vectorPaintToSvg; exports.vectorParentMatrix = vectorParentMatrix; exports.vectorPathData = vectorPathData; exports.vectorPathWorldBezier = vectorPathWorldBezier; exports.vectorResizeCursor = vectorResizeCursor; exports.vectorToolLabel = vectorToolLabel; exports.vectorWorldMatrix = vectorWorldMatrix; exports.walkVectorNodes = walkVectorNodes;
|
|
4377
6062
|
//# sourceMappingURL=index.cjs.map
|