@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.
@@ -4,33 +4,460 @@ import {
4
4
  import {
5
5
  bezierPathBounds,
6
6
  bezierPathToSvg,
7
+ closeContour,
8
+ contourSignedArea,
9
+ contoursToPath,
7
10
  distanceToBezierPath,
11
+ fitCubicWithTangents,
8
12
  fitCubics,
9
13
  formatNumber,
10
14
  gradientToSvgDef,
15
+ normalizeBezierPath,
16
+ outlineStroke,
11
17
  pathBoolean,
18
+ pathToContours,
12
19
  pointInBezierPath,
20
+ segmentBounds,
13
21
  segmentDerivative,
14
22
  segmentIntersections,
23
+ segmentLength,
15
24
  segmentNearest,
25
+ segmentParamAtLength,
16
26
  segmentPoint,
27
+ segmentTangent,
17
28
  splitSegment,
29
+ subSegment,
18
30
  toBezierPath,
19
- transformBezierPath
20
- } from "../chunk-RK6HUKDI.js";
31
+ transformBezierPath,
32
+ trimSegments,
33
+ windingNumber
34
+ } from "../chunk-EGPIVUEF.js";
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
+ curve;
56
+ straight;
57
+ length;
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(h1 ?? a), pt(h2 ?? b), pt(b)];
61
+ this.length = this.straight ? Math.hypot(b.x - a.x, b.y - a.y) : segmentLength(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 = segmentParamAtLength(this.curve, s, this.length);
80
+ const d = vec(segmentDerivative(this.curve, t));
81
+ const l = len(d);
82
+ return { t, p: vec(segmentPoint(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 = a.corner?.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, a.corner?.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(a.out ?? a), pt(b.in ?? b), pt(b)];
183
+ const part = subSegment(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 : a.in ?? null;
192
+ };
193
+ const handleOut = (i, a) => {
194
+ const t = trimmed[i];
195
+ return t ? t.out : 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 = kind ?? corner?.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 = pathToContours(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 = style.strokeAlign ?? "center";
257
+ return closedShape ? a : "center";
258
+ }
259
+ function hasStrokeExtras(style) {
260
+ return (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, (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) => segmentLength(s);
338
+ function endOf(segs, atStart) {
339
+ const long = (s2) => segLen(s2) > 1e-9;
340
+ if (atStart) {
341
+ const s2 = segs.find(long) ?? segs[0];
342
+ const t2 = segmentTangent(s2, 0);
343
+ return { p: toVec(segs[0][0]), out: v(-t2[0], -t2[1]) };
344
+ }
345
+ const s = [...segs].reverse().find(long) ?? segs[segs.length - 1];
346
+ const t = segmentTangent(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 = trimSegments([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 = style.strokeWidth ?? 1;
367
+ const contours = pathToContours(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 ? trimSegments(c.segments, s0, total - s1, lengths) : [] };
408
+ });
409
+ return { align, line: contoursToPath(trimmed), drawWidth, heads };
410
+ }
411
+ function vectorStrokeOutlinePath(path, style) {
412
+ const parts = vectorStrokeParts(path, style);
413
+ const stroke = outlineStroke(parts.line, {
414
+ width: parts.drawWidth,
415
+ join: style.strokeLinejoin ?? "miter",
416
+ cap: style.strokeLinecap ?? "butt",
417
+ ...style.strokeDasharray?.length ? { dash: style.strokeDasharray } : {}
418
+ });
419
+ const fill = { path, fillRule: style.fillRule ?? "nonzero" };
420
+ let area = stroke;
421
+ if (parts.align === "inside") area = pathBoolean("intersection", [stroke, fill]).path;
422
+ else if (parts.align === "outside") area = pathBoolean("difference", [stroke, fill]).path;
423
+ if (!parts.heads.length) return area;
424
+ return pathBoolean("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 = style.strokeWidth ?? 1;
430
+ for (const h of parts.heads) {
431
+ if (pointInBezierPath(h.path, point) || tolerance > 0 && distanceToBezierPath(h.path, point) <= tolerance) return true;
432
+ }
433
+ if (!parts.line.length) return false;
434
+ const d = distanceToBezierPath(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 = pointInBezierPath(path, point, style.fillRule ?? "nonzero");
439
+ return parts.align === "inside" ? inside : !inside;
440
+ }
441
+ function vectorStrokeReach(style) {
442
+ const width = style.strokeWidth ?? 1;
443
+ const align = 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) => 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 });
@@ -94,9 +521,9 @@ 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) {
@@ -239,8 +666,12 @@ function hitTestVectorNode(node, p, tolerance = 0, parent = IDENTITY_MATRIX) {
239
666
  if (path.length === 0) return false;
240
667
  const point = [p.x, p.y];
241
668
  if (hasPaint(node.fill) && pointInBezierPath(path, point, node.fillRule ?? "nonzero")) return true;
242
- const reach = (hasPaint(node.stroke) ? (node.strokeWidth ?? 1) / 2 : 0) + tolerance;
243
- return distanceToBezierPath(path, point) <= reach;
669
+ if (hasPaint(node.stroke)) return hitTestVectorStroke(path, p, node, tolerance);
670
+ return distanceToBezierPath(path, point) <= tolerance;
671
+ }
672
+ function vectorStrokeOutline(node, parent = IDENTITY_MATRIX) {
673
+ if (!hasPaint(node.stroke) || !((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--) {
@@ -258,8 +689,8 @@ function nearestSegment(node, p) {
258
689
  const seg = subpathSegment(sp, i);
259
690
  const hit = segmentNearest(seg, [p.x, p.y]);
260
691
  if (!best || hit.distance < best.distance) {
261
- const pt = segmentPoint(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 = segmentPoint(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
  }
@@ -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}:${formatNumber(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 = subpaths[+m[1]]?.anchors[+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
981
  return { value: `url(#${gradientId})`, gradient: gradientToSvgDef(paint, gradientId) };
541
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 = bezierPathBounds(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: bezierPathToSvg(parts.line, { precision }), width: parts.drawWidth, clip, heads: parts.heads.map((h) => bezierPathToSvg(h.path, { precision })) };
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
  }
@@ -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(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 (node.strokeDasharray?.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="${bezierPathToSvg(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 (node.strokeDasharray?.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(node.strokeWidth ?? 1) : ""}${live}/>`);
1045
+ return;
1046
+ }
1047
+ let data = ` data-vector-path="" data-vector-stroke-width="${num(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 = bezierPathToSvg(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);
@@ -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 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 = (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 || !subpaths?.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 = (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;
@@ -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",
@@ -1012,7 +1560,7 @@ var VectorSnapTargets = 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 _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
  }
@@ -1136,14 +1684,14 @@ var VectorSnapTargets = class _VectorSnapTargets {
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
1696
  if (leave?.angles?.length) {
1149
1697
  const c = construction(leave, p, tol);
@@ -1156,9 +1704,9 @@ var VectorSnapTargets = 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 _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 = segmentNearest(s.seg, pt);
1772
+ const hit = segmentNearest(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(segmentPoint(near[0].seg, near[0].t)) : null;
1777
+ onSegment(near2) {
1778
+ return near2.length ? toVec2(segmentPoint(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
1787
  for (const hit of segmentIntersections(list[i].seg, list[j].seg)) {
1240
- const q = toVec(hit.point);
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 _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)) {
@@ -1278,7 +1826,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
1278
1826
  const axes = { x: options.axes?.x !== false, y: options.axes?.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;
@@ -1321,7 +1869,7 @@ var VectorSnapTargets = 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 _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 _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 _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;
@@ -1444,9 +1992,9 @@ var VectorSnapTargets = 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 _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 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 };
@@ -1571,7 +2119,7 @@ function solveOnSegment(seg, base, cond) {
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];
@@ -1603,136 +2151,1136 @@ 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) => findVectorNode(state.document.nodes, id)?.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 = sp?.anchors[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(segmentPoint(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 fitCubicWithTangents(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 = node?.type === "path" ? node.subpaths[state.pen.subpath] : void 0;
1660
- if (node?.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 node?.type === "path" ? node.subpaths[ref.subpath]?.anchors[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 = 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: 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] = segmentPoint(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 = [
1727
- { x: box.x, y: box.y },
1728
- { x: box.x + box.width, y: box.y },
1729
- { x: box.x + box.width, y: box.y + box.height },
1730
- { x: box.x, y: box.y + box.height }
1731
- ].map(toScreen2);
1732
- 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 };
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;
1734
2357
  }
1735
- var RESIZE_CURSOR_ORDER = ["ew-resize", "nwse-resize", "ns-resize", "nesw-resize"];
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: 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: 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 = p.in ?? p;
2453
+ const hout = 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 segsToSubpath(segs, sp.closed, types) ?? { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed };
2546
+ }
2547
+
2548
+ // src/vector/pathCut.ts
2549
+ var loopArea = (l) => contourSignedArea(l.segs);
2550
+ function loopSample(l) {
2551
+ const k = l.chord.findIndex((c) => !c);
2552
+ return segmentPoint(l.segs[k >= 0 ? k : 0], 0.5);
2553
+ }
2554
+ var contains2 = (l, p) => windingNumber([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 contoursToPath(face.loops.map((l) => ({ segments: l.segs, closed: true })));
2571
+ }
2572
+ function facesOf(path, fillRule) {
2573
+ const normalized = normalizeBezierPath(path, fillRule);
2574
+ const loops = pathToContours(normalized).map((c) => {
2575
+ const segs = closeContour(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 = segmentPoint(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() : subSegment(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 = bezierPathBounds(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 = segmentBounds(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 windingNumber(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 outlineStroke(line2, { width, cap: "round", join: "round" });
2773
+ }
2774
+ function eraseFromFill(path, fillRule, region) {
2775
+ const overlap = pathBoolean("intersection", [{ path, fillRule }, { path: region }]).path;
2776
+ if (!pathToContours(overlap).some((c) => Math.abs(contourSignedArea(closeContour(c, true))) > 1e-9)) return null;
2777
+ const rest = pathBoolean("difference", [{ path, fillRule }, { path: region }]).path;
2778
+ return splitIntoFaces(rest);
2779
+ }
2780
+ function eraseFromOutline(sp, region) {
2781
+ const regionSegs = pathToContours(region).flatMap((c) => closeContour(c, true));
2782
+ if (!regionSegs.length) return null;
2783
+ const rb = bezierPathBounds(region);
2784
+ const count = subpathSegmentCount(sp);
2785
+ if (!rb || count === 0) return null;
2786
+ const removed = (p) => pointInBezierPath(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 = segmentBounds(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 = segmentBounds(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 segmentIntersections(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(segmentPoint(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 = subSegment(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 = class {
2890
+ /** `length` is the string length in the units of the points (0 turns smoothing off). */
2891
+ constructor(length2) {
2892
+ this.length = length2;
2893
+ }
2894
+ length;
2895
+ pos = null;
2896
+ 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
+ };
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 = 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) => findVectorNode(state.document.nodes, id)?.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 = sp?.anchors[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 = node?.type === "path" ? node.subpaths[state.pen.subpath] : void 0;
3185
+ if (node?.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 = 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 = a.corner?.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: a.corner?.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 node?.type === "path" ? node.subpaths[ref.subpath]?.anchors[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 = 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: 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 = [
3275
+ { x: box.x, y: box.y },
3276
+ { x: box.x + box.width, y: box.y },
3277
+ { x: box.x + box.width, y: box.y + box.height },
3278
+ { x: box.x, y: box.y + box.height }
3279
+ ].map(toScreen2);
3280
+ const r = (n) => Math.round(n * 1e3) / 1e3;
3281
+ return { box, angle: frame.angle, transform: frame.angle ? `rotate(${r(frame.angle)} ${r(viewport.x)} ${r(viewport.y)})` : "", handles, corners: corners2 };
3282
+ }
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 };
1737
3285
  function vectorResizeCursor(handle, angle = 0) {
1738
3286
  if (handle === "rotate") return "grab";
@@ -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,6 +3355,28 @@ 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)}`;
@@ -1811,6 +3391,7 @@ var VectorEditorController = class {
1811
3391
  defaults;
1812
3392
  viewport;
1813
3393
  snap;
3394
+ editing;
1814
3395
  hitRadius;
1815
3396
  hover = null;
1816
3397
  pointer = null;
@@ -1839,6 +3420,7 @@ var VectorEditorController = class {
1839
3420
  this.defaults = { ...DEFAULT_DEFAULTS, ...options.defaults ?? {} };
1840
3421
  this.viewport = { x: 0, y: 0, zoom: 1, ...options.viewport ?? {} };
1841
3422
  this.snap = { ...DEFAULT_VECTOR_SNAP, ...options.snap ?? {} };
3423
+ this.editing = { ...DEFAULT_VECTOR_PATH_EDIT, ...options.editing ?? {} };
1842
3424
  this.hitRadius = options.hitRadius ?? 6;
1843
3425
  this.createId = options.createId ?? defaultCreateId;
1844
3426
  this.onChange = options.onChange;
@@ -1887,6 +3469,7 @@ var VectorEditorController = 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,
@@ -1943,7 +3526,7 @@ var VectorEditorController = class {
1943
3526
  if (h?.transformHandle) return vectorResizeCursor(h.transformHandle, this.frameOf(this.selectedRoots())?.angle ?? 0);
1944
3527
  return h?.nodeId ? "move" : "default";
1945
3528
  case "direct":
1946
- if (h?.anchor || h?.handle) return "pointer";
3529
+ if (h?.anchor || h?.handle || h?.corner) return "pointer";
1947
3530
  if (h?.segment) return "copy";
1948
3531
  return h?.nodeId ? "move" : "default";
1949
3532
  case "pen":
@@ -2028,12 +3611,12 @@ var VectorEditorController = 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 = 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) };
@@ -2150,7 +3741,7 @@ var VectorEditorController = class {
2150
3741
  const leave = last ? { at: { x: last.x, y: last.y }, constrain: input.shift ? "angle" : null } : null;
2151
3742
  return this.snapDrawn(p, leave, input, () => this.drawingTargets(this.pen?.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 = class {
2250
3841
  let subpath;
2251
3842
  switch (input.kind) {
2252
3843
  case "rect":
2253
- subpath = rectSubpath(b, 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 = 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 = input.cornerRadius ?? d.cornerRadius;
3860
+ const kind = 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: input.name ?? names[input.kind], ...style, ...own, subpaths: [subpath] };
@@ -2291,6 +3885,8 @@ var VectorEditorController = 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 = 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, a.corner?.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 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 = 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) && (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: `${p.name ?? "Path"} Stroke`, subpaths: outline, fill: 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
  }
@@ -2753,12 +4438,47 @@ var VectorEditorController = 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 = 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 (node?.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 (node?.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 = node?.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
  }
@@ -2786,66 +4506,338 @@ var VectorEditorController = class {
2786
4506
  });
2787
4507
  return { ok: true };
2788
4508
  }
2789
- /** Adds an anchor on the nearest segment of `nodeId` to document point `point` (shape unchanged). */
2790
- insertAnchorAt(nodeId, point) {
4509
+ /** Adds an anchor on the nearest segment of `nodeId` to document point `point` (shape unchanged). */
4510
+ insertAnchorAt(nodeId, point) {
4511
+ const node = this.getNode(nodeId);
4512
+ if (node?.type !== "path") return null;
4513
+ const hit = nearestSegment(node, this.toLocal(nodeId, point));
4514
+ if (!hit) return null;
4515
+ const { subpath, index } = insertAnchor(node.subpaths[hit.subpath], hit.segment, hit.t);
4516
+ const ref = { nodeId, subpath: hit.subpath, index };
4517
+ this.change("Add Point", () => {
4518
+ this.replaceNode(nodeId, { ...node, subpaths: node.subpaths.map((sp, i) => i === hit.subpath ? subpath : sp) });
4519
+ this.editingId = nodeId;
4520
+ this.anchors = [ref];
4521
+ });
4522
+ return ref;
4523
+ }
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 = {}) {
4530
+ if (!refs.length) return { ok: false, message: "Select points to delete." };
4531
+ const keepShape = options.keepShape ?? this.editing.keepShapeOnDelete;
4532
+ this.change("Delete Points", () => {
4533
+ const nodes = mapTree(this.doc.nodes, (n) => {
4534
+ if (n.type !== "path" || n.locked) return n;
4535
+ const mine = refs.filter((r) => r.nodeId === n.id);
4536
+ if (!mine.length) return n;
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);
4542
+ return subpaths.length ? { ...n, subpaths } : null;
4543
+ });
4544
+ this.setDoc(nodes);
4545
+ this.anchors = [];
4546
+ this.selection = this.selection.filter((id) => this.getNode(id));
4547
+ if (this.editingId && !this.getNode(this.editingId)) this.editingId = null;
4548
+ if (this.pen && !this.getNode(this.pen.nodeId)) this.pen = null;
4549
+ });
4550
+ return { ok: true };
4551
+ }
4552
+ /** Opens or closes the subpaths that contain selected anchors (or all subpaths of `nodeId`). */
4553
+ setClosed(closed, nodeId) {
4554
+ const targets = nodeId ? (() => {
4555
+ const n = this.getNode(nodeId);
4556
+ return n?.type === "path" ? n.subpaths.map((_, s) => ({ nodeId, subpath: s })) : [];
4557
+ })() : this.anchors.map((r) => ({ nodeId: r.nodeId, subpath: r.subpath }));
4558
+ if (!targets.length) return { ok: false, message: "Select a path or points." };
4559
+ this.change(closed ? "Close Path" : "Open Path", () => {
4560
+ this.setDoc(
4561
+ mapTree(
4562
+ this.doc.nodes,
4563
+ (n) => n.type === "path" && targets.some((t) => t.nodeId === n.id) ? { ...n, subpaths: n.subpaths.map((sp, s) => targets.some((t) => t.nodeId === n.id && t.subpath === s) ? { ...sp, closed } : sp) } : n
4564
+ )
4565
+ );
4566
+ });
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 = 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, 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 = 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 = options.refs ?? (!options.ids && this.tool === "direct" && this.anchors.length ? this.anchors : null);
4688
+ const ids = 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, 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) => 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) {
2791
4709
  const node = this.getNode(nodeId);
2792
- if (node?.type !== "path") return null;
2793
- const hit = nearestSegment(node, this.toLocal(nodeId, point));
4710
+ if (node?.type !== "path" || node.locked) return null;
4711
+ const local = this.toLocal(nodeId, point);
4712
+ const hit = nearestSegment(node, local);
2794
4713
  if (!hit) return null;
2795
- const { subpath, index } = insertAnchor(node.subpaths[hit.subpath], hit.segment, hit.t);
2796
- const ref = { nodeId, subpath: hit.subpath, index };
2797
- this.change("Add Point", () => {
2798
- this.replaceNode(nodeId, { ...node, subpaths: node.subpaths.map((sp, i) => i === hit.subpath ? subpath : sp) });
2799
- this.editingId = nodeId;
2800
- this.anchors = [ref];
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();
2801
4727
  });
2802
- return ref;
4728
+ return ids;
2803
4729
  }
2804
- /** Deletes the selected anchors; empty subpaths and paths go too. */
2805
- deleteAnchors(refs = this.anchors) {
2806
- if (!refs.length) return { ok: false, message: "Select points to delete." };
2807
- this.change("Delete Points", () => {
2808
- const nodes = mapTree(this.doc.nodes, (n) => {
2809
- if (n.type !== "path") return n;
2810
- const mine = refs.filter((r) => r.nodeId === n.id);
2811
- 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);
2813
- return subpaths.length ? { ...n, subpaths } : null;
2814
- });
2815
- this.setDoc(nodes);
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, [...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;
2816
4748
  this.anchors = [];
2817
- this.selection = this.selection.filter((id) => this.getNode(id));
2818
- if (this.editingId && !this.getNode(this.editingId)) this.editingId = null;
2819
- if (this.pen && !this.getNode(this.pen.nodeId)) this.pen = null;
4749
+ this.dropStaleRefs();
2820
4750
  });
2821
- return { ok: true };
4751
+ return created.length ? { ok: true } : { ok: false, message: "Points at the ends of open paths do not cut." };
2822
4752
  }
2823
- /** Opens or closes the subpaths that contain selected anchors (or all subpaths of `nodeId`). */
2824
- setClosed(closed, nodeId) {
2825
- const targets = nodeId ? (() => {
2826
- const n = this.getNode(nodeId);
2827
- return n?.type === "path" ? n.subpaths.map((_, s) => ({ nodeId, subpath: s })) : [];
2828
- })() : this.anchors.map((r) => ({ nodeId: r.nodeId, subpath: r.subpath }));
2829
- if (!targets.length) return { ok: false, message: "Select a path or points." };
2830
- this.change(closed ? "Close Path" : "Open Path", () => {
2831
- this.setDoc(
2832
- mapTree(
2833
- this.doc.nodes,
2834
- (n) => n.type === "path" && targets.some((t) => t.nodeId === n.id) ? { ...n, subpaths: n.subpaths.map((sp, s) => targets.some((t) => t.nodeId === n.id && t.subpath === s) ? { ...sp, closed } : sp) } : n
2835
- )
2836
- );
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);
2837
4758
  });
2838
- return { ok: true };
4759
+ const visible = (p) => !blocked.has(p.id);
4760
+ if (ids?.length || this.selection.length) return this.pathTargets(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, vectorParentMatrix(this.doc.nodes, n.id) ?? IDENTITY_MATRIX);
4767
+ if (b && boundsIntersect(b, box)) out.push(n);
4768
+ });
4769
+ return out;
2839
4770
  }
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));
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), 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;
2846
4797
  this.anchors = [];
4798
+ this.dropStaleRefs();
2847
4799
  });
2848
- return { ok: true };
4800
+ return created;
4801
+ }
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 = 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 = bezierPathBounds(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 = transformBezierPath(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), 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) => 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));
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))])];
4839
+ });
4840
+ return changed;
2849
4841
  }
2850
4842
  /** Ends the pen path being drawn (a lone anchor is removed). */
2851
4843
  finishPath() {
@@ -2965,6 +4957,22 @@ var VectorEditorController = class {
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 = 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 = 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 = 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 = 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 = 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
  }
@@ -3198,10 +5254,10 @@ var VectorEditorController = 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 = 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 = 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 = 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 = 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 = 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);
@@ -3433,10 +5522,22 @@ var VectorEditorController = 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
5533
  if (node?.type !== "path") return;
5534
+ if (!moved) {
5535
+ if (string && this.interaction?.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 = 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;
@@ -3492,12 +5594,12 @@ var VectorEditorController = 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 = 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, a.corner?.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);
@@ -3560,6 +5688,13 @@ var VectorEditorController = class {
3560
5688
  this.notify(true);
3561
5689
  return;
3562
5690
  }
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
+ }
3563
5698
  const fitted = fitCubics(drag.points, 1.5 / this.viewport.zoom, { cornerAngleDeg: 60 });
3564
5699
  const anchors = [];
3565
5700
  for (const c of fitted) {
@@ -3666,6 +5801,8 @@ var VectorEditorController = 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;
@@ -3746,12 +5883,28 @@ var VectorEditorController = class {
3746
5883
  }
3747
5884
  }
3748
5885
  };
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,10 +5941,12 @@ 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,
@@ -3807,9 +5962,9 @@ function getVectorSceneRenderData(state, options = {}) {
3807
5962
  const defs = [];
3808
5963
  const prefix = 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,8 +5974,8 @@ 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 = vectorParentMatrix(state.document.nodes, node.id) ?? IDENTITY_MATRIX;
3825
5980
  return collectWithMatrix(node, parent).map(({ path, parent: m }) => bezierPathToSvg(transformBezierPath(vectorPathWorldBezier(path, m), view), { precision: 2 })).join(" ");
3826
5981
  }
@@ -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 (it?.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 (it?.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 = it?.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(state.hover?.corner, w.ref), active: !!dragged };
6091
+ });
6092
+ if (it?.type === "corner") status = `R ${fmt(it.radius)}`;
6093
+ const cutPoint = state.tool === "scissors" && state.hover?.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) };
@@ -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" && !!state.selectedAnchors?.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 (node?.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(get(p) ?? null) === JSON.stringify(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: corner?.radius ?? 0, kind: corner?.kind ?? "round" });
6149
+ }
6150
+ }
6151
+ const c0 = corners2[0] ?? { radius: 0, kind: "round" };
3957
6152
  return {
3958
6153
  fill: first.fill ?? null,
3959
6154
  stroke: first.stroke ?? null,
3960
6155
  strokeWidth: first.strokeWidth ?? 1,
3961
6156
  opacity: first.opacity ?? 1,
3962
- mixed: { fill: !same((p) => p.fill), stroke: !same((p) => p.stroke), strokeWidth: !same((p) => p.strokeWidth ?? 1), opacity: !same((p) => p.opacity ?? 1) },
6157
+ strokeAlign: first.strokeAlign ?? "center",
6158
+ startArrow: first.startArrow ?? null,
6159
+ endArrow: first.endArrow ?? null,
6160
+ arrowAlign: first.arrowAlign ?? "extend",
6161
+ startArrowScale: first.startArrowScale ?? 1,
6162
+ endArrowScale: 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) => p.strokeWidth ?? 1),
6171
+ opacity: !same((p) => p.opacity ?? 1),
6172
+ strokeAlign: !same((p) => p.strokeAlign ?? "center"),
6173
+ startArrow: !same((p) => p.startArrow ?? null),
6174
+ endArrow: !same((p) => p.endArrow ?? null),
6175
+ arrowAlign: !same((p) => p.arrowAlign ?? "extend"),
6176
+ startArrowScale: !same((p) => p.startArrowScale ?? 1),
6177
+ endArrowScale: !same((p) => 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 = {};
@@ -4008,6 +6270,14 @@ function createVectorCommands(controller, options = {}) {
4008
6270
  return list2;
4009
6271
  };
4010
6272
  const enabledFor = (_, params) => params?.ids?.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 (node?.type !== "path" || !node.subpaths[r.subpath]?.anchors[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 },
@@ -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 = params?.anchors ? anchorsOf(params.anchors) : void 0;
6401
+ const target = { refs, ids: params?.ids ? ids(params) : void 0 };
6402
+ if (params?.radius !== void 0) check(controller.setCornerRadius(params.radius, { ...target, kind: params.kind }));
6403
+ else if (params?.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: params?.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`,
@@ -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(params?.anchors), { keepShape: params?.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: params?.ids ? ids(params) : void 0, tolerance: params?.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(params?.axis ?? "both", anchorsOf(params?.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: params?.ids ? ids(params) : void 0, tolerance: params?.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: params?.ids ? ids(params) : void 0, amount: params?.amount, refs: params?.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 (params?.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(params?.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 (params?.[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`,
@@ -4284,13 +6767,26 @@ function createVectorCommands(controller, options = {}) {
4284
6767
  ];
4285
6768
  }
4286
6769
  export {
6770
+ DEFAULT_VECTOR_PATH_EDIT,
4287
6771
  DEFAULT_VECTOR_SNAP,
4288
6772
  IDENTITY_MATRIX,
4289
- KAPPA,
6773
+ KAPPA2 as KAPPA,
6774
+ LazyMouse,
6775
+ VECTOR_ARROWHEADS,
6776
+ VECTOR_ARROWHEAD_LABELS,
4290
6777
  VECTOR_CLIPBOARD_TYPE,
4291
6778
  VECTOR_CONSTRUCTION_ANGLES,
6779
+ VECTOR_CORNER_KINDS,
6780
+ VECTOR_CORNER_KIND_LABELS,
6781
+ VECTOR_CORNER_WIDGET_INSET,
6782
+ VECTOR_EDIT_TOGGLES,
6783
+ VECTOR_INK_MAX_CORNERS,
4292
6784
  VECTOR_OVERLAY_THEME,
6785
+ VECTOR_PATH_ACTIONS,
6786
+ VECTOR_PENCIL_STRING_PX,
4293
6787
  VECTOR_ROTATE_HANDLE_OFFSET,
6788
+ VECTOR_STROKE_ALIGN_LABELS,
6789
+ VECTOR_STYLE_KEYS,
4294
6790
  VECTOR_TOOLS,
4295
6791
  VECTOR_TOOL_GLYPHS,
4296
6792
  VECTOR_TOOL_SHORTCUTS,
@@ -4299,6 +6795,11 @@ export {
4299
6795
  VectorSnapTargets,
4300
6796
  applyMatrix,
4301
6797
  applyMatrixLinear,
6798
+ applyVectorCornersAttribute,
6799
+ arrowheadInset,
6800
+ arrowheadPath,
6801
+ arrowheadWeight,
6802
+ bendSegment,
4302
6803
  bezierToSubpaths,
4303
6804
  boundsFromPoints,
4304
6805
  boundsIntersect,
@@ -4309,14 +6810,27 @@ export {
4309
6810
  collectPathNodes,
4310
6811
  constrainToLeave,
4311
6812
  convertAnchor,
6813
+ cornerBisectorPoint,
6814
+ cornerRadiusChange,
6815
+ cornerSetback,
4312
6816
  createVectorCommands,
6817
+ curvatureMoveAnchor,
6818
+ cutSubpathAt,
4313
6819
  decomposeRigid,
6820
+ effectiveStrokeAlign,
4314
6821
  ellipseSubpath,
6822
+ eraseFromFill,
6823
+ eraseFromOutline,
6824
+ eraserRegion,
6825
+ expandVectorCorners,
6826
+ expandVectorCornersOf,
4315
6827
  findVectorNode,
6828
+ fitCornerRadius,
4316
6829
  getAnchor,
4317
6830
  getAnchorWorld,
4318
6831
  getEditablePathIds,
4319
6832
  getTransformHandlePositions,
6833
+ getVectorCornerWidgets,
4320
6834
  getVectorOverlayRenderData,
4321
6835
  getVectorSceneRenderData,
4322
6836
  getVectorSelectionBoxGeometry,
@@ -4325,31 +6839,50 @@ export {
4325
6839
  getVisibleAnchors,
4326
6840
  getVisibleHandles,
4327
6841
  hasPaint,
6842
+ hasStrokeExtras,
6843
+ hasVectorCorners,
4328
6844
  hitTestVectorNode,
4329
6845
  hitTestVectorNodes,
6846
+ hitTestVectorStroke,
4330
6847
  inferAnchorType,
4331
6848
  insertAnchor,
4332
6849
  invertMatrix,
6850
+ isClosedBezierShape,
4333
6851
  isIdentityMatrix,
4334
6852
  isVectorTextTarget,
6853
+ joinSubpaths,
6854
+ knifePath,
4335
6855
  lineSubpath,
4336
6856
  locateVectorNode,
4337
6857
  matrixRotation,
4338
6858
  moveAnchorHandle,
4339
6859
  multiplyMatrices,
4340
6860
  nearestSegment,
6861
+ nextVectorCornerKind,
4341
6862
  nodeMatrix,
4342
6863
  parseSvgTransform,
4343
6864
  parseVectorSvg,
4344
6865
  polygonSubpath,
6866
+ polylineBounds,
6867
+ recognizeInkShape,
4345
6868
  rectSubpath,
6869
+ removeAnchorKeepingShape,
4346
6870
  removeAnchors,
6871
+ removeAnchorsKeepingShape,
6872
+ removeRedundantPoints,
4347
6873
  reverseSubpath,
4348
6874
  rotateMatrix,
4349
6875
  roundSubpaths,
4350
6876
  scaleFor,
4351
6877
  scaleMatrix,
6878
+ setAnchorCorner,
6879
+ setSubpathCorners,
6880
+ simplifyPolyline,
6881
+ smoothSubpath,
6882
+ splitIntoFaces,
6883
+ splitSubpathAt,
4352
6884
  starSubpath,
6885
+ subpathPointAt,
4353
6886
  subpathSegment,
4354
6887
  subpathSegmentCount,
4355
6888
  subpathsToBezier,
@@ -4360,7 +6893,11 @@ export {
4360
6893
  translateAnchor,
4361
6894
  translateMatrix,
4362
6895
  unionBounds,
6896
+ vectorCornerAt,
6897
+ vectorCorners,
6898
+ vectorCornersAttribute,
4363
6899
  vectorDocumentToSvg,
6900
+ vectorInkShapeName,
4364
6901
  vectorNodeBounds,
4365
6902
  vectorNodeRotation,
4366
6903
  vectorNodeToBezier,
@@ -4370,6 +6907,11 @@ export {
4370
6907
  vectorPathData,
4371
6908
  vectorPathWorldBezier,
4372
6909
  vectorResizeCursor,
6910
+ vectorStrokeOutline,
6911
+ vectorStrokeOutlinePath,
6912
+ vectorStrokeParts,
6913
+ vectorStrokeReach,
6914
+ vectorStrokeSvgParts,
4373
6915
  vectorToolLabel,
4374
6916
  vectorWorldMatrix,
4375
6917
  walkVectorNodes