@pixerate/editor 0.34.0 → 0.36.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.
@@ -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,20 +17,447 @@ var _chunkQKLRAA4Tcjs = require('../chunk-QKLRAA4T.cjs');
17
17
 
18
18
 
19
19
 
20
- var _chunkUG5DVXJKcjs = require('../chunk-UG5DVXJK.cjs');
20
+
21
+
22
+
23
+
24
+
25
+
26
+
27
+
28
+
29
+
30
+
31
+
32
+
33
+
34
+ var _chunkBGNPT5PEcjs = require('../chunk-BGNPT5PE.cjs');
21
35
 
22
36
  // src/vector/types.ts
23
- var VECTOR_TOOLS = ["select", "direct", "pen", "pencil", "rect", "ellipse", "polygon", "star", "line"];
37
+ var VECTOR_CORNER_KINDS = ["round", "inverted", "chamfer"];
38
+ var VECTOR_ARROWHEADS = ["triangle", "arrow", "open", "circle", "square", "bar"];
39
+ var VECTOR_TOOLS = ["select", "direct", "pen", "pencil", "rect", "ellipse", "polygon", "star", "line", "scissors", "knife", "eraser"];
24
40
  var VECTOR_TRANSFORM_HANDLES = ["nw", "n", "ne", "e", "se", "s", "sw", "w", "rotate"];
25
41
 
26
- // src/vector/geometry.ts
42
+ // src/vector/corners.ts
27
43
  var EPS = 1e-9;
44
+ var MIN_SINE = 1e-6;
45
+ var sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
46
+ var add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
47
+ var mul = (a, k) => ({ x: a.x * k, y: a.y * k });
48
+ var dot = (a, b) => a.x * b.x + a.y * b.y;
49
+ var cross = (a, b) => a.x * b.y - a.y * b.x;
50
+ var len = (a) => Math.hypot(a.x, a.y);
51
+ var pt = (p) => [p.x, p.y];
52
+ var vec = (p) => ({ x: p[0], y: p[1] });
53
+ var onAnchor = (a, h) => !h || Math.abs(h.x - a.x) < EPS && Math.abs(h.y - a.y) < EPS;
54
+ var Side = class {
55
+
56
+
57
+
58
+ constructor(a, h1, h2, b) {
59
+ this.straight = onAnchor(a, h1) && onAnchor(b, h2);
60
+ this.curve = this.straight ? [pt(a), pt(b)] : [pt(a), pt(_nullishCoalesce(h1, () => ( a))), pt(_nullishCoalesce(h2, () => ( b))), pt(b)];
61
+ this.length = this.straight ? Math.hypot(b.x - a.x, b.y - a.y) : _chunkBGNPT5PEcjs.segmentLength.call(void 0, this.curve);
62
+ }
63
+ /** Unit direction the side leaves the corner in (towards its first control point off the corner). */
64
+ direction() {
65
+ const p0 = vec(this.curve[0]);
66
+ for (const q of this.curve.slice(1)) {
67
+ const d = sub(vec(q), p0);
68
+ const l = len(d);
69
+ if (l > EPS) return mul(d, 1 / l);
70
+ }
71
+ return null;
72
+ }
73
+ /** The point `s` along the side from the corner: its parameter, position and the direction the side runs on in there. */
74
+ at(s, u) {
75
+ if (this.straight) {
76
+ const t2 = this.length > EPS ? Math.min(1, Math.max(0, s / this.length)) : 0;
77
+ return { t: t2, p: add(vec(this.curve[0]), mul(u, s)), dir: u };
78
+ }
79
+ const t = _chunkBGNPT5PEcjs.segmentParamAtLength.call(void 0, this.curve, s, this.length);
80
+ const d = vec(_chunkBGNPT5PEcjs.segmentDerivative.call(void 0, this.curve, t));
81
+ const l = len(d);
82
+ return { t, p: vec(_chunkBGNPT5PEcjs.segmentPoint.call(void 0, this.curve, t)), dir: l > EPS ? mul(d, 1 / l) : u };
83
+ }
84
+ };
85
+ function cornerOf(sp, s, i) {
86
+ const n = sp.anchors.length;
87
+ if (n < 2 || !sp.closed && (i === 0 || i === n - 1)) return null;
88
+ if (n === 2 && sp.closed) return null;
89
+ const a = sp.anchors[i];
90
+ if (a.type === "smooth" || a.type === "symmetric") return null;
91
+ const prev = sp.anchors[(i - 1 + n) % n];
92
+ const next = sp.anchors[(i + 1) % n];
93
+ const sideU = new Side(a, a.in, prev.out, prev);
94
+ const sideV = new Side(a, a.out, next.in, next);
95
+ const u = sideU.direction();
96
+ const v2 = sideV.direction();
97
+ if (!u || !v2) return null;
98
+ const lu = sideU.length;
99
+ const lv = sideV.length;
100
+ if (!(lu > EPS && lv > EPS) || !Number.isFinite(lu) || !Number.isFinite(lv)) return null;
101
+ const cos = Math.max(-1, Math.min(1, dot(u, v2)));
102
+ const sin = Math.abs(cross(u, v2));
103
+ if (sin <= MIN_SINE) return null;
104
+ return { subpath: s, index: i, at: { x: a.x, y: a.y }, u, v: v2, lu, lv, cos, sin, angle: Math.atan2(sin, cos) * 180 / Math.PI, maxRadius: Math.min(lu, lv) / 2 * sin / (1 + cos), sideU, sideV };
105
+ }
106
+ var strip = ({ sideU: _u, sideV: _v, ...info }) => info;
107
+ function vectorCornerAt(sp, index, subpath = 0) {
108
+ const c = cornerOf(sp, subpath, index);
109
+ return c ? strip(c) : null;
110
+ }
111
+ function vectorCorners(subpaths) {
112
+ const out = [];
113
+ subpaths.forEach((sp, s) => {
114
+ for (let i = 0; i < sp.anchors.length; i++) {
115
+ const c = cornerOf(sp, s, i);
116
+ if (c) out.push(strip(c));
117
+ }
118
+ });
119
+ return out;
120
+ }
121
+ var cornerSetback = (c, r) => r * (1 + c.cos) / c.sin;
122
+ var fitCornerRadius = (c, radius) => Number.isFinite(radius) ? Math.min(Math.max(0, radius), c.maxRadius) : 0;
123
+ var cornerBisectorPoint = (c, k) => add(c.at, mul(add(c.u, c.v), k));
124
+ var cornerRadiusChange = (c, d) => dot(d, add(c.u, c.v)) * c.sin / (2 + 2 * c.cos);
125
+ function arcHandle(cos) {
126
+ const c = Math.max(-1, Math.min(1, cos));
127
+ const sinHalf = Math.sqrt(Math.max(0, (1 - c) / 2));
128
+ const cosHalf = Math.sqrt(Math.max(0, (1 + c) / 2));
129
+ return 4 / 3 * sinHalf / (1 + cosHalf);
130
+ }
131
+ function squareTo(a, b) {
132
+ const w = sub(b, mul(a, dot(a, b)));
133
+ const l = len(w);
134
+ return l > EPS ? mul(w, 1 / l) : { x: 0, y: 0 };
135
+ }
136
+ function cutCorner(c, radius, kind) {
137
+ const s = Math.min(cornerSetback(c, radius), Math.min(c.lu, c.lv) / 2);
138
+ if (!(s > EPS)) return null;
139
+ const a = c.sideU.at(s, c.u);
140
+ const b = c.sideV.at(s, c.v);
141
+ if (kind === "chamfer") return { tu: a.t, tv: b.t, start: a.p, out: null, in: null, end: b.p };
142
+ if (kind === "inverted") {
143
+ const h = s * arcHandle(dot(a.dir, b.dir));
144
+ return { tu: a.t, tv: b.t, start: a.p, out: add(a.p, mul(squareTo(a.dir, b.dir), h)), in: add(b.p, mul(squareTo(b.dir, a.dir), h)), end: b.p };
145
+ }
146
+ const d1 = mul(a.dir, -1);
147
+ const d2 = mul(b.dir, -1);
148
+ const turn = Math.acos(Math.max(-1, Math.min(1, -dot(d1, d2))));
149
+ const chord = len(sub(b.p, a.p));
150
+ const k = turn < 1e-9 ? chord / 3 : 4 / 3 * Math.tan(turn / 4) * chord / (2 * Math.sin(turn / 2));
151
+ if (!Number.isFinite(k)) return null;
152
+ return { tu: a.t, tv: b.t, start: a.p, out: add(a.p, mul(d1, k)), in: add(b.p, mul(d2, k)), end: b.p };
153
+ }
154
+ var radiusOf = (a) => {
155
+ const r = _optionalChain([a, 'access', _2 => _2.corner, 'optionalAccess', _3 => _3.radius]);
156
+ return typeof r === "number" && r > 0 && Number.isFinite(r) ? r : 0;
157
+ };
158
+ function hasVectorCorners(subpaths) {
159
+ return subpaths.some((sp) => sp.anchors.some((a) => radiusOf(a) > 0));
160
+ }
161
+ function expandVectorCornersOf(sp, subpath = 0) {
162
+ const n = sp.anchors.length;
163
+ const cuts = sp.anchors.map((a, i) => {
164
+ const r = radiusOf(a);
165
+ if (!r) return null;
166
+ const c = cornerOf(sp, subpath, i);
167
+ if (!c) return null;
168
+ const fitted = fitCornerRadius(c, r);
169
+ return fitted > EPS ? cutCorner(c, fitted, _nullishCoalesce(_optionalChain([a, 'access', _4 => _4.corner, 'optionalAccess', _5 => _5.kind]), () => ( "round"))) : null;
170
+ });
171
+ if (!cuts.some(Boolean)) return { subpath: { closed: sp.closed, anchors: sp.anchors.map(plain) }, sources: sp.anchors.map((_, i) => i) };
172
+ const trimmed = sp.anchors.map((a, j) => {
173
+ const k = (j + 1) % n;
174
+ if (k === 0 && !sp.closed) return void 0;
175
+ const b = sp.anchors[k];
176
+ const from = cuts[j];
177
+ const to = cuts[k];
178
+ if (!from && !to) return void 0;
179
+ if (onAnchor(a, a.out) && onAnchor(b, b.in)) return { out: null, in: null };
180
+ const s0 = from ? from.tv : 0;
181
+ const s1 = to ? 1 - to.tu : 1;
182
+ const seg = [pt(a), pt(_nullishCoalesce(a.out, () => ( a))), pt(_nullishCoalesce(b.in, () => ( b))), pt(b)];
183
+ const part = _chunkBGNPT5PEcjs.subSegment.call(void 0, seg, s0, Math.max(s0, s1));
184
+ return { out: vec(part[1]), in: vec(part[2]) };
185
+ });
186
+ const anchors = [];
187
+ const sources = [];
188
+ let closing = null;
189
+ const handleIn = (i, a) => {
190
+ const t = trimmed[(i - 1 + n) % n];
191
+ return t ? t.in : _nullishCoalesce(a.in, () => ( null));
192
+ };
193
+ const handleOut = (i, a) => {
194
+ const t = trimmed[i];
195
+ return t ? t.out : _nullishCoalesce(a.out, () => ( null));
196
+ };
197
+ sp.anchors.forEach((a, i) => {
198
+ const c = cuts[i];
199
+ if (!c) {
200
+ anchors.push(withHandles({ x: a.x, y: a.y, ...a.type ? { type: a.type } : {} }, handleIn(i, a), handleOut(i, a)));
201
+ sources.push(i);
202
+ return;
203
+ }
204
+ const start = withHandles({ x: c.start.x, y: c.start.y }, handleIn(i, a), c.out);
205
+ const end = withHandles({ x: c.end.x, y: c.end.y }, c.in, handleOut(i, a));
206
+ if (i === 0 && sp.closed) closing = start;
207
+ else {
208
+ anchors.push(start);
209
+ sources.push(i);
210
+ }
211
+ anchors.push(end);
212
+ sources.push(i);
213
+ });
214
+ if (closing) {
215
+ anchors.push(closing);
216
+ sources.push(0);
217
+ }
218
+ return { subpath: { closed: sp.closed, anchors }, sources };
219
+ }
220
+ function withHandles(a, hin, hout) {
221
+ if (hin && !onAnchor(a, hin)) a.in = { x: hin.x, y: hin.y };
222
+ if (hout && !onAnchor(a, hout)) a.out = { x: hout.x, y: hout.y };
223
+ return a;
224
+ }
225
+ function plain(a) {
226
+ if (!("corner" in a)) return a;
227
+ const { corner: _c, ...rest } = a;
228
+ return rest;
229
+ }
230
+ function expandVectorCorners(subpaths) {
231
+ if (!hasVectorCorners(subpaths)) return subpaths;
232
+ return subpaths.map((sp, s) => sp.anchors.some((a) => radiusOf(a) > 0) ? expandVectorCornersOf(sp, s).subpath : sp);
233
+ }
234
+ function setAnchorCorner(a, radius, kind) {
235
+ const { corner, ...rest } = a;
236
+ const r = Number.isFinite(radius) ? Math.max(0, radius) : 0;
237
+ const k = _nullishCoalesce(kind, () => ( _optionalChain([corner, 'optionalAccess', _6 => _6.kind])));
238
+ if (r <= 0 && !kind) return rest;
239
+ return { ...rest, corner: { radius: r, ...k && k !== "round" ? { kind: k } : {} } };
240
+ }
241
+ function nextVectorCornerKind(kind) {
242
+ return kind === "inverted" ? "chamfer" : kind === "chamfer" ? "round" : "inverted";
243
+ }
244
+ function setSubpathCorners(sp, radius, kind) {
245
+ return { closed: sp.closed, anchors: sp.anchors.map((a) => setAnchorCorner(a, radius, kind)) };
246
+ }
247
+
248
+ // src/vector/stroke.ts
249
+ var KAPPA = 0.5522847498307936;
250
+ var NOTCH = 0.7;
251
+ function isClosedBezierShape(path) {
252
+ const contours = _chunkBGNPT5PEcjs.pathToContours.call(void 0, path).filter((c) => c.segments.length > 0);
253
+ return contours.length > 0 && contours.every((c) => c.closed);
254
+ }
255
+ function effectiveStrokeAlign(style, closedShape) {
256
+ const a = _nullishCoalesce(style.strokeAlign, () => ( "center"));
257
+ return closedShape ? a : "center";
258
+ }
259
+ function hasStrokeExtras(style) {
260
+ return (_nullishCoalesce(style.strokeAlign, () => ( "center"))) !== "center" || !!style.startArrow || !!style.endArrow;
261
+ }
262
+ var capExtent = (style, width) => style.strokeLinecap === "round" || style.strokeLinecap === "square" ? width / 2 : 0;
263
+ function arrowheadWeight(style, end) {
264
+ const scale = end === "start" ? style.startArrowScale : style.endArrowScale;
265
+ const s = typeof scale === "number" && scale > 0 && Number.isFinite(scale) ? scale : 1;
266
+ return Math.max(0.25, (_nullishCoalesce(style.strokeWidth, () => ( 1))) * s);
267
+ }
268
+ function arrowheadInset(kind, hw, cap) {
269
+ const l = 4 * hw;
270
+ const solid = (back) => Math.max(back / 2, cap);
271
+ switch (kind) {
272
+ case "triangle":
273
+ case "circle":
274
+ case "square":
275
+ return solid(l);
276
+ case "arrow":
277
+ return solid(NOTCH * l);
278
+ case "bar":
279
+ return solid(0.75 * hw);
280
+ case "open":
281
+ return Math.max(hw * Math.sqrt(l * l + l * l / 4) / (l / 2), cap);
282
+ }
283
+ }
284
+ var v = (x, y) => ({ x, y });
285
+ var plus = (a, b, k = 1) => ({ x: a.x + b.x * k, y: a.y + b.y * k });
286
+ var unit = (a) => {
287
+ const l = Math.hypot(a.x, a.y);
288
+ return l > 0 ? { x: a.x / l, y: a.y / l } : { x: 1, y: 0 };
289
+ };
290
+ function polygon(pts) {
291
+ return [{ type: "M", x: pts[0].x, y: pts[0].y }, ...pts.slice(1).map((p) => ({ type: "L", x: p.x, y: p.y })), { type: "Z" }];
292
+ }
293
+ function circle(c, r) {
294
+ const k = r * KAPPA;
295
+ const { x, y } = c;
296
+ return [
297
+ { type: "M", x: x + r, y },
298
+ { type: "C", x1: x + r, y1: y + k, x2: x + k, y2: y + r, x, y: y + r },
299
+ { type: "C", x1: x - k, y1: y + r, x2: x - r, y2: y + k, x: x - r, y },
300
+ { type: "C", x1: x - r, y1: y - k, x2: x - k, y2: y - r, x, y: y - r },
301
+ { type: "C", x1: x + k, y1: y - r, x2: x + r, y2: y - k, x: x + r, y },
302
+ { type: "Z" }
303
+ ];
304
+ }
305
+ function arrowheadPath(kind, tip, dir, hw) {
306
+ const n = v(-dir.y, dir.x);
307
+ const l = 4 * hw;
308
+ const half = l / 2;
309
+ const base = plus(tip, dir, -l);
310
+ switch (kind) {
311
+ case "triangle":
312
+ return polygon([tip, plus(base, n, half), plus(base, n, -half)]);
313
+ case "arrow":
314
+ return polygon([tip, plus(base, n, half), plus(tip, dir, -NOTCH * l), plus(base, n, -half)]);
315
+ case "open": {
316
+ const t = hw;
317
+ const a = plus(base, n, half);
318
+ const b = plus(base, n, -half);
319
+ const ua = unit(plus(a, tip, -1));
320
+ const ub = unit(plus(b, tip, -1));
321
+ const na = v(-ua.y, ua.x);
322
+ const nb = v(ub.y, -ub.x);
323
+ const inner = plus(tip, dir, -(t * Math.hypot(a.x - tip.x, a.y - tip.y)) / half);
324
+ return polygon([tip, a, plus(a, na, t), inner, plus(b, nb, t), b]);
325
+ }
326
+ case "circle":
327
+ return circle(plus(tip, dir, -half), half);
328
+ case "square":
329
+ return polygon([plus(tip, n, half), plus(base, n, half), plus(base, n, -half), plus(tip, n, -half)]);
330
+ case "bar": {
331
+ const t = 0.75 * hw;
332
+ return polygon([plus(tip, n, half), plus(plus(tip, n, half), dir, -t), plus(plus(tip, n, -half), dir, -t), plus(tip, n, -half)]);
333
+ }
334
+ }
335
+ }
336
+ var toVec = (p) => ({ x: p[0], y: p[1] });
337
+ var segLen = (s) => _chunkBGNPT5PEcjs.segmentLength.call(void 0, s);
338
+ function endOf(segs, atStart) {
339
+ const long = (s2) => segLen(s2) > 1e-9;
340
+ if (atStart) {
341
+ const s2 = _nullishCoalesce(segs.find(long), () => ( segs[0]));
342
+ const t2 = _chunkBGNPT5PEcjs.segmentTangent.call(void 0, s2, 0);
343
+ return { p: toVec(segs[0][0]), out: v(-t2[0], -t2[1]) };
344
+ }
345
+ const s = _nullishCoalesce([...segs].reverse().find(long), () => ( segs[segs.length - 1]));
346
+ const t = _chunkBGNPT5PEcjs.segmentTangent.call(void 0, s, 1);
347
+ const last = segs[segs.length - 1];
348
+ return { p: toVec(last[last.length - 1]), out: v(t[0], t[1]) };
349
+ }
350
+ function pointAlong(segs, fromStart, dist3) {
351
+ const order = fromStart ? segs : [...segs].reverse();
352
+ let acc = 0;
353
+ for (const s of order) {
354
+ const l = segLen(s);
355
+ if (acc + l >= dist3) {
356
+ const want = fromStart ? dist3 - acc : l - (dist3 - acc);
357
+ const pts = _chunkBGNPT5PEcjs.trimSegments.call(void 0, [s], 0, want);
358
+ const p = pts.length ? pts[0][pts[0].length - 1] : s[0];
359
+ return toVec(p);
360
+ }
361
+ acc += l;
362
+ }
363
+ return null;
364
+ }
365
+ function vectorStrokeParts(path, style) {
366
+ const width = _nullishCoalesce(style.strokeWidth, () => ( 1));
367
+ const contours = _chunkBGNPT5PEcjs.pathToContours.call(void 0, path).filter((c) => c.segments.length > 0);
368
+ const closedShape = contours.length > 0 && contours.every((c) => c.closed);
369
+ const align = effectiveStrokeAlign(style, closedShape);
370
+ const drawWidth = align === "center" ? width : width * 2;
371
+ const heads = [];
372
+ if (!contours.length || !style.startArrow && !style.endArrow) return { align, line: path, drawWidth, heads };
373
+ const first = contours[0];
374
+ const last = contours[contours.length - 1];
375
+ const cap = capExtent(style, width);
376
+ const tipMode = style.arrowAlign === "tip";
377
+ const trims = contours.map(() => [0, 0]);
378
+ const place = (end, contour, ci) => {
379
+ const kind = end === "start" ? style.startArrow : style.endArrow;
380
+ if (!kind || contour.closed) return;
381
+ const hw = arrowheadWeight(style, end);
382
+ const inset = arrowheadInset(kind, hw, cap);
383
+ const atStart = end === "start";
384
+ const { p, out } = endOf(contour.segments, atStart);
385
+ let tip;
386
+ let dir;
387
+ if (tipMode) {
388
+ const q = pointAlong(contour.segments, atStart, inset);
389
+ const d = q ? v(p.x - q.x, p.y - q.y) : null;
390
+ dir = d && Math.hypot(d.x, d.y) > 1e-9 ? unit(d) : out;
391
+ tip = p;
392
+ trims[ci][atStart ? 0 : 1] = inset;
393
+ } else {
394
+ tip = plus(p, out, inset);
395
+ dir = out;
396
+ }
397
+ heads.push({ end, kind, tip, dir, inset, path: arrowheadPath(kind, tip, dir, hw) });
398
+ };
399
+ place("start", first, 0);
400
+ place("end", last, contours.length - 1);
401
+ if (!tipMode || !heads.length) return { align, line: path, drawWidth, heads };
402
+ const trimmed = contours.map((c, i) => {
403
+ const [s0, s1] = trims[i];
404
+ if (!s0 && !s1) return c;
405
+ const lengths = c.segments.map(segLen);
406
+ const total = lengths.reduce((a, b) => a + b, 0);
407
+ return { closed: false, segments: total - s1 > s0 ? _chunkBGNPT5PEcjs.trimSegments.call(void 0, c.segments, s0, total - s1, lengths) : [] };
408
+ });
409
+ return { align, line: _chunkBGNPT5PEcjs.contoursToPath.call(void 0, trimmed), drawWidth, heads };
410
+ }
411
+ function vectorStrokeOutlinePath(path, style) {
412
+ const parts = vectorStrokeParts(path, style);
413
+ const stroke = _chunkBGNPT5PEcjs.outlineStroke.call(void 0, parts.line, {
414
+ width: parts.drawWidth,
415
+ join: _nullishCoalesce(style.strokeLinejoin, () => ( "miter")),
416
+ cap: _nullishCoalesce(style.strokeLinecap, () => ( "butt")),
417
+ ..._optionalChain([style, 'access', _7 => _7.strokeDasharray, 'optionalAccess', _8 => _8.length]) ? { dash: style.strokeDasharray } : {}
418
+ });
419
+ const fill = { path, fillRule: _nullishCoalesce(style.fillRule, () => ( "nonzero")) };
420
+ let area = stroke;
421
+ if (parts.align === "inside") area = _chunkBGNPT5PEcjs.pathBoolean.call(void 0, "intersection", [stroke, fill]).path;
422
+ else if (parts.align === "outside") area = _chunkBGNPT5PEcjs.pathBoolean.call(void 0, "difference", [stroke, fill]).path;
423
+ if (!parts.heads.length) return area;
424
+ return _chunkBGNPT5PEcjs.pathBoolean.call(void 0, "union", [area, ...parts.heads.map((h) => h.path)]).path;
425
+ }
426
+ function hitTestVectorStroke(path, p, style, tolerance = 0) {
427
+ const parts = vectorStrokeParts(path, style);
428
+ const point = [p.x, p.y];
429
+ const width = _nullishCoalesce(style.strokeWidth, () => ( 1));
430
+ for (const h of parts.heads) {
431
+ if (_chunkBGNPT5PEcjs.pointInBezierPath.call(void 0, h.path, point) || tolerance > 0 && _chunkBGNPT5PEcjs.distanceToBezierPath.call(void 0, h.path, point) <= tolerance) return true;
432
+ }
433
+ if (!parts.line.length) return false;
434
+ const d = _chunkBGNPT5PEcjs.distanceToBezierPath.call(void 0, parts.line, point);
435
+ if (parts.align === "center") return d <= width / 2 + tolerance;
436
+ if (d > width + tolerance) return false;
437
+ if (d <= tolerance) return true;
438
+ const inside = _chunkBGNPT5PEcjs.pointInBezierPath.call(void 0, path, point, _nullishCoalesce(style.fillRule, () => ( "nonzero")));
439
+ return parts.align === "inside" ? inside : !inside;
440
+ }
441
+ function vectorStrokeReach(style) {
442
+ const width = _nullishCoalesce(style.strokeWidth, () => ( 1));
443
+ const align = _nullishCoalesce(style.strokeAlign, () => ( "center"));
444
+ let reach = align === "center" ? width / 2 : align === "outside" ? width : 0;
445
+ if (style.strokeLinejoin !== "round" && style.strokeLinejoin !== "bevel") reach *= 4;
446
+ for (const end of ["start", "end"]) {
447
+ if (end === "start" ? style.startArrow : style.endArrow) reach = Math.max(reach, 8 * arrowheadWeight(style, end) + width);
448
+ }
449
+ return reach;
450
+ }
451
+
452
+ // src/vector/geometry.ts
453
+ var EPS2 = 1e-9;
28
454
  var cloneAnchor = (a) => ({
29
455
  x: a.x,
30
456
  y: a.y,
31
457
  ...a.in ? { in: { x: a.in.x, y: a.in.y } } : {},
32
458
  ...a.out ? { out: { x: a.out.x, y: a.out.y } } : {},
33
- ...a.type && a.type !== "corner" ? { type: a.type } : {}
459
+ ...a.type && a.type !== "corner" ? { type: a.type } : {},
460
+ ...a.corner ? { corner: { ...a.corner } } : {}
34
461
  });
35
462
  var cloneSubpath = (s) => ({ anchors: s.anchors.map(cloneAnchor), closed: s.closed });
36
463
  function cloneVectorNode(node) {
@@ -48,7 +475,7 @@ function hasPaint(paint) {
48
475
  return true;
49
476
  }
50
477
  var handleOf = (a, side) => _nullishCoalesce(a[side], () => ( { x: a.x, y: a.y }));
51
- var isOnAnchor = (a, h) => !h || Math.abs(h.x - a.x) < EPS && Math.abs(h.y - a.y) < EPS;
478
+ var isOnAnchor = (a, h) => !h || Math.abs(h.x - a.x) < EPS2 && Math.abs(h.y - a.y) < EPS2;
52
479
  function subpathSegmentCount(sp) {
53
480
  const n = sp.anchors.length;
54
481
  if (n < 2) return 0;
@@ -62,9 +489,9 @@ function subpathSegment(sp, i) {
62
489
  const c2 = handleOf(b, "in");
63
490
  return [[a.x, a.y], [c1.x, c1.y], [c2.x, c2.y], [b.x, b.y]];
64
491
  }
65
- function subpathsToBezier(subpaths) {
492
+ function subpathsToBezier(subpaths, options = {}) {
66
493
  const out = [];
67
- for (const sp of subpaths) {
494
+ for (const sp of options.corners === false ? subpaths : expandVectorCorners(subpaths)) {
68
495
  if (sp.anchors.length === 0) continue;
69
496
  const first = sp.anchors[0];
70
497
  out.push({ type: "M", x: first.x, y: first.y });
@@ -84,7 +511,7 @@ function vectorNodeToBezier(node) {
84
511
  }
85
512
  function vectorPathData(input, precision = 3) {
86
513
  const subpaths = Array.isArray(input) ? input : input.subpaths;
87
- return _chunkUG5DVXJKcjs.bezierPathToSvg.call(void 0, subpathsToBezier(subpaths), { precision });
514
+ return _chunkBGNPT5PEcjs.bezierPathToSvg.call(void 0, subpathsToBezier(subpaths), { precision });
88
515
  }
89
516
  function inferAnchorType(a) {
90
517
  if (isOnAnchor(a, a.in) || isOnAnchor(a, a.out)) return "corner";
@@ -94,13 +521,13 @@ function inferAnchorType(a) {
94
521
  const oy = a.out.y - a.y;
95
522
  const li = Math.hypot(ix, iy);
96
523
  const lo = Math.hypot(ox, oy);
97
- const cross = (ix * oy - iy * ox) / (li * lo);
98
- const dot = (ix * ox + iy * oy) / (li * lo);
99
- if (Math.abs(cross) > 1e-3 || dot > 0) return "corner";
524
+ const cross2 = (ix * oy - iy * ox) / (li * lo);
525
+ const dot2 = (ix * ox + iy * oy) / (li * lo);
526
+ if (Math.abs(cross2) > 1e-3 || dot2 > 0) return "corner";
100
527
  return Math.abs(li - lo) <= 1e-3 * Math.max(li, lo) ? "symmetric" : "smooth";
101
528
  }
102
529
  function bezierToSubpaths(input) {
103
- const path = _chunkUG5DVXJKcjs.toBezierPath.call(void 0, input);
530
+ const path = _chunkBGNPT5PEcjs.toBezierPath.call(void 0, input);
104
531
  const out = [];
105
532
  let current = null;
106
533
  const last = () => current.anchors[current.anchors.length - 1];
@@ -197,14 +624,14 @@ function nodeMatrix(node) {
197
624
  function vectorPathWorldBezier(node, parent = IDENTITY_MATRIX) {
198
625
  const m = multiplyMatrices(nodeMatrix(node), parent);
199
626
  const path = subpathsToBezier(node.subpaths);
200
- return isIdentityMatrix(m) ? path : _chunkUG5DVXJKcjs.transformBezierPath.call(void 0, path, m);
627
+ return isIdentityMatrix(m) ? path : _chunkBGNPT5PEcjs.transformBezierPath.call(void 0, path, m);
201
628
  }
202
629
  function vectorNodeBounds(node, parent = IDENTITY_MATRIX) {
203
630
  if (node.type === "group") {
204
631
  const m = multiplyMatrices(nodeMatrix(node), parent);
205
632
  return unionBounds(node.children.map((c) => vectorNodeBounds(c, m)));
206
633
  }
207
- const b = _chunkUG5DVXJKcjs.bezierPathBounds.call(void 0, vectorPathWorldBezier(node, parent));
634
+ const b = _chunkBGNPT5PEcjs.bezierPathBounds.call(void 0, vectorPathWorldBezier(node, parent));
208
635
  if (!b) return null;
209
636
  return { x: b.minX, y: b.minY, width: b.maxX - b.minX, height: b.maxY - b.minY };
210
637
  }
@@ -238,9 +665,13 @@ function hitTestVectorNode(node, p, tolerance = 0, parent = IDENTITY_MATRIX) {
238
665
  const path = vectorPathWorldBezier(node, parent);
239
666
  if (path.length === 0) return false;
240
667
  const point = [p.x, p.y];
241
- if (hasPaint(node.fill) && _chunkUG5DVXJKcjs.pointInBezierPath.call(void 0, path, point, _nullishCoalesce(node.fillRule, () => ( "nonzero")))) return true;
242
- const reach = (hasPaint(node.stroke) ? (_nullishCoalesce(node.strokeWidth, () => ( 1))) / 2 : 0) + tolerance;
243
- return _chunkUG5DVXJKcjs.distanceToBezierPath.call(void 0, path, point) <= reach;
668
+ if (hasPaint(node.fill) && _chunkBGNPT5PEcjs.pointInBezierPath.call(void 0, path, point, _nullishCoalesce(node.fillRule, () => ( "nonzero")))) return true;
669
+ if (hasPaint(node.stroke)) return hitTestVectorStroke(path, p, node, tolerance);
670
+ return _chunkBGNPT5PEcjs.distanceToBezierPath.call(void 0, path, point) <= tolerance;
671
+ }
672
+ function vectorStrokeOutline(node, parent = IDENTITY_MATRIX) {
673
+ if (!hasPaint(node.stroke) || !((_nullishCoalesce(node.strokeWidth, () => ( 1))) > 0)) return [];
674
+ return vectorStrokeOutlinePath(vectorPathWorldBezier(node, parent), node);
244
675
  }
245
676
  function hitTestVectorNodes(nodes, p, tolerance = 0) {
246
677
  for (let i = nodes.length - 1; i >= 0; i--) {
@@ -256,10 +687,10 @@ function nearestSegment(node, p) {
256
687
  const count = subpathSegmentCount(sp);
257
688
  for (let i = 0; i < count; i++) {
258
689
  const seg = subpathSegment(sp, i);
259
- const hit = _chunkUG5DVXJKcjs.segmentNearest.call(void 0, seg, [p.x, p.y]);
690
+ const hit = _chunkBGNPT5PEcjs.segmentNearest.call(void 0, seg, [p.x, p.y]);
260
691
  if (!best || hit.distance < best.distance) {
261
- const pt = _chunkUG5DVXJKcjs.segmentPoint.call(void 0, seg, hit.t);
262
- best = { subpath: s, segment: i, t: hit.t, point: { x: pt[0], y: pt[1] }, distance: hit.distance };
692
+ const pt2 = _chunkBGNPT5PEcjs.segmentPoint.call(void 0, seg, hit.t);
693
+ best = { subpath: s, segment: i, t: hit.t, point: { x: pt2[0], y: pt2[1] }, distance: hit.distance };
263
694
  }
264
695
  }
265
696
  });
@@ -293,8 +724,8 @@ function invertMatrix(m) {
293
724
  if (!det || !Number.isFinite(det)) return IDENTITY_MATRIX;
294
725
  return [d / det, -b / det, -c / det, a / det, (c * f - d * e) / det, (b * e - a * f) / det];
295
726
  }
296
- function applyMatrixLinear(m, v) {
297
- return { x: m[0] * v.x + m[2] * v.y, y: m[1] * v.x + m[3] * v.y };
727
+ function applyMatrixLinear(m, v2) {
728
+ return { x: m[0] * v2.x + m[2] * v2.y, y: m[1] * v2.x + m[3] * v2.y };
298
729
  }
299
730
  function matrixRotation(m) {
300
731
  const [a, b, c, d] = m;
@@ -328,6 +759,7 @@ function bakeIntoGeometry(node, m) {
328
759
  if (a.in) next.in = applyMatrix(m, a.in);
329
760
  if (a.out) next.out = applyMatrix(m, a.out);
330
761
  if (a.type) next.type = a.type;
762
+ if (a.corner) next.corner = { ...a.corner };
331
763
  return next;
332
764
  })
333
765
  }))
@@ -379,12 +811,12 @@ function moveAnchorHandle(a, side, pos, breakHandles = false) {
379
811
  if (breakHandles) delete next.type;
380
812
  const dx = pos.x - a.x;
381
813
  const dy = pos.y - a.y;
382
- const len = Math.hypot(dx, dy);
814
+ const len2 = Math.hypot(dx, dy);
383
815
  if (type === "symmetric") next[other] = { x: a.x - dx, y: a.y - dy };
384
- else if (type === "smooth" && len > EPS) {
816
+ else if (type === "smooth" && len2 > EPS2) {
385
817
  const o = a[other];
386
- const olen = o ? Math.hypot(o.x - a.x, o.y - a.y) : len;
387
- next[other] = { x: a.x - dx / len * olen, y: a.y - dy / len * olen };
818
+ const olen = o ? Math.hypot(o.x - a.x, o.y - a.y) : len2;
819
+ next[other] = { x: a.x - dx / len2 * olen, y: a.y - dy / len2 * olen };
388
820
  }
389
821
  if (isOnAnchor(next, next.in)) delete next.in;
390
822
  if (isOnAnchor(next, next.out)) delete next.out;
@@ -411,15 +843,15 @@ function convertAnchor(sp, index, type) {
411
843
  else if (prev && next) dir = { x: next.x - prev.x, y: next.y - prev.y };
412
844
  else if (next) dir = { x: next.x - a.x, y: next.y - a.y };
413
845
  else if (prev) dir = { x: a.x - prev.x, y: a.y - prev.y };
414
- if (!dir || Math.hypot(dir.x, dir.y) < EPS) return { anchors, closed: sp.closed };
846
+ if (!dir || Math.hypot(dir.x, dir.y) < EPS2) return { anchors, closed: sp.closed };
415
847
  const dl = Math.hypot(dir.x, dir.y);
416
848
  const ux = dir.x / dl;
417
849
  const uy = dir.y / dl;
418
850
  lenIn = a.in && !isOnAnchor(a, a.in) ? Math.hypot(a.in.x - a.x, a.in.y - a.y) : prev ? Math.hypot(prev.x - a.x, prev.y - a.y) / 3 : 0;
419
851
  lenOut = a.out && !isOnAnchor(a, a.out) ? Math.hypot(a.out.x - a.x, a.out.y - a.y) : next ? Math.hypot(next.x - a.x, next.y - a.y) / 3 : 0;
420
852
  if (type === "symmetric") lenIn = lenOut = (lenIn + lenOut) / 2 || Math.max(lenIn, lenOut);
421
- if (lenIn > EPS) a.in = { x: a.x - ux * lenIn, y: a.y - uy * lenIn };
422
- if (lenOut > EPS) a.out = { x: a.x + ux * lenOut, y: a.y + uy * lenOut };
853
+ if (lenIn > EPS2) a.in = { x: a.x - ux * lenIn, y: a.y - uy * lenIn };
854
+ if (lenOut > EPS2) a.out = { x: a.x + ux * lenOut, y: a.y + uy * lenOut };
423
855
  a.type = type;
424
856
  return { anchors, closed: sp.closed };
425
857
  }
@@ -429,7 +861,7 @@ function insertAnchor(sp, segment, t) {
429
861
  const ia = segment;
430
862
  const ib = (segment + 1) % n;
431
863
  const seg = subpathSegment(sp, segment);
432
- const [left, right] = _chunkUG5DVXJKcjs.splitSegment.call(void 0, seg, t);
864
+ const [left, right] = _chunkBGNPT5PEcjs.splitSegment.call(void 0, seg, t);
433
865
  const mid = left[left.length - 1];
434
866
  const anchor = { x: mid[0], y: mid[1] };
435
867
  if (seg.length === 4) {
@@ -455,37 +887,24 @@ function reverseSubpath(sp) {
455
887
  if (a.out) next.in = { ...a.out };
456
888
  if (a.in) next.out = { ...a.in };
457
889
  if (a.type) next.type = a.type;
890
+ if (a.corner) next.corner = { ...a.corner };
458
891
  return next;
459
892
  });
460
893
  return { anchors, closed: sp.closed };
461
894
  }
462
- var KAPPA = 0.5522847498307936;
463
- function rectSubpath(b, radius = 0) {
895
+ var KAPPA2 = 0.5522847498307936;
896
+ function rectSubpath(b, radius = 0, kind) {
464
897
  const { x, y, width: w, height: h } = b;
465
- const r = Math.max(0, Math.min(radius, w / 2, h / 2));
466
- if (r <= EPS) {
467
- return { anchors: [{ x, y }, { x: x + w, y }, { x: x + w, y: y + h }, { x, y: y + h }], closed: true };
468
- }
469
- const k = r * KAPPA;
470
- const anchors = [
471
- { x: x + r, y, in: { x: x + r - k, y } },
472
- { x: x + w - r, y, out: { x: x + w - r + k, y } },
473
- { x: x + w, y: y + r, in: { x: x + w, y: y + r - k } },
474
- { x: x + w, y: y + h - r, out: { x: x + w, y: y + h - r + k } },
475
- { x: x + w - r, y: y + h, in: { x: x + w - r + k, y: y + h } },
476
- { x: x + r, y: y + h, out: { x: x + r - k, y: y + h } },
477
- { x, y: y + h - r, in: { x, y: y + h - r + k } },
478
- { x, y: y + r, out: { x, y: y + r - k } }
479
- ];
480
- return { anchors, closed: true };
898
+ const sp = { anchors: [{ x, y }, { x: x + w, y }, { x: x + w, y: y + h }, { x, y: y + h }], closed: true };
899
+ return radius > 0 ? setSubpathCorners(sp, radius, kind) : sp;
481
900
  }
482
901
  function ellipseSubpath(b) {
483
902
  const rx = b.width / 2;
484
903
  const ry = b.height / 2;
485
904
  const cx = b.x + rx;
486
905
  const cy = b.y + ry;
487
- const kx = rx * KAPPA;
488
- const ky = ry * KAPPA;
906
+ const kx = rx * KAPPA2;
907
+ const ky = ry * KAPPA2;
489
908
  return {
490
909
  closed: true,
491
910
  anchors: [
@@ -533,15 +952,52 @@ function roundSubpaths(subpaths, precision = 3) {
533
952
  }
534
953
 
535
954
  // src/vector/svg.ts
955
+ function vectorCornersAttribute(subpaths, precision = 3) {
956
+ const out = [];
957
+ subpaths.forEach(
958
+ (sp, s) => sp.anchors.forEach((a, i) => {
959
+ if (!a.corner || !(a.corner.radius > 0)) return;
960
+ out.push(`${s}.${i}:${_chunkBGNPT5PEcjs.formatNumber.call(void 0, a.corner.radius, precision)}${a.corner.kind && a.corner.kind !== "round" ? `:${a.corner.kind}` : ""}`);
961
+ })
962
+ );
963
+ return out.join(" ");
964
+ }
965
+ function applyVectorCornersAttribute(subpaths, value) {
966
+ for (const item of value.trim().split(/\s+/)) {
967
+ const m = /^(\d+)\.(\d+):([\d.eE+-]+)(?::([a-z]+))?$/.exec(item);
968
+ if (!m) continue;
969
+ const a = _optionalChain([subpaths, 'access', _9 => _9[+m[1]], 'optionalAccess', _10 => _10.anchors, 'access', _11 => _11[+m[2]]]);
970
+ const radius = parseFloat(m[3]);
971
+ if (!a || !(radius > 0)) continue;
972
+ const kind = VECTOR_CORNER_KINDS.includes(m[4]) ? m[4] : void 0;
973
+ a.corner = { radius, ...kind && kind !== "round" ? { kind } : {} };
974
+ }
975
+ return subpaths;
976
+ }
536
977
  var escapeAttr = (value) => value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
537
978
  function vectorPaintToSvg(paint, gradientId) {
538
979
  if (!hasPaint(paint)) return { value: "none", gradient: null };
539
980
  if (typeof paint === "string") return { value: paint, gradient: null };
540
- return { value: `url(#${gradientId})`, gradient: _chunkUG5DVXJKcjs.gradientToSvgDef.call(void 0, paint, gradientId) };
981
+ return { value: `url(#${gradientId})`, gradient: _chunkBGNPT5PEcjs.gradientToSvgDef.call(void 0, paint, gradientId) };
982
+ }
983
+ function vectorStrokeSvgParts(node, clipId, precision = 3) {
984
+ if (!hasPaint(node.stroke) || !hasStrokeExtras(node)) return void 0;
985
+ const path = subpathsToBezier(node.subpaths);
986
+ const parts = vectorStrokeParts(path, node);
987
+ if (parts.align === "center" && !parts.heads.length) return void 0;
988
+ let clip = null;
989
+ if (parts.align !== "center") {
990
+ const b = _chunkBGNPT5PEcjs.bezierPathBounds.call(void 0, path);
991
+ const pad = vectorStrokeReach(node) + 1;
992
+ const bounds = b ? { x: round2(b.minX - pad), y: round2(b.minY - pad), width: round2(b.maxX - b.minX + 2 * pad), height: round2(b.maxY - b.minY + 2 * pad) } : { x: 0, y: 0, width: 0, height: 0 };
993
+ clip = { kind: parts.align === "inside" ? "clip" : "mask", id: clipId, bounds };
994
+ }
995
+ return { align: parts.align, d: _chunkBGNPT5PEcjs.bezierPathToSvg.call(void 0, parts.line, { precision }), width: parts.drawWidth, clip, heads: parts.heads.map((h) => _chunkBGNPT5PEcjs.bezierPathToSvg.call(void 0, h.path, { precision })) };
541
996
  }
997
+ var round2 = (n) => Math.round(n * 100) / 100;
542
998
  function gradientMarkup(def) {
543
999
  const tag = def.type === "radial" ? "radialGradient" : "linearGradient";
544
- const attrs = Object.entries(def.attrs).map(([k, v]) => ` ${k}="${escapeAttr(String(v))}"`).join("");
1000
+ const attrs = Object.entries(def.attrs).map(([k, v2]) => ` ${k}="${escapeAttr(String(v2))}"`).join("");
545
1001
  const stops = def.stops.map((s) => `<stop offset="${s.offset}" stop-color="${escapeAttr(s.stopColor)}"${s.stopOpacity !== void 0 ? ` stop-opacity="${s.stopOpacity}"` : ""}/>`).join("");
546
1002
  return `<${tag} id="${escapeAttr(def.id)}"${attrs}>${stops}</${tag}>`;
547
1003
  }
@@ -551,11 +1007,11 @@ function vectorDocumentToSvg(doc, options = {}) {
551
1007
  const pretty = options.pretty !== false;
552
1008
  const defs = [];
553
1009
  const lines = [];
554
- const num = (n) => _chunkUG5DVXJKcjs.formatNumber.call(void 0, n, precision);
1010
+ const num = (n) => _chunkBGNPT5PEcjs.formatNumber.call(void 0, n, precision);
555
1011
  const indent = (depth) => pretty ? " ".repeat(depth) : "";
556
1012
  const common = (node) => {
557
1013
  let out2 = ` id="${escapeAttr(node.id)}"`;
558
- if (node.transform) out2 += ` transform="matrix(${node.transform.map((n) => _chunkUG5DVXJKcjs.formatNumber.call(void 0, n, 6)).join(" ")})"`;
1014
+ if (node.transform) out2 += ` transform="matrix(${node.transform.map((n) => _chunkBGNPT5PEcjs.formatNumber.call(void 0, n, 6)).join(" ")})"`;
559
1015
  if (node.opacity !== void 0 && node.opacity < 1) out2 += ` opacity="${num(node.opacity)}"`;
560
1016
  if (node.visible === false) out2 += ` display="none"`;
561
1017
  return out2;
@@ -572,15 +1028,53 @@ function vectorDocumentToSvg(doc, options = {}) {
572
1028
  const stroke = vectorPaintToSvg(node.stroke, `${prefix}stroke-${node.id}`);
573
1029
  if (fill.gradient) defs.push(gradientMarkup(fill.gradient));
574
1030
  if (stroke.gradient) defs.push(gradientMarkup(stroke.gradient));
575
- let attrs = `${common(node)} d="${vectorPathData(node, precision)}" fill="${escapeAttr(fill.value)}"`;
576
- if (node.fillRule === "evenodd") attrs += ` fill-rule="evenodd"`;
577
- if (stroke.value !== "none") {
578
- attrs += ` stroke="${escapeAttr(stroke.value)}" stroke-width="${num(_nullishCoalesce(node.strokeWidth, () => ( 1)))}"`;
579
- if (node.strokeLinecap && node.strokeLinecap !== "butt") attrs += ` stroke-linecap="${node.strokeLinecap}"`;
580
- if (node.strokeLinejoin && node.strokeLinejoin !== "miter") attrs += ` stroke-linejoin="${node.strokeLinejoin}"`;
581
- if (_optionalChain([node, 'access', _2 => _2.strokeDasharray, 'optionalAccess', _3 => _3.length])) attrs += ` stroke-dasharray="${node.strokeDasharray.map(num).join(" ")}"`;
582
- }
583
- lines.push(`${indent(depth)}<path${attrs}/>`);
1031
+ const d = vectorPathData(node, precision);
1032
+ const corners2 = vectorCornersAttribute(node.subpaths, precision);
1033
+ const live = corners2 ? ` data-vector-d="${_chunkBGNPT5PEcjs.bezierPathToSvg.call(void 0, subpathsToBezier(node.subpaths, { corners: false }), { precision })}" data-vector-corners="${corners2}"` : "";
1034
+ const fillAttrs = ` fill="${escapeAttr(fill.value)}"${node.fillRule === "evenodd" ? ` fill-rule="evenodd"` : ""}`;
1035
+ const lineAttrs = (width) => {
1036
+ let out2 = ` stroke="${escapeAttr(stroke.value)}" stroke-width="${num(width)}"`;
1037
+ if (node.strokeLinecap && node.strokeLinecap !== "butt") out2 += ` stroke-linecap="${node.strokeLinecap}"`;
1038
+ if (node.strokeLinejoin && node.strokeLinejoin !== "miter") out2 += ` stroke-linejoin="${node.strokeLinejoin}"`;
1039
+ if (_optionalChain([node, 'access', _12 => _12.strokeDasharray, 'optionalAccess', _13 => _13.length])) out2 += ` stroke-dasharray="${node.strokeDasharray.map(num).join(" ")}"`;
1040
+ return out2;
1041
+ };
1042
+ const parts = stroke.value !== "none" ? vectorStrokeSvgParts(node, `${prefix}clip-${node.id}`, precision) : void 0;
1043
+ if (!parts) {
1044
+ lines.push(`${indent(depth)}<path${common(node)} d="${d}"${fillAttrs}${stroke.value !== "none" ? lineAttrs(_nullishCoalesce(node.strokeWidth, () => ( 1))) : ""}${live}/>`);
1045
+ return;
1046
+ }
1047
+ let data = ` data-vector-path="" data-vector-stroke-width="${num(_nullishCoalesce(node.strokeWidth, () => ( 1)))}"`;
1048
+ if (parts.align !== "center") data += ` data-vector-stroke-align="${parts.align}"`;
1049
+ if (node.startArrow) data += ` data-vector-start-arrow="${node.startArrow}"`;
1050
+ if (node.endArrow) data += ` data-vector-end-arrow="${node.endArrow}"`;
1051
+ if (node.arrowAlign && node.arrowAlign !== "extend") data += ` data-vector-arrow-align="${node.arrowAlign}"`;
1052
+ if (node.startArrowScale !== void 0 && node.startArrowScale !== 1) data += ` data-vector-start-arrow-scale="${num(node.startArrowScale)}"`;
1053
+ if (node.endArrowScale !== void 0 && node.endArrowScale !== 1) data += ` data-vector-end-arrow-scale="${num(node.endArrowScale)}"`;
1054
+ lines.push(`${indent(depth)}<g${common(node)}${data}>`);
1055
+ lines.push(`${indent(depth + 1)}<path data-vector-part="fill" d="${d}"${fillAttrs}${live}/>`);
1056
+ if (options.strokes === "outline") {
1057
+ const area = _chunkBGNPT5PEcjs.bezierPathToSvg.call(void 0, vectorStrokeOutlinePath(subpathsToBezier(node.subpaths), node), { precision });
1058
+ lines.push(`${indent(depth + 1)}<path data-vector-part="stroke-outline" d="${area}" fill="${escapeAttr(stroke.value)}"/>`);
1059
+ } else {
1060
+ let clip = "";
1061
+ if (parts.clip) {
1062
+ const rule = node.fillRule === "evenodd" ? "evenodd" : "nonzero";
1063
+ const id = escapeAttr(parts.clip.id);
1064
+ if (parts.clip.kind === "clip") {
1065
+ defs.push(`<clipPath id="${id}"><path d="${d}" clip-rule="${rule}"/></clipPath>`);
1066
+ clip = ` clip-path="url(#${id})"`;
1067
+ } else {
1068
+ const b = parts.clip.bounds;
1069
+ const box = `x="${num(b.x)}" y="${num(b.y)}" width="${num(b.width)}" height="${num(b.height)}"`;
1070
+ defs.push(`<mask id="${id}" maskUnits="userSpaceOnUse" ${box}><rect ${box} fill="#fff"/><path d="${d}" fill="#000" fill-rule="${rule}"/></mask>`);
1071
+ clip = ` mask="url(#${id})"`;
1072
+ }
1073
+ }
1074
+ lines.push(`${indent(depth + 1)}<path data-vector-part="stroke" d="${parts.d}" fill="none"${lineAttrs(parts.width)}${clip}/>`);
1075
+ for (const h2 of parts.heads) lines.push(`${indent(depth + 1)}<path data-vector-part="head" d="${h2}" fill="${escapeAttr(stroke.value)}"/>`);
1076
+ }
1077
+ lines.push(`${indent(depth)}</g>`);
584
1078
  };
585
1079
  for (const node of doc.nodes) emit(node, 1);
586
1080
  const w = num(doc.width);
@@ -723,7 +1217,7 @@ function resolveGradient(id, raw, seen = /* @__PURE__ */ new Set()) {
723
1217
  seen.add(id);
724
1218
  let stops = readStops(el);
725
1219
  const href = _nullishCoalesce(el.attrs.href, () => ( el.attrs["xlink:href"]));
726
- if (!stops.length && _optionalChain([href, 'optionalAccess', _4 => _4.startsWith, 'call', _5 => _5("#")])) stops = _nullishCoalesce(_optionalChain([resolveGradient, 'call', _6 => _6(href.slice(1), raw, seen), 'optionalAccess', _7 => _7.stops]), () => ( []));
1220
+ if (!stops.length && _optionalChain([href, 'optionalAccess', _14 => _14.startsWith, 'call', _15 => _15("#")])) stops = _nullishCoalesce(_optionalChain([resolveGradient, 'call', _16 => _16(href.slice(1), raw, seen), 'optionalAccess', _17 => _17.stops]), () => ( []));
727
1221
  if (!stops.length) return null;
728
1222
  if (el.name === "radialGradient") {
729
1223
  return { type: "radial", cx: fraction(el.attrs.cx, 0.5), cy: fraction(el.attrs.cy, 0.5), r: fraction(el.attrs.r, 0.5), stops };
@@ -755,11 +1249,11 @@ function parseVectorSvg(svg, options = {}) {
755
1249
  const rawGradients = /* @__PURE__ */ new Map();
756
1250
  collectGradients(root, rawGradients);
757
1251
  const paint = (value) => {
758
- const v = value.trim();
759
- const url = /^url\(\s*['"]?#([^'")\s]+)['"]?\s*\)/.exec(v);
1252
+ const v2 = value.trim();
1253
+ const url = /^url\(\s*['"]?#([^'")\s]+)['"]?\s*\)/.exec(v2);
760
1254
  if (url) return _nullishCoalesce(resolveGradient(url[1], rawGradients), () => ( null));
761
- if (v === "none" || v === "") return null;
762
- return v;
1255
+ if (v2 === "none" || v2 === "") return null;
1256
+ return v2;
763
1257
  };
764
1258
  const viewBox = numberList(root.attrs.viewBox);
765
1259
  const width = viewBox.length === 4 ? viewBox[2] : length(root.attrs.width, 300);
@@ -769,8 +1263,13 @@ function parseVectorSvg(svg, options = {}) {
769
1263
  const shapeOf = (el) => {
770
1264
  const a = el.attrs;
771
1265
  switch (el.name) {
772
- case "path":
1266
+ case "path": {
1267
+ if (a["data-vector-d"] && a["data-vector-corners"]) {
1268
+ const sharp = bezierToSubpaths(a["data-vector-d"]);
1269
+ if (sharp.length) return applyVectorCornersAttribute(sharp, a["data-vector-corners"]);
1270
+ }
773
1271
  return a.d ? bezierToSubpaths(a.d) : null;
1272
+ }
774
1273
  case "rect": {
775
1274
  const w = length(a.width);
776
1275
  const h = length(a.height);
@@ -804,6 +1303,56 @@ function parseVectorSvg(svg, options = {}) {
804
1303
  }
805
1304
  return null;
806
1305
  };
1306
+ const strokeStyle = (s) => ({
1307
+ ...s.strokeWidth ? { strokeWidth: length(s.strokeWidth, 1) } : {},
1308
+ ...s.strokeLinecap ? { strokeLinecap: s.strokeLinecap } : {},
1309
+ ...s.strokeLinejoin ? { strokeLinejoin: s.strokeLinejoin } : {},
1310
+ ...s.strokeDasharray && s.strokeDasharray !== "none" ? { strokeDasharray: numberList(s.strokeDasharray) } : {},
1311
+ ...s.fillRule === "evenodd" ? { fillRule: "evenodd" } : {}
1312
+ });
1313
+ const placed = (node, m) => {
1314
+ if (m === IDENTITY_MATRIX) return node;
1315
+ const next = transformVectorNode(node, m);
1316
+ const strokeScale = Math.sqrt(Math.abs(m[0] * m[3] - m[1] * m[2]));
1317
+ if (hasPaint(next.stroke) && strokeScale > 0 && Math.abs(strokeScale - 1) > 1e-9) next.strokeWidth = (_nullishCoalesce(next.strokeWidth, () => ( 1))) * strokeScale;
1318
+ return next;
1319
+ };
1320
+ const mergedPath = (g, style, m, meta) => {
1321
+ const part = (name) => g.children.find((c) => c.name === "path" && c.attrs["data-vector-part"] === name);
1322
+ const fillEl = part("fill");
1323
+ const subpaths = fillEl ? shapeOf(fillEl) : null;
1324
+ if (!fillEl || !_optionalChain([subpaths, 'optionalAccess', _18 => _18.length])) return null;
1325
+ const a = g.attrs;
1326
+ const fs = readStyle(fillEl, style);
1327
+ const strokeEl = part("stroke");
1328
+ const outlineEl = part("stroke-outline");
1329
+ const ss = strokeEl ? readStyle(strokeEl, style) : null;
1330
+ const width2 = length(a["data-vector-stroke-width"], ss ? length(ss.strokeWidth, 1) : 1);
1331
+ const arrow = (v2) => VECTOR_ARROWHEADS.includes(v2) ? v2 : void 0;
1332
+ const scale = (v2) => {
1333
+ const n = v2 === void 0 ? NaN : parseFloat(v2);
1334
+ return n > 0 ? n : void 0;
1335
+ };
1336
+ const align = a["data-vector-stroke-align"];
1337
+ const node = {
1338
+ id: idFor(g),
1339
+ type: "path",
1340
+ ...meta,
1341
+ subpaths,
1342
+ fill: paint(fs.fill),
1343
+ stroke: ss ? paint(ss.stroke) : outlineEl ? paint(readStyle(outlineEl, style).fill) : null,
1344
+ ...ss ? strokeStyle(ss) : {},
1345
+ ...fs.fillRule === "evenodd" ? { fillRule: "evenodd" } : {},
1346
+ strokeWidth: width2,
1347
+ ...align === "inside" || align === "outside" ? { strokeAlign: align } : {},
1348
+ ...arrow(a["data-vector-start-arrow"]) ? { startArrow: arrow(a["data-vector-start-arrow"]) } : {},
1349
+ ...arrow(a["data-vector-end-arrow"]) ? { endArrow: arrow(a["data-vector-end-arrow"]) } : {},
1350
+ ...a["data-vector-arrow-align"] === "tip" ? { arrowAlign: "tip" } : {},
1351
+ ...scale(a["data-vector-start-arrow-scale"]) ? { startArrowScale: scale(a["data-vector-start-arrow-scale"]) } : {},
1352
+ ...scale(a["data-vector-end-arrow-scale"]) ? { endArrowScale: scale(a["data-vector-end-arrow-scale"]) } : {}
1353
+ };
1354
+ return placed(node, m);
1355
+ };
807
1356
  const visit = (el, style, matrix) => {
808
1357
  const out = [];
809
1358
  for (const child of el.children) {
@@ -817,6 +1366,11 @@ function parseVectorSvg(svg, options = {}) {
817
1366
  ...opacity !== void 0 && Number.isFinite(opacity) && opacity < 1 ? { opacity } : {},
818
1367
  ...hidden ? { visible: false } : {}
819
1368
  };
1369
+ if (child.name === "g" && child.attrs["data-vector-path"] !== void 0) {
1370
+ const node2 = mergedPath(child, s, m, meta);
1371
+ if (node2) out.push(node2);
1372
+ continue;
1373
+ }
820
1374
  if (child.name === "g" || child.name === "svg" || child.name === "a" || child.name === "switch") {
821
1375
  const children = visit(child, s, m);
822
1376
  if (!children.length) continue;
@@ -826,24 +1380,18 @@ function parseVectorSvg(svg, options = {}) {
826
1380
  }
827
1381
  const subpaths = shapeOf(child);
828
1382
  if (!subpaths || !subpaths.length) continue;
829
- let node = {
830
- id: idFor(child),
831
- type: "path",
832
- ...meta,
833
- subpaths,
834
- fill: child.name === "line" ? null : paint(s.fill),
835
- stroke: paint(s.stroke),
836
- ...s.strokeWidth ? { strokeWidth: length(s.strokeWidth, 1) } : {},
837
- ...s.strokeLinecap ? { strokeLinecap: s.strokeLinecap } : {},
838
- ...s.strokeLinejoin ? { strokeLinejoin: s.strokeLinejoin } : {},
839
- ...s.strokeDasharray && s.strokeDasharray !== "none" ? { strokeDasharray: numberList(s.strokeDasharray) } : {},
840
- ...s.fillRule === "evenodd" ? { fillRule: "evenodd" } : {}
841
- };
842
- if (m !== IDENTITY_MATRIX) {
843
- node = transformVectorNode(node, m);
844
- const strokeScale = Math.sqrt(Math.abs(m[0] * m[3] - m[1] * m[2]));
845
- if (hasPaint(node.stroke) && strokeScale > 0 && Math.abs(strokeScale - 1) > 1e-9) node.strokeWidth = (_nullishCoalesce(node.strokeWidth, () => ( 1))) * strokeScale;
846
- }
1383
+ const node = placed(
1384
+ {
1385
+ id: idFor(child),
1386
+ type: "path",
1387
+ ...meta,
1388
+ subpaths,
1389
+ fill: child.name === "line" ? null : paint(s.fill),
1390
+ stroke: paint(s.stroke),
1391
+ ...strokeStyle(s)
1392
+ },
1393
+ m
1394
+ );
847
1395
  out.push(node);
848
1396
  }
849
1397
  return out;
@@ -855,7 +1403,7 @@ function parseVectorSvg(svg, options = {}) {
855
1403
  // src/vector/snap.ts
856
1404
  var VECTOR_CONSTRUCTION_ANGLES = [0, 45, 90, 135];
857
1405
  function constrainToLeave(leave, p) {
858
- if (!_optionalChain([leave, 'optionalAccess', _8 => _8.constrain])) return p;
1406
+ if (!_optionalChain([leave, 'optionalAccess', _19 => _19.constrain])) return p;
859
1407
  const dx = p.x - leave.at.x;
860
1408
  const dy = p.y - leave.at.y;
861
1409
  switch (leave.constrain) {
@@ -866,9 +1414,9 @@ function constrainToLeave(leave, p) {
866
1414
  return { x: leave.at.x + Math.sign(dx || 1) * s, y: leave.at.y + Math.sign(dy || 1) * s };
867
1415
  }
868
1416
  default: {
869
- const len = Math.hypot(dx, dy);
1417
+ const len2 = Math.hypot(dx, dy);
870
1418
  const a = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);
871
- return { x: leave.at.x + Math.cos(a) * len, y: leave.at.y + Math.sin(a) * len };
1419
+ return { x: leave.at.x + Math.cos(a) * len2, y: leave.at.y + Math.sin(a) * len2 };
872
1420
  }
873
1421
  }
874
1422
  }
@@ -877,24 +1425,24 @@ var CROSSING_PAIRS = 32;
877
1425
  var CURVE_SAMPLES = 48;
878
1426
  var BISECT = 50;
879
1427
  var EQUAL = 1e-6;
880
- function bound(sorted, key, v, strict) {
1428
+ function bound(sorted, key, v2, strict) {
881
1429
  let lo = 0;
882
1430
  let hi = sorted.length;
883
1431
  while (lo < hi) {
884
1432
  const mid = lo + hi >>> 1;
885
1433
  const k = key(sorted[mid]);
886
- if (strict ? k <= v : k < v) lo = mid + 1;
1434
+ if (strict ? k <= v2 : k < v2) lo = mid + 1;
887
1435
  else hi = mid;
888
1436
  }
889
1437
  return lo;
890
1438
  }
891
- function within(sorted, key, v, tol) {
892
- return sorted.slice(bound(sorted, key, v - tol, false), bound(sorted, key, v + tol, true));
1439
+ function within(sorted, key, v2, tol) {
1440
+ return sorted.slice(bound(sorted, key, v2 - tol, false), bound(sorted, key, v2 + tol, true));
893
1441
  }
894
- var linesNear = (lines, v, tol) => within(lines, (l) => l.v, v, tol);
1442
+ var linesNear = (lines, v2, tol) => within(lines, (l) => l.v, v2, tol);
895
1443
  var distance = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
896
1444
  var toPoint = (p) => [p.x, p.y];
897
- var toVec = (p) => ({ x: p[0], y: p[1] });
1445
+ var toVec2 = (p) => ({ x: p[0], y: p[1] });
898
1446
  var LABELS = {
899
1447
  anchor: "anchor",
900
1448
  centre: "centre",
@@ -966,7 +1514,7 @@ var VectorSnapTargets = (_class = class _VectorSnapTargets {
966
1514
  moving.set(r.nodeId, set);
967
1515
  }
968
1516
  const t = _VectorSnapTargets.collect(doc, new Set(moving.keys()), "segments", false);
969
- t.pushPaths(doc, new Set(moving.keys()), (id, s, i) => !!_optionalChain([moving, 'access', _9 => _9.get, 'call', _10 => _10(id), 'optionalAccess', _11 => _11.has, 'call', _12 => _12(`${s}:${i}`)]), true);
1517
+ t.pushPaths(doc, new Set(moving.keys()), (id, s, i) => !!_optionalChain([moving, 'access', _20 => _20.get, 'call', _21 => _21(id), 'optionalAccess', _22 => _22.has, 'call', _23 => _23(`${s}:${i}`)]), true);
970
1518
  return t.index();
971
1519
  }
972
1520
  /**
@@ -1012,7 +1560,7 @@ var VectorSnapTargets = (_class = class _VectorSnapTargets {
1012
1560
  const count = subpathSegmentCount(sp);
1013
1561
  for (let i = 0; i < count && budget > 0; i++) {
1014
1562
  if (moving(i) || moving((i + 1) % n)) continue;
1015
- const seg = subpathSegment(sp, i).map((q) => toPoint(applyMatrix(m, toVec(q))));
1563
+ const seg = subpathSegment(sp, i).map((q) => toPoint(applyMatrix(m, toVec2(q))));
1016
1564
  let minX = Infinity;
1017
1565
  let minY = Infinity;
1018
1566
  let maxX = -Infinity;
@@ -1029,13 +1577,13 @@ var VectorSnapTargets = (_class = class _VectorSnapTargets {
1029
1577
  return budget;
1030
1578
  }
1031
1579
  pushRect(r, kind, centre) {
1032
- const corners = [
1580
+ const corners2 = [
1033
1581
  { x: r.x, y: r.y },
1034
1582
  { x: r.x + r.width, y: r.y },
1035
1583
  { x: r.x + r.width, y: r.y + r.height },
1036
1584
  { x: r.x, y: r.y + r.height }
1037
1585
  ];
1038
- for (const p of corners) {
1586
+ for (const p of corners2) {
1039
1587
  this.xs.push({ v: p.x, from: p, kind });
1040
1588
  this.ys.push({ v: p.y, from: p, kind });
1041
1589
  }
@@ -1084,7 +1632,7 @@ var VectorSnapTargets = (_class = class _VectorSnapTargets {
1084
1632
  }
1085
1633
  const b = vectorNodeBounds(n, parent);
1086
1634
  const f = nodeFrame(n, parent, b);
1087
- if (b) self.pushRect(b, "edge", _nullishCoalesce(_optionalChain([f, 'optionalAccess', _13 => _13.centre]), () => ( null)));
1635
+ if (b) self.pushRect(b, "edge", _nullishCoalesce(_optionalChain([f, 'optionalAccess', _24 => _24.centre]), () => ( null)));
1088
1636
  }
1089
1637
  };
1090
1638
  visit(doc.nodes, IDENTITY_MATRIX);
@@ -1129,23 +1677,23 @@ var VectorSnapTargets = (_class = class _VectorSnapTargets {
1129
1677
  lines: lines2,
1130
1678
  labels: [this.label(q, kind)]
1131
1679
  });
1132
- if (_optionalChain([leave, 'optionalAccess', _14 => _14.constrain])) {
1680
+ if (_optionalChain([leave, 'optionalAccess', _25 => _25.constrain])) {
1133
1681
  const q = constrainToLeave(leave, p);
1134
1682
  const hit = this.slide(leave.at, { x: q.x - leave.at.x, y: q.y - leave.at.y }, q, tol, true);
1135
1683
  return hit ? { point: hit.point, x: true, y: true, ...this.linedUp(hit.point, hit.from) } : none(q);
1136
1684
  }
1137
1685
  const point = this.nearestPoint(p, tol);
1138
1686
  if (point) return at(point.p, point.kind);
1139
- const near = this.segmentsNear(p, tol);
1140
- const crossing = this.crossing(near, p, tol);
1687
+ const near2 = this.segmentsNear(p, tol);
1688
+ const crossing = this.crossing(near2, p, tol);
1141
1689
  if (crossing) return at(crossing, "intersect");
1142
1690
  if (leave && leave.curves !== false) {
1143
- const foot = this.curvePoint(near, leave.at, p, tol);
1691
+ const foot = this.curvePoint(near2, leave.at, p, tol);
1144
1692
  if (foot) return at(foot.point, foot.kind, [line("construction", leave.at, foot.point)]);
1145
1693
  }
1146
- const on = this.onSegment(near);
1694
+ const on = this.onSegment(near2);
1147
1695
  if (on) return at(on, "path");
1148
- if (_optionalChain([leave, 'optionalAccess', _15 => _15.angles, 'optionalAccess', _16 => _16.length])) {
1696
+ if (_optionalChain([leave, 'optionalAccess', _26 => _26.angles, 'optionalAccess', _27 => _27.length])) {
1149
1697
  const c = construction(leave, p, tol);
1150
1698
  if (c) {
1151
1699
  const hit = this.slide(leave.at, c.dir, c.point, tol, false);
@@ -1156,9 +1704,9 @@ var VectorSnapTargets = (_class = class _VectorSnapTargets {
1156
1704
  return { point: c.point, kind: "construction", x: true, y: true, lines: [line("construction", leave.at, c.point)], labels: [] };
1157
1705
  }
1158
1706
  }
1159
- const nearest = (lines2, v) => {
1707
+ const nearest = (lines2, v2) => {
1160
1708
  let best = null;
1161
- for (const l of linesNear(lines2, v, tol)) if (!best || Math.abs(l.v - v) < Math.abs(best.v - v)) best = l;
1709
+ for (const l of linesNear(lines2, v2, tol)) if (!best || Math.abs(l.v - v2) < Math.abs(best.v - v2)) best = l;
1162
1710
  return best;
1163
1711
  };
1164
1712
  const bx = nearest(this.xs, p.x);
@@ -1218,26 +1766,26 @@ var VectorSnapTargets = (_class = class _VectorSnapTargets {
1218
1766
  /** Target segments whose control box reaches `p` and which pass within `tol` of it, nearest first. */
1219
1767
  segmentsNear(p, tol) {
1220
1768
  const out = [];
1221
- const pt = toPoint(p);
1769
+ const pt2 = toPoint(p);
1222
1770
  for (const s of this.segments) {
1223
1771
  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 = _chunkUG5DVXJKcjs.segmentNearest.call(void 0, s.seg, pt);
1772
+ const hit = _chunkBGNPT5PEcjs.segmentNearest.call(void 0, s.seg, pt2);
1225
1773
  if (hit.distance <= tol) out.push({ seg: s.seg, t: hit.t, distance: hit.distance });
1226
1774
  }
1227
1775
  return out.sort((a, b) => a.distance - b.distance);
1228
1776
  }
1229
- onSegment(near) {
1230
- return near.length ? toVec(_chunkUG5DVXJKcjs.segmentPoint.call(void 0, near[0].seg, near[0].t)) : null;
1777
+ onSegment(near2) {
1778
+ return near2.length ? toVec2(_chunkBGNPT5PEcjs.segmentPoint.call(void 0, near2[0].seg, near2[0].t)) : null;
1231
1779
  }
1232
1780
  /** Where two of the target segments near `p` cross, the crossing nearest `p` within `tol`. */
1233
- crossing(near, p, tol) {
1234
- const list = near.slice(0, CROSSING_PAIRS);
1781
+ crossing(near2, p, tol) {
1782
+ const list = near2.slice(0, CROSSING_PAIRS);
1235
1783
  let best = null;
1236
1784
  let bestD = Infinity;
1237
1785
  for (let i = 0; i < list.length; i++) {
1238
1786
  for (let j = i + 1; j < list.length; j++) {
1239
- for (const hit of _chunkUG5DVXJKcjs.segmentIntersections.call(void 0, list[i].seg, list[j].seg)) {
1240
- const q = toVec(hit.point);
1787
+ for (const hit of _chunkBGNPT5PEcjs.segmentIntersections.call(void 0, list[i].seg, list[j].seg)) {
1788
+ const q = toVec2(hit.point);
1241
1789
  const d = distance(q, p);
1242
1790
  if (d <= tol && d < bestD) {
1243
1791
  best = q;
@@ -1253,9 +1801,9 @@ var VectorSnapTargets = (_class = class _VectorSnapTargets {
1253
1801
  * (within `tol` of it): roots of `(B(t) − base) · B′(t)` and `(B(t) − base) × B′(t)`
1254
1802
  * bracketed by sign changes of samples and narrowed by bisection.
1255
1803
  */
1256
- curvePoint(near, base, p, tol) {
1804
+ curvePoint(near2, base, p, tol) {
1257
1805
  let best = null;
1258
- for (const { seg } of near) {
1806
+ for (const { seg } of near2) {
1259
1807
  for (const kind of ["perpendicular", "tangent"]) {
1260
1808
  if (kind === "tangent" && seg.length === 2) continue;
1261
1809
  for (const q of solveOnSegment(seg, base, kind === "perpendicular" ? dotCondition : crossCondition)) {
@@ -1275,15 +1823,15 @@ var VectorSnapTargets = (_class = class _VectorSnapTargets {
1275
1823
  * with nothing spaces the box evenly in its row or column. Returns the correction and guides.
1276
1824
  */
1277
1825
  snapRect(r, tol, options = {}) {
1278
- const axes = { x: _optionalChain([options, 'access', _17 => _17.axes, 'optionalAccess', _18 => _18.x]) !== false, y: _optionalChain([options, 'access', _19 => _19.axes, 'optionalAccess', _20 => _20.y]) !== false };
1826
+ const axes = { x: _optionalChain([options, 'access', _28 => _28.axes, 'optionalAccess', _29 => _29.x]) !== false, y: _optionalChain([options, 'access', _30 => _30.axes, 'optionalAccess', _31 => _31.y]) !== false };
1279
1827
  const nearest = (lines, values) => {
1280
1828
  let best = null;
1281
- for (const v of values) for (const l of linesNear(lines, v, tol)) if (!best || Math.abs(l.v - v) < Math.abs(best.d)) best = { d: l.v - v, from: l.from };
1829
+ for (const v2 of values) for (const l of linesNear(lines, v2, tol)) if (!best || Math.abs(l.v - v2) < Math.abs(best.d)) best = { d: l.v - v2, from: l.from };
1282
1830
  return best;
1283
1831
  };
1284
1832
  const bx = axes.x ? nearest(this.xs, [r.x, r.x + r.width / 2, r.x + r.width]) : null;
1285
1833
  const by = axes.y ? nearest(this.ys, [r.y, r.y + r.height / 2, r.y + r.height]) : null;
1286
- const out = { dx: _nullishCoalesce(_optionalChain([bx, 'optionalAccess', _21 => _21.d]), () => ( 0)), dy: _nullishCoalesce(_optionalChain([by, 'optionalAccess', _22 => _22.d]), () => ( 0)), x: !!bx, y: !!by, lines: [], labels: [] };
1834
+ const out = { dx: _nullishCoalesce(_optionalChain([bx, 'optionalAccess', _32 => _32.d]), () => ( 0)), dy: _nullishCoalesce(_optionalChain([by, 'optionalAccess', _33 => _33.d]), () => ( 0)), x: !!bx, y: !!by, lines: [], labels: [] };
1287
1835
  const done = { ...r, x: r.x + out.dx, y: r.y + out.dy };
1288
1836
  if (bx) {
1289
1837
  const x = bx.from.x;
@@ -1321,7 +1869,7 @@ var VectorSnapTargets = (_class = class _VectorSnapTargets {
1321
1869
  evenSpacing(r, horizontal, tol) {
1322
1870
  const span = (b, x) => x ? [b.x, b.x + b.width] : [b.y, b.y + b.height];
1323
1871
  const [r0, r1] = span(r, horizontal);
1324
- const len = r1 - r0;
1872
+ const len2 = r1 - r0;
1325
1873
  const [c0, c1] = span(r, !horizontal);
1326
1874
  const row = this.boxes.map((b) => ({ b, a0: span(b, horizontal)[0], a1: span(b, horizontal)[1] })).filter((e) => span(e.b, !horizontal)[0] < c1 && span(e.b, !horizontal)[1] > c0 && (e.a1 <= r0 + tol || e.a0 >= r1 - tol)).sort((a, b) => a.a0 - b.a0);
1327
1875
  if (!row.length) return null;
@@ -1341,17 +1889,17 @@ var VectorSnapTargets = (_class = class _VectorSnapTargets {
1341
1889
  if (e.a0 >= r1 - tol && (!after || e.a0 < after.a0)) after = e;
1342
1890
  }
1343
1891
  const starts = [];
1344
- if (before && after && after.a0 - before.a1 > len) starts.push({ start: (before.a1 + after.a0 - len) / 2, repeats: null });
1892
+ if (before && after && after.a0 - before.a1 > len2) starts.push({ start: (before.a1 + after.a0 - len2) / 2, repeats: null });
1345
1893
  for (const { g } of pairs) {
1346
1894
  if (before) starts.push({ start: before.a1 + g, repeats: g });
1347
- if (after) starts.push({ start: after.a0 - g - len, repeats: g });
1895
+ if (after) starts.push({ start: after.a0 - g - len2, repeats: g });
1348
1896
  }
1349
1897
  let pick = null;
1350
1898
  for (const s of starts) if (Math.abs(s.start - r0) <= tol && (!pick || Math.abs(s.start - r0) < Math.abs(pick.start - r0))) pick = s;
1351
1899
  if (!pick) return null;
1352
1900
  const { start, repeats } = pick;
1353
1901
  const at = horizontal ? { ...r, x: start } : { ...r, y: start };
1354
- const end = start + len;
1902
+ const end = start + len2;
1355
1903
  const gaps = [];
1356
1904
  if (before && (repeats === null || Math.abs(start - before.a1 - repeats) <= EQUAL)) gaps.push([before.a1, start, mid(before.b, at)]);
1357
1905
  if (after && (repeats === null || Math.abs(after.a0 - end - repeats) <= EQUAL)) gaps.push([end, after.a0, mid(after.b, at)]);
@@ -1362,9 +1910,9 @@ var VectorSnapTargets = (_class = class _VectorSnapTargets {
1362
1910
  }
1363
1911
  /** A gap mark: a line across the gap with a tick at each end, and its length as a label. */
1364
1912
  gapMarks([u0, u1, c], horizontal, tick, out) {
1365
- const pt = (u, v) => horizontal ? { x: u, y: v } : { x: v, y: u };
1366
- out.lines.push(line("spacing", pt(u0, c), pt(u1, c)), line("spacing", pt(u0, c - tick), pt(u0, c + tick)), line("spacing", pt(u1, c - tick), pt(u1, c + tick)));
1367
- out.labels.push(this.label(pt((u0 + u1) / 2, c), "spacing", fmtLength(u1 - u0)));
1913
+ const pt2 = (u, v2) => horizontal ? { x: u, y: v2 } : { x: v2, y: u };
1914
+ out.lines.push(line("spacing", pt2(u0, c), pt2(u1, c)), line("spacing", pt2(u0, c - tick), pt2(u0, c + tick)), line("spacing", pt2(u1, c - tick), pt2(u1, c + tick)));
1915
+ out.labels.push(this.label(pt2((u0 + u1) / 2, c), "spacing", fmtLength(u1 - u0)));
1368
1916
  }
1369
1917
  /** The other objects' side length nearest `length`, within `tol`, or null. */
1370
1918
  matchSize(length2, tol) {
@@ -1391,9 +1939,9 @@ var VectorSnapTargets = (_class = class _VectorSnapTargets {
1391
1939
  const hp = handlePos(handle, frame);
1392
1940
  const d0 = { x: hp.x - origin.x, y: hp.y - origin.y };
1393
1941
  const valid = (n, s) => Number.isFinite(n) && n * s > 0;
1394
- const nearest = (v, lines) => {
1942
+ const nearest = (v2, lines) => {
1395
1943
  let best = null;
1396
- for (const l of linesNear(lines, v, tol)) if (!best || Math.abs(l.v - v) < Math.abs(best.v - v)) best = l;
1944
+ for (const l of linesNear(lines, v2, tol)) if (!best || Math.abs(l.v - v2) < Math.abs(best.v - v2)) best = l;
1397
1945
  return best;
1398
1946
  };
1399
1947
  let nx = sx;
@@ -1406,9 +1954,9 @@ var VectorSnapTargets = (_class = class _VectorSnapTargets {
1406
1954
  const on = this.nearestPoint(p, tol);
1407
1955
  const tries = [];
1408
1956
  if (on) tries.push({ tx: on.p.x, ty: on.p.y, s: { hx: null, hy: null, on } });
1409
- tries.push({ tx: _nullishCoalesce(_optionalChain([hx, 'optionalAccess', _23 => _23.v]), () => ( null)), ty: _nullishCoalesce(_optionalChain([hy, 'optionalAccess', _24 => _24.v]), () => ( null)), s: { hx, hy, on: null } });
1410
- tries.push({ tx: _nullishCoalesce(_optionalChain([hx, 'optionalAccess', _25 => _25.v]), () => ( null)), ty: null, s: { hx, hy: null, on: null } });
1411
- tries.push({ tx: null, ty: _nullishCoalesce(_optionalChain([hy, 'optionalAccess', _26 => _26.v]), () => ( null)), s: { hx: null, hy, on: null } });
1957
+ tries.push({ tx: _nullishCoalesce(_optionalChain([hx, 'optionalAccess', _34 => _34.v]), () => ( null)), ty: _nullishCoalesce(_optionalChain([hy, 'optionalAccess', _35 => _35.v]), () => ( null)), s: { hx, hy, on: null } });
1958
+ tries.push({ tx: _nullishCoalesce(_optionalChain([hx, 'optionalAccess', _36 => _36.v]), () => ( null)), ty: null, s: { hx, hy: null, on: null } });
1959
+ tries.push({ tx: null, ty: _nullishCoalesce(_optionalChain([hy, 'optionalAccess', _37 => _37.v]), () => ( null)), s: { hx: null, hy, on: null } });
1412
1960
  for (const { tx, ty, s } of tries) {
1413
1961
  if (tx === null && ty === null) continue;
1414
1962
  const q = applyMatrix(toLocal, { x: _nullishCoalesce(tx, () => ( p.x)), y: _nullishCoalesce(ty, () => ( p.y)) });
@@ -1444,9 +1992,9 @@ var VectorSnapTargets = (_class = class _VectorSnapTargets {
1444
1992
  };
1445
1993
  for (const h of [handle, ...ends]) {
1446
1994
  const hv = handlePos(h, frame);
1447
- const v = { x: hv.x - origin.x, y: hv.y - origin.y };
1448
- const base = applyMatrix(toDoc, { x: origin.x + c.x * v.x, y: origin.y + c.y * v.y });
1449
- const dir = applyMatrix(toDoc, { x: e.x * v.x, y: e.y * v.y });
1995
+ const v2 = { x: hv.x - origin.x, y: hv.y - origin.y };
1996
+ const base = applyMatrix(toDoc, { x: origin.x + c.x * v2.x, y: origin.y + c.y * v2.y });
1997
+ const dir = applyMatrix(toDoc, { x: e.x * v2.x, y: e.y * v2.y });
1450
1998
  const p = { x: base.x + dir.x * s0, y: base.y + dir.y * s0 };
1451
1999
  const l2 = dir.x * dir.x + dir.y * dir.y;
1452
2000
  const on = l2 > 1e-18 ? this.nearestPoint(p, tol) : null;
@@ -1479,16 +2027,16 @@ var VectorSnapTargets = (_class = class _VectorSnapTargets {
1479
2027
  const sized = [];
1480
2028
  if (!proportional) {
1481
2029
  if (movesX && !snap.hx && !(frame.angle && snap.hy) && frame.width > 1e-9) {
1482
- const len = this.matchSize(Math.abs(out.sx) * frame.width, tol);
1483
- if (len !== null && len > 0) {
1484
- out.sx = Math.sign(out.sx || 1) * (len / frame.width);
2030
+ const len2 = this.matchSize(Math.abs(out.sx) * frame.width, tol);
2031
+ if (len2 !== null && len2 > 0) {
2032
+ out.sx = Math.sign(out.sx || 1) * (len2 / frame.width);
1485
2033
  sized.push("x");
1486
2034
  }
1487
2035
  }
1488
2036
  if (movesY && !snap.hy && !(frame.angle && snap.hx) && frame.height > 1e-9) {
1489
- const len = this.matchSize(Math.abs(out.sy) * frame.height, tol);
1490
- if (len !== null && len > 0) {
1491
- out.sy = Math.sign(out.sy || 1) * (len / frame.height);
2037
+ const len2 = this.matchSize(Math.abs(out.sy) * frame.height, tol);
2038
+ if (len2 !== null && len2 > 0) {
2039
+ out.sy = Math.sign(out.sy || 1) * (len2 / frame.height);
1492
2040
  sized.push("y");
1493
2041
  }
1494
2042
  }
@@ -1539,14 +2087,14 @@ function line(kind, a, b) {
1539
2087
  return { kind, x1: a.x, y1: a.y, x2: b.x, y2: b.y };
1540
2088
  }
1541
2089
  function construction(leave, p, tol) {
1542
- const v = { x: p.x - leave.at.x, y: p.y - leave.at.y };
1543
- if (Math.hypot(v.x, v.y) <= tol) return null;
2090
+ const v2 = { x: p.x - leave.at.x, y: p.y - leave.at.y };
2091
+ if (Math.hypot(v2.x, v2.y) <= tol) return null;
1544
2092
  let best = null;
1545
2093
  for (const a of _nullishCoalesce(leave.angles, () => ( []))) {
1546
2094
  const r = a * Math.PI / 180;
1547
2095
  const exact = (n) => Math.abs(n) < 1e-12 ? 0 : n;
1548
2096
  const dir = { x: exact(Math.cos(r)), y: exact(-Math.sin(r)) };
1549
- const k = v.x * dir.x + v.y * dir.y;
2097
+ const k = v2.x * dir.x + v2.y * dir.y;
1550
2098
  const point = { x: leave.at.x + dir.x * k, y: leave.at.y + dir.y * k };
1551
2099
  const d = distance(point, p);
1552
2100
  if (d <= tol && (!best || d < best.d)) best = { dir, point, d };
@@ -1558,20 +2106,20 @@ var crossCondition = (r, d) => r[0] * d[1] - r[1] * d[0];
1558
2106
  function solveOnSegment(seg, base, cond) {
1559
2107
  const b = [base.x, base.y];
1560
2108
  const g = (t) => {
1561
- const p = _chunkUG5DVXJKcjs.segmentPoint.call(void 0, seg, t);
1562
- return cond([p[0] - b[0], p[1] - b[1]], _chunkUG5DVXJKcjs.segmentDerivative.call(void 0, seg, t));
2109
+ const p = _chunkBGNPT5PEcjs.segmentPoint.call(void 0, seg, t);
2110
+ return cond([p[0] - b[0], p[1] - b[1]], _chunkBGNPT5PEcjs.segmentDerivative.call(void 0, seg, t));
1563
2111
  };
1564
2112
  const vals = [];
1565
2113
  let scale = 0;
1566
2114
  for (let i = 0; i <= CURVE_SAMPLES; i++) {
1567
2115
  const t = i / CURVE_SAMPLES;
1568
- const p = _chunkUG5DVXJKcjs.segmentPoint.call(void 0, seg, t);
1569
- const d = _chunkUG5DVXJKcjs.segmentDerivative.call(void 0, seg, t);
2116
+ const p = _chunkBGNPT5PEcjs.segmentPoint.call(void 0, seg, t);
2117
+ const d = _chunkBGNPT5PEcjs.segmentDerivative.call(void 0, seg, t);
1570
2118
  const r = [p[0] - b[0], p[1] - b[1]];
1571
2119
  vals.push(cond(r, d));
1572
2120
  scale = Math.max(scale, Math.hypot(r[0], r[1]) * Math.hypot(d[0], d[1]));
1573
2121
  }
1574
- if (!(scale > 0) || vals.every((v) => Math.abs(v) <= scale * 1e-12)) return [];
2122
+ if (!(scale > 0) || vals.every((v2) => Math.abs(v2) <= scale * 1e-12)) return [];
1575
2123
  const out = [];
1576
2124
  for (let i = 0; i < CURVE_SAMPLES; i++) {
1577
2125
  const ga = vals[i];
@@ -1593,8 +2141,8 @@ function solveOnSegment(seg, base, cond) {
1593
2141
  } else hi = m;
1594
2142
  }
1595
2143
  const t = (lo + hi) / 2;
1596
- const p = _chunkUG5DVXJKcjs.segmentPoint.call(void 0, seg, t);
1597
- const dv = _chunkUG5DVXJKcjs.segmentDerivative.call(void 0, seg, t);
2144
+ const p = _chunkBGNPT5PEcjs.segmentPoint.call(void 0, seg, t);
2145
+ const dv = _chunkBGNPT5PEcjs.segmentDerivative.call(void 0, seg, t);
1598
2146
  const r = Math.hypot(p[0] - b[0], p[1] - b[1]);
1599
2147
  const dl = Math.hypot(dv[0], dv[1]);
1600
2148
  if (!(r > 1e-9 && dl > 0) || Math.abs(cond([p[0] - b[0], p[1] - b[1]], dv)) > 1e-6 * r * dl) continue;
@@ -1603,134 +2151,1134 @@ function solveOnSegment(seg, base, cond) {
1603
2151
  return out;
1604
2152
  }
1605
2153
 
1606
- // src/vector/controller.ts
1607
- var DEFAULT_VECTOR_SNAP = { grid: 0, objects: true, guides: true, threshold: 5, angles: VECTOR_CONSTRUCTION_ANGLES };
1608
- var VECTOR_CLIPBOARD_TYPE = "pixerate/vector";
1609
- var DEFAULT_DEFAULTS = {
1610
- fill: "#d4d4d8",
1611
- stroke: "#18181b",
1612
- strokeWidth: 2,
1613
- polygonSides: 6,
1614
- starPoints: 5,
1615
- starInnerRatio: 0.5,
1616
- cornerRadius: 0
2154
+ // src/vector/pathEdit.ts
2155
+ var EPS3 = 1e-9;
2156
+ var unit2 = (x, y) => {
2157
+ const l = Math.hypot(x, y);
2158
+ return l > 1e-12 && Number.isFinite(l) ? [x / l, y / l] : null;
1617
2159
  };
1618
- var MIN_ZOOM = 0.05;
1619
- var MAX_ZOOM = 64;
1620
- var VECTOR_ROTATE_HANDLE_OFFSET = 24;
1621
- var sameRef = (a, b) => a.nodeId === b.nodeId && a.subpath === b.subpath && a.index === b.index;
1622
- function getEditablePathIds(state) {
1623
- if (state.tool !== "direct" && state.tool !== "pen") return [];
1624
- const ids = /* @__PURE__ */ new Set();
1625
- if (state.pen) ids.add(state.pen.nodeId);
1626
- if (state.tool === "direct") {
1627
- if (state.editingId) ids.add(state.editingId);
1628
- for (const id of state.selection) {
1629
- const node = findVectorNode(state.document.nodes, id);
1630
- if (node && !node.locked) {
1631
- for (const p of collectPathNodes(node)) if (!p.locked) ids.add(p.id);
1632
- }
1633
- }
2160
+ function cubicOf(sp, i) {
2161
+ const s = subpathSegment(sp, i);
2162
+ if (s.length === 4) return s;
2163
+ const [a, b] = s;
2164
+ 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];
2165
+ }
2166
+ var isLine = (sp, i) => subpathSegment(sp, i).length === 2;
2167
+ function startTangent(c) {
2168
+ for (let k = 1; k < 4; k++) {
2169
+ const u = unit2(c[k][0] - c[0][0], c[k][1] - c[0][1]);
2170
+ if (u) return u;
1634
2171
  }
1635
- return [...ids].filter((id) => _optionalChain([findVectorNode, 'call', _27 => _27(state.document.nodes, id), 'optionalAccess', _28 => _28.type]) === "path");
2172
+ return [1, 0];
1636
2173
  }
1637
- function getVisibleAnchors(state) {
1638
- const out = [];
1639
- for (const id of getEditablePathIds(state)) {
1640
- const node = findVectorNode(state.document.nodes, id);
1641
- node.subpaths.forEach((sp, s) => sp.anchors.forEach((_, i) => out.push({ nodeId: id, subpath: s, index: i })));
2174
+ function endTangent(c) {
2175
+ for (let k = 2; k >= 0; k--) {
2176
+ const u = unit2(c[3][0] - c[k][0], c[3][1] - c[k][1]);
2177
+ if (u) return u;
1642
2178
  }
1643
- return out;
2179
+ return [1, 0];
1644
2180
  }
1645
- function getVisibleHandles(state) {
1646
- const out = [];
1647
- const seen = /* @__PURE__ */ new Set();
1648
- const push = (node, s, i, handle) => {
1649
- const sp = node.subpaths[s];
1650
- const a = _optionalChain([sp, 'optionalAccess', _29 => _29.anchors, 'access', _30 => _30[i]]);
1651
- if (!a || !a[handle]) return;
1652
- const key = `${node.id}:${s}:${i}:${handle}`;
1653
- if (seen.has(key)) return;
1654
- seen.add(key);
1655
- out.push({ nodeId: node.id, subpath: s, index: i, handle });
2181
+ function sample(c, n, out, withStart) {
2182
+ for (let i = withStart ? 0 : 1; i <= n; i++) out.push(_chunkBGNPT5PEcjs.segmentPoint.call(void 0, c, i / n));
2183
+ }
2184
+ var onAnchor2 = (a, h) => !h || Math.abs(h.x - a.x) < EPS3 && Math.abs(h.y - a.y) < EPS3;
2185
+ function tidy(a, keepType = true) {
2186
+ const next = cloneAnchor(a);
2187
+ if (onAnchor2(next, next.in)) delete next.in;
2188
+ if (onAnchor2(next, next.out)) delete next.out;
2189
+ const shown = inferAnchorType(next);
2190
+ const type = keepType && a.type ? a.type === "symmetric" && shown !== "symmetric" ? shown === "corner" ? "corner" : "smooth" : a.type === "smooth" && shown === "corner" ? "corner" : a.type : shown;
2191
+ if (type === "corner") delete next.type;
2192
+ else next.type = type;
2193
+ return next;
2194
+ }
2195
+ function neighbours(sp, i) {
2196
+ const n = sp.anchors.length;
2197
+ const wrap = sp.closed && n > 1;
2198
+ return [i > 0 ? i - 1 : wrap ? n - 1 : -1, i < n - 1 ? i + 1 : wrap ? 0 : -1];
2199
+ }
2200
+ function goldenMin(f, a, b) {
2201
+ const R = 0.618033988749895;
2202
+ let x1 = b - R * (b - a);
2203
+ let x2 = a + R * (b - a);
2204
+ let f1 = f(x1);
2205
+ let f2 = f(x2);
2206
+ for (let i = 0; i < 40; i++) {
2207
+ if (f1.error < f2.error || Number.isNaN(f2.error)) {
2208
+ b = x2;
2209
+ x2 = x1;
2210
+ f2 = f1;
2211
+ x1 = b - R * (b - a);
2212
+ f1 = f(x1);
2213
+ } else {
2214
+ a = x1;
2215
+ x1 = x2;
2216
+ f1 = f2;
2217
+ x2 = a + R * (b - a);
2218
+ f2 = f(x2);
2219
+ }
2220
+ }
2221
+ return f1.error <= f2.error ? f1 : f2;
2222
+ }
2223
+ function fitAcross(pts, N, t0, t1) {
2224
+ const fit = (k) => {
2225
+ const u = [];
2226
+ for (let i = 0; i <= N; i++) u.push(k * i / N);
2227
+ for (let i = 1; i <= N; i++) u.push(k + (1 - k) * i / N);
2228
+ return _chunkBGNPT5PEcjs.fitCubicWithTangents.call(void 0, pts, u, t0, t1);
1656
2229
  };
1657
- if (state.tool === "pen" && state.pen) {
1658
- const node = findVectorNode(state.document.nodes, state.pen.nodeId);
1659
- const sp = _optionalChain([node, 'optionalAccess', _31 => _31.type]) === "path" ? node.subpaths[state.pen.subpath] : void 0;
1660
- if (_optionalChain([node, 'optionalAccess', _32 => _32.type]) === "path" && sp) {
1661
- const last = sp.anchors.length - 1;
1662
- push(node, state.pen.subpath, last, "in");
1663
- push(node, state.pen.subpath, last, "out");
2230
+ return goldenMin(fit, 0, 1);
2231
+ }
2232
+ function removeAnchorKeepingShape(sp, index) {
2233
+ const anchors = sp.anchors.map(cloneAnchor);
2234
+ const n = anchors.length;
2235
+ if (index < 0 || index >= n) return { anchors, closed: sp.closed };
2236
+ if ((sp.closed || index > 0 && index < n - 1) && n >= 3) {
2237
+ const pi = (index + n - 1) % n;
2238
+ const ni = (index + 1) % n;
2239
+ if (!(isLine(sp, pi) && isLine(sp, index))) {
2240
+ const left = cubicOf(sp, pi);
2241
+ const right = cubicOf(sp, index);
2242
+ const N = 16;
2243
+ const pts = [];
2244
+ sample(left, N, pts, true);
2245
+ sample(right, N, pts, false);
2246
+ const t0 = startTangent(left);
2247
+ const t1 = endTangent(right);
2248
+ const { curve } = fitAcross(pts, N, t0, t1);
2249
+ if ([curve.p1, curve.p2].every((p) => Number.isFinite(p[0]) && Number.isFinite(p[1]))) {
2250
+ anchors[pi] = tidy({ ...anchors[pi], out: { x: curve.p1[0], y: curve.p1[1] } });
2251
+ anchors[ni] = tidy({ ...anchors[ni], in: { x: curve.p2[0], y: curve.p2[1] } });
2252
+ }
1664
2253
  }
1665
- return out;
1666
2254
  }
1667
- if (state.tool !== "direct") return out;
1668
- const editable = new Set(getEditablePathIds(state));
1669
- for (const ref of state.selectedAnchors) {
1670
- if (!editable.has(ref.nodeId)) continue;
1671
- const node = findVectorNode(state.document.nodes, ref.nodeId);
1672
- const sp = node.subpaths[ref.subpath];
1673
- if (!sp) continue;
1674
- const n = sp.anchors.length;
1675
- push(node, ref.subpath, ref.index, "in");
1676
- push(node, ref.subpath, ref.index, "out");
1677
- const prev = ref.index > 0 ? ref.index - 1 : sp.closed ? n - 1 : -1;
1678
- const next = ref.index < n - 1 ? ref.index + 1 : sp.closed ? 0 : -1;
1679
- if (prev >= 0) push(node, ref.subpath, prev, "out");
1680
- if (next >= 0) push(node, ref.subpath, next, "in");
2255
+ anchors.splice(index, 1);
2256
+ let closed = sp.closed;
2257
+ if (!closed && anchors.length) {
2258
+ anchors[0] = tidy({ ...anchors[0], in: null });
2259
+ const last = anchors.length - 1;
2260
+ anchors[last] = tidy({ ...anchors[last], out: null });
1681
2261
  }
1682
- return out;
2262
+ if (anchors.length < 3) closed = closed && anchors.length === 2 && anchors.some((a) => a.in || a.out);
2263
+ return { anchors, closed };
1683
2264
  }
1684
- function getAnchor(doc, ref) {
1685
- const node = findVectorNode(doc.nodes, ref.nodeId);
1686
- return _optionalChain([node, 'optionalAccess', _33 => _33.type]) === "path" ? _optionalChain([node, 'access', _34 => _34.subpaths, 'access', _35 => _35[ref.subpath], 'optionalAccess', _36 => _36.anchors, 'access', _37 => _37[ref.index]]) : void 0;
2265
+ function removeAnchorsKeepingShape(sp, indices) {
2266
+ const order = [...new Set(indices)].filter((i) => i >= 0 && i < sp.anchors.length).sort((a, b) => b - a);
2267
+ return order.reduce((acc, i) => removeAnchorKeepingShape(acc, i), { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed });
1687
2268
  }
1688
- function getAnchorWorld(doc, ref) {
1689
- const a = getAnchor(doc, ref);
1690
- if (!a) return void 0;
1691
- const m = _nullishCoalesce(vectorWorldMatrix(doc.nodes, ref.nodeId), () => ( IDENTITY_MATRIX));
1692
- const p = applyMatrix(m, a);
1693
- return { x: p.x, y: p.y, ...a.in ? { in: applyMatrix(m, a.in) } : {}, ...a.out ? { out: applyMatrix(m, a.out) } : {}, ...a.type ? { type: a.type } : {} };
2269
+ function bendSegment(sp, segment, t, p) {
2270
+ const anchors = sp.anchors.map(cloneAnchor);
2271
+ const n = anchors.length;
2272
+ if (segment < 0 || segment >= subpathSegmentCount(sp) || !Number.isFinite(t) || !Number.isFinite(p.x) || !Number.isFinite(p.y)) return { anchors, closed: sp.closed };
2273
+ const tc = Math.min(0.95, Math.max(0.05, t));
2274
+ const ia = segment;
2275
+ const ib = (segment + 1) % n;
2276
+ const a = anchors[ia];
2277
+ const b = anchors[ib];
2278
+ const k = 3 * tc * (1 - tc);
2279
+ const ox = (p.x - (a.x + (b.x - a.x) * tc)) / k;
2280
+ const oy = (p.y - (a.y + (b.y - a.y) * tc)) / k;
2281
+ const c1 = { x: a.x + (b.x - a.x) / 3 + ox, y: a.y + (b.y - a.y) / 3 + oy };
2282
+ const c2 = { x: b.x + (a.x - b.x) / 3 + ox, y: b.y + (a.y - b.y) / 3 + oy };
2283
+ anchors[ia] = moveAnchorHandle(a, "out", c1);
2284
+ anchors[ib] = moveAnchorHandle(b, "in", c2);
2285
+ return { anchors, closed: sp.closed };
1694
2286
  }
1695
- function getVectorSelectionFrame(doc, ids) {
1696
- const nodes = ids.map((id) => ({ node: findVectorNode(doc.nodes, id), parent: _nullishCoalesce(vectorParentMatrix(doc.nodes, id), () => ( IDENTITY_MATRIX)) })).filter((n) => n.node);
1697
- if (!nodes.length) return null;
1698
- const angles = nodes.map((n) => matrixRotation(multiplyMatrices(nodeMatrix(n.node), n.parent)));
1699
- const same = angles.every((a) => Math.abs(normalizeAngle(a - angles[0])) < 1e-6);
1700
- const angle = same ? angles[0] : 0;
1701
- const unrotate = rotateMatrix(-angle);
1702
- const b = unionBounds(nodes.map((n) => vectorNodeBounds(n.node, multiplyMatrices(n.parent, unrotate))));
1703
- return b ? { ...b, angle } : null;
2287
+ function subpathPointAt(sp, segment, t) {
2288
+ const [x, y] = _chunkBGNPT5PEcjs.segmentPoint.call(void 0, subpathSegment(sp, segment), t);
2289
+ return { x, y };
1704
2290
  }
1705
- var normalizeAngle = (a) => ((a + 180) % 360 + 360) % 360 - 180;
1706
- function getTransformHandlePositions(box) {
1707
- const { x, y, width: w, height: h } = box;
1708
- return [
1709
- { handle: "nw", x, y },
1710
- { handle: "n", x: x + w / 2, y },
1711
- { handle: "ne", x: x + w, y },
1712
- { handle: "e", x: x + w, y: y + h / 2 },
1713
- { handle: "se", x: x + w, y: y + h },
1714
- { handle: "s", x: x + w / 2, y: y + h },
1715
- { handle: "sw", x, y: y + h },
1716
- { handle: "w", x, y: y + h / 2 },
1717
- { handle: "rotate", x: x + w / 2, y: y - VECTOR_ROTATE_HANDLE_OFFSET }
2291
+ function curvatureMoveAnchor(sp, index, q) {
2292
+ const anchors = sp.anchors.map(cloneAnchor);
2293
+ const a = sp.anchors[index];
2294
+ if (!a) return { anchors, closed: sp.closed };
2295
+ const own = [
2296
+ a.in && !onAnchor2(a, a.in) ? { x: a.in.x - a.x, y: a.in.y - a.y } : null,
2297
+ a.out && !onAnchor2(a, a.out) ? { x: a.out.x - a.x, y: a.out.y - a.y } : null
1718
2298
  ];
2299
+ const ends = [null, null];
2300
+ neighbours(sp, index).forEach((j, side) => {
2301
+ if (j < 0 || j === index) return;
2302
+ const b = anchors[j];
2303
+ const old = Math.hypot(a.x - b.x, a.y - b.y);
2304
+ const s = old > 1e-9 ? Math.hypot(q.x - b.x, q.y - b.y) / old : 1;
2305
+ const key = side === 0 ? "out" : "in";
2306
+ const h = b[key];
2307
+ if (h) b[key] = { x: b.x + (h.x - b.x) * s, y: b.y + (h.y - b.y) * s };
2308
+ const o = own[side];
2309
+ if (o) own[side] = { x: o.x * s, y: o.y * s };
2310
+ ends[side] = { x: b.x, y: b.y };
2311
+ });
2312
+ const moved = { x: q.x, y: q.y };
2313
+ if (own[0]) moved.in = { x: q.x + own[0].x, y: q.y + own[0].y };
2314
+ if (own[1]) moved.out = { x: q.x + own[1].x, y: q.y + own[1].y };
2315
+ if (a.type) moved.type = a.type;
2316
+ if (a.corner) moved.corner = { ...a.corner };
2317
+ const [prev, next] = ends;
2318
+ if (a.type && a.type !== "corner" && prev && next) {
2319
+ const dx = next.x - prev.x;
2320
+ const dy = next.y - prev.y;
2321
+ const l = Math.hypot(dx, dy);
2322
+ if (l > 1e-9) {
2323
+ const len2 = (h, to) => h ? Math.hypot(h.x, h.y) : Math.hypot(q.x - to.x, q.y - to.y) / 3;
2324
+ let li = len2(own[0], prev);
2325
+ let lo = len2(own[1], next);
2326
+ if (a.type === "symmetric") li = lo = (li + lo) / 2;
2327
+ moved.in = { x: q.x - dx * li / l, y: q.y - dy * li / l };
2328
+ moved.out = { x: q.x + dx * lo / l, y: q.y + dy * lo / l };
2329
+ }
2330
+ }
2331
+ anchors[index] = moved;
2332
+ return { anchors, closed: sp.closed };
1719
2333
  }
1720
- function getVectorSelectionBoxGeometry(frame, viewport) {
1721
- const z = viewport.zoom;
1722
- const box = { x: frame.x * z + viewport.x, y: frame.y * z + viewport.y, width: frame.width * z, height: frame.height * z };
1723
- const rot = rotateMatrix(frame.angle, { x: viewport.x, y: viewport.y });
1724
- const toScreen2 = (p) => frame.angle ? applyMatrix(rot, p) : { x: p.x, y: p.y };
1725
- const handles = getTransformHandlePositions(box).map((h) => ({ ...h, screen: toScreen2(h) }));
1726
- const corners = [
2334
+ function cutSubpathAt(sp, at) {
2335
+ const n = sp.anchors.length;
2336
+ 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);
2337
+ if (!cuts.length || n < 2) return [{ anchors: sp.anchors.map(cloneAnchor), closed: sp.closed }];
2338
+ const first = cuts[0];
2339
+ const run = sp.closed ? Array.from({ length: n + 1 }, (_, i) => sp.anchors[(first + i) % n]) : sp.anchors;
2340
+ const cutSet = new Set(sp.closed ? [...cuts.map((c) => (c - first + n) % n), n] : cuts);
2341
+ const pieces = [];
2342
+ let cur = [];
2343
+ run.forEach((a, i) => {
2344
+ if (!cutSet.has(i)) {
2345
+ cur.push(cloneAnchor(a));
2346
+ return;
2347
+ }
2348
+ if (cur.length) {
2349
+ cur.push(tidy({ ...a, out: null }, false));
2350
+ pieces.push({ anchors: cur, closed: false });
2351
+ cur = [];
2352
+ }
2353
+ if (i < n) cur.push(tidy({ ...a, in: null }, false));
2354
+ });
2355
+ if (cur.length > 1) pieces.push({ anchors: cur, closed: false });
2356
+ return pieces;
2357
+ }
2358
+ function splitSubpathAt(sp, segment, t, endTolerance = 1e-6) {
2359
+ const n = sp.anchors.length;
2360
+ if (segment < 0 || segment >= subpathSegmentCount(sp)) return [{ anchors: sp.anchors.map(cloneAnchor), closed: sp.closed }];
2361
+ if (t <= endTolerance) return cutSubpathAt(sp, [segment]);
2362
+ if (t >= 1 - endTolerance) return cutSubpathAt(sp, [(segment + 1) % n]);
2363
+ const { subpath, index } = insertAnchor(sp, segment, t);
2364
+ return cutSubpathAt(subpath, [index]);
2365
+ }
2366
+ var endOf2 = (sp, last) => sp.anchors[last ? sp.anchors.length - 1 : 0];
2367
+ var dist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
2368
+ function reverse(sp) {
2369
+ return {
2370
+ closed: sp.closed,
2371
+ anchors: sp.anchors.slice().reverse().map((a) => {
2372
+ const next = { x: a.x, y: a.y };
2373
+ if (a.out) next.in = { ...a.out };
2374
+ if (a.in) next.out = { ...a.in };
2375
+ if (a.type) next.type = a.type;
2376
+ if (a.corner) next.corner = { ...a.corner };
2377
+ return next;
2378
+ })
2379
+ };
2380
+ }
2381
+ function joinSubpaths(subpaths, tolerance) {
2382
+ const closed = [];
2383
+ let open = [];
2384
+ for (const sp of subpaths) {
2385
+ const copy = { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed };
2386
+ if (sp.closed || sp.anchors.length < 2) {
2387
+ if (sp.anchors.length) closed.push(copy);
2388
+ } else open.push(copy);
2389
+ }
2390
+ if (open.length === 1) {
2391
+ const sp = open[0];
2392
+ const first = sp.anchors[0];
2393
+ const last = sp.anchors[sp.anchors.length - 1];
2394
+ if (sp.anchors.length > 2 && dist(first, last) <= tolerance) {
2395
+ sp.anchors.pop();
2396
+ sp.anchors[0] = tidy({ ...first, in: _nullishCoalesce(last.in, () => ( null)) }, false);
2397
+ } else {
2398
+ sp.anchors[0] = tidy({ ...first, in: null }, false);
2399
+ sp.anchors[sp.anchors.length - 1] = tidy({ ...sp.anchors[sp.anchors.length - 1], out: null }, false);
2400
+ }
2401
+ sp.closed = true;
2402
+ return [...closed, sp];
2403
+ }
2404
+ while (open.length > 1) {
2405
+ let best = { d: Infinity, i: 0, j: 1, ei: true, ej: false };
2406
+ for (let i = 0; i < open.length; i++) {
2407
+ for (let j = i + 1; j < open.length; j++) {
2408
+ for (const ei of [false, true]) {
2409
+ for (const ej of [false, true]) {
2410
+ const d = dist(endOf2(open[i], ei), endOf2(open[j], ej));
2411
+ if (d < best.d) best = { d, i, j, ei, ej };
2412
+ }
2413
+ }
2414
+ }
2415
+ }
2416
+ let a = open[best.i];
2417
+ let b = open[best.j];
2418
+ open = open.filter((_, k) => k !== best.j);
2419
+ if (!best.ei) a = reverse(a);
2420
+ if (best.ej) b = reverse(b);
2421
+ const anchors = a.anchors;
2422
+ if (best.d <= tolerance) {
2423
+ const [head, ...rest] = b.anchors;
2424
+ const l = anchors[anchors.length - 1];
2425
+ anchors[anchors.length - 1] = tidy({ ...l, out: _nullishCoalesce(head.out, () => ( null)) }, false);
2426
+ anchors.push(...rest);
2427
+ } else {
2428
+ const l = anchors[anchors.length - 1];
2429
+ anchors[anchors.length - 1] = tidy({ ...l, out: null }, false);
2430
+ anchors.push(tidy({ ...b.anchors[0], in: null }, false), ...b.anchors.slice(1));
2431
+ }
2432
+ open[best.i > best.j ? best.i - 1 : best.i] = { anchors, closed: false };
2433
+ }
2434
+ return [...closed, ...open];
2435
+ }
2436
+ function smoothSubpath(sp, amount = 1, only) {
2437
+ const t = Math.min(1, Math.max(0, amount));
2438
+ const orig = sp.anchors;
2439
+ const n = orig.length;
2440
+ const anchors = orig.map(cloneAnchor);
2441
+ if (n < 3 || t === 0) return { anchors, closed: sp.closed };
2442
+ for (let i = 0; i < n; i++) {
2443
+ if (!sp.closed && (i === 0 || i === n - 1)) continue;
2444
+ if (only && !only.has(i)) continue;
2445
+ const prev = orig[(i + n - 1) % n];
2446
+ const next = orig[(i + 1) % n];
2447
+ const p = orig[i];
2448
+ const dir = unit2(next.x - prev.x, next.y - prev.y);
2449
+ if (!dir) continue;
2450
+ const li = dist(p, prev) / 3;
2451
+ const lo = dist(p, next) / 3;
2452
+ const hin = _nullishCoalesce(p.in, () => ( p));
2453
+ const hout = _nullishCoalesce(p.out, () => ( p));
2454
+ const target = (h, x, y) => ({ x: h.x + (x - h.x) * t, y: h.y + (y - h.y) * t });
2455
+ anchors[i] = tidy(
2456
+ {
2457
+ x: p.x,
2458
+ y: p.y,
2459
+ in: target(hin, p.x - dir[0] * li, p.y - dir[1] * li),
2460
+ out: target(hout, p.x + dir[0] * lo, p.y + dir[1] * lo)
2461
+ },
2462
+ false
2463
+ );
2464
+ }
2465
+ return { anchors, closed: sp.closed };
2466
+ }
2467
+ function segsOf(sp) {
2468
+ const out = [];
2469
+ for (let i = 0; i < subpathSegmentCount(sp); i++) {
2470
+ const s = subpathSegment(sp, i);
2471
+ out.push(s.length === 2 ? { c: s, line: true } : { c: s, line: false });
2472
+ }
2473
+ return out;
2474
+ }
2475
+ function segsToSubpath(segs, closed, types) {
2476
+ if (!segs.length) return null;
2477
+ const anchors = [];
2478
+ const at = (p) => ({ x: p[0], y: p[1] });
2479
+ anchors.push(at(segs[0].c[0]));
2480
+ for (const s of segs) {
2481
+ const end = s.c[s.c.length - 1];
2482
+ if (!s.line) anchors[anchors.length - 1].out = { x: s.c[1][0], y: s.c[1][1] };
2483
+ const next = at(end);
2484
+ if (!s.line) next.in = { x: s.c[2][0], y: s.c[2][1] };
2485
+ anchors.push(next);
2486
+ }
2487
+ if (closed) {
2488
+ const last = anchors.pop();
2489
+ if (last.in) anchors[0].in = last.in;
2490
+ }
2491
+ const typed = anchors.map((a) => {
2492
+ const type = types.get(`${a.x},${a.y}`);
2493
+ return tidy(type ? { ...a, type } : a, !!type);
2494
+ });
2495
+ return { anchors: typed, closed };
2496
+ }
2497
+ 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);
2498
+ function mergePair(a, b, tol) {
2499
+ const a0 = a.c[0];
2500
+ const a3 = a.c[a.c.length - 1];
2501
+ const b3 = b.c[b.c.length - 1];
2502
+ if (a.line && b.line) {
2503
+ const cx = b3[0] - a0[0];
2504
+ const cy = b3[1] - a0[1];
2505
+ const len2 = Math.hypot(cx, cy);
2506
+ if (len2 < 1e-12) return null;
2507
+ const mx = a3[0] - a0[0];
2508
+ const my = a3[1] - a0[1];
2509
+ const along = (mx * cx + my * cy) / len2;
2510
+ return Math.abs((mx * cy - my * cx) / len2) <= tol && along >= 0 && along <= len2 ? { c: [a0, b3], line: true } : null;
2511
+ }
2512
+ const ca = a.line ? [a0, a0, a3, a3] : a.c;
2513
+ const cb = b.line ? [a3, a3, b3, b3] : b.c;
2514
+ const ta = endTangent(ca);
2515
+ const tb = startTangent(cb);
2516
+ if (ta[0] * tb[0] + ta[1] * tb[1] < 0.99) return null;
2517
+ const pts = [];
2518
+ sample(ca, 16, pts, true);
2519
+ sample(cb, 16, pts, false);
2520
+ const { curve, error } = fitAcross(pts, 16, startTangent(ca), endTangent(cb));
2521
+ return error <= tol ? { c: [curve.p0, curve.p1, curve.p2, curve.p3], line: false } : null;
2522
+ }
2523
+ function removeRedundantPoints(sp, tolerance) {
2524
+ const tol = Math.max(tolerance, 1e-9);
2525
+ const types = new Map(sp.anchors.filter((a) => a.type).map((a) => [`${a.x},${a.y}`, a.type]));
2526
+ const segs = segsOf(sp).filter((s) => !isZeroSeg(s));
2527
+ if (!segs.length) return { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed };
2528
+ const minSegs = sp.closed ? 2 : 1;
2529
+ let changed = true;
2530
+ while (changed && segs.length > minSegs) {
2531
+ changed = false;
2532
+ const n = segs.length;
2533
+ const joints = sp.closed ? n : n - 1;
2534
+ for (let j = 0; j < joints; j++) {
2535
+ const [i0, i1] = sp.closed ? [(j + n - 1) % n, j] : [j, j + 1];
2536
+ const m = mergePair(segs[i0], segs[i1], tol);
2537
+ if (!m) continue;
2538
+ segs[i0] = m;
2539
+ segs.splice(i1, 1);
2540
+ if (sp.closed && i1 === 0) segs.unshift(segs.pop());
2541
+ changed = true;
2542
+ break;
2543
+ }
2544
+ }
2545
+ return _nullishCoalesce(segsToSubpath(segs, sp.closed, types), () => ( { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed }));
2546
+ }
2547
+
2548
+ // src/vector/pathCut.ts
2549
+ var loopArea = (l) => _chunkBGNPT5PEcjs.contourSignedArea.call(void 0, l.segs);
2550
+ function loopSample(l) {
2551
+ const k = l.chord.findIndex((c) => !c);
2552
+ return _chunkBGNPT5PEcjs.segmentPoint.call(void 0, l.segs[k >= 0 ? k : 0], 0.5);
2553
+ }
2554
+ var contains2 = (l, p) => _chunkBGNPT5PEcjs.windingNumber.call(void 0, [l.segs], p) !== 0;
2555
+ function groupFaces(loops) {
2556
+ const usable = loops.filter((l) => l.segs.length && Math.abs(loopArea(l)) > 1e-12);
2557
+ if (!usable.length) return [];
2558
+ const largest = usable.reduce((a, b) => Math.abs(loopArea(b)) > Math.abs(loopArea(a)) ? b : a);
2559
+ const sign = Math.sign(loopArea(largest));
2560
+ const outers = usable.filter((l) => Math.sign(loopArea(l)) === sign).sort((a, b) => Math.abs(loopArea(a)) - Math.abs(loopArea(b)));
2561
+ const faces = new Map(outers.map((o) => [o, { loops: [o] }]));
2562
+ for (const hole of usable.filter((l) => Math.sign(loopArea(l)) !== sign)) {
2563
+ const p = loopSample(hole);
2564
+ const parent = outers.find((o) => Math.abs(loopArea(o)) > Math.abs(loopArea(hole)) && contains2(o, p));
2565
+ if (parent) faces.get(parent).loops.push(hole);
2566
+ }
2567
+ return outers.reverse().map((o) => faces.get(o));
2568
+ }
2569
+ function faceToPath(face) {
2570
+ return _chunkBGNPT5PEcjs.contoursToPath.call(void 0, face.loops.map((l) => ({ segments: l.segs, closed: true })));
2571
+ }
2572
+ function facesOf(path, fillRule) {
2573
+ const normalized = _chunkBGNPT5PEcjs.normalizeBezierPath.call(void 0, path, fillRule);
2574
+ const loops = _chunkBGNPT5PEcjs.pathToContours.call(void 0, normalized).map((c) => {
2575
+ const segs = _chunkBGNPT5PEcjs.closeContour.call(void 0, c, true).map((s) => s.slice());
2576
+ return { segs, chord: segs.map(() => false) };
2577
+ });
2578
+ return groupFaces(loops);
2579
+ }
2580
+ function splitIntoFaces(path, fillRule = "nonzero") {
2581
+ return facesOf(path, fillRule).map(faceToPath);
2582
+ }
2583
+ function rootsInUnit(a, b, c, d, scale) {
2584
+ const f = (t) => ((a * t + b) * t + c) * t + d;
2585
+ const tol = 1e-12 * Math.max(1, scale);
2586
+ const cuts = [0];
2587
+ const qa = 3 * a;
2588
+ const qb = 2 * b;
2589
+ if (Math.abs(qa) > 1e-18) {
2590
+ const disc = qb * qb - 4 * qa * c;
2591
+ if (disc >= 0) {
2592
+ const r = Math.sqrt(disc);
2593
+ 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);
2594
+ }
2595
+ } else if (Math.abs(qb) > 1e-18) {
2596
+ const t = -c / qb;
2597
+ if (t > 0 && t < 1) cuts.push(t);
2598
+ }
2599
+ cuts.push(1);
2600
+ const roots = [];
2601
+ const push = (t) => {
2602
+ if (!roots.some((r) => Math.abs(r - t) < 1e-9)) roots.push(t);
2603
+ };
2604
+ for (let i = 0; i < cuts.length - 1; i++) {
2605
+ let lo = cuts[i];
2606
+ let hi = cuts[i + 1];
2607
+ let flo = f(lo);
2608
+ const fhi = f(hi);
2609
+ if (Math.abs(flo) <= tol) push(lo);
2610
+ if (Math.abs(fhi) <= tol) push(hi);
2611
+ if (flo * fhi >= 0) continue;
2612
+ for (let k = 0; k < 80; k++) {
2613
+ const m = (lo + hi) / 2;
2614
+ const fm = f(m);
2615
+ if (fm === 0) {
2616
+ lo = hi = m;
2617
+ break;
2618
+ }
2619
+ if (fm * flo < 0) hi = m;
2620
+ else {
2621
+ lo = m;
2622
+ flo = fm;
2623
+ }
2624
+ }
2625
+ push((lo + hi) / 2);
2626
+ }
2627
+ return roots.sort((x, y) => x - y);
2628
+ }
2629
+ function lineHits(p, q, seg) {
2630
+ const dx = q[0] - p[0];
2631
+ const dy = q[1] - p[1];
2632
+ const len2 = dx * dx + dy * dy;
2633
+ if (len2 < 1e-24) return [];
2634
+ const h = (pt2) => (pt2[0] - p[0]) * -dy + (pt2[1] - p[1]) * dx;
2635
+ let coeffs;
2636
+ if (seg.length === 2) coeffs = [0, 0, h(seg[1]) - h(seg[0]), h(seg[0])];
2637
+ else {
2638
+ const [h0, h1, h2, h3] = seg.map(h);
2639
+ coeffs = [-h0 + 3 * h1 - 3 * h2 + h3, 3 * h0 - 6 * h1 + 3 * h2, -3 * h0 + 3 * h1, h0];
2640
+ }
2641
+ if (coeffs.every((c) => Math.abs(c) < 1e-12 * Math.max(1, len2))) return [];
2642
+ const scale = Math.sqrt(len2) * Math.max(1, ...seg.map((pt2) => Math.abs(pt2[0]) + Math.abs(pt2[1])));
2643
+ const out = [];
2644
+ for (const t of rootsInUnit(...coeffs, scale)) {
2645
+ const point = _chunkBGNPT5PEcjs.segmentPoint.call(void 0, seg, t);
2646
+ const s = ((point[0] - p[0]) * dx + (point[1] - p[1]) * dy) / len2;
2647
+ if (s >= -1e-9 && s <= 1 + 1e-9) out.push({ s: Math.min(1, Math.max(0, s)), t, point });
2648
+ }
2649
+ return out;
2650
+ }
2651
+ var near = (a, b, eps) => Math.abs(a[0] - b[0]) <= eps && Math.abs(a[1] - b[1]) <= eps;
2652
+ var degenerate = (seg, eps) => seg.every((p) => near(p, seg[0], eps));
2653
+ function arc(l, a, b, eps) {
2654
+ const n = l.segs.length;
2655
+ const segs = [];
2656
+ const chord = [];
2657
+ const push = (k, t0, t1) => {
2658
+ const piece = t0 <= 0 && t1 >= 1 ? l.segs[k].slice() : _chunkBGNPT5PEcjs.subSegment.call(void 0, l.segs[k], t0, t1);
2659
+ if (!degenerate(piece, eps)) {
2660
+ segs.push(piece);
2661
+ chord.push(l.chord[k]);
2662
+ }
2663
+ };
2664
+ if (a.seg === b.seg && b.t >= a.t + 1e-12) {
2665
+ push(a.seg, a.t, b.t);
2666
+ return { segs, chord };
2667
+ }
2668
+ push(a.seg, a.t, 1);
2669
+ for (let k = (a.seg + 1) % n; k !== b.seg; k = (k + 1) % n) push(k, 0, 1);
2670
+ push(b.seg, 0, b.t);
2671
+ return { segs, chord };
2672
+ }
2673
+ function polySegs(points, eps) {
2674
+ const out = [];
2675
+ for (let i = 1; i < points.length; i++) if (!near(points[i - 1], points[i], eps)) out.push([points[i - 1], points[i]]);
2676
+ return out;
2677
+ }
2678
+ function knifePath(path, fillRule, knife) {
2679
+ const pts = knife.filter((p) => Number.isFinite(p[0]) && Number.isFinite(p[1]));
2680
+ if (pts.length < 2) return null;
2681
+ let faces = facesOf(path, fillRule);
2682
+ if (!faces.length) return null;
2683
+ const b = _chunkBGNPT5PEcjs.bezierPathBounds.call(void 0, path);
2684
+ const size = b ? Math.max(b.maxX - b.minX, b.maxY - b.minY, 1) : 1;
2685
+ const eps = size * 1e-9;
2686
+ let cursor = -Infinity;
2687
+ let cuts = 0;
2688
+ for (let guard = 0; guard < 512; guard++) {
2689
+ let best = null;
2690
+ faces.forEach((face2, fi2) => {
2691
+ const hits = [];
2692
+ for (let m = 0; m < pts.length - 1; m++) {
2693
+ if (m + 1 < cursor - 1e-9) continue;
2694
+ 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]) };
2695
+ face2.loops.forEach(
2696
+ (l, li) => l.segs.forEach((seg, si) => {
2697
+ const sb = _chunkBGNPT5PEcjs.segmentBounds.call(void 0, seg);
2698
+ if (sb.maxX < kb.minX - eps || sb.minX > kb.maxX + eps || sb.maxY < kb.minY - eps || sb.minY > kb.maxY + eps) return;
2699
+ for (const h of lineHits(pts[m], pts[m + 1], seg)) {
2700
+ const s = m + h.s;
2701
+ if (s >= cursor - 1e-9) hits.push({ s, loop: li, seg: si, t: h.t, point: h.point });
2702
+ }
2703
+ })
2704
+ );
2705
+ }
2706
+ hits.sort((x, y) => x.s - y.s);
2707
+ 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));
2708
+ const inside = (s) => {
2709
+ const m = Math.min(pts.length - 2, Math.floor(s));
2710
+ const u = s - m;
2711
+ const q = [pts[m][0] + (pts[m + 1][0] - pts[m][0]) * u, pts[m][1] + (pts[m + 1][1] - pts[m][1]) * u];
2712
+ return _chunkBGNPT5PEcjs.windingNumber.call(void 0, face2.loops.map((l) => l.segs), q) !== 0;
2713
+ };
2714
+ for (let i = 0; i < unique.length - 1; i++) {
2715
+ const from2 = unique[i];
2716
+ const to2 = unique[i + 1];
2717
+ if (near(from2.point, to2.point, eps * 10) || !inside((from2.s + to2.s) / 2)) continue;
2718
+ if (!best || from2.s < best.from.s) best = { face: fi2, from: from2, to: to2 };
2719
+ break;
2720
+ }
2721
+ });
2722
+ if (!best) break;
2723
+ const { face: fi, from, to } = best;
2724
+ const face = faces[fi];
2725
+ const corners2 = [from.point];
2726
+ 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]);
2727
+ corners2.push(to.point);
2728
+ const forward = polySegs(corners2, eps);
2729
+ const back = polySegs(corners2.slice().reverse(), eps);
2730
+ const la = face.loops[from.loop];
2731
+ const loops = face.loops.filter((_, i) => i !== from.loop && i !== to.loop);
2732
+ if (from.loop === to.loop) {
2733
+ const ab = arc(la, from, to, eps);
2734
+ const ba = arc(la, to, from, eps);
2735
+ loops.push(
2736
+ { segs: [...ab.segs, ...back], chord: [...ab.chord, ...back.map(() => true)] },
2737
+ { segs: [...ba.segs, ...forward], chord: [...ba.chord, ...forward.map(() => true)] }
2738
+ );
2739
+ } else {
2740
+ const lb = face.loops[to.loop];
2741
+ const roundA = arc(la, from, from, eps);
2742
+ const roundB = arc(lb, to, to, eps);
2743
+ loops.push({
2744
+ segs: [...roundA.segs, ...forward, ...roundB.segs, ...back],
2745
+ chord: [...roundA.chord, ...forward.map(() => true), ...roundB.chord, ...back.map(() => true)]
2746
+ });
2747
+ }
2748
+ faces = [...faces.slice(0, fi), ...groupFaces(loops), ...faces.slice(fi + 1)];
2749
+ cursor = to.s;
2750
+ cuts++;
2751
+ }
2752
+ return cuts ? faces.map(faceToPath) : null;
2753
+ }
2754
+ function eraserRegion(points, width) {
2755
+ const pts = points.filter((p) => Number.isFinite(p[0]) && Number.isFinite(p[1]));
2756
+ const r = width / 2;
2757
+ if (!pts.length || !(r > 0)) return [];
2758
+ const moved = pts.some((p) => !near(p, pts[0], 1e-9));
2759
+ if (!moved) {
2760
+ const [cx, cy] = pts[0];
2761
+ const k = r * 0.5522847498307936;
2762
+ return [
2763
+ { type: "M", x: cx, y: cy - r },
2764
+ { type: "C", x1: cx + k, y1: cy - r, x2: cx + r, y2: cy - k, x: cx + r, y: cy },
2765
+ { type: "C", x1: cx + r, y1: cy + k, x2: cx + k, y2: cy + r, x: cx, y: cy + r },
2766
+ { type: "C", x1: cx - k, y1: cy + r, x2: cx - r, y2: cy + k, x: cx - r, y: cy },
2767
+ { type: "C", x1: cx - r, y1: cy - k, x2: cx - k, y2: cy - r, x: cx, y: cy - r },
2768
+ { type: "Z" }
2769
+ ];
2770
+ }
2771
+ 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] }))];
2772
+ return _chunkBGNPT5PEcjs.outlineStroke.call(void 0, line2, { width, cap: "round", join: "round" });
2773
+ }
2774
+ function eraseFromFill(path, fillRule, region) {
2775
+ const overlap = _chunkBGNPT5PEcjs.pathBoolean.call(void 0, "intersection", [{ path, fillRule }, { path: region }]).path;
2776
+ 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;
2777
+ const rest = _chunkBGNPT5PEcjs.pathBoolean.call(void 0, "difference", [{ path, fillRule }, { path: region }]).path;
2778
+ return splitIntoFaces(rest);
2779
+ }
2780
+ function eraseFromOutline(sp, region) {
2781
+ const regionSegs = _chunkBGNPT5PEcjs.pathToContours.call(void 0, region).flatMap((c) => _chunkBGNPT5PEcjs.closeContour.call(void 0, c, true));
2782
+ if (!regionSegs.length) return null;
2783
+ const rb = _chunkBGNPT5PEcjs.bezierPathBounds.call(void 0, region);
2784
+ const count = subpathSegmentCount(sp);
2785
+ if (!rb || count === 0) return null;
2786
+ const removed = (p) => _chunkBGNPT5PEcjs.pointInBezierPath.call(void 0, region, p, "nonzero");
2787
+ const runs = [];
2788
+ let cur = [];
2789
+ let any = false;
2790
+ for (let i = 0; i < count; i++) {
2791
+ const seg = subpathSegment(sp, i);
2792
+ const sb = _chunkBGNPT5PEcjs.segmentBounds.call(void 0, seg);
2793
+ const ts = [0, 1];
2794
+ if (!(sb.maxX < rb.minX || sb.minX > rb.maxX || sb.maxY < rb.minY || sb.minY > rb.maxY)) {
2795
+ for (const r of regionSegs) {
2796
+ const b = _chunkBGNPT5PEcjs.segmentBounds.call(void 0, r);
2797
+ if (b.maxX < sb.minX || b.minX > sb.maxX || b.maxY < sb.minY || b.minY > sb.maxY) continue;
2798
+ 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);
2799
+ }
2800
+ }
2801
+ ts.sort((a, b) => a - b);
2802
+ for (let k = 0; k < ts.length - 1; k++) {
2803
+ const t0 = ts[k];
2804
+ const t1 = ts[k + 1];
2805
+ if (t1 - t0 < 1e-9) continue;
2806
+ if (removed(_chunkBGNPT5PEcjs.segmentPoint.call(void 0, seg, (t0 + t1) / 2))) {
2807
+ any = true;
2808
+ if (cur.length) runs.push(cur);
2809
+ cur = [];
2810
+ } else cur.push({ seg: i, t0, t1 });
2811
+ }
2812
+ }
2813
+ if (cur.length) runs.push(cur);
2814
+ if (!any) return null;
2815
+ if (sp.closed && runs.length >= 2) {
2816
+ const first = runs[0][0];
2817
+ const lastRun = runs[runs.length - 1];
2818
+ const last = lastRun[lastRun.length - 1];
2819
+ if (first.seg === 0 && first.t0 === 0 && last.seg === count - 1 && last.t1 === 1) lastRun.push(...runs.shift());
2820
+ }
2821
+ const out = [];
2822
+ for (const run of runs) {
2823
+ const anchors = [];
2824
+ for (const { seg, t0, t1 } of run) {
2825
+ const c = _chunkBGNPT5PEcjs.subSegment.call(void 0, subpathSegment(sp, seg), t0, t1);
2826
+ const line2 = c.length === 2;
2827
+ const start = c[0];
2828
+ const end = c[c.length - 1];
2829
+ if (!anchors.length) anchors.push({ x: start[0], y: start[1] });
2830
+ if (!line2) anchors[anchors.length - 1].out = { x: c[1][0], y: c[1][1] };
2831
+ const next = { x: end[0], y: end[1] };
2832
+ if (!line2) next.in = { x: c[2][0], y: c[2][1] };
2833
+ anchors.push(next);
2834
+ }
2835
+ if (anchors.length < 2) continue;
2836
+ for (const a of anchors) {
2837
+ const type = inferAnchorType(a);
2838
+ if (type !== "corner") a.type = type;
2839
+ }
2840
+ out.push({ anchors, closed: false });
2841
+ }
2842
+ return out;
2843
+ }
2844
+ function polylineBounds(points, pad = 0) {
2845
+ if (!points.length) return null;
2846
+ let minX = Infinity;
2847
+ let minY = Infinity;
2848
+ let maxX = -Infinity;
2849
+ let maxY = -Infinity;
2850
+ for (const [x, y] of points) {
2851
+ minX = Math.min(minX, x);
2852
+ minY = Math.min(minY, y);
2853
+ maxX = Math.max(maxX, x);
2854
+ maxY = Math.max(maxY, y);
2855
+ }
2856
+ return { x: minX - pad, y: minY - pad, width: maxX - minX + 2 * pad, height: maxY - minY + 2 * pad };
2857
+ }
2858
+ function simplifyPolyline(points, tolerance) {
2859
+ const n = points.length;
2860
+ if (n < 3) return points.slice();
2861
+ const keep = new Array(n).fill(false);
2862
+ keep[0] = keep[n - 1] = true;
2863
+ const stack = [[0, n - 1]];
2864
+ while (stack.length) {
2865
+ const [a, b] = stack.pop();
2866
+ const [ax, ay] = points[a];
2867
+ const dx = points[b][0] - ax;
2868
+ const dy = points[b][1] - ay;
2869
+ const len2 = Math.hypot(dx, dy);
2870
+ let far = -1;
2871
+ let farD = 0;
2872
+ for (let i = a + 1; i < b; i++) {
2873
+ const [px, py] = points[i];
2874
+ const d = len2 < 1e-12 ? Math.hypot(px - ax, py - ay) : Math.abs((px - ax) * dy - (py - ay) * dx) / len2;
2875
+ if (d > farD) {
2876
+ farD = d;
2877
+ far = i;
2878
+ }
2879
+ }
2880
+ if (far >= 0 && farD > tolerance) {
2881
+ keep[far] = true;
2882
+ stack.push([a, far], [far, b]);
2883
+ }
2884
+ }
2885
+ return points.filter((_, i) => keep[i]);
2886
+ }
2887
+
2888
+ // src/vector/ink.ts
2889
+ var LazyMouse = (_class2 = class {
2890
+ /** `length` is the string length in the units of the points (0 turns smoothing off). */
2891
+ constructor(length2) {;_class2.prototype.__init7.call(this);_class2.prototype.__init8.call(this);
2892
+ this.length = length2;
2893
+ }
2894
+
2895
+ __init7() {this.pos = null}
2896
+ __init8() {this.last = null}
2897
+ push(p) {
2898
+ this.last = p;
2899
+ if (!(this.length > 0)) return [p];
2900
+ const pos = this.pos;
2901
+ if (!pos) {
2902
+ this.pos = p;
2903
+ return [p];
2904
+ }
2905
+ const dx = p[0] - pos[0];
2906
+ const dy = p[1] - pos[1];
2907
+ const d = Math.hypot(dx, dy);
2908
+ if (d <= this.length) return [];
2909
+ const k = (d - this.length) / d;
2910
+ const next = [pos[0] + dx * k, pos[1] + dy * k];
2911
+ this.pos = next;
2912
+ return [next];
2913
+ }
2914
+ /** The string from the pen to the pointer while they are apart, else null. */
2915
+ string() {
2916
+ const { pos, last } = this;
2917
+ if (!(this.length > 0) || !pos || !last || pos[0] === last[0] && pos[1] === last[1]) return null;
2918
+ return [pos, last];
2919
+ }
2920
+ /** End of stroke: the pen catches up with the pointer (the point to add, if it lagged). */
2921
+ finish() {
2922
+ const { pos, last } = this;
2923
+ if (!(this.length > 0) || !pos || !last || pos[0] === last[0] && pos[1] === last[1]) return [];
2924
+ this.pos = last;
2925
+ return [last];
2926
+ }
2927
+ }, _class2);
2928
+ var VECTOR_INK_MAX_CORNERS = 8;
2929
+ var MAX_POINTS = 1e5;
2930
+ function vectorInkShapeName(shape) {
2931
+ switch (shape.kind) {
2932
+ case "line":
2933
+ return "line";
2934
+ case "rect":
2935
+ return shape.width === shape.height ? "square" : "rectangle";
2936
+ case "ellipse":
2937
+ return shape.width === shape.height ? "circle" : "ellipse";
2938
+ case "polygon":
2939
+ return shape.points.length === 3 ? "triangle" : "polygon";
2940
+ }
2941
+ }
2942
+ var dist2 = (a, b) => Math.hypot(b[0] - a[0], b[1] - a[1]);
2943
+ function bbox(pts) {
2944
+ if (!pts.length) return [0, 0, 0, 0];
2945
+ let [x0, y0] = pts[0];
2946
+ let [x1, y1] = pts[0];
2947
+ for (const [x, y] of pts) {
2948
+ x0 = Math.min(x0, x);
2949
+ y0 = Math.min(y0, y);
2950
+ x1 = Math.max(x1, x);
2951
+ y1 = Math.max(y1, y);
2952
+ }
2953
+ return [x0, y0, x1 - x0, y1 - y0];
2954
+ }
2955
+ function segDistance(a, b, p) {
2956
+ const dx = b[0] - a[0];
2957
+ const dy = b[1] - a[1];
2958
+ const len2 = dx * dx + dy * dy;
2959
+ const t = len2 > 0 ? Math.min(1, Math.max(0, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / len2)) : 0;
2960
+ return dist2([a[0] + t * dx, a[1] + t * dy], p);
2961
+ }
2962
+ function polygonDistance(c, p) {
2963
+ let best = Infinity;
2964
+ for (let i = 0; i < c.length; i++) best = Math.min(best, segDistance(c[i], c[(i + 1) % c.length], p));
2965
+ return best;
2966
+ }
2967
+ function ellipseRms(p, x0, y0, w, h) {
2968
+ const rx = w / 2;
2969
+ const ry = h / 2;
2970
+ if (rx <= 0 || ry <= 0 || !p.length) return Infinity;
2971
+ const cx = x0 + rx;
2972
+ const cy = y0 + ry;
2973
+ let sum = 0;
2974
+ for (const [x, y] of p) sum += (Math.hypot((x - cx) / rx, (y - cy) / ry) - 1) ** 2;
2975
+ return Math.sqrt(sum / p.length);
2976
+ }
2977
+ function snapSquare(x, y, w, h) {
2978
+ if (Math.abs(w - h) <= 0.12 * Math.max(w, h)) {
2979
+ const s = (w + h) / 2;
2980
+ return [x + (w - s) / 2, y + (h - s) / 2, s, s];
2981
+ }
2982
+ return [x, y, w, h];
2983
+ }
2984
+ function turning(a, b, c) {
2985
+ const u = [b[0] - a[0], b[1] - a[1]];
2986
+ const v2 = [c[0] - b[0], c[1] - b[1]];
2987
+ const lu = Math.hypot(u[0], u[1]);
2988
+ const lv = Math.hypot(v2[0], v2[1]);
2989
+ if (lu <= 0 || lv <= 0) return 0;
2990
+ return Math.acos(Math.min(1, Math.max(-1, (u[0] * v2[0] + u[1] * v2[1]) / (lu * lv)))) * 180 / Math.PI;
2991
+ }
2992
+ function rdp(p, eps) {
2993
+ const n = p.length;
2994
+ if (n < 3) return p.slice();
2995
+ const keep = new Array(n).fill(false);
2996
+ keep[0] = keep[n - 1] = true;
2997
+ const stack = [[0, n - 1]];
2998
+ while (stack.length) {
2999
+ const [a, b] = stack.pop();
3000
+ let far = a;
3001
+ let farD = 0;
3002
+ for (let i = a + 1; i < b; i++) {
3003
+ const d = segDistance(p[a], p[b], p[i]);
3004
+ if (d > farD) {
3005
+ farD = d;
3006
+ far = i;
3007
+ }
3008
+ }
3009
+ if (farD > eps) {
3010
+ keep[far] = true;
3011
+ stack.push([a, far], [far, b]);
3012
+ }
3013
+ }
3014
+ return p.filter((_, i) => keep[i]);
3015
+ }
3016
+ function corners(p, diag) {
3017
+ const eps = 0.05 * diag;
3018
+ const start = p[0];
3019
+ let far = 0;
3020
+ let farD = 0;
3021
+ p.forEach((q, i) => {
3022
+ const d = dist2(start, q);
3023
+ if (d > farD) {
3024
+ farD = d;
3025
+ far = i;
3026
+ }
3027
+ });
3028
+ const a = p.slice(0, far);
3029
+ const b = p.slice(far);
3030
+ const ring = rdp([...a, ...b.slice(0, 1)], eps);
3031
+ const back = rdp([...b, start], eps);
3032
+ ring.pop();
3033
+ back.pop();
3034
+ ring.push(...back);
3035
+ while (ring.length > 3) {
3036
+ const n = ring.length;
3037
+ const turn = (i) => turning(ring[(i + n - 1) % n], ring[i], ring[(i + 1) % n]);
3038
+ let flat = 0;
3039
+ let flatT = Infinity;
3040
+ for (let i = 0; i < n; i++) {
3041
+ const t = turn(i);
3042
+ if (t < flatT) {
3043
+ flatT = t;
3044
+ flat = i;
3045
+ }
3046
+ }
3047
+ let short = -1;
3048
+ for (let j = 0; j < n; j++) {
3049
+ if (dist2(ring[j], ring[(j + 1) % n]) < 0.1 * diag) {
3050
+ short = j;
3051
+ break;
3052
+ }
3053
+ }
3054
+ if (flatT < 30) ring.splice(flat, 1);
3055
+ else if (short >= 0) {
3056
+ const k = turn(short) < turn((short + 1) % n) ? short : (short + 1) % n;
3057
+ ring.splice(k, 1);
3058
+ } else break;
3059
+ }
3060
+ return ring;
3061
+ }
3062
+ function upright(c) {
3063
+ const n = c.length;
3064
+ const dirs = c.map((a, i) => {
3065
+ const b = c[(i + 1) % n];
3066
+ const deg = (Math.atan2(b[1] - a[1], b[0] - a[0]) * 180 / Math.PI % 180 + 180) % 180;
3067
+ if (deg <= 15 || deg >= 165) return "across";
3068
+ if (deg >= 75 && deg <= 105) return "down";
3069
+ return null;
3070
+ });
3071
+ return dirs.every((d) => d !== null) && dirs.every((d, i) => d !== dirs[(i + 1) % n]);
3072
+ }
3073
+ function recognizeInkShape(points, options = {}) {
3074
+ const minSize = _nullishCoalesce(options.minSize, () => ( 6));
3075
+ const p = [];
3076
+ for (const q of points.slice(0, MAX_POINTS)) {
3077
+ if (!Number.isFinite(q[0]) || !Number.isFinite(q[1]) || Math.abs(q[0]) >= 1e9 || Math.abs(q[1]) >= 1e9) continue;
3078
+ const last2 = p[p.length - 1];
3079
+ if (!last2 || last2[0] !== q[0] || last2[1] !== q[1]) p.push([q[0], q[1]]);
3080
+ }
3081
+ if (p.length < 2) return null;
3082
+ const first = p[0];
3083
+ const last = p[p.length - 1];
3084
+ const [x0, y0, w, h] = bbox(p);
3085
+ const size = Math.max(w, h);
3086
+ const diag = Math.hypot(w, h);
3087
+ if (size < minSize) return null;
3088
+ let length2 = 0;
3089
+ for (let i = 1; i < p.length; i++) length2 += dist2(p[i - 1], p[i]);
3090
+ const gap = dist2(first, last);
3091
+ if (gap > 0.85 * length2 && gap >= minSize) {
3092
+ const off = Math.max(...p.map((q) => segDistance(first, last, q)));
3093
+ if (off <= Math.max(0.06 * gap, 1.5 * minSize / 6)) return { kind: "line", a: first, b: last };
3094
+ }
3095
+ if (gap > 0.2 * size || length2 < 2 * size) return null;
3096
+ const rms = ellipseRms(p, x0, y0, w, h);
3097
+ const roundish = w > 0.2 * size && h > 0.2 * size;
3098
+ const ellipse = () => {
3099
+ const [x, y, sw, sh] = snapSquare(x0, y0, w, h);
3100
+ return { kind: "ellipse", x, y, width: sw, height: sh };
3101
+ };
3102
+ if (roundish && rms < 0.06) return ellipse();
3103
+ const c = corners(p, diag);
3104
+ const fits = c.length >= 3 && p.every((q) => polygonDistance(c, q) <= 0.08 * diag);
3105
+ if (fits && c.length === 4 && upright(c)) {
3106
+ const [bx, by, bw, bh] = bbox(c);
3107
+ const [x, y, sw, sh] = snapSquare(bx, by, bw, bh);
3108
+ return { kind: "rect", x, y, width: sw, height: sh };
3109
+ }
3110
+ if (fits && c.length <= VECTOR_INK_MAX_CORNERS) return { kind: "polygon", points: c };
3111
+ return roundish && rms < 0.1 ? ellipse() : null;
3112
+ }
3113
+
3114
+ // src/vector/controller.ts
3115
+ var DEFAULT_VECTOR_SNAP = { grid: 0, objects: true, guides: true, threshold: 5, angles: VECTOR_CONSTRUCTION_ANGLES };
3116
+ var DEFAULT_VECTOR_PATH_EDIT = { keepShapeOnDelete: true, curvatureMoves: false, pencilSmoothing: 0, recognizeShapes: false, eraserSize: 10 };
3117
+ var VECTOR_PENCIL_STRING_PX = 100;
3118
+ var VECTOR_STYLE_KEYS = [
3119
+ "fill",
3120
+ "stroke",
3121
+ "strokeWidth",
3122
+ "strokeLinecap",
3123
+ "strokeLinejoin",
3124
+ "strokeDasharray",
3125
+ "fillRule",
3126
+ "strokeAlign",
3127
+ "startArrow",
3128
+ "endArrow",
3129
+ "arrowAlign",
3130
+ "startArrowScale",
3131
+ "endArrowScale"
3132
+ ];
3133
+ var VECTOR_CLIPBOARD_TYPE = "pixerate/vector";
3134
+ var DEFAULT_DEFAULTS = {
3135
+ fill: "#d4d4d8",
3136
+ stroke: "#18181b",
3137
+ strokeWidth: 2,
3138
+ polygonSides: 6,
3139
+ starPoints: 5,
3140
+ starInnerRatio: 0.5,
3141
+ cornerRadius: 0
3142
+ };
3143
+ var MIN_ZOOM = 0.05;
3144
+ var MAX_ZOOM = 64;
3145
+ var VECTOR_ROTATE_HANDLE_OFFSET = 24;
3146
+ var sameRef = (a, b) => a.nodeId === b.nodeId && a.subpath === b.subpath && a.index === b.index;
3147
+ function getEditablePathIds(state) {
3148
+ if (state.tool !== "direct" && state.tool !== "pen") return [];
3149
+ const ids = /* @__PURE__ */ new Set();
3150
+ if (state.pen) ids.add(state.pen.nodeId);
3151
+ if (state.tool === "direct") {
3152
+ if (state.editingId) ids.add(state.editingId);
3153
+ for (const id of state.selection) {
3154
+ const node = findVectorNode(state.document.nodes, id);
3155
+ if (node && !node.locked) {
3156
+ for (const p of collectPathNodes(node)) if (!p.locked) ids.add(p.id);
3157
+ }
3158
+ }
3159
+ }
3160
+ return [...ids].filter((id) => _optionalChain([findVectorNode, 'call', _38 => _38(state.document.nodes, id), 'optionalAccess', _39 => _39.type]) === "path");
3161
+ }
3162
+ function getVisibleAnchors(state) {
3163
+ const out = [];
3164
+ for (const id of getEditablePathIds(state)) {
3165
+ const node = findVectorNode(state.document.nodes, id);
3166
+ node.subpaths.forEach((sp, s) => sp.anchors.forEach((_, i) => out.push({ nodeId: id, subpath: s, index: i })));
3167
+ }
3168
+ return out;
3169
+ }
3170
+ function getVisibleHandles(state) {
3171
+ const out = [];
3172
+ const seen = /* @__PURE__ */ new Set();
3173
+ const push = (node, s, i, handle) => {
3174
+ const sp = node.subpaths[s];
3175
+ const a = _optionalChain([sp, 'optionalAccess', _40 => _40.anchors, 'access', _41 => _41[i]]);
3176
+ if (!a || !a[handle]) return;
3177
+ const key = `${node.id}:${s}:${i}:${handle}`;
3178
+ if (seen.has(key)) return;
3179
+ seen.add(key);
3180
+ out.push({ nodeId: node.id, subpath: s, index: i, handle });
3181
+ };
3182
+ if (state.tool === "pen" && state.pen) {
3183
+ const node = findVectorNode(state.document.nodes, state.pen.nodeId);
3184
+ const sp = _optionalChain([node, 'optionalAccess', _42 => _42.type]) === "path" ? node.subpaths[state.pen.subpath] : void 0;
3185
+ if (_optionalChain([node, 'optionalAccess', _43 => _43.type]) === "path" && sp) {
3186
+ const last = sp.anchors.length - 1;
3187
+ push(node, state.pen.subpath, last, "in");
3188
+ push(node, state.pen.subpath, last, "out");
3189
+ }
3190
+ return out;
3191
+ }
3192
+ if (state.tool !== "direct") return out;
3193
+ const editable = new Set(getEditablePathIds(state));
3194
+ for (const ref of state.selectedAnchors) {
3195
+ if (!editable.has(ref.nodeId)) continue;
3196
+ const node = findVectorNode(state.document.nodes, ref.nodeId);
3197
+ const sp = node.subpaths[ref.subpath];
3198
+ if (!sp) continue;
3199
+ const n = sp.anchors.length;
3200
+ push(node, ref.subpath, ref.index, "in");
3201
+ push(node, ref.subpath, ref.index, "out");
3202
+ const prev = ref.index > 0 ? ref.index - 1 : sp.closed ? n - 1 : -1;
3203
+ const next = ref.index < n - 1 ? ref.index + 1 : sp.closed ? 0 : -1;
3204
+ if (prev >= 0) push(node, ref.subpath, prev, "out");
3205
+ if (next >= 0) push(node, ref.subpath, next, "in");
3206
+ }
3207
+ return out;
3208
+ }
3209
+ var VECTOR_CORNER_WIDGET_INSET = 10;
3210
+ function getVectorCornerWidgets(state) {
3211
+ if (state.tool !== "direct" || state.pen) return [];
3212
+ const zoom = state.viewport.zoom;
3213
+ const out = [];
3214
+ for (const id of getEditablePathIds(state)) {
3215
+ const node = findVectorNode(state.document.nodes, id);
3216
+ const picked = state.selectedAnchors.filter((r) => r.nodeId === id);
3217
+ const m = _nullishCoalesce(vectorWorldMatrix(state.document.nodes, id), () => ( IDENTITY_MATRIX));
3218
+ for (const c of vectorCorners(node.subpaths)) {
3219
+ if (picked.length && !picked.some((r) => r.subpath === c.subpath && r.index === c.index)) continue;
3220
+ const side = Math.min(c.lu, c.lv);
3221
+ if (side * zoom < 3 * VECTOR_CORNER_WIDGET_INSET) continue;
3222
+ const a = node.subpaths[c.subpath].anchors[c.index];
3223
+ const radius = _nullishCoalesce(_optionalChain([a, 'access', _44 => _44.corner, 'optionalAccess', _45 => _45.radius]), () => ( 0));
3224
+ const drawn = fitCornerRadius(c, radius);
3225
+ const k = Math.min(Math.max(drawn / c.sin, VECTOR_CORNER_WIDGET_INSET / (zoom * Math.sqrt(1 + c.cos))), side / 2);
3226
+ const p = applyMatrix(m, cornerBisectorPoint(c, k));
3227
+ out.push({ ref: { nodeId: id, subpath: c.subpath, index: c.index }, x: p.x, y: p.y, radius, drawnRadius: drawn, kind: _nullishCoalesce(_optionalChain([a, 'access', _46 => _46.corner, 'optionalAccess', _47 => _47.kind]), () => ( "round")), corner: c });
3228
+ }
3229
+ }
3230
+ return out;
3231
+ }
3232
+ function getAnchor(doc, ref) {
3233
+ const node = findVectorNode(doc.nodes, ref.nodeId);
3234
+ return _optionalChain([node, 'optionalAccess', _48 => _48.type]) === "path" ? _optionalChain([node, 'access', _49 => _49.subpaths, 'access', _50 => _50[ref.subpath], 'optionalAccess', _51 => _51.anchors, 'access', _52 => _52[ref.index]]) : void 0;
3235
+ }
3236
+ function getAnchorWorld(doc, ref) {
3237
+ const a = getAnchor(doc, ref);
3238
+ if (!a) return void 0;
3239
+ const m = _nullishCoalesce(vectorWorldMatrix(doc.nodes, ref.nodeId), () => ( IDENTITY_MATRIX));
3240
+ const p = applyMatrix(m, a);
3241
+ return { x: p.x, y: p.y, ...a.in ? { in: applyMatrix(m, a.in) } : {}, ...a.out ? { out: applyMatrix(m, a.out) } : {}, ...a.type ? { type: a.type } : {} };
3242
+ }
3243
+ function getVectorSelectionFrame(doc, ids) {
3244
+ const nodes = ids.map((id) => ({ node: findVectorNode(doc.nodes, id), parent: _nullishCoalesce(vectorParentMatrix(doc.nodes, id), () => ( IDENTITY_MATRIX)) })).filter((n) => n.node);
3245
+ if (!nodes.length) return null;
3246
+ const angles = nodes.map((n) => matrixRotation(multiplyMatrices(nodeMatrix(n.node), n.parent)));
3247
+ const same = angles.every((a) => Math.abs(normalizeAngle(a - angles[0])) < 1e-6);
3248
+ const angle = same ? angles[0] : 0;
3249
+ const unrotate = rotateMatrix(-angle);
3250
+ const b = unionBounds(nodes.map((n) => vectorNodeBounds(n.node, multiplyMatrices(n.parent, unrotate))));
3251
+ return b ? { ...b, angle } : null;
3252
+ }
3253
+ var normalizeAngle = (a) => ((a + 180) % 360 + 360) % 360 - 180;
3254
+ function getTransformHandlePositions(box) {
3255
+ const { x, y, width: w, height: h } = box;
3256
+ return [
3257
+ { handle: "nw", x, y },
3258
+ { handle: "n", x: x + w / 2, y },
3259
+ { handle: "ne", x: x + w, y },
3260
+ { handle: "e", x: x + w, y: y + h / 2 },
3261
+ { handle: "se", x: x + w, y: y + h },
3262
+ { handle: "s", x: x + w / 2, y: y + h },
3263
+ { handle: "sw", x, y: y + h },
3264
+ { handle: "w", x, y: y + h / 2 },
3265
+ { handle: "rotate", x: x + w / 2, y: y - VECTOR_ROTATE_HANDLE_OFFSET }
3266
+ ];
3267
+ }
3268
+ function getVectorSelectionBoxGeometry(frame, viewport) {
3269
+ const z = viewport.zoom;
3270
+ const box = { x: frame.x * z + viewport.x, y: frame.y * z + viewport.y, width: frame.width * z, height: frame.height * z };
3271
+ const rot = rotateMatrix(frame.angle, { x: viewport.x, y: viewport.y });
3272
+ const toScreen2 = (p) => frame.angle ? applyMatrix(rot, p) : { x: p.x, y: p.y };
3273
+ const handles = getTransformHandlePositions(box).map((h) => ({ ...h, screen: toScreen2(h) }));
3274
+ const corners2 = [
1727
3275
  { x: box.x, y: box.y },
1728
3276
  { x: box.x + box.width, y: box.y },
1729
3277
  { x: box.x + box.width, y: box.y + box.height },
1730
3278
  { x: box.x, y: box.y + box.height }
1731
3279
  ].map(toScreen2);
1732
3280
  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 };
3281
+ return { box, angle: frame.angle, transform: frame.angle ? `rotate(${r(frame.angle)} ${r(viewport.x)} ${r(viewport.y)})` : "", handles, corners: corners2 };
1734
3282
  }
1735
3283
  var RESIZE_CURSOR_ORDER = ["ew-resize", "nwse-resize", "ns-resize", "nesw-resize"];
1736
3284
  var HANDLE_ANGLES = { e: 0, se: 45, s: 90, sw: 135, w: 180, nw: 225, n: 270, ne: 315 };
@@ -1748,9 +3296,12 @@ var TOOL_LABELS = {
1748
3296
  ellipse: "Ellipse",
1749
3297
  polygon: "Polygon",
1750
3298
  star: "Star",
1751
- line: "Line"
3299
+ line: "Line",
3300
+ scissors: "Scissors",
3301
+ knife: "Knife",
3302
+ eraser: "Eraser"
1752
3303
  };
1753
- var TOOL_KEYS = { v: "select", a: "direct", p: "pen", n: "pencil", r: "rect", o: "ellipse", e: "ellipse", y: "polygon", s: "star", l: "line" };
3304
+ 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
3305
  function vectorToolLabel(tool) {
1755
3306
  return TOOL_LABELS[tool];
1756
3307
  }
@@ -1763,7 +3314,10 @@ var VECTOR_TOOL_SHORTCUTS = {
1763
3314
  ellipse: "O",
1764
3315
  polygon: "Y",
1765
3316
  star: "S",
1766
- line: "L"
3317
+ line: "L",
3318
+ scissors: "C",
3319
+ knife: "K",
3320
+ eraser: "E"
1767
3321
  };
1768
3322
  var VECTOR_TOOL_GLYPHS = {
1769
3323
  select: "\u2196",
@@ -1774,8 +3328,12 @@ var VECTOR_TOOL_GLYPHS = {
1774
3328
  ellipse: "\u25EF",
1775
3329
  polygon: "\u2B21",
1776
3330
  star: "\u2606",
1777
- line: "\u2571"
3331
+ line: "\u2571",
3332
+ scissors: "\u2702",
3333
+ knife: "\u27CB",
3334
+ eraser: "\u232B"
1778
3335
  };
3336
+ var SHAPE_TOOLS = /* @__PURE__ */ new Set(["rect", "ellipse", "polygon", "star", "line"]);
1779
3337
  var BOOLEAN_OPS = {
1780
3338
  union: "union",
1781
3339
  subtract: "difference",
@@ -1797,48 +3355,72 @@ function mapTree(nodes, fn) {
1797
3355
  }
1798
3356
  return changed ? out : nodes;
1799
3357
  }
3358
+ function spliceTree(nodes, id, replacements) {
3359
+ let changed = false;
3360
+ const out = [];
3361
+ for (const node of nodes) {
3362
+ if (node.id === id) {
3363
+ changed = true;
3364
+ out.push(...replacements);
3365
+ continue;
3366
+ }
3367
+ if (node.type === "group") {
3368
+ const children = spliceTree(node.children, id, replacements);
3369
+ if (children !== node.children) {
3370
+ changed = true;
3371
+ out.push({ ...node, children });
3372
+ continue;
3373
+ }
3374
+ }
3375
+ out.push(node);
3376
+ }
3377
+ return changed ? out : nodes;
3378
+ }
3379
+ var toPoint2 = (p) => [p.x, p.y];
1800
3380
  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
3381
  var idCounter = 0;
1802
3382
  var defaultCreateId = () => `vn-${Date.now().toString(36)}${(++idCounter).toString(36)}`;
1803
- var VectorEditorController = (_class2 = class {
3383
+ var VectorEditorController = (_class3 = class {
1804
3384
 
1805
3385
 
1806
- __init7() {this.selection = []}
1807
- __init8() {this.anchors = []}
1808
- __init9() {this.editingId = null}
1809
- __init10() {this.pen = null}
3386
+ __init9() {this.selection = []}
3387
+ __init10() {this.anchors = []}
3388
+ __init11() {this.editingId = null}
3389
+ __init12() {this.pen = null}
1810
3390
 
1811
3391
 
1812
3392
 
1813
3393
 
1814
3394
 
1815
- __init11() {this.hover = null}
1816
- __init12() {this.pointer = null}
1817
- __init13() {this.drag = null}
1818
- __init14() {this.interaction = null}
1819
- __init15() {this.guides = null}
3395
+
3396
+ __init13() {this.hover = null}
3397
+ __init14() {this.pointer = null}
3398
+ __init15() {this.drag = null}
3399
+ __init16() {this.interaction = null}
3400
+ __init17() {this.guides = null}
1820
3401
  /** Whether the last `snapDrawn` landed on an object (guides may be hidden). */
1821
- __init16() {this.snappedToObject = false}
3402
+ __init18() {this.snappedToObject = false}
1822
3403
  /** Drawing targets for hovering the pen and shape tools, kept while the document and the excluded path stay the same. */
1823
- __init17() {this.hoverTargets = null}
1824
- __init18() {this.spaceHeld = false}
1825
- __init19() {this.clipboard = null}
3404
+ __init19() {this.hoverTargets = null}
3405
+ __init20() {this.spaceHeld = false}
3406
+ __init21() {this.clipboard = null}
1826
3407
 
1827
3408
 
1828
- __init20() {this.listeners = /* @__PURE__ */ new Set()}
1829
- __init21() {this.cleanups = []}
1830
- __init22() {this.applying = false}
3409
+ __init22() {this.listeners = /* @__PURE__ */ new Set()}
3410
+ __init23() {this.cleanups = []}
3411
+ __init24() {this.applying = false}
1831
3412
  /** Set while a commit records a change that was already applied, so `restore` still reports it. */
1832
- __init23() {this.committing = false}
1833
- __init24() {this.revision = 0}
3413
+ __init25() {this.committing = false}
3414
+ __init26() {this.revision = 0}
1834
3415
 
1835
- constructor(options = {}) {;_class2.prototype.__init7.call(this);_class2.prototype.__init8.call(this);_class2.prototype.__init9.call(this);_class2.prototype.__init10.call(this);_class2.prototype.__init11.call(this);_class2.prototype.__init12.call(this);_class2.prototype.__init13.call(this);_class2.prototype.__init14.call(this);_class2.prototype.__init15.call(this);_class2.prototype.__init16.call(this);_class2.prototype.__init17.call(this);_class2.prototype.__init18.call(this);_class2.prototype.__init19.call(this);_class2.prototype.__init20.call(this);_class2.prototype.__init21.call(this);_class2.prototype.__init22.call(this);_class2.prototype.__init23.call(this);_class2.prototype.__init24.call(this);_class2.prototype.__init25.call(this);_class2.prototype.__init26.call(this);
3416
+ 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
3417
  this.history = _nullishCoalesce(options.history, () => ( new (0, _chunkQKLRAA4Tcjs.HistoryManager)()));
1837
3418
  this.doc = options.document ? cloneVectorDocument(options.document) : { width: 800, height: 600, nodes: [] };
1838
3419
  this.tool = _nullishCoalesce(options.tool, () => ( "select"));
1839
3420
  this.defaults = { ...DEFAULT_DEFAULTS, ..._nullishCoalesce(options.defaults, () => ( {})) };
1840
3421
  this.viewport = { x: 0, y: 0, zoom: 1, ..._nullishCoalesce(options.viewport, () => ( {})) };
1841
3422
  this.snap = { ...DEFAULT_VECTOR_SNAP, ..._nullishCoalesce(options.snap, () => ( {})) };
3423
+ this.editing = { ...DEFAULT_VECTOR_PATH_EDIT, ..._nullishCoalesce(options.editing, () => ( {})) };
1842
3424
  this.hitRadius = _nullishCoalesce(options.hitRadius, () => ( 6));
1843
3425
  this.createId = _nullishCoalesce(options.createId, () => ( defaultCreateId));
1844
3426
  this.onChange = options.onChange;
@@ -1860,9 +3442,9 @@ var VectorEditorController = (_class2 = class {
1860
3442
  // State
1861
3443
  // -------------------------------------------------------------------------
1862
3444
  /** The current snapshot (stable until the next change). */
1863
- __init25() {this.getState = () => this.state}
3445
+ __init27() {this.getState = () => this.state}
1864
3446
  /** Calls `listener` after every change. Returns an unsubscribe function. */
1865
- __init26() {this.subscribe = (listener) => {
3447
+ __init28() {this.subscribe = (listener) => {
1866
3448
  this.listeners.add(listener);
1867
3449
  return () => {
1868
3450
  this.listeners.delete(listener);
@@ -1887,6 +3469,7 @@ var VectorEditorController = (_class2 = class {
1887
3469
  viewport: this.viewport,
1888
3470
  defaults: this.defaults,
1889
3471
  snap: this.snap,
3472
+ editing: this.editing,
1890
3473
  guides: this.guides,
1891
3474
  hitRadius: this.hitRadius,
1892
3475
  hover: this.hover,
@@ -1904,7 +3487,7 @@ var VectorEditorController = (_class2 = class {
1904
3487
  notify(docChanged = false) {
1905
3488
  this.revision++;
1906
3489
  this.state = this.computeState();
1907
- if (docChanged) _optionalChain([this, 'access', _38 => _38.onChange, 'optionalCall', _39 => _39(this.doc)]);
3490
+ if (docChanged) _optionalChain([this, 'access', _53 => _53.onChange, 'optionalCall', _54 => _54(this.doc)]);
1908
3491
  const state = this.state;
1909
3492
  this.listeners.forEach((listener) => {
1910
3493
  try {
@@ -1934,20 +3517,20 @@ var VectorEditorController = (_class2 = class {
1934
3517
  return applyMatrix(invertMatrix(this.worldOf(id, doc)), p);
1935
3518
  }
1936
3519
  cursorFor() {
1937
- if (_optionalChain([this, 'access', _40 => _40.drag, 'optionalAccess', _41 => _41.kind]) === "pan" || this.spaceHeld) return this.drag ? "grabbing" : "grab";
1938
- if (_optionalChain([this, 'access', _42 => _42.drag, 'optionalAccess', _43 => _43.kind]) === "rotate") return "grabbing";
1939
- if (_optionalChain([this, 'access', _44 => _44.drag, 'optionalAccess', _45 => _45.kind]) === "move") return "move";
3520
+ if (_optionalChain([this, 'access', _55 => _55.drag, 'optionalAccess', _56 => _56.kind]) === "pan" || this.spaceHeld) return this.drag ? "grabbing" : "grab";
3521
+ if (_optionalChain([this, 'access', _57 => _57.drag, 'optionalAccess', _58 => _58.kind]) === "rotate") return "grabbing";
3522
+ if (_optionalChain([this, 'access', _59 => _59.drag, 'optionalAccess', _60 => _60.kind]) === "move") return "move";
1940
3523
  const h = this.hover;
1941
3524
  switch (this.tool) {
1942
3525
  case "select":
1943
- if (_optionalChain([h, 'optionalAccess', _46 => _46.transformHandle])) return vectorResizeCursor(h.transformHandle, _nullishCoalesce(_optionalChain([this, 'access', _47 => _47.frameOf, 'call', _48 => _48(this.selectedRoots()), 'optionalAccess', _49 => _49.angle]), () => ( 0)));
1944
- return _optionalChain([h, 'optionalAccess', _50 => _50.nodeId]) ? "move" : "default";
3526
+ if (_optionalChain([h, 'optionalAccess', _61 => _61.transformHandle])) return vectorResizeCursor(h.transformHandle, _nullishCoalesce(_optionalChain([this, 'access', _62 => _62.frameOf, 'call', _63 => _63(this.selectedRoots()), 'optionalAccess', _64 => _64.angle]), () => ( 0)));
3527
+ return _optionalChain([h, 'optionalAccess', _65 => _65.nodeId]) ? "move" : "default";
1945
3528
  case "direct":
1946
- if (_optionalChain([h, 'optionalAccess', _51 => _51.anchor]) || _optionalChain([h, 'optionalAccess', _52 => _52.handle])) return "pointer";
1947
- if (_optionalChain([h, 'optionalAccess', _53 => _53.segment])) return "copy";
1948
- return _optionalChain([h, 'optionalAccess', _54 => _54.nodeId]) ? "move" : "default";
3529
+ if (_optionalChain([h, 'optionalAccess', _66 => _66.anchor]) || _optionalChain([h, 'optionalAccess', _67 => _67.handle]) || _optionalChain([h, 'optionalAccess', _68 => _68.corner])) return "pointer";
3530
+ if (_optionalChain([h, 'optionalAccess', _69 => _69.segment])) return "copy";
3531
+ return _optionalChain([h, 'optionalAccess', _70 => _70.nodeId]) ? "move" : "default";
1949
3532
  case "pen":
1950
- return _optionalChain([h, 'optionalAccess', _55 => _55.anchor]) ? "pointer" : "crosshair";
3533
+ return _optionalChain([h, 'optionalAccess', _71 => _71.anchor]) ? "pointer" : "crosshair";
1951
3534
  default:
1952
3535
  return "crosshair";
1953
3536
  }
@@ -1967,7 +3550,7 @@ var VectorEditorController = (_class2 = class {
1967
3550
  this.selection = s.selection.filter((id) => this.getNode(id));
1968
3551
  this.anchors = s.anchors.filter((r) => getAnchor(this.doc, r));
1969
3552
  this.editingId = s.editingId && this.getNode(s.editingId) ? s.editingId : null;
1970
- this.pen = s.pen && _optionalChain([this, 'access', _56 => _56.getNode, 'call', _57 => _57(s.pen.nodeId), 'optionalAccess', _58 => _58.subpaths, 'optionalAccess', _59 => _59[s.pen.subpath]]) ? s.pen : null;
3553
+ this.pen = s.pen && _optionalChain([this, 'access', _72 => _72.getNode, 'call', _73 => _73(s.pen.nodeId), 'optionalAccess', _74 => _74.subpaths, 'optionalAccess', _75 => _75[s.pen.subpath]]) ? s.pen : null;
1971
3554
  } finally {
1972
3555
  this.applying = false;
1973
3556
  }
@@ -2028,12 +3611,12 @@ var VectorEditorController = (_class2 = class {
2028
3611
  // Viewport
2029
3612
  // -------------------------------------------------------------------------
2030
3613
  screenToDocument(p) {
2031
- const v = this.viewport;
2032
- return { x: (p.x - v.x) / v.zoom, y: (p.y - v.y) / v.zoom };
3614
+ const v2 = this.viewport;
3615
+ return { x: (p.x - v2.x) / v2.zoom, y: (p.y - v2.y) / v2.zoom };
2033
3616
  }
2034
3617
  documentToScreen(p) {
2035
- const v = this.viewport;
2036
- return { x: p.x * v.zoom + v.x, y: p.y * v.zoom + v.y };
3618
+ const v2 = this.viewport;
3619
+ return { x: p.x * v2.zoom + v2.x, y: p.y * v2.zoom + v2.y };
2037
3620
  }
2038
3621
  setViewport(viewport) {
2039
3622
  const next = { ...this.viewport, ...viewport };
@@ -2088,13 +3671,21 @@ var VectorEditorController = (_class2 = class {
2088
3671
  if (!this.snap.guides || !this.snap.objects) this.guides = null;
2089
3672
  this.notify();
2090
3673
  }
3674
+ /** Changes how deleting, dragging anchors, the pencil and the eraser behave. */
3675
+ setEditing(editing) {
3676
+ const next = { ...this.editing, ...editing };
3677
+ next.pencilSmoothing = Math.min(1, Math.max(0, Number.isFinite(next.pencilSmoothing) ? next.pencilSmoothing : 0));
3678
+ if (!(next.eraserSize > 0)) next.eraserSize = this.editing.eraserSize;
3679
+ this.editing = next;
3680
+ this.notify();
3681
+ }
2091
3682
  /** Turns object snapping on or off (the toolbar's snapping toggle). */
2092
3683
  toggleSnapping(on = !this.snap.objects) {
2093
3684
  this.setSnap({ objects: on });
2094
3685
  }
2095
- gridValue(v) {
3686
+ gridValue(v2) {
2096
3687
  const g = this.snap.grid;
2097
- return g > 0 ? Math.round(v / g) * g : v;
3688
+ return g > 0 ? Math.round(v2 / g) * g : v2;
2098
3689
  }
2099
3690
  snapPoint(p) {
2100
3691
  return { x: this.gridValue(p.x), y: this.gridValue(p.y) };
@@ -2123,7 +3714,7 @@ var VectorEditorController = (_class2 = class {
2123
3714
  if (res.kind) {
2124
3715
  this.snappedToObject = true;
2125
3716
  this.setGuides(res);
2126
- return _optionalChain([leave, 'optionalAccess', _60 => _60.constrain]) ? res.point : { x: res.x ? res.point.x : this.gridValue(res.point.x), y: res.y ? res.point.y : this.gridValue(res.point.y) };
3717
+ return _optionalChain([leave, 'optionalAccess', _76 => _76.constrain]) ? res.point : { x: res.x ? res.point.x : this.gridValue(res.point.x), y: res.y ? res.point.y : this.gridValue(res.point.y) };
2127
3718
  }
2128
3719
  }
2129
3720
  this.setGuides(null);
@@ -2141,16 +3732,16 @@ var VectorEditorController = (_class2 = class {
2141
3732
  /** Where the pointer at `p` would place a point with the pen or a shape tool (and the guides showing why). */
2142
3733
  hoverSnap(p, input) {
2143
3734
  if (this.tool === "pen") {
2144
- const sp = this.pen ? _optionalChain([this, 'access', _61 => _61.getNode, 'call', _62 => _62(this.pen.nodeId), 'optionalAccess', _63 => _63.subpaths, 'access', _64 => _64[this.pen.subpath]]) : void 0;
2145
- if (this.pen && sp && _optionalChain([this, 'access', _65 => _65.hover, 'optionalAccess', _66 => _66.anchor])) {
3735
+ const sp = this.pen ? _optionalChain([this, 'access', _77 => _77.getNode, 'call', _78 => _78(this.pen.nodeId), 'optionalAccess', _79 => _79.subpaths, 'access', _80 => _80[this.pen.subpath]]) : void 0;
3736
+ if (this.pen && sp && _optionalChain([this, 'access', _81 => _81.hover, 'optionalAccess', _82 => _82.anchor])) {
2146
3737
  this.setGuides(null);
2147
3738
  return p;
2148
3739
  }
2149
3740
  const last = this.pen && sp ? getAnchorWorld(this.doc, { nodeId: this.pen.nodeId, subpath: this.pen.subpath, index: sp.anchors.length - 1 }) : void 0;
2150
3741
  const leave = last ? { at: { x: last.x, y: last.y }, constrain: input.shift ? "angle" : null } : null;
2151
- return this.snapDrawn(p, leave, input, () => this.drawingTargets(_nullishCoalesce(_optionalChain([this, 'access', _67 => _67.pen, 'optionalAccess', _68 => _68.nodeId]), () => ( null))));
3742
+ return this.snapDrawn(p, leave, input, () => this.drawingTargets(_nullishCoalesce(_optionalChain([this, 'access', _83 => _83.pen, 'optionalAccess', _84 => _84.nodeId]), () => ( null))));
2152
3743
  }
2153
- if (this.tool !== "select" && this.tool !== "direct" && this.tool !== "pencil") return this.snapDrawn(p, null, input, () => this.drawingTargets(null));
3744
+ if (SHAPE_TOOLS.has(this.tool)) return this.snapDrawn(p, null, input, () => this.drawingTargets(null));
2154
3745
  this.setGuides(null);
2155
3746
  return p;
2156
3747
  }
@@ -2250,7 +3841,7 @@ var VectorEditorController = (_class2 = class {
2250
3841
  let subpath;
2251
3842
  switch (input.kind) {
2252
3843
  case "rect":
2253
- subpath = rectSubpath(b, _nullishCoalesce(input.cornerRadius, () => ( d.cornerRadius)));
3844
+ subpath = rectSubpath(b);
2254
3845
  break;
2255
3846
  case "ellipse":
2256
3847
  subpath = ellipseSubpath(b);
@@ -2265,10 +3856,13 @@ var VectorEditorController = (_class2 = class {
2265
3856
  subpath = lineSubpath({ x: b.x, y: b.y }, { x: b.x + b.width, y: b.y + b.height });
2266
3857
  break;
2267
3858
  }
3859
+ const radius = _nullishCoalesce(input.cornerRadius, () => ( d.cornerRadius));
3860
+ const kind = _nullishCoalesce(input.cornerKind, () => ( d.cornerKind));
3861
+ if ((input.kind === "rect" || input.kind === "polygon" || input.kind === "star") && radius > 0) subpath = setSubpathCorners(subpath, radius, kind);
2268
3862
  const names = { rect: "Rectangle", ellipse: "Ellipse", polygon: "Polygon", star: "Star", line: "Line" };
2269
3863
  const style = this.styleFor(input.kind === "line");
2270
3864
  const own = {};
2271
- for (const key of ["fill", "stroke", "strokeWidth", "strokeLinecap", "strokeLinejoin", "strokeDasharray", "fillRule"]) {
3865
+ for (const key of VECTOR_STYLE_KEYS) {
2272
3866
  if (input[key] !== void 0) own[key] = input[key];
2273
3867
  }
2274
3868
  return { id: this.newId(), type: "path", name: _nullishCoalesce(input.name, () => ( names[input.kind])), ...style, ...own, subpaths: [subpath] };
@@ -2291,6 +3885,8 @@ var VectorEditorController = (_class2 = class {
2291
3885
  if (d.strokeLinejoin) out.strokeLinejoin = d.strokeLinejoin;
2292
3886
  if (d.strokeDasharray) out.strokeDasharray = d.strokeDasharray;
2293
3887
  if (d.fillRule) out.fillRule = d.fillRule;
3888
+ if (d.strokeAlign && d.strokeAlign !== "center") out.strokeAlign = d.strokeAlign;
3889
+ for (const key of ["startArrow", "endArrow", "arrowAlign", "startArrowScale", "endArrowScale"]) if (d[key]) out[key] = d[key];
2294
3890
  return out;
2295
3891
  }
2296
3892
  /** Merges `patch` into a node (paths take style and geometry; groups name / visibility / opacity). */
@@ -2315,6 +3911,95 @@ var VectorEditorController = (_class2 = class {
2315
3911
  });
2316
3912
  return { ok: true };
2317
3913
  }
3914
+ /**
3915
+ * Sets the live corner radius (and optionally the kind) of anchors: `refs`, else the selected
3916
+ * anchors (direct tool), else every anchor of the `ids` / selected paths. Radius 0 makes the
3917
+ * corners sharp again. Corners shrink to fit when drawn, so any radius is accepted.
3918
+ */
3919
+ setCornerRadius(radius, options = {}) {
3920
+ if (!Number.isFinite(radius) || radius < 0) return { ok: false, message: "The radius must be a number of at least 0." };
3921
+ const refs = this.cornerTargets(options);
3922
+ if (!refs.length) return { ok: false, message: "Nothing is selected." };
3923
+ this.change(radius > 0 ? "Corner Radius" : "Sharpen Corners", () => {
3924
+ this.doc = this.updateAnchors(this.doc, refs, (a) => setAnchorCorner(a, radius, options.kind));
3925
+ });
3926
+ return { ok: true };
3927
+ }
3928
+ /** Sets the live corner kind of the same anchors as `setCornerRadius`, keeping their radii. */
3929
+ setCornerKind(kind, options = {}) {
3930
+ const refs = this.cornerTargets(options);
3931
+ if (!refs.length) return { ok: false, message: "Nothing is selected." };
3932
+ this.change("Corner Kind", () => {
3933
+ this.doc = this.updateAnchors(this.doc, refs, (a) => setAnchorCorner(a, _nullishCoalesce(_optionalChain([a, 'access', _85 => _85.corner, 'optionalAccess', _86 => _86.radius]), () => ( 0)), kind));
3934
+ });
3935
+ return { ok: true };
3936
+ }
3937
+ /** Anchors a corner change applies to (see `setCornerRadius`). */
3938
+ cornerTargets(options) {
3939
+ if (options.refs) return options.refs.filter((r) => getAnchor(this.doc, r));
3940
+ if (!options.ids && this.tool === "direct" && this.anchors.length) return this.anchors.slice();
3941
+ const out = [];
3942
+ for (const id of _nullishCoalesce(options.ids, () => ( this.selection))) {
3943
+ const node = this.getNode(id);
3944
+ if (!node) continue;
3945
+ for (const p of collectPathNodes(node)) {
3946
+ if (p.locked) continue;
3947
+ p.subpaths.forEach((sp, s) => sp.anchors.forEach((_, i) => out.push({ nodeId: p.id, subpath: s, index: i })));
3948
+ }
3949
+ }
3950
+ return out;
3951
+ }
3952
+ /**
3953
+ * Outline Stroke: replaces each selected (or given) path's stroke with a filled path of the
3954
+ * area it paints (alignment, joins, caps, dashes and arrowheads included), placed above a copy
3955
+ * of the path that keeps the fill (left out when there is none). Returns the new paths' ids.
3956
+ */
3957
+ outlineStroke(options = {}) {
3958
+ const roots = _nullishCoalesce(options.ids, () => ( this.selectedRoots()));
3959
+ const paths = [];
3960
+ for (const id of roots) {
3961
+ const node = this.getNode(id);
3962
+ if (node) {
3963
+ for (const p of collectPathNodes(node)) if (!p.locked && hasPaint(p.stroke) && (_nullishCoalesce(p.strokeWidth, () => ( 1))) > 0) paths.push(p);
3964
+ }
3965
+ }
3966
+ if (!paths.length) return [];
3967
+ const created = [];
3968
+ const replace = /* @__PURE__ */ new Map();
3969
+ for (const p of paths) {
3970
+ const outline = bezierToSubpaths(vectorStrokeOutlinePath(subpathsToBezier(p.subpaths), p));
3971
+ if (!outline.length) continue;
3972
+ const {
3973
+ stroke,
3974
+ strokeWidth: _w,
3975
+ strokeLinecap: _c,
3976
+ strokeLinejoin: _j,
3977
+ strokeDasharray: _d,
3978
+ strokeAlign: _a,
3979
+ startArrow: _s,
3980
+ endArrow: _e,
3981
+ arrowAlign: _al,
3982
+ startArrowScale: _ss,
3983
+ endArrowScale: _es,
3984
+ ...rest
3985
+ } = cloneVectorNode(p);
3986
+ const id = this.newId();
3987
+ const strokeNode = { ...rest, id, name: `${_nullishCoalesce(p.name, () => ( "Path"))} Stroke`, subpaths: outline, fill: _nullishCoalesce(stroke, () => ( null)), stroke: null, fillRule: "nonzero" };
3988
+ const list = hasPaint(p.fill) ? [{ ...rest, stroke: null }, strokeNode] : [strokeNode];
3989
+ created.push(id);
3990
+ replace.set(p.id, list);
3991
+ }
3992
+ if (!created.length) return [];
3993
+ this.change("Outline Stroke", () => {
3994
+ let nodes = this.doc.nodes;
3995
+ for (const [id, list] of replace) nodes = spliceTree(nodes, id, list);
3996
+ this.setDoc(nodes);
3997
+ this.selection = created;
3998
+ this.anchors = [];
3999
+ this.editingId = null;
4000
+ });
4001
+ return created;
4002
+ }
2318
4003
  rename(id, name) {
2319
4004
  return this.updateNode(id, { name }, { label: "Rename" });
2320
4005
  }
@@ -2383,10 +4068,10 @@ var VectorEditorController = (_class2 = class {
2383
4068
  return this.selection.filter((id) => {
2384
4069
  let covered = false;
2385
4070
  const loc = locateVectorNode(this.doc.nodes, id);
2386
- let parent = _nullishCoalesce(_optionalChain([loc, 'optionalAccess', _69 => _69.parent]), () => ( null));
4071
+ let parent = _nullishCoalesce(_optionalChain([loc, 'optionalAccess', _87 => _87.parent]), () => ( null));
2387
4072
  while (parent) {
2388
4073
  if (this.selection.includes(parent.id)) covered = true;
2389
- parent = _nullishCoalesce(_optionalChain([locateVectorNode, 'call', _70 => _70(this.doc.nodes, parent.id), 'optionalAccess', _71 => _71.parent]), () => ( null));
4074
+ parent = _nullishCoalesce(_optionalChain([locateVectorNode, 'call', _88 => _88(this.doc.nodes, parent.id), 'optionalAccess', _89 => _89.parent]), () => ( null));
2390
4075
  }
2391
4076
  return !covered;
2392
4077
  });
@@ -2569,8 +4254,8 @@ var VectorEditorController = (_class2 = class {
2569
4254
  if (!ids.length) return null;
2570
4255
  const locs = ids.map((id2) => locateVectorNode(this.doc.nodes, id2));
2571
4256
  if (locs.some((l) => !l)) return null;
2572
- const parent = _nullishCoalesce(_optionalChain([locs, 'access', _72 => _72[0], 'access', _73 => _73.parent, 'optionalAccess', _74 => _74.id]), () => ( null));
2573
- if (locs.some((l) => (_nullishCoalesce(_optionalChain([l, 'access', _75 => _75.parent, 'optionalAccess', _76 => _76.id]), () => ( null))) !== parent)) return null;
4257
+ const parent = _nullishCoalesce(_optionalChain([locs, 'access', _90 => _90[0], 'access', _91 => _91.parent, 'optionalAccess', _92 => _92.id]), () => ( null));
4258
+ if (locs.some((l) => (_nullishCoalesce(_optionalChain([l, 'access', _93 => _93.parent, 'optionalAccess', _94 => _94.id]), () => ( null))) !== parent)) return null;
2574
4259
  const id = this.newId();
2575
4260
  this.change("Group", () => {
2576
4261
  const set = new Set(ids);
@@ -2593,7 +4278,7 @@ var VectorEditorController = (_class2 = class {
2593
4278
  }
2594
4279
  /** Replaces selected groups with their children. */
2595
4280
  ungroup(options = {}) {
2596
- const groups = (_nullishCoalesce(options.ids, () => ( this.selection))).map((id) => this.getNode(id)).filter((n) => _optionalChain([n, 'optionalAccess', _77 => _77.type]) === "group");
4281
+ const groups = (_nullishCoalesce(options.ids, () => ( this.selection))).map((id) => this.getNode(id)).filter((n) => _optionalChain([n, 'optionalAccess', _95 => _95.type]) === "group");
2597
4282
  if (!groups.length) return { ok: false, message: "Select a group to ungroup." };
2598
4283
  const set = new Set(groups.map((g) => g.id));
2599
4284
  this.change("Ungroup", () => {
@@ -2653,7 +4338,7 @@ var VectorEditorController = (_class2 = class {
2653
4338
  });
2654
4339
  const paths = [...new Set(order)].map((id2) => this.getNode(id2));
2655
4340
  if (paths.length < 2) return null;
2656
- const result = _chunkUG5DVXJKcjs.pathBoolean.call(void 0,
4341
+ const result = _chunkBGNPT5PEcjs.pathBoolean.call(void 0,
2657
4342
  BOOLEAN_OPS[op],
2658
4343
  paths.map((p) => ({
2659
4344
  path: vectorPathWorldBezier(
@@ -2703,11 +4388,11 @@ var VectorEditorController = (_class2 = class {
2703
4388
  /** A document-space path mapped into the parent space of node `id`. */
2704
4389
  intoParentSpace(id, path) {
2705
4390
  const parent = _nullishCoalesce(vectorParentMatrix(this.doc.nodes, id), () => ( IDENTITY_MATRIX));
2706
- return isRoot(parent) ? path : _chunkUG5DVXJKcjs.transformBezierPath.call(void 0, path, invertMatrix(parent));
4391
+ return isRoot(parent) ? path : _chunkBGNPT5PEcjs.transformBezierPath.call(void 0, path, invertMatrix(parent));
2707
4392
  }
2708
4393
  /** Splits selected compound paths into one path per subpath. */
2709
4394
  breakApart(options = {}) {
2710
- const paths = (_nullishCoalesce(options.ids, () => ( this.selection))).map((id) => this.getNode(id)).filter((n) => _optionalChain([n, 'optionalAccess', _78 => _78.type]) === "path" && n.subpaths.length > 1);
4395
+ const paths = (_nullishCoalesce(options.ids, () => ( this.selection))).map((id) => this.getNode(id)).filter((n) => _optionalChain([n, 'optionalAccess', _96 => _96.type]) === "path" && n.subpaths.length > 1);
2711
4396
  if (!paths.length) return { ok: false, message: "Select a compound path to break apart." };
2712
4397
  const created = [];
2713
4398
  const set = new Set(paths.map((p) => p.id));
@@ -2753,12 +4438,47 @@ var VectorEditorController = (_class2 = class {
2753
4438
  return translateAnchor(a, d.x, d.y);
2754
4439
  });
2755
4440
  }
2756
- /** Moves one anchor to a document position (handles follow). */
2757
- moveAnchor(ref, to) {
4441
+ /**
4442
+ * Moves one anchor to a document position. Its handles follow; with `curvature` (default:
4443
+ * the `curvatureMoves` setting) its two segments bend instead, as the Curvature tool does.
4444
+ */
4445
+ moveAnchor(ref, to, options = {}) {
2758
4446
  const a = getAnchorWorld(this.doc, ref);
2759
4447
  if (!a) return { ok: false, message: "There is no such anchor." };
4448
+ const curvature = _nullishCoalesce(options.curvature, () => ( this.editing.curvatureMoves));
2760
4449
  this.change("Move Point", () => {
2761
- this.doc = this.movedAnchors(this.doc, [ref], to.x - a.x, to.y - a.y);
4450
+ this.doc = curvature ? this.curvatureMoved(this.doc, ref, to) : this.movedAnchors(this.doc, [ref], to.x - a.x, to.y - a.y);
4451
+ });
4452
+ return { ok: true };
4453
+ }
4454
+ /** `doc` with anchor `ref` moved to document point `to` by a Curvature-tool move. */
4455
+ curvatureMoved(doc, ref, to) {
4456
+ const node = findVectorNode(doc.nodes, ref.nodeId);
4457
+ if (_optionalChain([node, 'optionalAccess', _97 => _97.type]) !== "path" || node.locked || !node.subpaths[ref.subpath]) return doc;
4458
+ const q = this.toLocal(ref.nodeId, to, doc);
4459
+ const subpaths = node.subpaths.map((sp, i) => i === ref.subpath ? curvatureMoveAnchor(sp, ref.index, q) : sp);
4460
+ const nodes = mapTree(doc.nodes, (n) => n.id === ref.nodeId ? { ...node, subpaths } : n);
4461
+ return { ...doc, nodes };
4462
+ }
4463
+ /** `doc` with segment `ref` bent through document point `p` at parameter `t`. */
4464
+ bent(doc, ref, t, p) {
4465
+ const node = findVectorNode(doc.nodes, ref.nodeId);
4466
+ if (_optionalChain([node, 'optionalAccess', _98 => _98.type]) !== "path" || node.locked || !node.subpaths[ref.subpath]) return doc;
4467
+ const q = this.toLocal(ref.nodeId, p, doc);
4468
+ const subpaths = node.subpaths.map((sp, i) => i === ref.subpath ? bendSegment(sp, ref.segment, t, q) : sp);
4469
+ return { ...doc, nodes: mapTree(doc.nodes, (n) => n.id === ref.nodeId ? { ...node, subpaths } : n) };
4470
+ }
4471
+ /**
4472
+ * Bends a segment so the curve passes through document point `to` at parameter `t`
4473
+ * (default 0.5, clamped to 0.05–0.95); see `bendSegment` in `pathEdit`.
4474
+ */
4475
+ bendSegment(ref, to, t = 0.5) {
4476
+ const node = this.getNode(ref.nodeId);
4477
+ const sp = _optionalChain([node, 'optionalAccess', _99 => _99.type]) === "path" ? node.subpaths[ref.subpath] : void 0;
4478
+ 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." };
4479
+ if (node.locked) return { ok: false, message: "The path is locked." };
4480
+ this.change("Bend Segment", () => {
4481
+ this.doc = this.bent(this.doc, ref, t, to);
2762
4482
  });
2763
4483
  return { ok: true };
2764
4484
  }
@@ -2789,7 +4509,7 @@ var VectorEditorController = (_class2 = class {
2789
4509
  /** Adds an anchor on the nearest segment of `nodeId` to document point `point` (shape unchanged). */
2790
4510
  insertAnchorAt(nodeId, point) {
2791
4511
  const node = this.getNode(nodeId);
2792
- if (_optionalChain([node, 'optionalAccess', _79 => _79.type]) !== "path") return null;
4512
+ if (_optionalChain([node, 'optionalAccess', _100 => _100.type]) !== "path") return null;
2793
4513
  const hit = nearestSegment(node, this.toLocal(nodeId, point));
2794
4514
  if (!hit) return null;
2795
4515
  const { subpath, index } = insertAnchor(node.subpaths[hit.subpath], hit.segment, hit.t);
@@ -2801,15 +4521,24 @@ var VectorEditorController = (_class2 = class {
2801
4521
  });
2802
4522
  return ref;
2803
4523
  }
2804
- /** Deletes the selected anchors; empty subpaths and paths go too. */
2805
- deleteAnchors(refs = this.anchors) {
4524
+ /**
4525
+ * Deletes the selected (or given) anchors; subpaths left with one anchor and empty paths go
4526
+ * too. With `keepShape` (default: the `keepShapeOnDelete` setting, on) the neighbours'
4527
+ * handles are refitted so the path keeps its shape; off, the neighbours are joined as they are.
4528
+ */
4529
+ deleteAnchors(refs = this.anchors, options = {}) {
2806
4530
  if (!refs.length) return { ok: false, message: "Select points to delete." };
4531
+ const keepShape = _nullishCoalesce(options.keepShape, () => ( this.editing.keepShapeOnDelete));
2807
4532
  this.change("Delete Points", () => {
2808
4533
  const nodes = mapTree(this.doc.nodes, (n) => {
2809
- if (n.type !== "path") return n;
4534
+ if (n.type !== "path" || n.locked) return n;
2810
4535
  const mine = refs.filter((r) => r.nodeId === n.id);
2811
4536
  if (!mine.length) return n;
2812
- const subpaths = n.subpaths.map((sp, s) => removeAnchors(sp, mine.filter((r) => r.subpath === s).map((r) => r.index))).filter((sp) => sp.anchors.length > 1);
4537
+ const subpaths = n.subpaths.map((sp, s) => {
4538
+ const indices = mine.filter((r) => r.subpath === s).map((r) => r.index);
4539
+ if (!indices.length) return sp;
4540
+ return keepShape ? removeAnchorsKeepingShape(sp, indices) : removeAnchors(sp, indices);
4541
+ }).filter((sp) => sp.anchors.length > 1);
2813
4542
  return subpaths.length ? { ...n, subpaths } : null;
2814
4543
  });
2815
4544
  this.setDoc(nodes);
@@ -2824,7 +4553,7 @@ var VectorEditorController = (_class2 = class {
2824
4553
  setClosed(closed, nodeId) {
2825
4554
  const targets = nodeId ? (() => {
2826
4555
  const n = this.getNode(nodeId);
2827
- return _optionalChain([n, 'optionalAccess', _80 => _80.type]) === "path" ? n.subpaths.map((_, s) => ({ nodeId, subpath: s })) : [];
4556
+ return _optionalChain([n, 'optionalAccess', _101 => _101.type]) === "path" ? n.subpaths.map((_, s) => ({ nodeId, subpath: s })) : [];
2828
4557
  })() : this.anchors.map((r) => ({ nodeId: r.nodeId, subpath: r.subpath }));
2829
4558
  if (!targets.length) return { ok: false, message: "Select a path or points." };
2830
4559
  this.change(closed ? "Close Path" : "Open Path", () => {
@@ -2835,23 +4564,286 @@ var VectorEditorController = (_class2 = class {
2835
4564
  )
2836
4565
  );
2837
4566
  });
2838
- return { ok: true };
4567
+ return { ok: true };
4568
+ }
4569
+ /** Reverses the direction of every subpath of the selected paths. */
4570
+ reversePaths(ids = this.selection) {
4571
+ const set = new Set(ids.flatMap((id) => this.getNode(id) ? collectPathNodes(this.getNode(id)) : []).map((p) => p.id));
4572
+ if (!set.size) return { ok: false, message: "Select a path." };
4573
+ this.change("Reverse Path", () => {
4574
+ this.setDoc(mapTree(this.doc.nodes, (n) => n.type === "path" && set.has(n.id) ? { ...n, subpaths: n.subpaths.map(reverseSubpath) } : n));
4575
+ this.anchors = [];
4576
+ });
4577
+ return { ok: true };
4578
+ }
4579
+ // -------------------------------------------------------------------------
4580
+ // Path clean-up, Scissors, Knife and Eraser
4581
+ // -------------------------------------------------------------------------
4582
+ /** Unlocked path nodes in the given roots (default: the paths being edited, else the selection). */
4583
+ pathTargets(ids) {
4584
+ const roots = _nullishCoalesce(ids, () => ( (this.tool === "direct" && this.anchors.length ? [...new Set(this.anchors.map((r) => r.nodeId))] : this.selection)));
4585
+ const out = /* @__PURE__ */ new Map();
4586
+ for (const id of roots) {
4587
+ const node = this.getNode(id);
4588
+ if (node && !node.locked) {
4589
+ for (const p of collectPathNodes(node)) if (!p.locked) out.set(p.id, p);
4590
+ }
4591
+ }
4592
+ const order = [];
4593
+ walkVectorNodes(this.doc.nodes, (n) => {
4594
+ if (out.has(n.id)) order.push(n.id);
4595
+ });
4596
+ return order.map((id) => out.get(id));
4597
+ }
4598
+ /** Subpaths of node `from` mapped into node `to`'s own space. */
4599
+ subpathsIn(from, to) {
4600
+ const m = multiplyMatrices(this.worldOf(from.id), invertMatrix(this.worldOf(to)));
4601
+ if (isRoot(m)) return from.subpaths;
4602
+ const map = (p) => applyMatrix(m, p);
4603
+ return from.subpaths.map((sp) => ({
4604
+ closed: sp.closed,
4605
+ anchors: sp.anchors.map((a) => ({ ...map(a), ...a.in ? { in: map(a.in) } : {}, ...a.out ? { out: map(a.out) } : {}, ...a.type ? { type: a.type } : {}, ...a.corner ? { corner: { ...a.corner } } : {} }))
4606
+ }));
4607
+ }
4608
+ /**
4609
+ * Replaces a path with pieces: each entry becomes one path (the first keeps the node's id and
4610
+ * place, the others go just above it with the same style). No pieces removes the path.
4611
+ * Returns the ids of the paths now standing in for it.
4612
+ */
4613
+ replaceWithPieces(node, pieces) {
4614
+ const nodes = pieces.filter((p) => p.length).map((subpaths, i) => ({ ...node, id: i === 0 ? node.id : this.newId(), subpaths }));
4615
+ this.setDoc(spliceTree(this.doc.nodes, node.id, nodes));
4616
+ return nodes.map((n) => n.id);
4617
+ }
4618
+ /** Clears anchor selections, editing and pen state that no longer point at anything. */
4619
+ dropStaleRefs() {
4620
+ this.selection = this.selection.filter((id) => this.getNode(id));
4621
+ this.anchors = this.anchors.filter((r) => getAnchor(this.doc, r));
4622
+ if (this.editingId && !this.getNode(this.editingId)) this.editingId = null;
4623
+ if (this.pen && !this.getNode(this.pen.nodeId)) this.pen = null;
4624
+ }
4625
+ /**
4626
+ * Join: the open subpaths of the selected (or given) paths become one open path, joined
4627
+ * nearest endpoints first (ends within `tolerance` document units merge into one anchor,
4628
+ * others get a straight segment); a single open subpath is closed. The result replaces the
4629
+ * lowest path, in its space and style. Default tolerance: the pick distance.
4630
+ */
4631
+ joinPaths(options = {}) {
4632
+ const paths = this.pathTargets(options.ids);
4633
+ if (!paths.length) return { ok: false, message: "Select paths to join." };
4634
+ const base = paths[0];
4635
+ const all = paths.flatMap((p) => this.subpathsIn(p, base.id));
4636
+ if (!all.some((sp) => !sp.closed && sp.anchors.length > 1)) return { ok: false, message: "There are no open paths to join." };
4637
+ const subpaths = joinSubpaths(all, _nullishCoalesce(options.tolerance, () => ( this.tolerance())));
4638
+ const drop = new Set(paths.slice(1).map((p) => p.id));
4639
+ this.change("Join", () => {
4640
+ this.setDoc(mapTree(this.doc.nodes, (n) => n.id === base.id ? { ...base, subpaths } : drop.has(n.id) ? null : n));
4641
+ this.selection = [base.id];
4642
+ this.anchors = [];
4643
+ if (this.editingId && drop.has(this.editingId)) this.editingId = base.id;
4644
+ this.dropStaleRefs();
4645
+ });
4646
+ return { ok: true };
4647
+ }
4648
+ /** Average: moves the selected (or given) anchors onto their average position along `axis`, in document space. */
4649
+ averageAnchors(axis = "both", refs = this.anchors) {
4650
+ const points = refs.map((r) => ({ ref: r, at: getAnchorWorld(this.doc, r) })).filter((p) => !!p.at);
4651
+ if (points.length < 2) return { ok: false, message: "Select at least two points to average." };
4652
+ const cx = points.reduce((s, p) => s + p.at.x, 0) / points.length;
4653
+ const cy = points.reduce((s, p) => s + p.at.y, 0) / points.length;
4654
+ this.change("Average", () => {
4655
+ for (const { ref, at } of points) {
4656
+ const dx = axis === "horizontal" ? 0 : cx - at.x;
4657
+ const dy = axis === "vertical" ? 0 : cy - at.y;
4658
+ if (dx || dy) this.doc = this.movedAnchors(this.doc, [ref], dx, dy);
4659
+ }
4660
+ });
4661
+ return { ok: true };
4662
+ }
4663
+ /** Applies `fn` to every subpath of the target paths as one undo step. */
4664
+ editSubpaths(label, ids, fn) {
4665
+ const paths = this.pathTargets(ids);
4666
+ if (!paths.length) return { ok: false, message: "Select a path." };
4667
+ const set = new Set(paths.map((p) => p.id));
4668
+ this.change(label, () => {
4669
+ 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));
4670
+ this.dropStaleRefs();
4671
+ });
4672
+ return { ok: true };
4673
+ }
4674
+ /**
4675
+ * Remove Redundant Points: drops anchors of the selected (or given) paths that can go
4676
+ * without moving the outline more than `tolerance` document units (default 0.5).
4677
+ */
4678
+ removeRedundantPoints(options = {}) {
4679
+ const tolerance = _nullishCoalesce(options.tolerance, () => ( 0.5));
4680
+ return this.editSubpaths("Remove Redundant Points", options.ids, (sp) => removeRedundantPoints(sp, tolerance));
4681
+ }
4682
+ /**
4683
+ * Smooth: pulls handles towards smooth tangents by `amount` (0–1, default 1). With the direct
4684
+ * tool and points selected, only those points change.
4685
+ */
4686
+ smoothPaths(options = {}) {
4687
+ const refs = _nullishCoalesce(options.refs, () => ( (!options.ids && this.tool === "direct" && this.anchors.length ? this.anchors : null)));
4688
+ const ids = _nullishCoalesce(options.ids, () => ( (refs ? [...new Set(refs.map((r) => r.nodeId))] : void 0)));
4689
+ return this.editSubpaths("Smooth", ids, (sp, node, i) => {
4690
+ const only = refs ? new Set(refs.filter((r) => r.nodeId === node.id && r.subpath === i).map((r) => r.index)) : void 0;
4691
+ return only && !only.size ? sp : smoothSubpath(sp, _nullishCoalesce(options.amount, () => ( 1)), only);
4692
+ });
4693
+ }
4694
+ /** Splits a cut subpath into pieces: separate paths for a one-subpath node, subpaths in place for a compound one. */
4695
+ applyCuts(node, cut) {
4696
+ const results = node.subpaths.map((sp, i) => cut(sp, i));
4697
+ if (results.every((r) => !r)) return null;
4698
+ const subpaths = node.subpaths.flatMap((sp, i) => _nullishCoalesce(results[i], () => ( [sp])));
4699
+ if (node.subpaths.length === 1) return this.replaceWithPieces(node, subpaths.map((sp) => [sp]));
4700
+ this.replaceNode(node.id, { ...node, subpaths });
4701
+ return [node.id];
4702
+ }
4703
+ /**
4704
+ * Scissors: cuts path `nodeId` at the outline point nearest document point `point` (at an
4705
+ * anchor when within the pick distance of one). A closed subpath opens there; an open one
4706
+ * splits in two (a one-subpath path becomes two paths). Returns the ids of the paths.
4707
+ */
4708
+ cutPathAt(nodeId, point) {
4709
+ const node = this.getNode(nodeId);
4710
+ if (_optionalChain([node, 'optionalAccess', _102 => _102.type]) !== "path" || node.locked) return null;
4711
+ const local = this.toLocal(nodeId, point);
4712
+ const hit = nearestSegment(node, local);
4713
+ if (!hit) return null;
4714
+ const sp = node.subpaths[hit.subpath];
4715
+ const n = sp.anchors.length;
4716
+ 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());
4717
+ if (!sp.closed && n > 1 && (near2 === 0 || near2 === n - 1) && !(hit.t > 1e-6 && hit.t < 1 - 1e-6)) return null;
4718
+ let ids = null;
4719
+ this.change("Cut Path", () => {
4720
+ ids = this.applyCuts(node, (s, i) => i !== hit.subpath ? null : near2 !== void 0 ? cutSubpathAt(s, [near2]) : splitSubpathAt(s, hit.segment, hit.t));
4721
+ if (ids) {
4722
+ this.selection = ids;
4723
+ this.anchors = [];
4724
+ if (this.editingId === nodeId) this.editingId = null;
4725
+ }
4726
+ this.dropStaleRefs();
4727
+ });
4728
+ return ids;
4729
+ }
4730
+ /** Scissors on the selected (or given) anchors: each cuts its subpath there (see `cutPathAt`). */
4731
+ cutAtAnchors(refs = this.anchors) {
4732
+ const byNode = /* @__PURE__ */ new Map();
4733
+ for (const r of refs) if (getAnchor(this.doc, r)) byNode.set(r.nodeId, [..._nullishCoalesce(byNode.get(r.nodeId), () => ( [])), r]);
4734
+ if (!byNode.size) return { ok: false, message: "Select points to cut at." };
4735
+ const created = [];
4736
+ this.change("Cut Path", () => {
4737
+ for (const [id, list] of byNode) {
4738
+ const node = this.getNode(id);
4739
+ if (node.locked) continue;
4740
+ const ids = this.applyCuts(node, (sp, i) => {
4741
+ const at = list.filter((r) => r.subpath === i).map((r) => r.index);
4742
+ const pieces = at.length ? cutSubpathAt(sp, at) : null;
4743
+ return pieces && !(pieces.length === 1 && pieces[0].closed === sp.closed) ? pieces : null;
4744
+ });
4745
+ if (ids) created.push(...ids);
4746
+ }
4747
+ if (created.length) this.selection = created;
4748
+ this.anchors = [];
4749
+ this.dropStaleRefs();
4750
+ });
4751
+ return created.length ? { ok: true } : { ok: false, message: "Points at the ends of open paths do not cut." };
4752
+ }
4753
+ /** Visible, unlocked paths a stroke over `points` can reach: in `ids`, the selection, or (with neither) any whose bounds it crosses. */
4754
+ strokeTargets(points, ids, pad) {
4755
+ const blocked = /* @__PURE__ */ new Set();
4756
+ walkVectorNodes(this.doc.nodes, (n, parent) => {
4757
+ if (n.visible === false || n.locked || parent && blocked.has(parent.id)) blocked.add(n.id);
4758
+ });
4759
+ const visible = (p) => !blocked.has(p.id);
4760
+ if (_optionalChain([ids, 'optionalAccess', _103 => _103.length]) || this.selection.length) return this.pathTargets(_nullishCoalesce(ids, () => ( this.selection))).filter(visible);
4761
+ const box = polylineBounds(points.map(toPoint2), pad);
4762
+ if (!box) return [];
4763
+ const out = [];
4764
+ walkVectorNodes(this.doc.nodes, (n) => {
4765
+ if (n.type !== "path" || !visible(n)) return;
4766
+ const b = vectorNodeBounds(n, _nullishCoalesce(vectorParentMatrix(this.doc.nodes, n.id), () => ( IDENTITY_MATRIX)));
4767
+ if (b && boundsIntersect(b, box)) out.push(n);
4768
+ });
4769
+ return out;
4770
+ }
4771
+ /** A stroke's points in node `id`'s own space. */
4772
+ localPoints(id, points) {
4773
+ const inv = invertMatrix(this.worldOf(id));
4774
+ return points.map((p) => toPoint2(applyMatrix(inv, p)));
4775
+ }
4776
+ /**
4777
+ * Knife: cuts closed paths along the polyline `points` (document space) wherever it crosses
4778
+ * them from edge to edge, with no gap between the pieces. It cuts the given paths, the
4779
+ * selection, or (with neither) every path it crosses. Returns the ids of the pieces (empty
4780
+ * when nothing was cut).
4781
+ */
4782
+ knife(points, options = {}) {
4783
+ const pts = simplifyPolyline(points.map(toPoint2), 0.25 / this.viewport.zoom).map(([x, y]) => ({ x, y }));
4784
+ if (pts.length < 2) return [];
4785
+ const plan = [];
4786
+ for (const node of this.strokeTargets(pts, options.ids, 0)) {
4787
+ const closed = node.subpaths.filter((sp) => sp.anchors.length > 1);
4788
+ if (!closed.length || closed.some((sp) => !sp.closed)) continue;
4789
+ const result = knifePath(subpathsToBezier(closed), _nullishCoalesce(node.fillRule, () => ( "nonzero")), this.localPoints(node.id, pts));
4790
+ if (result) plan.push({ node, pieces: result.map((path) => bezierToSubpaths(path)) });
4791
+ }
4792
+ if (!plan.length) return [];
4793
+ const created = [];
4794
+ this.change("Knife", () => {
4795
+ for (const { node, pieces } of plan) created.push(...this.replaceWithPieces(node, pieces));
4796
+ this.selection = created;
4797
+ this.anchors = [];
4798
+ this.dropStaleRefs();
4799
+ });
4800
+ return created;
2839
4801
  }
2840
- /** Reverses the direction of every subpath of the selected paths. */
2841
- reversePaths(ids = this.selection) {
2842
- const set = new Set(ids.flatMap((id) => this.getNode(id) ? collectPathNodes(this.getNode(id)) : []).map((p) => p.id));
2843
- if (!set.size) return { ok: false, message: "Select a path." };
2844
- this.change("Reverse Path", () => {
2845
- this.setDoc(mapTree(this.doc.nodes, (n) => n.type === "path" && set.has(n.id) ? { ...n, subpaths: n.subpaths.map(reverseSubpath) } : n));
4802
+ /**
4803
+ * Eraser: rubs out a round stroke of diameter `size` (default: the `eraserSize` setting)
4804
+ * along `points` (document space). Filled closed paths lose that area (pieces that come
4805
+ * apart become separate paths); outlines are cut where they pass under it. It erases from
4806
+ * the given paths, the selection, or (with neither) every path it reaches. Returns the ids
4807
+ * of the paths it changed or created.
4808
+ */
4809
+ erase(points, options = {}) {
4810
+ const width = _nullishCoalesce(options.size, () => ( this.editing.eraserSize));
4811
+ if (!(width > 0) || !points.length) return [];
4812
+ const pts = simplifyPolyline(points.map(toPoint2), Math.min(width * 0.05, 0.25 / this.viewport.zoom));
4813
+ const region = eraserRegion(pts, width);
4814
+ if (!region.length) return [];
4815
+ const plan = [];
4816
+ const rb = _chunkBGNPT5PEcjs.bezierPathBounds.call(void 0, region);
4817
+ const reach = rb ? [{ x: rb.minX, y: rb.minY }, { x: rb.maxX, y: rb.maxY }] : points;
4818
+ for (const node of this.strokeTargets(reach, options.ids, 0)) {
4819
+ const local = _chunkBGNPT5PEcjs.transformBezierPath.call(void 0, region, invertMatrix(this.worldOf(node.id)));
4820
+ const live = node.subpaths.filter((sp) => sp.anchors.length > 1);
4821
+ if (!live.length) continue;
4822
+ if (hasPaint(node.fill) && live.every((sp) => sp.closed)) {
4823
+ const faces = eraseFromFill(subpathsToBezier(live), _nullishCoalesce(node.fillRule, () => ( "nonzero")), local);
4824
+ if (faces) plan.push({ node, pieces: faces.map((f) => bezierToSubpaths(f)) });
4825
+ continue;
4826
+ }
4827
+ const results = live.map((sp) => eraseFromOutline(sp, local));
4828
+ if (results.every((r) => !r)) continue;
4829
+ const subpaths = live.flatMap((sp, i) => _nullishCoalesce(results[i], () => ( [sp])));
4830
+ plan.push({ node, pieces: live.length === 1 ? subpaths.map((sp) => [sp]) : [subpaths] });
4831
+ }
4832
+ if (!plan.length) return [];
4833
+ const changed = [];
4834
+ this.change("Erase", () => {
4835
+ for (const { node, pieces } of plan) changed.push(...this.replaceWithPieces(node, pieces));
2846
4836
  this.anchors = [];
4837
+ this.dropStaleRefs();
4838
+ if (this.selection.length || options.ids) this.selection = [.../* @__PURE__ */ new Set([...this.selection, ...changed.filter((id) => this.getNode(id))])];
2847
4839
  });
2848
- return { ok: true };
4840
+ return changed;
2849
4841
  }
2850
4842
  /** Ends the pen path being drawn (a lone anchor is removed). */
2851
4843
  finishPath() {
2852
4844
  if (!this.pen) return;
2853
4845
  const node = this.getNode(this.pen.nodeId);
2854
- const sp = _optionalChain([node, 'optionalAccess', _81 => _81.subpaths, 'access', _82 => _82[this.pen.subpath]]);
4846
+ const sp = _optionalChain([node, 'optionalAccess', _104 => _104.subpaths, 'access', _105 => _105[this.pen.subpath]]);
2855
4847
  const pen = this.pen;
2856
4848
  this.pen = null;
2857
4849
  this.pointer = null;
@@ -2895,7 +4887,7 @@ var VectorEditorController = (_class2 = class {
2895
4887
  if (trimmed.startsWith("{")) {
2896
4888
  try {
2897
4889
  const data = JSON.parse(trimmed);
2898
- if (_optionalChain([data, 'optionalAccess', _83 => _83.type]) === VECTOR_CLIPBOARD_TYPE && Array.isArray(data.nodes)) nodes = data.nodes;
4890
+ if (_optionalChain([data, 'optionalAccess', _106 => _106.type]) === VECTOR_CLIPBOARD_TYPE && Array.isArray(data.nodes)) nodes = data.nodes;
2899
4891
  } catch (e2) {
2900
4892
  nodes = null;
2901
4893
  }
@@ -2907,7 +4899,7 @@ var VectorEditorController = (_class2 = class {
2907
4899
  }
2908
4900
  }
2909
4901
  } else nodes = this.clipboard;
2910
- if (!_optionalChain([nodes, 'optionalAccess', _84 => _84.length])) return [];
4902
+ if (!_optionalChain([nodes, 'optionalAccess', _107 => _107.length])) return [];
2911
4903
  const offset = _nullishCoalesce(options.offset, () => ( (text && !text.trim().startsWith("{") ? { x: 0, y: 0 } : { x: 10, y: 10 })));
2912
4904
  const copies = nodes.map((n) => transformVectorNode(this.freshIds(n), translateMatrix(offset.x, offset.y)));
2913
4905
  this.change("Paste", () => {
@@ -2958,13 +4950,29 @@ var VectorEditorController = (_class2 = class {
2958
4950
  hitHandle(screen) {
2959
4951
  for (const ref of getVisibleHandles(this.view()).reverse()) {
2960
4952
  const a = getAnchorWorld(this.doc, ref);
2961
- const h = _optionalChain([a, 'optionalAccess', _85 => _85[ref.handle]]);
4953
+ const h = _optionalChain([a, 'optionalAccess', _108 => _108[ref.handle]]);
2962
4954
  if (!h) continue;
2963
4955
  const s = this.documentToScreen(h);
2964
4956
  if (Math.hypot(s.x - screen.x, s.y - screen.y) <= this.hitRadius) return ref;
2965
4957
  }
2966
4958
  return null;
2967
4959
  }
4960
+ cornerView() {
4961
+ return { ...this.view(), viewport: this.viewport };
4962
+ }
4963
+ hitCornerWidget(screen) {
4964
+ let best = null;
4965
+ let bestD = Infinity;
4966
+ for (const w of getVectorCornerWidgets(this.cornerView())) {
4967
+ const s = this.documentToScreen(w);
4968
+ const d = Math.hypot(s.x - screen.x, s.y - screen.y);
4969
+ if (d <= this.hitRadius && d < bestD) {
4970
+ best = w;
4971
+ bestD = d;
4972
+ }
4973
+ }
4974
+ return best;
4975
+ }
2968
4976
  hitAnchor(screen, refs) {
2969
4977
  let best = null;
2970
4978
  let bestD = Infinity;
@@ -3023,6 +5031,8 @@ var VectorEditorController = (_class2 = class {
3023
5031
  if (handle) return { nodeId: handle.nodeId, handle };
3024
5032
  const anchor = this.hitAnchor(screen, getVisibleAnchors(this.view()));
3025
5033
  if (anchor) return { nodeId: anchor.nodeId, anchor };
5034
+ const widget = this.hitCornerWidget(screen);
5035
+ if (widget) return { nodeId: widget.ref.nodeId, corner: widget.ref };
3026
5036
  for (const id of getEditablePathIds(this.view())) {
3027
5037
  const node = this.getNode(id);
3028
5038
  const seg = nearestSegment(node, this.toLocal(id, p));
@@ -3039,10 +5049,38 @@ var VectorEditorController = (_class2 = class {
3039
5049
  const anchor = this.hitAnchor(screen, refs);
3040
5050
  return anchor ? { nodeId: anchor.nodeId, anchor } : null;
3041
5051
  }
5052
+ case "scissors": {
5053
+ const seg = this.nearestOutline(p);
5054
+ return seg ? { nodeId: seg.nodeId, segment: seg } : null;
5055
+ }
3042
5056
  default:
3043
5057
  return null;
3044
5058
  }
3045
5059
  }
5060
+ /** The nearest outline point of any visible, unlocked path within the pick distance of document point `p`. */
5061
+ nearestOutline(p) {
5062
+ let best = null;
5063
+ const tol = this.tolerance();
5064
+ const visit = (list, parent) => {
5065
+ for (let i = list.length - 1; i >= 0; i--) {
5066
+ const n = list[i];
5067
+ if (n.visible === false || n.locked) continue;
5068
+ const m = multiplyMatrices(nodeMatrix(n), parent);
5069
+ if (n.type === "group") {
5070
+ visit(n.children, m);
5071
+ continue;
5072
+ }
5073
+ const hit = nearestSegment(n, applyMatrix(invertMatrix(m), p));
5074
+ if (hit && hit.distance <= tol && (!best || hit.distance < best.distance - 1e-9)) {
5075
+ best = { nodeId: n.id, subpath: hit.subpath, segment: hit.segment, t: hit.t, point: applyMatrix(m, hit.point), distance: hit.distance };
5076
+ }
5077
+ }
5078
+ };
5079
+ visit(this.doc.nodes, IDENTITY_MATRIX);
5080
+ if (!best) return null;
5081
+ const { distance: _d, ...seg } = best;
5082
+ return seg;
5083
+ }
3046
5084
  sameHover(a, b) {
3047
5085
  return JSON.stringify(a) === JSON.stringify(b);
3048
5086
  }
@@ -3067,6 +5105,17 @@ var VectorEditorController = (_class2 = class {
3067
5105
  return this.downPen(screen, p, input);
3068
5106
  case "pencil":
3069
5107
  return this.downPencil(p);
5108
+ case "scissors": {
5109
+ const seg = this.nearestOutline(p);
5110
+ if (seg) this.cutPathAt(seg.nodeId, seg.point);
5111
+ return;
5112
+ }
5113
+ case "knife":
5114
+ case "eraser":
5115
+ this.drag = { kind: "stroke", tool: this.tool, points: [p], before: this.snapshot() };
5116
+ this.interaction = { type: "stroke", tool: this.tool, points: [p], width: this.tool === "eraser" ? this.editing.eraserSize : 0 };
5117
+ this.notify();
5118
+ return;
3070
5119
  default:
3071
5120
  return this.downShape(p, input);
3072
5121
  }
@@ -3141,6 +5190,13 @@ var VectorEditorController = (_class2 = class {
3141
5190
  this.notify();
3142
5191
  return;
3143
5192
  }
5193
+ const widget = this.hitCornerWidget(screen);
5194
+ if (widget) {
5195
+ const refs = getVectorCornerWidgets(this.cornerView()).map((w) => w.ref);
5196
+ this.drag = { kind: "corner", widget, refs, start: p, before: this.snapshot(), base: this.doc, moved: false, alt: !!input.alt, shift: !!input.shift };
5197
+ this.notify();
5198
+ return;
5199
+ }
3144
5200
  for (const id of getEditablePathIds(this.view())) {
3145
5201
  const node = this.getNode(id);
3146
5202
  const seg = nearestSegment(node, this.toLocal(id, p));
@@ -3161,7 +5217,7 @@ var VectorEditorController = (_class2 = class {
3161
5217
  const b = { nodeId: id, subpath: seg.subpath, index: (seg.segment + 1) % sp.anchors.length };
3162
5218
  this.anchors = input.shift ? [...this.anchors.filter((r) => !sameRef(r, a) && !sameRef(r, b)), a, b] : [a, b];
3163
5219
  this.editingId = id;
3164
- this.drag = { kind: "anchors", start: p, before: this.snapshot(), base: this.doc, moved: false, clickRef: null, lead: null };
5220
+ 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
5221
  this.notify();
3166
5222
  return;
3167
5223
  }
@@ -3184,7 +5240,7 @@ var VectorEditorController = (_class2 = class {
3184
5240
  }
3185
5241
  downPen(screen, p, input) {
3186
5242
  const before = this.snapshot();
3187
- const sp = this.pen ? _optionalChain([this, 'access', _86 => _86.getNode, 'call', _87 => _87(this.pen.nodeId), 'optionalAccess', _88 => _88.subpaths, 'access', _89 => _89[this.pen.subpath]]) : void 0;
5243
+ const sp = this.pen ? _optionalChain([this, 'access', _109 => _109.getNode, 'call', _110 => _110(this.pen.nodeId), 'optionalAccess', _111 => _111.subpaths, 'access', _112 => _112[this.pen.subpath]]) : void 0;
3188
5244
  if (this.pen && sp) {
3189
5245
  const pen = this.pen;
3190
5246
  const first = { nodeId: pen.nodeId, subpath: pen.subpath, index: 0 };
@@ -3198,10 +5254,10 @@ var VectorEditorController = (_class2 = class {
3198
5254
  }
3199
5255
  const last = getAnchorWorld(this.doc, { nodeId: pen.nodeId, subpath: pen.subpath, index: sp.anchors.length - 1 });
3200
5256
  const leave = { at: { x: last.x, y: last.y }, constrain: input.shift ? "angle" : null };
3201
- let pt2 = this.snapDrawn(p, leave, input, () => this.drawingTargets(pen.nodeId));
3202
- pt2 = this.toLocal(pen.nodeId, pt2);
5257
+ let pt3 = this.snapDrawn(p, leave, input, () => this.drawingTargets(pen.nodeId));
5258
+ pt3 = this.toLocal(pen.nodeId, pt3);
3203
5259
  const node2 = this.getNode(pen.nodeId);
3204
- const anchors = [...sp.anchors, { x: pt2.x, y: pt2.y }];
5260
+ const anchors = [...sp.anchors, { x: pt3.x, y: pt3.y }];
3205
5261
  this.replaceNode(pen.nodeId, { ...node2, subpaths: node2.subpaths.map((s, i) => i === pen.subpath ? { ...s, anchors } : s) });
3206
5262
  const ref2 = { nodeId: pen.nodeId, subpath: pen.subpath, index: anchors.length - 1 };
3207
5263
  this.anchors = [ref2];
@@ -3229,8 +5285,8 @@ var VectorEditorController = (_class2 = class {
3229
5285
  this.notify(this.doc !== before.doc);
3230
5286
  return;
3231
5287
  }
3232
- const pt = this.snapDrawn(p, null, input, () => this.drawingTargets(null));
3233
- const node = { id: this.newId(), type: "path", name: "Path", ...this.styleFor(true), subpaths: [{ anchors: [{ x: pt.x, y: pt.y }], closed: false }] };
5288
+ const pt2 = this.snapDrawn(p, null, input, () => this.drawingTargets(null));
5289
+ const node = { id: this.newId(), type: "path", name: "Path", ...this.styleFor(true), subpaths: [{ anchors: [{ x: pt2.x, y: pt2.y }], closed: false }] };
3234
5290
  this.setDoc([...this.doc.nodes, node]);
3235
5291
  this.pen = { nodeId: node.id, subpath: 0 };
3236
5292
  this.selection = [node.id];
@@ -3253,7 +5309,9 @@ var VectorEditorController = (_class2 = class {
3253
5309
  subpaths: [{ anchors: [{ x: p.x, y: p.y }], closed: false }]
3254
5310
  };
3255
5311
  this.setDoc([...this.doc.nodes, node]);
3256
- this.drag = { kind: "pencil", nodeId: node.id, points: [[p.x, p.y]], before };
5312
+ const lazy = new LazyMouse(this.editing.pencilSmoothing * VECTOR_PENCIL_STRING_PX / this.viewport.zoom);
5313
+ lazy.push([p.x, p.y]);
5314
+ this.drag = { kind: "pencil", nodeId: node.id, points: [[p.x, p.y]], before, lazy };
3257
5315
  this.interaction = { type: "draw", tool: "pencil", nodeId: node.id, bounds: { x: p.x, y: p.y, width: 0, height: 0 } };
3258
5316
  this.notify(true);
3259
5317
  }
@@ -3365,7 +5423,10 @@ var VectorEditorController = (_class2 = class {
3365
5423
  dx = q.x - lead.x;
3366
5424
  dy = q.y - lead.y;
3367
5425
  } else if (input.shift) Math.abs(dx) > Math.abs(dy) ? dy = 0 : dx = 0;
3368
- this.doc = this.movedAnchors(drag.base, this.anchors, dx, dy);
5426
+ const single = this.anchors.length === 1 ? this.anchors[0] : null;
5427
+ const from = single ? getAnchorWorld(drag.base, single) : void 0;
5428
+ if (single && from && this.editing.curvatureMoves !== !!input.alt) this.doc = this.curvatureMoved(drag.base, single, { x: from.x + dx, y: from.y + dy });
5429
+ else this.doc = this.movedAnchors(drag.base, this.anchors, dx, dy);
3369
5430
  this.interaction = { type: "anchors", dx, dy };
3370
5431
  this.notify(true);
3371
5432
  return;
@@ -3373,13 +5434,41 @@ var VectorEditorController = (_class2 = class {
3373
5434
  case "handle": {
3374
5435
  const anchor = getAnchorWorld(drag.base, drag.ref);
3375
5436
  const leave = anchor ? { at: { x: anchor.x, y: anchor.y }, constrain: input.shift ? "angle" : null } : null;
3376
- const pt = this.snapDrawn(p, leave, input, () => drag.targets ??= VectorSnapTargets.forHandle(drag.base, drag.ref.nodeId));
3377
- const local = this.toLocal(drag.ref.nodeId, pt, drag.base);
5437
+ const pt2 = this.snapDrawn(p, leave, input, () => drag.targets ??= VectorSnapTargets.forHandle(drag.base, drag.ref.nodeId));
5438
+ const local = this.toLocal(drag.ref.nodeId, pt2, drag.base);
3378
5439
  this.doc = this.updateAnchors(drag.base, [drag.ref], (a) => moveAnchorHandle(a, drag.ref.handle, local, input.alt));
3379
5440
  this.interaction = { type: "handle", ref: drag.ref };
3380
5441
  this.notify(true);
3381
5442
  return;
3382
5443
  }
5444
+ case "corner": {
5445
+ if (!drag.moved && Math.hypot(p.x - drag.start.x, p.y - drag.start.y) * this.viewport.zoom < 3) return;
5446
+ drag.moved = true;
5447
+ const c = drag.widget.corner;
5448
+ const inv = invertMatrix(this.worldOf(drag.widget.ref.nodeId, drag.base));
5449
+ const d = applyMatrixLinear(inv, { x: p.x - drag.start.x, y: p.y - drag.start.y });
5450
+ const radius = Math.max(0, Math.min(c.maxRadius, drag.widget.drawnRadius + cornerRadiusChange(c, d)));
5451
+ this.doc = this.updateAnchors(drag.base, drag.refs, (a) => setAnchorCorner(a, radius));
5452
+ this.interaction = { type: "corner", ref: drag.widget.ref, radius };
5453
+ this.notify(true);
5454
+ return;
5455
+ }
5456
+ case "bend": {
5457
+ if (!drag.moved && Math.hypot(p.x - drag.start.x, p.y - drag.start.y) * this.viewport.zoom < 2) return;
5458
+ drag.moved = true;
5459
+ this.doc = this.bent(drag.base, drag.ref, drag.t, p);
5460
+ this.interaction = { type: "bend", ref: drag.ref };
5461
+ this.notify(true);
5462
+ return;
5463
+ }
5464
+ case "stroke": {
5465
+ const last = drag.points[drag.points.length - 1];
5466
+ if (Math.hypot(p.x - last.x, p.y - last.y) * this.viewport.zoom < 1.5) return;
5467
+ drag.points.push(p);
5468
+ this.interaction = { type: "stroke", tool: drag.tool, points: drag.points.slice(), width: drag.tool === "eraser" ? this.editing.eraserSize : 0 };
5469
+ this.notify();
5470
+ return;
5471
+ }
3383
5472
  case "pull": {
3384
5473
  const anchor = getAnchorWorld(drag.base, drag.ref);
3385
5474
  if (!anchor) return;
@@ -3389,10 +5478,10 @@ var VectorEditorController = (_class2 = class {
3389
5478
  return;
3390
5479
  }
3391
5480
  this.guides = null;
3392
- let pt = p;
3393
- if (input.shift) pt = constrain45(anchor, pt);
5481
+ let pt2 = p;
5482
+ if (input.shift) pt2 = constrain45(anchor, pt2);
3394
5483
  const side = drag.closing ? "in" : "out";
3395
- const pos = this.toLocal(drag.ref.nodeId, drag.closing ? { x: 2 * anchor.x - pt.x, y: 2 * anchor.y - pt.y } : pt, drag.base);
5484
+ const pos = this.toLocal(drag.ref.nodeId, drag.closing ? { x: 2 * anchor.x - pt2.x, y: 2 * anchor.y - pt2.y } : pt2, drag.base);
3396
5485
  this.doc = this.updateAnchors(drag.base, [drag.ref], (a) => {
3397
5486
  if (drag.outOnly || drag.altBreaks && input.alt) return moveAnchorHandle(a, side, pos, true);
3398
5487
  return moveAnchorHandle({ ...a, type: "symmetric" }, side, pos);
@@ -3402,7 +5491,7 @@ var VectorEditorController = (_class2 = class {
3402
5491
  }
3403
5492
  case "shape": {
3404
5493
  const node = this.getNode(drag.nodeId);
3405
- if (_optionalChain([node, 'optionalAccess', _90 => _90.type]) !== "path") return;
5494
+ if (_optionalChain([node, 'optionalAccess', _113 => _113.type]) !== "path") return;
3406
5495
  const leave = { at: drag.start, constrain: input.shift ? drag.tool === "line" ? "angle" : "square" : null, curves: false };
3407
5496
  let end = this.snapDrawn(p, leave, input, () => drag.targets ??= this.drawingTargets(null));
3408
5497
  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 +5522,22 @@ var VectorEditorController = (_class2 = class {
3433
5522
  }
3434
5523
  case "pencil": {
3435
5524
  const last = drag.points[drag.points.length - 1];
3436
- if (Math.hypot(p.x - last[0], p.y - last[1]) * this.viewport.zoom < 1.5) return;
3437
- drag.points.push([p.x, p.y]);
5525
+ let moved = false;
5526
+ for (const q of drag.lazy.push([p.x, p.y])) {
5527
+ if (Math.hypot(q[0] - last[0], q[1] - last[1]) * this.viewport.zoom < 1.5) continue;
5528
+ drag.points.push(q);
5529
+ moved = true;
5530
+ }
5531
+ const string = drag.lazy.string();
3438
5532
  const node = this.getNode(drag.nodeId);
3439
- if (_optionalChain([node, 'optionalAccess', _91 => _91.type]) !== "path") return;
5533
+ if (_optionalChain([node, 'optionalAccess', _114 => _114.type]) !== "path") return;
5534
+ if (!moved) {
5535
+ if (string && _optionalChain([this, 'access', _115 => _115.interaction, 'optionalAccess', _116 => _116.type]) === "draw") {
5536
+ this.interaction = { ...this.interaction, string: [{ x: string[0][0], y: string[0][1] }, { x: string[1][0], y: string[1][1] }] };
5537
+ this.notify();
5538
+ }
5539
+ return;
5540
+ }
3440
5541
  this.replaceNode(drag.nodeId, { ...node, subpaths: [{ anchors: drag.points.map(([x, y]) => ({ x, y })), closed: false }] });
3441
5542
  const xs = drag.points.map((q) => q[0]);
3442
5543
  const ys = drag.points.map((q) => q[1]);
@@ -3444,7 +5545,8 @@ var VectorEditorController = (_class2 = class {
3444
5545
  type: "draw",
3445
5546
  tool: "pencil",
3446
5547
  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) }
5548
+ bounds: { x: Math.min(...xs), y: Math.min(...ys), width: Math.max(...xs) - Math.min(...xs), height: Math.max(...ys) - Math.min(...ys) },
5549
+ ...string ? { string: [{ x: string[0][0], y: string[0][1] }, { x: string[1][0], y: string[1][1] }] } : {}
3448
5550
  };
3449
5551
  this.notify(true);
3450
5552
  return;
@@ -3466,8 +5568,8 @@ var VectorEditorController = (_class2 = class {
3466
5568
  }
3467
5569
  this.setGuides(res);
3468
5570
  return {
3469
- dx: _optionalChain([res, 'optionalAccess', _92 => _92.x]) ? dx + res.dx : axes.x ? this.gridValue(origin.x + dx) - origin.x : dx,
3470
- dy: _optionalChain([res, 'optionalAccess', _93 => _93.y]) ? dy + res.dy : axes.y ? this.gridValue(origin.y + dy) - origin.y : dy
5571
+ dx: _optionalChain([res, 'optionalAccess', _117 => _117.x]) ? dx + res.dx : axes.x ? this.gridValue(origin.x + dx) - origin.x : dx,
5572
+ dy: _optionalChain([res, 'optionalAccess', _118 => _118.y]) ? dy + res.dy : axes.y ? this.gridValue(origin.y + dy) - origin.y : dy
3471
5573
  };
3472
5574
  }
3473
5575
  /** The anchor a point drag snaps by, where it started: the grabbed one, else the moving one nearest the press. */
@@ -3492,12 +5594,12 @@ var VectorEditorController = (_class2 = class {
3492
5594
  const guides = { lines: [], labels: [] };
3493
5595
  for (const axis of ["x", "y"]) {
3494
5596
  const d = end[axis] - start[axis];
3495
- const len = Math.abs(d) > 1e-9 ? targets.matchSize(Math.abs(d) * k, tol) : null;
3496
- if (len === null || len <= 0) continue;
3497
- out[axis] = start[axis] + Math.sign(d) * len / k;
5597
+ const len2 = Math.abs(d) > 1e-9 ? targets.matchSize(Math.abs(d) * k, tol) : null;
5598
+ if (len2 === null || len2 <= 0) continue;
5599
+ out[axis] = start[axis] + Math.sign(d) * len2 / k;
3498
5600
  const from = { ...out, [axis]: fromCentre ? 2 * start[axis] - out[axis] : start[axis] };
3499
5601
  guides.lines.push({ kind: "size", x1: from.x, y1: from.y, x2: out.x, y2: out.y });
3500
- guides.labels.push({ kind: "size", x: (from.x + out.x) / 2, y: (from.y + out.y) / 2, text: `${axis === "x" ? "W" : "H"} ${Math.round(len * 100) / 100}` });
5602
+ guides.labels.push({ kind: "size", x: (from.x + out.x) / 2, y: (from.y + out.y) / 2, text: `${axis === "x" ? "W" : "H"} ${Math.round(len2 * 100) / 100}` });
3501
5603
  }
3502
5604
  this.setGuides(guides);
3503
5605
  return out;
@@ -3536,6 +5638,32 @@ var VectorEditorController = (_class2 = class {
3536
5638
  return this.commit(drag.before.doc !== drag.base ? "Add Point" : "Move Points", drag.before);
3537
5639
  case "handle":
3538
5640
  return this.commit("Move Handle", drag.before);
5641
+ case "bend":
5642
+ if (!drag.moved) {
5643
+ this.notify();
5644
+ return;
5645
+ }
5646
+ return this.commit("Bend Segment", drag.before);
5647
+ case "corner": {
5648
+ if (drag.moved) return this.commit("Corner Radius", drag.before);
5649
+ if (drag.alt) {
5650
+ const kind = nextVectorCornerKind(drag.widget.kind);
5651
+ this.doc = this.updateAnchors(drag.base, drag.refs, (a) => setAnchorCorner(a, _nullishCoalesce(_optionalChain([a, 'access', _119 => _119.corner, 'optionalAccess', _120 => _120.radius]), () => ( 0)), kind));
5652
+ return this.commit("Corner Kind", drag.before);
5653
+ }
5654
+ const ref = drag.widget.ref;
5655
+ const has = this.anchors.some((a) => sameRef(a, ref));
5656
+ this.editingId = ref.nodeId;
5657
+ this.anchors = drag.shift ? has ? this.anchors.filter((a) => !sameRef(a, ref)) : [...this.anchors, ref] : [ref];
5658
+ this.notify();
5659
+ return;
5660
+ }
5661
+ case "stroke": {
5662
+ this.notify();
5663
+ if (drag.tool === "knife") this.knife(drag.points);
5664
+ else this.erase(drag.points);
5665
+ return;
5666
+ }
3539
5667
  case "pull":
3540
5668
  if (drag.closing) this.pen = null;
3541
5669
  return this.commit(drag.label, drag.before);
@@ -3555,12 +5683,19 @@ var VectorEditorController = (_class2 = class {
3555
5683
  }
3556
5684
  case "pencil": {
3557
5685
  const node = this.getNode(drag.nodeId);
3558
- if (_optionalChain([node, 'optionalAccess', _94 => _94.type]) !== "path" || drag.points.length < 2) {
5686
+ if (_optionalChain([node, 'optionalAccess', _121 => _121.type]) !== "path" || drag.points.length < 2) {
3559
5687
  this.doc = drag.before.doc;
3560
5688
  this.notify(true);
3561
5689
  return;
3562
5690
  }
3563
- const fitted = _chunkUG5DVXJKcjs.fitCubics.call(void 0, drag.points, 1.5 / this.viewport.zoom, { cornerAngleDeg: 60 });
5691
+ for (const q of drag.lazy.finish()) drag.points.push(q);
5692
+ const shape = this.editing.recognizeShapes ? recognizeInkShape(drag.points, { minSize: 6 / this.viewport.zoom }) : null;
5693
+ if (shape) {
5694
+ this.replaceNode(drag.nodeId, { ...node, name: inkShapeLabel(shape), subpaths: [inkShapeSubpath(shape)] });
5695
+ this.selection = [drag.nodeId];
5696
+ return this.commit(`Draw ${inkShapeLabel(shape)}`, drag.before);
5697
+ }
5698
+ const fitted = _chunkBGNPT5PEcjs.fitCubics.call(void 0, drag.points, 1.5 / this.viewport.zoom, { cornerAngleDeg: 60 });
3564
5699
  const anchors = [];
3565
5700
  for (const c of fitted) {
3566
5701
  if (!anchors.length) anchors.push({ x: c.p0[0], y: c.p0[1] });
@@ -3622,7 +5757,7 @@ var VectorEditorController = (_class2 = class {
3622
5757
  this.convertAnchors(a.in || a.out ? "corner" : "smooth", [anchor]);
3623
5758
  return;
3624
5759
  }
3625
- const id = _nullishCoalesce(_optionalChain([this, 'access', _95 => _95.hover, 'optionalAccess', _96 => _96.segment, 'optionalAccess', _97 => _97.nodeId]), () => ( this.editingId));
5760
+ const id = _nullishCoalesce(_optionalChain([this, 'access', _122 => _122.hover, 'optionalAccess', _123 => _123.segment, 'optionalAccess', _124 => _124.nodeId]), () => ( this.editingId));
3626
5761
  if (id) {
3627
5762
  const node = this.getNode(id);
3628
5763
  const seg = node ? nearestSegment(node, this.toLocal(id, p)) : null;
@@ -3666,6 +5801,8 @@ var VectorEditorController = (_class2 = class {
3666
5801
  case "g":
3667
5802
  input.shift ? this.ungroup() : this.group();
3668
5803
  return true;
5804
+ case "j":
5805
+ return input.alt ? this.averageAnchors("both").ok : this.joinPaths().ok;
3669
5806
  case "]":
3670
5807
  this.arrange(input.alt ? "front" : "forward");
3671
5808
  return true;
@@ -3700,7 +5837,7 @@ var VectorEditorController = (_class2 = class {
3700
5837
  this.finishPath();
3701
5838
  return true;
3702
5839
  }
3703
- if (this.tool === "select" && this.selection.length === 1 && _optionalChain([this, 'access', _98 => _98.getNode, 'call', _99 => _99(this.selection[0]), 'optionalAccess', _100 => _100.type]) === "path") {
5840
+ if (this.tool === "select" && this.selection.length === 1 && _optionalChain([this, 'access', _125 => _125.getNode, 'call', _126 => _126(this.selection[0]), 'optionalAccess', _127 => _127.type]) === "path") {
3704
5841
  this.tool = "direct";
3705
5842
  this.editingId = this.selection[0];
3706
5843
  this.notify();
@@ -3745,13 +5882,29 @@ var VectorEditorController = (_class2 = class {
3745
5882
  this.notify();
3746
5883
  }
3747
5884
  }
3748
- }, _class2);
5885
+ }, _class3);
5886
+ function inkShapeLabel(shape) {
5887
+ const name = vectorInkShapeName(shape);
5888
+ return name[0].toUpperCase() + name.slice(1);
5889
+ }
5890
+ function inkShapeSubpath(shape) {
5891
+ switch (shape.kind) {
5892
+ case "line":
5893
+ return lineSubpath({ x: shape.a[0], y: shape.a[1] }, { x: shape.b[0], y: shape.b[1] });
5894
+ case "rect":
5895
+ return rectSubpath(shape);
5896
+ case "ellipse":
5897
+ return ellipseSubpath(shape);
5898
+ case "polygon":
5899
+ return { anchors: shape.points.map(([x, y]) => ({ x, y })), closed: true };
5900
+ }
5901
+ }
3749
5902
  function constrain45(origin, p) {
3750
5903
  const dx = p.x - origin.x;
3751
5904
  const dy = p.y - origin.y;
3752
- const len = Math.hypot(dx, dy);
5905
+ const len2 = Math.hypot(dx, dy);
3753
5906
  const a = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);
3754
- return { x: origin.x + Math.cos(a) * len, y: origin.y + Math.sin(a) * len };
5907
+ return { x: origin.x + Math.cos(a) * len2, y: origin.y + Math.sin(a) * len2 };
3755
5908
  }
3756
5909
  function scaleFor(handle, b, p, mods = {}) {
3757
5910
  const west = handle.includes("w");
@@ -3788,17 +5941,19 @@ function renderNode(node, defs, idPrefix) {
3788
5941
  const stroke = vectorPaintToSvg(node.stroke, `${idPrefix}stroke-${node.id}`);
3789
5942
  if (fill.gradient) defs.push(fill.gradient);
3790
5943
  if (stroke.gradient) defs.push(stroke.gradient);
5944
+ const strokeParts = stroke.value !== "none" ? vectorStrokeSvgParts(node, `${idPrefix}clip-${node.id}`) : void 0;
3791
5945
  return {
3792
5946
  kind: "path",
3793
5947
  id: node.id,
3794
5948
  ...node.transform ? { transform: fmtMatrix(node.transform) } : {},
5949
+ ...strokeParts ? { strokeParts } : {},
3795
5950
  d: vectorPathData(node),
3796
5951
  fill: fill.value,
3797
5952
  stroke: stroke.value,
3798
5953
  strokeWidth: _nullishCoalesce(node.strokeWidth, () => ( 1)),
3799
5954
  ...node.strokeLinecap ? { strokeLinecap: node.strokeLinecap } : {},
3800
5955
  ...node.strokeLinejoin ? { strokeLinejoin: node.strokeLinejoin } : {},
3801
- ..._optionalChain([node, 'access', _101 => _101.strokeDasharray, 'optionalAccess', _102 => _102.length]) ? { strokeDasharray: node.strokeDasharray.join(" ") } : {},
5956
+ ..._optionalChain([node, 'access', _128 => _128.strokeDasharray, 'optionalAccess', _129 => _129.length]) ? { strokeDasharray: node.strokeDasharray.join(" ") } : {},
3802
5957
  fillRule: _nullishCoalesce(node.fillRule, () => ( "nonzero")),
3803
5958
  opacity: _nullishCoalesce(node.opacity, () => ( 1))
3804
5959
  };
@@ -3807,9 +5962,9 @@ function getVectorSceneRenderData(state, options = {}) {
3807
5962
  const defs = [];
3808
5963
  const prefix = _nullishCoalesce(options.idPrefix, () => ( "ve-"));
3809
5964
  const nodes = state.document.nodes.map((n) => renderNode(n, defs, prefix)).filter((n) => !!n);
3810
- const v = state.viewport;
5965
+ const v2 = state.viewport;
3811
5966
  return {
3812
- transform: `translate(${fmt(v.x)} ${fmt(v.y)}) scale(${v.zoom})`,
5967
+ transform: `translate(${fmt(v2.x)} ${fmt(v2.y)}) scale(${v2.zoom})`,
3813
5968
  width: state.document.width,
3814
5969
  height: state.document.height,
3815
5970
  ...state.document.background ? { background: state.document.background } : {},
@@ -3819,10 +5974,10 @@ function getVectorSceneRenderData(state, options = {}) {
3819
5974
  }
3820
5975
  var toScreen = (state, p) => ({ x: p.x * state.viewport.zoom + state.viewport.x, y: p.y * state.viewport.zoom + state.viewport.y });
3821
5976
  function screenOutline(state, node) {
3822
- const v = state.viewport;
3823
- const view = [v.zoom, 0, 0, v.zoom, v.x, v.y];
5977
+ const v2 = state.viewport;
5978
+ const view = [v2.zoom, 0, 0, v2.zoom, v2.x, v2.y];
3824
5979
  const parent = _nullishCoalesce(vectorParentMatrix(state.document.nodes, node.id), () => ( IDENTITY_MATRIX));
3825
- return collectWithMatrix(node, parent).map(({ path, parent: m }) => _chunkUG5DVXJKcjs.bezierPathToSvg.call(void 0, _chunkUG5DVXJKcjs.transformBezierPath.call(void 0, vectorPathWorldBezier(path, m), view), { precision: 2 })).join(" ");
5980
+ 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
5981
  }
3827
5982
  function collectWithMatrix(node, parent) {
3828
5983
  if (node.type === "path") return [{ path: node, parent }];
@@ -3847,9 +6002,9 @@ function getVectorOverlayRenderData(state) {
3847
6002
  };
3848
6003
  for (const id of state.selection) outline(id, true);
3849
6004
  if (state.editingId) outline(state.editingId, true);
3850
- if (_optionalChain([state, 'access', _103 => _103.hover, 'optionalAccess', _104 => _104.nodeId]) && !state.interaction) outline(state.hover.nodeId, false);
6005
+ if (_optionalChain([state, 'access', _130 => _130.hover, 'optionalAccess', _131 => _131.nodeId]) && !state.interaction) outline(state.hover.nodeId, false);
3851
6006
  let selectionBox = null;
3852
- if (state.tool === "select" && state.selectionFrame && _optionalChain([state, 'access', _105 => _105.interaction, 'optionalAccess', _106 => _106.type]) !== "marquee") {
6007
+ if (state.tool === "select" && state.selectionFrame && _optionalChain([state, 'access', _132 => _132.interaction, 'optionalAccess', _133 => _133.type]) !== "marquee") {
3853
6008
  const g = getVectorSelectionBoxGeometry(state.selectionFrame, state.viewport);
3854
6009
  selectionBox = { ...g.box, angle: g.angle, transform: g.transform, corners: g.corners, handles: g.handles };
3855
6010
  }
@@ -3859,9 +6014,9 @@ function getVectorOverlayRenderData(state) {
3859
6014
  const a = getAnchorWorld(doc, ref);
3860
6015
  const s = toScreen(state, a);
3861
6016
  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', _107 => _107.hover, 'optionalAccess', _108 => _108.anchor]), ref), closes };
6017
+ return { ref, x: s.x, y: s.y, type: _nullishCoalesce(a.type, () => ( inferAnchorType(a))), selected: selected(ref), hovered: sameAnchor(_optionalChain([state, 'access', _134 => _134.hover, 'optionalAccess', _135 => _135.anchor]), ref), closes };
3863
6018
  });
3864
- if (state.tool === "pen" && !state.pen && _optionalChain([state, 'access', _109 => _109.hover, 'optionalAccess', _110 => _110.anchor])) {
6019
+ if (state.tool === "pen" && !state.pen && _optionalChain([state, 'access', _136 => _136.hover, 'optionalAccess', _137 => _137.anchor])) {
3865
6020
  const a = getAnchorWorld(doc, state.hover.anchor);
3866
6021
  if (a) {
3867
6022
  const s = toScreen(state, a);
@@ -3870,15 +6025,15 @@ function getVectorOverlayRenderData(state) {
3870
6025
  }
3871
6026
  const handles = getVisibleHandles(state).flatMap((ref) => {
3872
6027
  const a = getAnchorWorld(doc, ref);
3873
- const h = _optionalChain([a, 'optionalAccess', _111 => _111[ref.handle]]);
6028
+ const h = _optionalChain([a, 'optionalAccess', _138 => _138[ref.handle]]);
3874
6029
  if (!a || !h) return [];
3875
6030
  const s = toScreen(state, h);
3876
6031
  const sa = toScreen(state, a);
3877
- const hovered = !!_optionalChain([state, 'access', _112 => _112.hover, 'optionalAccess', _113 => _113.handle]) && sameAnchor(state.hover.handle, ref) && state.hover.handle.handle === ref.handle;
6032
+ const hovered = !!_optionalChain([state, 'access', _139 => _139.hover, 'optionalAccess', _140 => _140.handle]) && sameAnchor(state.hover.handle, ref) && state.hover.handle.handle === ref.handle;
3878
6033
  return [{ ref, x: s.x, y: s.y, anchorX: sa.x, anchorY: sa.y, hovered }];
3879
6034
  });
3880
6035
  let marquee = null;
3881
- if (_optionalChain([state, 'access', _114 => _114.interaction, 'optionalAccess', _115 => _115.type]) === "marquee") {
6036
+ if (_optionalChain([state, 'access', _141 => _141.interaction, 'optionalAccess', _142 => _142.type]) === "marquee") {
3882
6037
  const b = state.interaction.bounds;
3883
6038
  const tl = toScreen(state, b);
3884
6039
  marquee = { x: tl.x, y: tl.y, width: b.width * z, height: b.height * z };
@@ -3886,7 +6041,7 @@ function getVectorOverlayRenderData(state) {
3886
6041
  let penPreview = null;
3887
6042
  if (state.tool === "pen" && state.pen && state.pointer && !state.interaction) {
3888
6043
  const node = findVectorNode(doc.nodes, state.pen.nodeId);
3889
- const sp = _optionalChain([node, 'optionalAccess', _116 => _116.type]) === "path" ? node.subpaths[state.pen.subpath] : void 0;
6044
+ const sp = _optionalChain([node, 'optionalAccess', _143 => _143.type]) === "path" ? node.subpaths[state.pen.subpath] : void 0;
3890
6045
  const last = sp ? getAnchorWorld(doc, { nodeId: state.pen.nodeId, subpath: state.pen.subpath, index: sp.anchors.length - 1 }) : void 0;
3891
6046
  if (last) {
3892
6047
  const a = toScreen(state, last);
@@ -3899,9 +6054,9 @@ function getVectorOverlayRenderData(state) {
3899
6054
  const artboard = { x: origin.x, y: origin.y, width: doc.width * z, height: doc.height * z };
3900
6055
  let status = null;
3901
6056
  const it = state.interaction;
3902
- if (_optionalChain([it, 'optionalAccess', _117 => _117.type]) === "rotate") status = `${Math.round(it.angle)}\xB0`;
3903
- else if (_optionalChain([it, 'optionalAccess', _118 => _118.type]) === "move" || _optionalChain([it, 'optionalAccess', _119 => _119.type]) === "anchors") status = `\u0394 ${fmt(it.dx)}, ${fmt(it.dy)}`;
3904
- else if (_optionalChain([it, 'optionalAccess', _120 => _120.type]) === "scale" || _optionalChain([it, 'optionalAccess', _121 => _121.type]) === "draw") {
6057
+ if (_optionalChain([it, 'optionalAccess', _144 => _144.type]) === "rotate") status = `${Math.round(it.angle)}\xB0`;
6058
+ else if (_optionalChain([it, 'optionalAccess', _145 => _145.type]) === "move" || _optionalChain([it, 'optionalAccess', _146 => _146.type]) === "anchors") status = `\u0394 ${fmt(it.dx)}, ${fmt(it.dy)}`;
6059
+ else if (_optionalChain([it, 'optionalAccess', _147 => _147.type]) === "scale" || _optionalChain([it, 'optionalAccess', _148 => _148.type]) === "draw") {
3905
6060
  const b = it.type === "draw" ? findVectorNode(doc.nodes, it.nodeId) ? vectorNodeBounds(findVectorNode(doc.nodes, it.nodeId)) : null : state.selectionFrame;
3906
6061
  if (b) status = `W ${fmt(b.width)} \xD7 H ${fmt(b.height)}`;
3907
6062
  }
@@ -3917,7 +6072,26 @@ function getVectorOverlayRenderData(state) {
3917
6072
  guides.labels.push({ kind: l.kind, x: fmt(p.x), y: fmt(p.y), text: l.text });
3918
6073
  }
3919
6074
  }
3920
- return { outlines, selectionBox, anchors, handles, marquee, penPreview, artboard, guides, status };
6075
+ let toolStroke = null;
6076
+ if (_optionalChain([it, 'optionalAccess', _149 => _149.type]) === "stroke") {
6077
+ const pts = it.points.map((q) => toScreen(state, q));
6078
+ 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)}` : "");
6079
+ toolStroke = { tool: it.tool, d, width: fmt(it.width * z) };
6080
+ }
6081
+ let pencilString = null;
6082
+ if (_optionalChain([it, 'optionalAccess', _150 => _150.type]) === "draw" && it.string) {
6083
+ const a = toScreen(state, it.string[0]);
6084
+ const b = toScreen(state, it.string[1]);
6085
+ pencilString = { x1: fmt(a.x), y1: fmt(a.y), x2: fmt(b.x), y2: fmt(b.y) };
6086
+ }
6087
+ const dragged = _optionalChain([it, 'optionalAccess', _151 => _151.type]) === "corner" ? it.ref : null;
6088
+ const cornerWidgets = getVectorCornerWidgets(state).map((w) => {
6089
+ const s = toScreen(state, w);
6090
+ return { ref: w.ref, x: fmt(s.x), y: fmt(s.y), radius: w.radius, kind: w.kind, hovered: sameAnchor(_optionalChain([state, 'access', _152 => _152.hover, 'optionalAccess', _153 => _153.corner]), w.ref), active: !!dragged };
6091
+ });
6092
+ if (_optionalChain([it, 'optionalAccess', _154 => _154.type]) === "corner") status = `R ${fmt(it.radius)}`;
6093
+ const cutPoint = state.tool === "scissors" && _optionalChain([state, 'access', _155 => _155.hover, 'optionalAccess', _156 => _156.segment]) && !it ? (({ x, y }) => ({ x: fmt(x), y: fmt(y) }))(toScreen(state, state.hover.segment.point)) : null;
6094
+ return { outlines, selectionBox, anchors, handles, marquee, penPreview, artboard, guides, status, toolStroke, pencilString, cutPoint, cornerWidgets };
3921
6095
  }
3922
6096
  var VECTOR_OVERLAY_THEME = {
3923
6097
  accent: "#0ea5e9",
@@ -3925,9 +6099,13 @@ var VECTOR_OVERLAY_THEME = {
3925
6099
  guide: "#ff3dfc",
3926
6100
  handleFill: "#ffffff",
3927
6101
  marqueeFill: "rgba(14, 165, 233, 0.08)",
6102
+ /** The eraser's swath while it is dragged. */
6103
+ eraserFill: "rgba(239, 68, 68, 0.25)",
3928
6104
  transformHandleSize: 8,
3929
6105
  anchorSize: 7,
3930
- handleRadius: 3.5
6106
+ handleRadius: 3.5,
6107
+ /** Radius of a live corner widget (a ring with a dot). */
6108
+ cornerWidgetRadius: 4
3931
6109
  };
3932
6110
  function toVectorModifiers(event) {
3933
6111
  return { shift: !!event.shiftKey, alt: !!event.altKey, mod: !!(event.metaKey || event.ctrlKey) };
@@ -3943,7 +6121,7 @@ function isVectorTextTarget(target) {
3943
6121
  if (!el || typeof el !== "object") return false;
3944
6122
  const tag = (_nullishCoalesce(el.tagName, () => ( ""))).toUpperCase();
3945
6123
  if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
3946
- return !!el.isContentEditable || _optionalChain([el, 'access', _122 => _122.getAttribute, 'optionalCall', _123 => _123("contenteditable")]) === "true";
6124
+ return !!el.isContentEditable || _optionalChain([el, 'access', _157 => _157.getAttribute, 'optionalCall', _158 => _158("contenteditable")]) === "true";
3947
6125
  }
3948
6126
  function getVectorSelectionStyle(state) {
3949
6127
  const paths = [];
@@ -3951,18 +6129,68 @@ function getVectorSelectionStyle(state) {
3951
6129
  const node = findVectorNode(state.document.nodes, id);
3952
6130
  if (node) paths.push(...collect(node));
3953
6131
  }
6132
+ const anchorMode = state.tool === "direct" && !!_optionalChain([state, 'access', _159 => _159.selectedAnchors, 'optionalAccess', _160 => _160.length]);
6133
+ if (anchorMode) {
6134
+ for (const r of state.selectedAnchors) {
6135
+ if (paths.some((p) => p.id === r.nodeId)) continue;
6136
+ const node = findVectorNode(state.document.nodes, r.nodeId);
6137
+ if (_optionalChain([node, 'optionalAccess', _161 => _161.type]) === "path") paths.push(node);
6138
+ }
6139
+ }
3954
6140
  if (!paths.length) return null;
3955
6141
  const first = paths[0];
3956
6142
  const same = (get) => paths.every((p) => JSON.stringify(_nullishCoalesce(get(p), () => ( null))) === JSON.stringify(_nullishCoalesce(get(first), () => ( null))));
6143
+ const corners2 = [];
6144
+ for (const p of paths) {
6145
+ for (const c of vectorCorners(p.subpaths)) {
6146
+ if (anchorMode && !state.selectedAnchors.some((r) => r.nodeId === p.id && r.subpath === c.subpath && r.index === c.index)) continue;
6147
+ const corner = p.subpaths[c.subpath].anchors[c.index].corner;
6148
+ corners2.push({ radius: _nullishCoalesce(_optionalChain([corner, 'optionalAccess', _162 => _162.radius]), () => ( 0)), kind: _nullishCoalesce(_optionalChain([corner, 'optionalAccess', _163 => _163.kind]), () => ( "round")) });
6149
+ }
6150
+ }
6151
+ const c0 = _nullishCoalesce(corners2[0], () => ( { radius: 0, kind: "round" }));
3957
6152
  return {
3958
6153
  fill: _nullishCoalesce(first.fill, () => ( null)),
3959
6154
  stroke: _nullishCoalesce(first.stroke, () => ( null)),
3960
6155
  strokeWidth: _nullishCoalesce(first.strokeWidth, () => ( 1)),
3961
6156
  opacity: _nullishCoalesce(first.opacity, () => ( 1)),
3962
- mixed: { fill: !same((p) => p.fill), stroke: !same((p) => p.stroke), strokeWidth: !same((p) => _nullishCoalesce(p.strokeWidth, () => ( 1))), opacity: !same((p) => _nullishCoalesce(p.opacity, () => ( 1))) },
6157
+ strokeAlign: _nullishCoalesce(first.strokeAlign, () => ( "center")),
6158
+ startArrow: _nullishCoalesce(first.startArrow, () => ( null)),
6159
+ endArrow: _nullishCoalesce(first.endArrow, () => ( null)),
6160
+ arrowAlign: _nullishCoalesce(first.arrowAlign, () => ( "extend")),
6161
+ startArrowScale: _nullishCoalesce(first.startArrowScale, () => ( 1)),
6162
+ endArrowScale: _nullishCoalesce(first.endArrowScale, () => ( 1)),
6163
+ cornerRadius: c0.radius,
6164
+ cornerKind: c0.kind,
6165
+ corners: corners2.length,
6166
+ hasOpenPath: paths.some((p) => p.subpaths.some((sp) => !sp.closed && sp.anchors.length > 1)),
6167
+ mixed: {
6168
+ fill: !same((p) => p.fill),
6169
+ stroke: !same((p) => p.stroke),
6170
+ strokeWidth: !same((p) => _nullishCoalesce(p.strokeWidth, () => ( 1))),
6171
+ opacity: !same((p) => _nullishCoalesce(p.opacity, () => ( 1))),
6172
+ strokeAlign: !same((p) => _nullishCoalesce(p.strokeAlign, () => ( "center"))),
6173
+ startArrow: !same((p) => _nullishCoalesce(p.startArrow, () => ( null))),
6174
+ endArrow: !same((p) => _nullishCoalesce(p.endArrow, () => ( null))),
6175
+ arrowAlign: !same((p) => _nullishCoalesce(p.arrowAlign, () => ( "extend"))),
6176
+ startArrowScale: !same((p) => _nullishCoalesce(p.startArrowScale, () => ( 1))),
6177
+ endArrowScale: !same((p) => _nullishCoalesce(p.endArrowScale, () => ( 1))),
6178
+ cornerRadius: corners2.some((c) => c.radius !== c0.radius),
6179
+ cornerKind: corners2.some((c) => c.kind !== c0.kind)
6180
+ },
3963
6181
  pathIds: paths.map((p) => p.id)
3964
6182
  };
3965
6183
  }
6184
+ var VECTOR_STROKE_ALIGN_LABELS = { center: "Center", inside: "Inside", outside: "Outside" };
6185
+ var VECTOR_CORNER_KIND_LABELS = { round: "Round", inverted: "Inverted", chamfer: "Chamfer" };
6186
+ var VECTOR_ARROWHEAD_LABELS = {
6187
+ triangle: "Triangle",
6188
+ arrow: "Arrow",
6189
+ open: "Open arrow",
6190
+ circle: "Circle",
6191
+ square: "Square",
6192
+ bar: "Bar"
6193
+ };
3966
6194
  function vectorPaintToHex(paint) {
3967
6195
  if (typeof paint !== "string") return null;
3968
6196
  const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(paint.trim());
@@ -3970,12 +6198,40 @@ function vectorPaintToHex(paint) {
3970
6198
  const hex = m[1].length === 3 ? m[1].replace(/./g, (c) => c + c) : m[1];
3971
6199
  return `#${hex.toLowerCase()}`;
3972
6200
  }
6201
+ var VECTOR_PATH_ACTIONS = [
6202
+ { id: "join", label: "\u22C8", title: "Join", shortcut: "J", enabled: (s, p) => s + p > 0, run: (c) => c.joinPaths() },
6203
+ { id: "average", label: "\u22B9", title: "Average Points", shortcut: "Alt+J", enabled: (_s, p) => p > 1, run: (c) => c.averageAnchors("both") },
6204
+ { id: "cut", label: "\u2702\xB7", title: "Cut at Points", enabled: (_s, p) => p > 0, run: (c) => c.cutAtAnchors() },
6205
+ { id: "clean", label: "\u2301", title: "Remove Redundant Points", enabled: (s, p) => s + p > 0, run: (c) => c.removeRedundantPoints() },
6206
+ { id: "smooth", label: "\u223F", title: "Smooth", enabled: (s, p) => s + p > 0, run: (c) => c.smoothPaths() },
6207
+ { id: "outline", label: "\u22A1", title: "Outline Stroke", enabled: (s) => s > 0, run: (c) => c.outlineStroke() }
6208
+ ];
6209
+ var VECTOR_EDIT_TOGGLES = [
6210
+ { key: "keepShapeOnDelete", label: "Keep", title: "Keep shape when deleting points" },
6211
+ { key: "curvatureMoves", label: "Curv", title: "Curvature point moves (Alt flips while dragging)" },
6212
+ { key: "recognizeShapes", label: "Shapes", title: "Turn pencil scribbles into shapes" }
6213
+ ];
3973
6214
 
3974
6215
  // src/vector/commands.ts
3975
6216
  var fail = (message) => {
3976
6217
  throw new Error(message);
3977
6218
  };
3978
6219
  var IDS = { type: "array", items: { type: "string" }, description: "Node ids (default: the selection)." };
6220
+ var ANCHORS = {
6221
+ type: "array",
6222
+ items: {
6223
+ type: "object",
6224
+ properties: { nodeId: { type: "string" }, subpath: { type: "integer", minimum: 0 }, index: { type: "integer", minimum: 0 } },
6225
+ required: ["nodeId", "subpath", "index"]
6226
+ },
6227
+ description: "Anchors as { nodeId, subpath, index } (default: the selected points)."
6228
+ };
6229
+ var POINTS = {
6230
+ type: "array",
6231
+ items: { type: "array", items: { type: "number" }, minItems: 2, maxItems: 2 },
6232
+ minItems: 1,
6233
+ description: "Polyline [[x, y], \u2026] in document units."
6234
+ };
3979
6235
  var PAINT = {
3980
6236
  description: 'A CSS colour, `null` for none, or a gradient `{ type: "linear", angle, stops: [{ offset, color }] }`.'
3981
6237
  };
@@ -3986,7 +6242,13 @@ var STYLE_PROPS = {
3986
6242
  strokeLinecap: { type: "string", enum: ["butt", "round", "square"] },
3987
6243
  strokeLinejoin: { type: "string", enum: ["miter", "round", "bevel"] },
3988
6244
  strokeDasharray: { type: "array", items: { type: "number" } },
3989
- fillRule: { type: "string", enum: ["nonzero", "evenodd"] }
6245
+ fillRule: { type: "string", enum: ["nonzero", "evenodd"] },
6246
+ strokeAlign: { type: "string", enum: ["center", "inside", "outside"], description: "Where the stroke sits on the outline (closed shapes only; open paths stroke centred)." },
6247
+ startArrow: { type: ["string", "null"], enum: [...VECTOR_ARROWHEADS, null], description: "Head at the start of an open path, or null for none." },
6248
+ endArrow: { type: ["string", "null"], enum: [...VECTOR_ARROWHEADS, null], description: "Head at the end of an open path, or null for none." },
6249
+ arrowAlign: { type: "string", enum: ["extend", "tip"], description: "`extend`: the tip sits past the end point; `tip`: on it, the stroke shortened under the head." },
6250
+ startArrowScale: { type: "number", exclusiveMinimum: 0, description: "Start head size as a multiple of the stroke width (default 1)." },
6251
+ endArrowScale: { type: "number", exclusiveMinimum: 0, description: "End head size as a multiple of the stroke width (default 1)." }
3990
6252
  };
3991
6253
  var pickStyle = (params) => {
3992
6254
  const out = {};
@@ -4002,12 +6264,20 @@ function createVectorCommands(controller, options = {}) {
4002
6264
  const base = { group, ...options.scope ? { scope: options.scope } : {} };
4003
6265
  const hasSelection = () => controller.getState().selection.length ? true : "Nothing is selected.";
4004
6266
  const ids = (params) => {
4005
- const list2 = _nullishCoalesce(_optionalChain([params, 'optionalAccess', _124 => _124.ids]), () => ( controller.getState().selection));
6267
+ const list2 = _nullishCoalesce(_optionalChain([params, 'optionalAccess', _164 => _164.ids]), () => ( controller.getState().selection));
4006
6268
  for (const id of list2) if (!controller.getNode(id)) fail(`There is no node "${id}".`);
4007
6269
  if (!list2.length) fail("Nothing is selected; pass ids.");
4008
6270
  return list2;
4009
6271
  };
4010
- const enabledFor = (_, params) => _optionalChain([params, 'optionalAccess', _125 => _125.ids, 'optionalAccess', _126 => _126.length]) ? true : hasSelection();
6272
+ const enabledFor = (_, params) => _optionalChain([params, 'optionalAccess', _165 => _165.ids, 'optionalAccess', _166 => _166.length]) ? true : hasSelection();
6273
+ const anchorsOf = (refs) => {
6274
+ if (!refs) return [...controller.getState().selectedAnchors];
6275
+ for (const r of refs) {
6276
+ const node = controller.getNode(r.nodeId);
6277
+ if (_optionalChain([node, 'optionalAccess', _167 => _167.type]) !== "path" || !_optionalChain([node, 'access', _168 => _168.subpaths, 'access', _169 => _169[r.subpath], 'optionalAccess', _170 => _170.anchors, 'access', _171 => _171[r.index]])) fail(`There is no anchor ${r.index} on subpath ${r.subpath} of "${r.nodeId}".`);
6278
+ }
6279
+ return refs;
6280
+ };
4011
6281
  const list = () => {
4012
6282
  const state = controller.getState();
4013
6283
  const out = [];
@@ -4071,7 +6341,8 @@ function createVectorCommands(controller, options = {}) {
4071
6341
  y: { type: "number" },
4072
6342
  width: { type: "number", description: "For a line: the x offset of its end." },
4073
6343
  height: { type: "number", description: "For a line: the y offset of its end." },
4074
- cornerRadius: { type: "number", minimum: 0 },
6344
+ cornerRadius: { type: "number", minimum: 0, description: "Live corner radius of a rect, polygon or star." },
6345
+ cornerKind: { type: "string", enum: VECTOR_CORNER_KINDS },
4075
6346
  sides: { type: "integer", minimum: 3 },
4076
6347
  points: { type: "integer", minimum: 3 },
4077
6348
  innerRatio: { type: "number", minimum: 0, maximum: 1 },
@@ -4094,7 +6365,7 @@ function createVectorCommands(controller, options = {}) {
4094
6365
  run: (_ctx, params) => {
4095
6366
  const id = controller.addPath(params.d, { ...pickStyle(params), ...params.name ? { name: params.name } : {} });
4096
6367
  const node = controller.getNode(id);
4097
- if (_optionalChain([node, 'optionalAccess', _127 => _127.type]) === "path" && !node.subpaths.length) {
6368
+ if (_optionalChain([node, 'optionalAccess', _172 => _172.type]) === "path" && !node.subpaths.length) {
4098
6369
  controller.undo();
4099
6370
  fail("The path data has no points.");
4100
6371
  }
@@ -4117,6 +6388,38 @@ function createVectorCommands(controller, options = {}) {
4117
6388
  return { ok: true };
4118
6389
  }
4119
6390
  },
6391
+ {
6392
+ ...base,
6393
+ id: `${prefix}setCorners`,
6394
+ label: "Corner Radius\u2026",
6395
+ keywords: ["round", "rounded", "corners", "radius", "fillet", "chamfer", "live corners"],
6396
+ description: "Sets the live corner radius (and optionally `kind`: round, inverted or chamfer) of `anchors`, else the selected points (direct tool), else every corner of `ids` / the selection. Radius 0 makes corners sharp; radii shrink to fit short sides. The anchors stay editable.",
6397
+ params: { type: "object", properties: { radius: { type: "number", minimum: 0 }, kind: { type: "string", enum: VECTOR_CORNER_KINDS }, ids: IDS, anchors: ANCHORS } },
6398
+ undoable: true,
6399
+ run: (_ctx, params) => {
6400
+ const refs = _optionalChain([params, 'optionalAccess', _173 => _173.anchors]) ? anchorsOf(params.anchors) : void 0;
6401
+ const target = { refs, ids: _optionalChain([params, 'optionalAccess', _174 => _174.ids]) ? ids(params) : void 0 };
6402
+ if (_optionalChain([params, 'optionalAccess', _175 => _175.radius]) !== void 0) check(controller.setCornerRadius(params.radius, { ...target, kind: params.kind }));
6403
+ else if (_optionalChain([params, 'optionalAccess', _176 => _176.kind])) check(controller.setCornerKind(params.kind, target));
6404
+ else fail("Pass `radius` and / or `kind`.");
6405
+ return { ok: true };
6406
+ }
6407
+ },
6408
+ {
6409
+ ...base,
6410
+ id: `${prefix}outlineStroke`,
6411
+ label: "Outline Stroke",
6412
+ keywords: ["expand", "stroke to path", "outline", "convert stroke"],
6413
+ description: "Turns the stroke of paths into filled shapes of the area it paints (alignment, caps, joins, dashes and arrowheads included). Returns the new ids.",
6414
+ params: { type: "object", properties: { ids: IDS } },
6415
+ enabled: enabledFor,
6416
+ undoable: true,
6417
+ run: (_ctx, params) => {
6418
+ const created = controller.outlineStroke({ ids: _optionalChain([params, 'optionalAccess', _177 => _177.ids]) ? ids(params) : void 0 });
6419
+ if (!created.length) fail("No selected path has a stroke.");
6420
+ return { ids: created };
6421
+ }
6422
+ },
4120
6423
  {
4121
6424
  ...base,
4122
6425
  id: `${prefix}transform`,
@@ -4227,7 +6530,7 @@ function createVectorCommands(controller, options = {}) {
4227
6530
  enabled: enabledFor,
4228
6531
  undoable: true,
4229
6532
  run: (_ctx, params) => {
4230
- const id = controller.group({ ids: ids(params), name: _optionalChain([params, 'optionalAccess', _128 => _128.name]) });
6533
+ const id = controller.group({ ids: ids(params), name: _optionalChain([params, 'optionalAccess', _178 => _178.name]) });
4231
6534
  if (!id) fail("Only nodes that share a parent can be grouped.");
4232
6535
  return { id };
4233
6536
  }
@@ -4246,6 +6549,186 @@ function createVectorCommands(controller, options = {}) {
4246
6549
  return { selection: controller.getState().selection };
4247
6550
  }
4248
6551
  },
6552
+ {
6553
+ ...base,
6554
+ id: `${prefix}deleteAnchors`,
6555
+ label: "Delete Points",
6556
+ keywords: ["remove", "anchor", "point", "node"],
6557
+ 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.",
6558
+ params: { type: "object", properties: { anchors: ANCHORS, keepShape: { type: "boolean" } } },
6559
+ undoable: true,
6560
+ run: (_ctx, params) => {
6561
+ check(controller.deleteAnchors(anchorsOf(_optionalChain([params, 'optionalAccess', _179 => _179.anchors])), { keepShape: _optionalChain([params, 'optionalAccess', _180 => _180.keepShape]) }));
6562
+ return { ok: true };
6563
+ }
6564
+ },
6565
+ {
6566
+ ...base,
6567
+ id: `${prefix}moveAnchor`,
6568
+ label: "Move Point\u2026",
6569
+ keywords: ["anchor", "drag", "curvature"],
6570
+ 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.",
6571
+ params: {
6572
+ type: "object",
6573
+ properties: { nodeId: { type: "string" }, subpath: { type: "integer", minimum: 0 }, index: { type: "integer", minimum: 0 }, x: { type: "number" }, y: { type: "number" }, curvature: { type: "boolean" } },
6574
+ required: ["nodeId", "subpath", "index", "x", "y"]
6575
+ },
6576
+ undoable: true,
6577
+ run: (_ctx, params) => {
6578
+ check(controller.moveAnchor({ nodeId: params.nodeId, subpath: params.subpath, index: params.index }, { x: params.x, y: params.y }, { curvature: params.curvature }));
6579
+ return { ok: true };
6580
+ }
6581
+ },
6582
+ {
6583
+ ...base,
6584
+ id: `${prefix}bendSegment`,
6585
+ label: "Bend Segment\u2026",
6586
+ keywords: ["curve", "segment", "drag"],
6587
+ description: "Bends segment `segment` (from anchor `segment` to the next) so the curve passes through (x, y) at parameter `t` (default 0.5).",
6588
+ params: {
6589
+ type: "object",
6590
+ properties: {
6591
+ nodeId: { type: "string" },
6592
+ subpath: { type: "integer", minimum: 0 },
6593
+ segment: { type: "integer", minimum: 0 },
6594
+ x: { type: "number" },
6595
+ y: { type: "number" },
6596
+ t: { type: "number", minimum: 0, maximum: 1 }
6597
+ },
6598
+ required: ["nodeId", "subpath", "segment", "x", "y"]
6599
+ },
6600
+ undoable: true,
6601
+ run: (_ctx, params) => {
6602
+ check(controller.bendSegment({ nodeId: params.nodeId, subpath: params.subpath, segment: params.segment }, { x: params.x, y: params.y }, params.t));
6603
+ return { ok: true };
6604
+ }
6605
+ },
6606
+ {
6607
+ ...base,
6608
+ id: `${prefix}join`,
6609
+ label: "Join",
6610
+ shortcut: "Mod+J",
6611
+ keywords: ["connect", "close", "merge", "endpoints"],
6612
+ description: "Joins the open subpaths of paths nearest endpoints first (ends within `tolerance` merge); a single open path is closed.",
6613
+ params: { type: "object", properties: { ids: IDS, tolerance: { type: "number", minimum: 0 } } },
6614
+ undoable: true,
6615
+ run: (_ctx, params) => {
6616
+ check(controller.joinPaths({ ids: _optionalChain([params, 'optionalAccess', _181 => _181.ids]) ? ids(params) : void 0, tolerance: _optionalChain([params, 'optionalAccess', _182 => _182.tolerance]) }));
6617
+ return { selection: controller.getState().selection };
6618
+ }
6619
+ },
6620
+ {
6621
+ ...base,
6622
+ id: `${prefix}average`,
6623
+ label: "Average Points\u2026",
6624
+ shortcut: "Mod+Alt+J",
6625
+ keywords: ["align", "points", "anchors", "collapse"],
6626
+ description: "Moves anchors onto their average position: the same y (`horizontal`), the same x (`vertical`) or the same point (`both`, default).",
6627
+ params: { type: "object", properties: { axis: { type: "string", enum: ["horizontal", "vertical", "both"] }, anchors: ANCHORS } },
6628
+ undoable: true,
6629
+ run: (_ctx, params) => {
6630
+ check(controller.averageAnchors(_nullishCoalesce(_optionalChain([params, 'optionalAccess', _183 => _183.axis]), () => ( "both")), anchorsOf(_optionalChain([params, 'optionalAccess', _184 => _184.anchors]))));
6631
+ return { ok: true };
6632
+ }
6633
+ },
6634
+ {
6635
+ ...base,
6636
+ id: `${prefix}removeRedundantPoints`,
6637
+ label: "Remove Redundant Points",
6638
+ keywords: ["simplify", "clean up", "anchors", "collinear"],
6639
+ description: "Drops anchors that can go without moving the outline more than `tolerance` document units (default 0.5).",
6640
+ params: { type: "object", properties: { ids: IDS, tolerance: { type: "number", minimum: 0 } } },
6641
+ enabled: enabledFor,
6642
+ undoable: true,
6643
+ run: (_ctx, params) => {
6644
+ check(controller.removeRedundantPoints({ ids: _optionalChain([params, 'optionalAccess', _185 => _185.ids]) ? ids(params) : void 0, tolerance: _optionalChain([params, 'optionalAccess', _186 => _186.tolerance]) }));
6645
+ return { ok: true };
6646
+ }
6647
+ },
6648
+ {
6649
+ ...base,
6650
+ id: `${prefix}smooth`,
6651
+ label: "Smooth Path",
6652
+ keywords: ["round", "soften", "handles", "curves"],
6653
+ description: "Pulls the handles of paths (or of the selected points) towards smooth tangents by `amount` (0\u20131, default 1).",
6654
+ params: { type: "object", properties: { ids: IDS, amount: { type: "number", minimum: 0, maximum: 1 }, anchors: ANCHORS } },
6655
+ undoable: true,
6656
+ run: (_ctx, params) => {
6657
+ check(controller.smoothPaths({ ids: _optionalChain([params, 'optionalAccess', _187 => _187.ids]) ? ids(params) : void 0, amount: _optionalChain([params, 'optionalAccess', _188 => _188.amount]), refs: _optionalChain([params, 'optionalAccess', _189 => _189.anchors]) }));
6658
+ return { ok: true };
6659
+ }
6660
+ },
6661
+ {
6662
+ ...base,
6663
+ id: `${prefix}cut`,
6664
+ label: "Cut Path (Scissors)\u2026",
6665
+ keywords: ["scissors", "split", "open", "break"],
6666
+ 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.",
6667
+ params: { type: "object", properties: { nodeId: { type: "string" }, x: { type: "number" }, y: { type: "number" }, anchors: ANCHORS } },
6668
+ undoable: true,
6669
+ run: (_ctx, params) => {
6670
+ if (_optionalChain([params, 'optionalAccess', _190 => _190.nodeId]) !== void 0) {
6671
+ if (!controller.getNode(params.nodeId)) fail(`There is no node "${params.nodeId}".`);
6672
+ if (params.x === void 0 || params.y === void 0) fail("Pass x and y with nodeId.");
6673
+ const created = controller.cutPathAt(params.nodeId, { x: params.x, y: params.y });
6674
+ if (!created) fail("Nothing to cut there (the end of an open path, or a locked path).");
6675
+ return { ids: created };
6676
+ }
6677
+ check(controller.cutAtAnchors(anchorsOf(_optionalChain([params, 'optionalAccess', _191 => _191.anchors]))));
6678
+ return { ids: controller.getState().selection };
6679
+ }
6680
+ },
6681
+ {
6682
+ ...base,
6683
+ id: `${prefix}knife`,
6684
+ label: "Knife\u2026",
6685
+ keywords: ["slice", "cut", "split", "divide"],
6686
+ 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.",
6687
+ params: { type: "object", properties: { points: { ...POINTS, minItems: 2 }, ids: IDS }, required: ["points"] },
6688
+ undoable: true,
6689
+ run: (_ctx, params) => {
6690
+ const created = controller.knife(params.points.map(([x, y]) => ({ x, y })), { ids: params.ids ? ids(params) : void 0 });
6691
+ if (!created.length) fail("The knife did not cross any closed path from edge to edge.");
6692
+ return { ids: created };
6693
+ }
6694
+ },
6695
+ {
6696
+ ...base,
6697
+ id: `${prefix}erase`,
6698
+ label: "Erase\u2026",
6699
+ keywords: ["eraser", "rub out", "remove"],
6700
+ 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.",
6701
+ params: { type: "object", properties: { points: POINTS, size: { type: "number", exclusiveMinimum: 0 }, ids: IDS }, required: ["points"] },
6702
+ undoable: true,
6703
+ run: (_ctx, params) => {
6704
+ const changed = controller.erase(params.points.map(([x, y]) => ({ x, y })), { size: params.size, ids: params.ids ? ids(params) : void 0 });
6705
+ if (!changed.length) fail("The eraser did not touch any path.");
6706
+ return { ids: changed.filter((id) => controller.getNode(id)) };
6707
+ }
6708
+ },
6709
+ {
6710
+ ...base,
6711
+ id: `${prefix}setEditing`,
6712
+ label: "Path Editing Options\u2026",
6713
+ keywords: ["settings", "curvature", "pencil", "smoothing", "lazy mouse", "shapes", "eraser"],
6714
+ 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`.",
6715
+ params: {
6716
+ type: "object",
6717
+ properties: {
6718
+ keepShapeOnDelete: { type: "boolean" },
6719
+ curvatureMoves: { type: "boolean" },
6720
+ pencilSmoothing: { type: "number", minimum: 0, maximum: 1 },
6721
+ recognizeShapes: { type: "boolean" },
6722
+ eraserSize: { type: "number", exclusiveMinimum: 0 }
6723
+ }
6724
+ },
6725
+ run: (_ctx, params) => {
6726
+ const patch = {};
6727
+ for (const key of ["keepShapeOnDelete", "curvatureMoves", "pencilSmoothing", "recognizeShapes", "eraserSize"]) if (_optionalChain([params, 'optionalAccess', _192 => _192[key]]) !== void 0) patch[key] = params[key];
6728
+ controller.setEditing(patch);
6729
+ return { editing: controller.getState().editing };
6730
+ }
6731
+ },
4249
6732
  {
4250
6733
  ...base,
4251
6734
  id: `${prefix}setTool`,
@@ -4266,7 +6749,7 @@ function createVectorCommands(controller, options = {}) {
4266
6749
  keywords: ["download", "save", "markup"],
4267
6750
  description: "Returns the document as SVG markup.",
4268
6751
  params: { type: "object", properties: { precision: { type: "integer", minimum: 0, maximum: 8 } } },
4269
- run: (_ctx, params) => ({ svg: controller.toSvg({ precision: _optionalChain([params, 'optionalAccess', _129 => _129.precision]) }) })
6752
+ run: (_ctx, params) => ({ svg: controller.toSvg({ precision: _optionalChain([params, 'optionalAccess', _193 => _193.precision]) }) })
4270
6753
  },
4271
6754
  {
4272
6755
  ...base,
@@ -4373,5 +6856,64 @@ function createVectorCommands(controller, options = {}) {
4373
6856
 
4374
6857
 
4375
6858
 
4376
- exports.DEFAULT_VECTOR_SNAP = DEFAULT_VECTOR_SNAP; exports.IDENTITY_MATRIX = IDENTITY_MATRIX; exports.KAPPA = KAPPA; exports.VECTOR_CLIPBOARD_TYPE = VECTOR_CLIPBOARD_TYPE; exports.VECTOR_CONSTRUCTION_ANGLES = VECTOR_CONSTRUCTION_ANGLES; exports.VECTOR_OVERLAY_THEME = VECTOR_OVERLAY_THEME; 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.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.decomposeRigid = decomposeRigid; exports.ellipseSubpath = ellipseSubpath; 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.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.rectSubpath = rectSubpath; exports.removeAnchors = removeAnchors; exports.reverseSubpath = reverseSubpath; exports.rotateMatrix = rotateMatrix; exports.roundSubpaths = roundSubpaths; exports.scaleFor = scaleFor; exports.scaleMatrix = scaleMatrix; exports.starSubpath = starSubpath; 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.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;
6859
+
6860
+
6861
+
6862
+
6863
+
6864
+
6865
+
6866
+
6867
+
6868
+
6869
+
6870
+
6871
+
6872
+
6873
+
6874
+
6875
+
6876
+
6877
+
6878
+
6879
+
6880
+
6881
+
6882
+
6883
+
6884
+
6885
+
6886
+
6887
+
6888
+
6889
+
6890
+
6891
+
6892
+
6893
+
6894
+
6895
+
6896
+
6897
+
6898
+
6899
+
6900
+
6901
+
6902
+
6903
+
6904
+
6905
+
6906
+
6907
+
6908
+
6909
+
6910
+
6911
+
6912
+
6913
+
6914
+
6915
+
6916
+
6917
+
6918
+ exports.DEFAULT_VECTOR_PATH_EDIT = DEFAULT_VECTOR_PATH_EDIT; exports.DEFAULT_VECTOR_SNAP = DEFAULT_VECTOR_SNAP; exports.IDENTITY_MATRIX = IDENTITY_MATRIX; exports.KAPPA = KAPPA2; exports.LazyMouse = LazyMouse; exports.VECTOR_ARROWHEADS = VECTOR_ARROWHEADS; exports.VECTOR_ARROWHEAD_LABELS = VECTOR_ARROWHEAD_LABELS; exports.VECTOR_CLIPBOARD_TYPE = VECTOR_CLIPBOARD_TYPE; exports.VECTOR_CONSTRUCTION_ANGLES = VECTOR_CONSTRUCTION_ANGLES; exports.VECTOR_CORNER_KINDS = VECTOR_CORNER_KINDS; exports.VECTOR_CORNER_KIND_LABELS = VECTOR_CORNER_KIND_LABELS; exports.VECTOR_CORNER_WIDGET_INSET = VECTOR_CORNER_WIDGET_INSET; 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_STROKE_ALIGN_LABELS = VECTOR_STROKE_ALIGN_LABELS; exports.VECTOR_STYLE_KEYS = VECTOR_STYLE_KEYS; 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.applyVectorCornersAttribute = applyVectorCornersAttribute; exports.arrowheadInset = arrowheadInset; exports.arrowheadPath = arrowheadPath; exports.arrowheadWeight = arrowheadWeight; 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.cornerBisectorPoint = cornerBisectorPoint; exports.cornerRadiusChange = cornerRadiusChange; exports.cornerSetback = cornerSetback; exports.createVectorCommands = createVectorCommands; exports.curvatureMoveAnchor = curvatureMoveAnchor; exports.cutSubpathAt = cutSubpathAt; exports.decomposeRigid = decomposeRigid; exports.effectiveStrokeAlign = effectiveStrokeAlign; exports.ellipseSubpath = ellipseSubpath; exports.eraseFromFill = eraseFromFill; exports.eraseFromOutline = eraseFromOutline; exports.eraserRegion = eraserRegion; exports.expandVectorCorners = expandVectorCorners; exports.expandVectorCornersOf = expandVectorCornersOf; exports.findVectorNode = findVectorNode; exports.fitCornerRadius = fitCornerRadius; exports.getAnchor = getAnchor; exports.getAnchorWorld = getAnchorWorld; exports.getEditablePathIds = getEditablePathIds; exports.getTransformHandlePositions = getTransformHandlePositions; exports.getVectorCornerWidgets = getVectorCornerWidgets; 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.hasStrokeExtras = hasStrokeExtras; exports.hasVectorCorners = hasVectorCorners; exports.hitTestVectorNode = hitTestVectorNode; exports.hitTestVectorNodes = hitTestVectorNodes; exports.hitTestVectorStroke = hitTestVectorStroke; exports.inferAnchorType = inferAnchorType; exports.insertAnchor = insertAnchor; exports.invertMatrix = invertMatrix; exports.isClosedBezierShape = isClosedBezierShape; 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.nextVectorCornerKind = nextVectorCornerKind; 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.setAnchorCorner = setAnchorCorner; exports.setSubpathCorners = setSubpathCorners; 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.vectorCornerAt = vectorCornerAt; exports.vectorCorners = vectorCorners; exports.vectorCornersAttribute = vectorCornersAttribute; 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.vectorStrokeOutline = vectorStrokeOutline; exports.vectorStrokeOutlinePath = vectorStrokeOutlinePath; exports.vectorStrokeParts = vectorStrokeParts; exports.vectorStrokeReach = vectorStrokeReach; exports.vectorStrokeSvgParts = vectorStrokeSvgParts; exports.vectorToolLabel = vectorToolLabel; exports.vectorWorldMatrix = vectorWorldMatrix; exports.walkVectorNodes = walkVectorNodes;
4377
6919
  //# sourceMappingURL=index.cjs.map