@pixerate/editor 0.35.0 → 0.37.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.
@@ -10,37 +10,1011 @@ import {
10
10
  distanceToBezierPath,
11
11
  fitCubicWithTangents,
12
12
  fitCubics,
13
+ flattenChain,
13
14
  formatNumber,
14
15
  gradientToSvgDef,
16
+ isStraightCubic,
15
17
  normalizeBezierPath,
16
18
  outlineStroke,
17
19
  pathBoolean,
18
20
  pathToContours,
19
21
  pointInBezierPath,
22
+ polygonBoolean,
20
23
  segmentBounds,
21
24
  segmentDerivative,
22
25
  segmentIntersections,
26
+ segmentLength,
23
27
  segmentNearest,
28
+ segmentParamAtLength,
24
29
  segmentPoint,
30
+ segmentTangent,
25
31
  splitSegment,
26
32
  subSegment,
27
33
  toBezierPath,
28
34
  transformBezierPath,
35
+ trimSegments,
29
36
  windingNumber
30
- } from "../chunk-EGPIVUEF.js";
37
+ } from "../chunk-HPEKB352.js";
31
38
 
32
39
  // src/vector/types.ts
33
- var VECTOR_TOOLS = ["select", "direct", "pen", "pencil", "rect", "ellipse", "polygon", "star", "line", "scissors", "knife", "eraser"];
40
+ var VECTOR_CORNER_KINDS = ["round", "inverted", "chamfer"];
41
+ var VECTOR_ARROWHEADS = ["triangle", "arrow", "open", "circle", "square", "bar"];
42
+ var VECTOR_TOOLS = ["select", "direct", "pen", "pencil", "rect", "ellipse", "polygon", "star", "line", "scissors", "knife", "eraser", "width"];
34
43
  var VECTOR_TRANSFORM_HANDLES = ["nw", "n", "ne", "e", "se", "s", "sw", "w", "rotate"];
35
44
 
36
- // src/vector/geometry.ts
45
+ // src/vector/corners.ts
37
46
  var EPS = 1e-9;
47
+ var MIN_SINE = 1e-6;
48
+ var sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
49
+ var add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
50
+ var mul = (a, k) => ({ x: a.x * k, y: a.y * k });
51
+ var dot = (a, b) => a.x * b.x + a.y * b.y;
52
+ var cross = (a, b) => a.x * b.y - a.y * b.x;
53
+ var len = (a) => Math.hypot(a.x, a.y);
54
+ var pt = (p) => [p.x, p.y];
55
+ var vec = (p) => ({ x: p[0], y: p[1] });
56
+ var onAnchor = (a, h) => !h || Math.abs(h.x - a.x) < EPS && Math.abs(h.y - a.y) < EPS;
57
+ var Side = class {
58
+ curve;
59
+ straight;
60
+ length;
61
+ constructor(a, h1, h2, b) {
62
+ this.straight = onAnchor(a, h1) && onAnchor(b, h2);
63
+ this.curve = this.straight ? [pt(a), pt(b)] : [pt(a), pt(h1 ?? a), pt(h2 ?? b), pt(b)];
64
+ this.length = this.straight ? Math.hypot(b.x - a.x, b.y - a.y) : segmentLength(this.curve);
65
+ }
66
+ /** Unit direction the side leaves the corner in (towards its first control point off the corner). */
67
+ direction() {
68
+ const p0 = vec(this.curve[0]);
69
+ for (const q of this.curve.slice(1)) {
70
+ const d = sub(vec(q), p0);
71
+ const l = len(d);
72
+ if (l > EPS) return mul(d, 1 / l);
73
+ }
74
+ return null;
75
+ }
76
+ /** The point `s` along the side from the corner: its parameter, position and the direction the side runs on in there. */
77
+ at(s, u) {
78
+ if (this.straight) {
79
+ const t2 = this.length > EPS ? Math.min(1, Math.max(0, s / this.length)) : 0;
80
+ return { t: t2, p: add(vec(this.curve[0]), mul(u, s)), dir: u };
81
+ }
82
+ const t = segmentParamAtLength(this.curve, s, this.length);
83
+ const d = vec(segmentDerivative(this.curve, t));
84
+ const l = len(d);
85
+ return { t, p: vec(segmentPoint(this.curve, t)), dir: l > EPS ? mul(d, 1 / l) : u };
86
+ }
87
+ };
88
+ function cornerOf(sp, s, i) {
89
+ const n = sp.anchors.length;
90
+ if (n < 2 || !sp.closed && (i === 0 || i === n - 1)) return null;
91
+ if (n === 2 && sp.closed) return null;
92
+ const a = sp.anchors[i];
93
+ if (a.type === "smooth" || a.type === "symmetric") return null;
94
+ const prev = sp.anchors[(i - 1 + n) % n];
95
+ const next = sp.anchors[(i + 1) % n];
96
+ const sideU = new Side(a, a.in, prev.out, prev);
97
+ const sideV = new Side(a, a.out, next.in, next);
98
+ const u = sideU.direction();
99
+ const v2 = sideV.direction();
100
+ if (!u || !v2) return null;
101
+ const lu = sideU.length;
102
+ const lv = sideV.length;
103
+ if (!(lu > EPS && lv > EPS) || !Number.isFinite(lu) || !Number.isFinite(lv)) return null;
104
+ const cos = Math.max(-1, Math.min(1, dot(u, v2)));
105
+ const sin = Math.abs(cross(u, v2));
106
+ if (sin <= MIN_SINE) return null;
107
+ 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 };
108
+ }
109
+ var strip = ({ sideU: _u, sideV: _v, ...info }) => info;
110
+ function vectorCornerAt(sp, index, subpath = 0) {
111
+ const c = cornerOf(sp, subpath, index);
112
+ return c ? strip(c) : null;
113
+ }
114
+ function vectorCorners(subpaths) {
115
+ const out = [];
116
+ subpaths.forEach((sp, s) => {
117
+ for (let i = 0; i < sp.anchors.length; i++) {
118
+ const c = cornerOf(sp, s, i);
119
+ if (c) out.push(strip(c));
120
+ }
121
+ });
122
+ return out;
123
+ }
124
+ var cornerSetback = (c, r) => r * (1 + c.cos) / c.sin;
125
+ var fitCornerRadius = (c, radius) => Number.isFinite(radius) ? Math.min(Math.max(0, radius), c.maxRadius) : 0;
126
+ var cornerBisectorPoint = (c, k) => add(c.at, mul(add(c.u, c.v), k));
127
+ var cornerRadiusChange = (c, d) => dot(d, add(c.u, c.v)) * c.sin / (2 + 2 * c.cos);
128
+ function arcHandle(cos) {
129
+ const c = Math.max(-1, Math.min(1, cos));
130
+ const sinHalf = Math.sqrt(Math.max(0, (1 - c) / 2));
131
+ const cosHalf = Math.sqrt(Math.max(0, (1 + c) / 2));
132
+ return 4 / 3 * sinHalf / (1 + cosHalf);
133
+ }
134
+ function squareTo(a, b) {
135
+ const w = sub(b, mul(a, dot(a, b)));
136
+ const l = len(w);
137
+ return l > EPS ? mul(w, 1 / l) : { x: 0, y: 0 };
138
+ }
139
+ function cutCorner(c, radius, kind) {
140
+ const s = Math.min(cornerSetback(c, radius), Math.min(c.lu, c.lv) / 2);
141
+ if (!(s > EPS)) return null;
142
+ const a = c.sideU.at(s, c.u);
143
+ const b = c.sideV.at(s, c.v);
144
+ if (kind === "chamfer") return { tu: a.t, tv: b.t, start: a.p, out: null, in: null, end: b.p };
145
+ if (kind === "inverted") {
146
+ const h = s * arcHandle(dot(a.dir, b.dir));
147
+ 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 };
148
+ }
149
+ const d1 = mul(a.dir, -1);
150
+ const d2 = mul(b.dir, -1);
151
+ const turn = Math.acos(Math.max(-1, Math.min(1, -dot(d1, d2))));
152
+ const chord = len(sub(b.p, a.p));
153
+ const k = turn < 1e-9 ? chord / 3 : 4 / 3 * Math.tan(turn / 4) * chord / (2 * Math.sin(turn / 2));
154
+ if (!Number.isFinite(k)) return null;
155
+ 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 };
156
+ }
157
+ var radiusOf = (a) => {
158
+ const r = a.corner?.radius;
159
+ return typeof r === "number" && r > 0 && Number.isFinite(r) ? r : 0;
160
+ };
161
+ function hasVectorCorners(subpaths) {
162
+ return subpaths.some((sp) => sp.anchors.some((a) => radiusOf(a) > 0));
163
+ }
164
+ function expandVectorCornersOf(sp, subpath = 0) {
165
+ const n = sp.anchors.length;
166
+ const cuts = sp.anchors.map((a, i) => {
167
+ const r = radiusOf(a);
168
+ if (!r) return null;
169
+ const c = cornerOf(sp, subpath, i);
170
+ if (!c) return null;
171
+ const fitted = fitCornerRadius(c, r);
172
+ return fitted > EPS ? cutCorner(c, fitted, a.corner?.kind ?? "round") : null;
173
+ });
174
+ if (!cuts.some(Boolean)) return { subpath: { closed: sp.closed, anchors: sp.anchors.map(plain) }, sources: sp.anchors.map((_, i) => i) };
175
+ const trimmed = sp.anchors.map((a, j) => {
176
+ const k = (j + 1) % n;
177
+ if (k === 0 && !sp.closed) return void 0;
178
+ const b = sp.anchors[k];
179
+ const from = cuts[j];
180
+ const to = cuts[k];
181
+ if (!from && !to) return void 0;
182
+ if (onAnchor(a, a.out) && onAnchor(b, b.in)) return { out: null, in: null };
183
+ const s0 = from ? from.tv : 0;
184
+ const s1 = to ? 1 - to.tu : 1;
185
+ const seg = [pt(a), pt(a.out ?? a), pt(b.in ?? b), pt(b)];
186
+ const part = subSegment(seg, s0, Math.max(s0, s1));
187
+ return { out: vec(part[1]), in: vec(part[2]) };
188
+ });
189
+ const anchors = [];
190
+ const sources = [];
191
+ let closing = null;
192
+ const handleIn = (i, a) => {
193
+ const t = trimmed[(i - 1 + n) % n];
194
+ return t ? t.in : a.in ?? null;
195
+ };
196
+ const handleOut = (i, a) => {
197
+ const t = trimmed[i];
198
+ return t ? t.out : a.out ?? null;
199
+ };
200
+ sp.anchors.forEach((a, i) => {
201
+ const c = cuts[i];
202
+ if (!c) {
203
+ anchors.push(withHandles({ x: a.x, y: a.y, ...a.type ? { type: a.type } : {} }, handleIn(i, a), handleOut(i, a)));
204
+ sources.push(i);
205
+ return;
206
+ }
207
+ const start = withHandles({ x: c.start.x, y: c.start.y }, handleIn(i, a), c.out);
208
+ const end = withHandles({ x: c.end.x, y: c.end.y }, c.in, handleOut(i, a));
209
+ if (i === 0 && sp.closed) closing = start;
210
+ else {
211
+ anchors.push(start);
212
+ sources.push(i);
213
+ }
214
+ anchors.push(end);
215
+ sources.push(i);
216
+ });
217
+ if (closing) {
218
+ anchors.push(closing);
219
+ sources.push(0);
220
+ }
221
+ return { subpath: { closed: sp.closed, anchors }, sources };
222
+ }
223
+ function withHandles(a, hin, hout) {
224
+ if (hin && !onAnchor(a, hin)) a.in = { x: hin.x, y: hin.y };
225
+ if (hout && !onAnchor(a, hout)) a.out = { x: hout.x, y: hout.y };
226
+ return a;
227
+ }
228
+ function plain(a) {
229
+ if (!("corner" in a)) return a;
230
+ const { corner: _c, ...rest } = a;
231
+ return rest;
232
+ }
233
+ function expandVectorCorners(subpaths) {
234
+ if (!hasVectorCorners(subpaths)) return subpaths;
235
+ return subpaths.map((sp, s) => sp.anchors.some((a) => radiusOf(a) > 0) ? expandVectorCornersOf(sp, s).subpath : sp);
236
+ }
237
+ function setAnchorCorner(a, radius, kind) {
238
+ const { corner, ...rest } = a;
239
+ const r = Number.isFinite(radius) ? Math.max(0, radius) : 0;
240
+ const k = kind ?? corner?.kind;
241
+ if (r <= 0 && !kind) return rest;
242
+ return { ...rest, corner: { radius: r, ...k && k !== "round" ? { kind: k } : {} } };
243
+ }
244
+ function nextVectorCornerKind(kind) {
245
+ return kind === "inverted" ? "chamfer" : kind === "chamfer" ? "round" : "inverted";
246
+ }
247
+ function setSubpathCorners(sp, radius, kind) {
248
+ return { closed: sp.closed, anchors: sp.anchors.map((a) => setAnchorCorner(a, radius, kind)) };
249
+ }
250
+
251
+ // src/vector/width.ts
252
+ var VECTOR_WIDTH_PROFILE_MAX_POINTS = 64;
253
+ var VECTOR_WIDTH_PROFILE_MAX_SCALE = 1e3;
254
+ var VECTOR_WIDTH_SAME_T = 1e-6;
255
+ var sym = (t, s) => ({ t, left: s, right: s });
256
+ var VECTOR_WIDTH_PRESETS = [
257
+ { id: "uniform", label: "Uniform", points: [] },
258
+ { id: "taperStart", label: "Taper start", points: [sym(0, 0), sym(1, 1)] },
259
+ { id: "taperEnd", label: "Taper end", points: [sym(0, 1), sym(1, 0)] },
260
+ { id: "taperBoth", label: "Taper both", points: [sym(0, 0), sym(0.5, 1), sym(1, 0)] },
261
+ // Full width at the ends, swelling to twice the width in the middle.
262
+ { id: "bulge", label: "Bulge", points: [sym(0, 1), sym(0.5, 2), sym(1, 1)] },
263
+ // Full width at the ends, pinched to a quarter in the middle.
264
+ { id: "pinch", label: "Pinch", points: [sym(0, 1), sym(0.5, 0.25), sym(1, 1)] },
265
+ // A round head a fifth of the way along, then a long taper to the end.
266
+ { id: "teardrop", label: "Teardrop", points: [sym(0, 0), sym(0.2, 1), sym(1, 0)] },
267
+ // Two swells between narrow necks.
268
+ { id: "wave", label: "Wave", points: [sym(0, 0.3), sym(0.25, 1), sym(0.5, 0.3), sym(0.75, 1), sym(1, 0.3)] },
269
+ // A pen stroke: a quick press, a slow lift.
270
+ { id: "pressure", label: "Pressure", points: [sym(0, 0.15), sym(0.12, 0.85), sym(0.45, 1), sym(0.8, 0.7), sym(1, 0.05)] }
271
+ ];
272
+ var VECTOR_WIDTH_PRESET_IDS = VECTOR_WIDTH_PRESETS.map((p) => p.id);
273
+ var clean = (v2, max) => typeof v2 === "number" && Number.isFinite(v2) ? Math.min(Math.max(v2, 0), max) : null;
274
+ function sanitizeWidthProfile(points) {
275
+ if (!Array.isArray(points)) return [];
276
+ const out = [];
277
+ for (const p of points) {
278
+ if (!p || typeof p !== "object") continue;
279
+ const t = clean(p.t, 1);
280
+ const l = clean(p.left, VECTOR_WIDTH_PROFILE_MAX_SCALE);
281
+ const r = clean(p.right, VECTOR_WIDTH_PROFILE_MAX_SCALE);
282
+ if (t === null || l === null && r === null) continue;
283
+ out.push({ t, left: l ?? r, right: r ?? l });
284
+ if (out.length >= VECTOR_WIDTH_PROFILE_MAX_POINTS) break;
285
+ }
286
+ return out.map((p, i) => [p, i]).sort((a, b) => a[0].t - b[0].t || a[1] - b[1]).map(([p]) => p);
287
+ }
288
+ function isUniformWidthProfile(points) {
289
+ return !points?.length || points.every((p) => p.left === 1 && p.right === 1);
290
+ }
291
+ function secant(pts, i, k) {
292
+ const a = pts[i];
293
+ const b = pts[i + 1];
294
+ return a && b && b.t - a.t > 1e-12 ? (b[k] - a[k]) / (b.t - a.t) : 0;
295
+ }
296
+ function tangent(pts, i, k) {
297
+ const last = pts.length - 1;
298
+ if (i <= 0) return secant(pts, 0, k);
299
+ if (i >= last) return secant(pts, last - 1, k);
300
+ const d0 = secant(pts, i - 1, k);
301
+ const d1 = secant(pts, i, k);
302
+ return d0 * d1 <= 0 ? 0 : 2 * d0 * d1 / (d0 + d1);
303
+ }
304
+ function interp(pts, t, k) {
305
+ const first = pts[0];
306
+ const last = pts[pts.length - 1];
307
+ if (!(t > first.t)) return first[k];
308
+ if (t >= last.t) return last[k];
309
+ let i = 0;
310
+ while (i + 1 < pts.length && pts[i + 1].t <= t) i++;
311
+ const a = pts[i];
312
+ const b = pts[i + 1];
313
+ const h = b.t - a.t;
314
+ if (h <= 1e-12) return b[k];
315
+ const u = (t - a.t) / h;
316
+ const u2 = u * u;
317
+ const u3 = u2 * u;
318
+ const w = (2 * u3 - 3 * u2 + 1) * a[k] + (u3 - 2 * u2 + u) * h * tangent(pts, i, k) + (3 * u2 - 2 * u3) * b[k] + (u3 - u2) * h * tangent(pts, i + 1, k);
319
+ return Math.max(0, w);
320
+ }
321
+ function widthProfileAt(points, t) {
322
+ if (!points?.length) return { left: 1, right: 1 };
323
+ return { left: interp(points, t, "left"), right: interp(points, t, "right") };
324
+ }
325
+ function widthProfileMaxScale(points) {
326
+ if (!points?.length) return 1;
327
+ return points.reduce((m, p) => Math.max(m, p.left, p.right), 0);
328
+ }
329
+ function widthProfilePreset(id) {
330
+ const p = VECTOR_WIDTH_PRESETS.find((q) => q.id === id);
331
+ return p ? p.points.map((q) => ({ ...q })) : null;
332
+ }
333
+ function widthProfilePresetId(points) {
334
+ if (isUniformWidthProfile(points)) return "uniform";
335
+ const same = (a, b) => a.length === b.length && a.every((p, i) => Math.abs(p.t - b[i].t) < 1e-9 && Math.abs(p.left - b[i].left) < 1e-9 && Math.abs(p.right - b[i].right) < 1e-9);
336
+ return VECTOR_WIDTH_PRESETS.find((p) => p.points.length && same(points, p.points))?.id ?? "custom";
337
+ }
338
+ function reverseWidthProfile(points) {
339
+ return points.map((p) => ({ t: 1 - p.t, left: p.right, right: p.left })).reverse();
340
+ }
341
+ function mirrorWidthProfile(points) {
342
+ return points.map((p) => ({ t: p.t, left: p.right, right: p.left }));
343
+ }
344
+ function editable(points) {
345
+ return points?.length ? points.map((p) => ({ ...p })) : [sym(0, 1), sym(1, 1)];
346
+ }
347
+ function insertWidthPoint(points, t, sides = {}, options = {}) {
348
+ const pts = editable(points);
349
+ const tt = Math.min(Math.max(Number.isFinite(t) ? t : 0, 0), 1);
350
+ const here = widthProfileAt(points?.length ? points : null, tt);
351
+ const p = {
352
+ t: tt,
353
+ left: clean(sides.left, VECTOR_WIDTH_PROFILE_MAX_SCALE) ?? here.left,
354
+ right: clean(sides.right, VECTOR_WIDTH_PROFILE_MAX_SCALE) ?? here.right
355
+ };
356
+ const same = pts.findIndex((q) => Math.abs(q.t - tt) <= VECTOR_WIDTH_SAME_T);
357
+ if (same >= 0 && !options.step) {
358
+ pts[same] = { ...p, t: pts[same].t };
359
+ return { points: pts, index: same };
360
+ }
361
+ let i = 0;
362
+ while (i < pts.length && pts[i].t <= tt) i++;
363
+ if (pts.length >= VECTOR_WIDTH_PROFILE_MAX_POINTS) return { points: pts, index: Math.max(0, i - 1) };
364
+ pts.splice(i, 0, p);
365
+ return { points: pts, index: i };
366
+ }
367
+ function setWidthPoint(points, index, patch) {
368
+ const pts = points.map((p2) => ({ ...p2 }));
369
+ const p = pts[index];
370
+ if (!p) return pts;
371
+ if (typeof patch.t === "number" && Number.isFinite(patch.t)) {
372
+ const lo = index > 0 ? pts[index - 1].t : 0;
373
+ const hi = index < pts.length - 1 ? pts[index + 1].t : 1;
374
+ p.t = Math.min(Math.max(patch.t, lo), Math.max(hi, lo));
375
+ }
376
+ const l = clean(patch.left, VECTOR_WIDTH_PROFILE_MAX_SCALE);
377
+ const r = clean(patch.right, VECTOR_WIDTH_PROFILE_MAX_SCALE);
378
+ if (l !== null) p.left = l;
379
+ if (r !== null) p.right = r;
380
+ return pts;
381
+ }
382
+ function removeWidthPoints(points, indices) {
383
+ const drop = new Set(indices);
384
+ return points.filter((_, i) => !drop.has(i)).map((p) => ({ ...p }));
385
+ }
386
+ function ease(u, e) {
387
+ const x = Math.min(Math.max(u, 0), 1);
388
+ const round = Math.sqrt(Math.max(0, 1 - (1 - x) * (1 - x)));
389
+ return x + (round - x) * Math.min(Math.max(e, 0), 1);
390
+ }
391
+ function taperWaveFactor(t, taper, wave) {
392
+ let k = 1;
393
+ if (taper) {
394
+ let ls = Math.max(0, taper.startLength ?? 0);
395
+ let le = Math.max(0, taper.endLength ?? 0);
396
+ if (ls + le > 1) {
397
+ const f = 1 / (ls + le);
398
+ ls *= f;
399
+ le *= f;
400
+ }
401
+ const sw = Math.min(Math.max(taper.startWidth ?? 0, 0), 1);
402
+ const ew = Math.min(Math.max(taper.endWidth ?? 0, 0), 1);
403
+ if (ls > 0 && t < ls) k = Math.min(k, sw + (1 - sw) * ease(t / ls, taper.startEase ?? 0));
404
+ if (le > 0 && t > 1 - le) k = Math.min(k, ew + (1 - ew) * ease((1 - t) / le, taper.endEase ?? 0));
405
+ }
406
+ if (wave) {
407
+ const cycles = wave.cycles ?? 4;
408
+ const amount = Math.min(Math.max(wave.amount ?? 0.5, 0), 1);
409
+ if (cycles > 0 && amount > 0) {
410
+ const ph = 2 * Math.PI * t * cycles + (wave.phase ?? 0) * Math.PI / 180;
411
+ k *= 1 - amount * (0.5 - 0.5 * Math.cos(ph));
412
+ }
413
+ }
414
+ return k;
415
+ }
416
+ function generateWidthProfile(options) {
417
+ const cycles = options.wave ? Math.max(0, options.wave.cycles ?? 4) : 0;
418
+ const n = Math.max(8, Math.min(VECTOR_WIDTH_PROFILE_MAX_POINTS * 4, options.samples ?? Math.ceil(32 + cycles * 16)));
419
+ const raw = [];
420
+ for (let i = 0; i <= n; i++) raw.push([i / n, taperWaveFactor(i / n, options.taper, options.wave)]);
421
+ const ts = [options.taper?.startLength, options.taper?.endLength !== void 0 ? 1 - options.taper.endLength : void 0];
422
+ for (const t of ts) if (typeof t === "number" && t > 0 && t < 1) raw.push([t, taperWaveFactor(t, options.taper, options.wave)]);
423
+ raw.sort((a, b) => a[0] - b[0]);
424
+ const kept = simplifySamples(raw, options.tolerance ?? 0.01, VECTOR_WIDTH_PROFILE_MAX_POINTS);
425
+ return kept.map(([t, k]) => sym(t, k));
426
+ }
427
+ function simplifySamples(samples, tolerance, max) {
428
+ if (samples.length <= 2) return samples.slice();
429
+ const keep = new Array(samples.length).fill(false);
430
+ keep[0] = keep[samples.length - 1] = true;
431
+ const stack = [[0, samples.length - 1]];
432
+ while (stack.length) {
433
+ const [a, b] = stack.pop();
434
+ const [ta, va] = samples[a];
435
+ const [tb, vb] = samples[b];
436
+ let worst = -1;
437
+ let err = tolerance;
438
+ for (let i = a + 1; i < b; i++) {
439
+ const [t, v2] = samples[i];
440
+ const lin = tb - ta > 1e-12 ? va + (vb - va) * (t - ta) / (tb - ta) : va;
441
+ const e = Math.abs(v2 - lin);
442
+ if (e > err) {
443
+ err = e;
444
+ worst = i;
445
+ }
446
+ }
447
+ if (worst >= 0) {
448
+ keep[worst] = true;
449
+ stack.push([a, worst], [worst, b]);
450
+ }
451
+ }
452
+ let out = samples.filter((_, i) => keep[i]);
453
+ while (out.length > max) out = out.filter((_, i) => i === 0 || i === out.length - 1 || i % 2 === 0);
454
+ return out;
455
+ }
456
+ var VECTOR_PRESSURE_NOMINAL = 0.5;
457
+ function pressureWidthProfile(samples, options = {}) {
458
+ const raw = samples.filter((s) => Number.isFinite(s.t) && Number.isFinite(s.pressure)).map((s) => [Math.min(Math.max(s.t, 0), 1), Math.max(0.05, Math.min(1, Math.max(0, s.pressure)) / VECTOR_PRESSURE_NOMINAL)]);
459
+ if (raw.length < 2) return [];
460
+ if (raw.every((s) => Math.abs(s[1] - raw[0][1]) < 1e-6)) return [];
461
+ return simplifySamples(raw, options.tolerance ?? 0.02, VECTOR_WIDTH_PROFILE_MAX_POINTS).map(([t, k]) => sym(t, Math.round(k * 1e3) / 1e3));
462
+ }
463
+
464
+ // src/vector/varstroke.ts
465
+ var SAMPLES_PER_SUBPATH = 128;
466
+ var MAX_DASHES = 5e3;
467
+ var sub2 = (a, b) => [a[0] - b[0], a[1] - b[1]];
468
+ var along = (p, v2, k) => [p[0] + v2[0] * k, p[1] + v2[1] * k];
469
+ var lerp = (a, b, u) => [a[0] + (b[0] - a[0]) * u, a[1] + (b[1] - a[1]) * u];
470
+ var dist = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1]);
471
+ var unitOf = (v2) => {
472
+ const l = Math.hypot(v2[0], v2[1]);
473
+ return l > 0 ? [v2[0] / l, v2[1] / l] : [1, 0];
474
+ };
475
+ var leftNormal = (d) => [d[1], -d[0]];
476
+ function flatten(segments, closed, tol) {
477
+ const pts = [];
478
+ const corner = [];
479
+ const push = (p, isCorner) => {
480
+ const last = pts[pts.length - 1];
481
+ if (last && dist(last, p) <= 1e-9) {
482
+ corner[corner.length - 1] ||= isCorner;
483
+ return;
484
+ }
485
+ pts.push(p);
486
+ corner.push(isCorner);
487
+ };
488
+ for (const seg of segments) {
489
+ const flat = flattenChain([seg], tol);
490
+ if (dist(seg[0], seg[seg.length - 1]) <= 1e-12 && flat.length > 1) flat.push(seg[seg.length - 1]);
491
+ flat.forEach((p, i) => push(p, i === 0 || i === flat.length - 1));
492
+ }
493
+ if (closed && pts.length > 2 && dist(pts[0], pts[pts.length - 1]) > 1e-9) push(pts[0], true);
494
+ if (pts.length < 2) return null;
495
+ let total = 0;
496
+ for (let i = 1; i < pts.length; i++) total += dist(pts[i - 1], pts[i]);
497
+ if (!(total > 0)) return null;
498
+ const step = Math.max(total / SAMPLES_PER_SUBPATH, tol);
499
+ const fine = [pts[0]];
500
+ const fineCorner = [corner[0]];
501
+ for (let i = 1; i < pts.length; i++) {
502
+ const n = Math.max(1, Math.ceil(dist(pts[i - 1], pts[i]) / step));
503
+ for (let k = 1; k <= n; k++) {
504
+ fine.push(k === n ? pts[i] : lerp(pts[i - 1], pts[i], k / n));
505
+ fineCorner.push(k === n ? corner[i] : false);
506
+ }
507
+ }
508
+ const s = [0];
509
+ for (let i = 1; i < fine.length; i++) s.push(s[i - 1] + dist(fine[i - 1], fine[i]));
510
+ return { pts: fine, s, corner: fineCorner, closed };
511
+ }
512
+ function dashIntervals(pattern, offset, len2) {
513
+ let pat = pattern.map((v2) => Number.isFinite(v2) ? Math.max(0, v2) : 0);
514
+ if (pat.length % 2 === 1) pat = [...pat, ...pat];
515
+ const period = pat.reduce((a, b) => a + b, 0);
516
+ if (pat.length < 2 || !(period > 1e-9) || !(len2 > 0)) return [[0, len2]];
517
+ let pos = ((Number.isFinite(offset) ? offset : 0) % period + period) % period;
518
+ let idx = 0;
519
+ while (pos >= pat[idx]) {
520
+ pos -= pat[idx];
521
+ idx = (idx + 1) % pat.length;
522
+ }
523
+ const out = [];
524
+ let s = 0;
525
+ let guard = 0;
526
+ while (s < len2 && guard++ < MAX_DASHES * pat.length) {
527
+ const e = Math.min(s + pat[idx] - pos, len2);
528
+ if (idx % 2 === 0 && e >= s) out.push([s, e]);
529
+ s += pat[idx] - pos;
530
+ pos = 0;
531
+ idx = (idx + 1) % pat.length;
532
+ }
533
+ return out;
534
+ }
535
+ function at(poly, t) {
536
+ let i = 1;
537
+ while (i < poly.pts.length - 1 && poly.s[i] <= t) i++;
538
+ const a = poly.s[i - 1];
539
+ const b = poly.s[i];
540
+ const u = b > a ? Math.min(Math.max((t - a) / (b - a), 0), 1) : 0;
541
+ return [lerp(poly.pts[i - 1], poly.pts[i], u), i];
542
+ }
543
+ function arcPoints(c, a0, sweep, r0, r1, tol) {
544
+ const r = Math.max(r0, r1);
545
+ if (!(r > tol * 0.5) || !Number.isFinite(sweep)) return [];
546
+ const stepAngle = 2 * Math.acos(Math.min(Math.max(1 - tol / r, -1), 1));
547
+ const n = Math.min(128, Math.max(2, Math.ceil(Math.abs(sweep) / Math.max(stepAngle, 1e-3))));
548
+ const out = [];
549
+ for (let k = 1; k < n; k++) {
550
+ const f = k / n;
551
+ const a = a0 + sweep * f;
552
+ const rr = r0 + (r1 - r0) * f;
553
+ out.push([c[0] + Math.cos(a) * rr, c[1] + Math.sin(a) * rr]);
554
+ }
555
+ return out;
556
+ }
557
+ function crossing(a0, a1, b0, b1) {
558
+ const r = sub2(a1, a0);
559
+ const q = sub2(b1, b0);
560
+ const den = r[0] * q[1] - r[1] * q[0];
561
+ if (Math.abs(den) < 1e-12) return null;
562
+ const w = sub2(b0, a0);
563
+ const t = (w[0] * q[1] - w[1] * q[0]) / den;
564
+ const u = (w[0] * r[1] - w[1] * r[0]) / den;
565
+ return t > 1e-9 && t <= 1 && u >= 0 && u < 1 - 1e-9 ? [a0[0] + r[0] * t, a0[1] + r[1] * t] : null;
566
+ }
567
+ function variableStrokeRings(path, options) {
568
+ const half = options.width / 2;
569
+ if (!(half > 0) || !Number.isFinite(half)) return [];
570
+ const tol = options.tolerance && options.tolerance > 0 ? options.tolerance : 0.05;
571
+ const join = options.join ?? "miter";
572
+ const cap = options.cap ?? "butt";
573
+ const miterLimit = options.miterLimit && options.miterLimit >= 1 ? options.miterLimit : 4;
574
+ const rings = [];
575
+ const push = (out, p) => {
576
+ const last = out[out.length - 1];
577
+ if (!last || dist(last, p) > 1e-12) out.push(p);
578
+ };
579
+ for (const contour of pathToContours(path)) {
580
+ if (!contour.segments.length) continue;
581
+ const poly = flatten(contour.segments, contour.closed, Math.min(tol, half) / 4);
582
+ if (!poly) continue;
583
+ const len2 = poly.s[poly.s.length - 1];
584
+ const sides = (s) => {
585
+ const w = widthProfileAt(options.profile, s / len2);
586
+ return [w.left * half, w.right * half];
587
+ };
588
+ const intervals = options.dash?.some((v2) => v2 > 0) ? dashIntervals(options.dash, options.dashOffset ?? 0, len2) : [[0, len2]];
589
+ const whole = intervals.length === 1 && intervals[0][0] <= 0 && intervals[0][1] >= len2;
590
+ for (const [a, b] of intervals) {
591
+ const [pa, ia] = at(poly, a);
592
+ const [pb, ib] = at(poly, b);
593
+ const pts = [{ p: pa, s: a, corner: poly.corner[0] && a <= 0 }];
594
+ for (let i = ia; i < ib; i++) pts.push({ p: poly.pts[i], s: poly.s[i], corner: poly.corner[i] });
595
+ pts.push({ p: pb, s: b, corner: false });
596
+ const run = pts.filter((q, i) => i === 0 || dist(q.p, pts[i - 1].p) > 1e-9);
597
+ if (run.length < 2) {
598
+ const [l, r] = sides(a);
599
+ const w2 = (l + r) / 2;
600
+ if (cap === "round" && w2 > 0) rings.push([[pa[0] + w2, pa[1]], ...arcPoints(pa, 0, 2 * Math.PI, w2, w2, tol)]);
601
+ continue;
602
+ }
603
+ const closed = whole && poly.closed;
604
+ const m = run.length - 1;
605
+ const dirs = [];
606
+ for (let i = 0; i < m; i++) dirs.push(unitOf(sub2(run[i + 1].p, run[i].p)));
607
+ const normals = dirs.map(leftNormal);
608
+ const w = run.map((q) => sides(q.s));
609
+ const side = (sigma) => {
610
+ const k = sigma > 0 ? 0 : 1;
611
+ const S = (i) => along(run[i].p, normals[i], sigma * w[i][k]);
612
+ const E = (i) => along(run[i + 1].p, normals[i], sigma * w[i + 1][k]);
613
+ const out = [S(0)];
614
+ for (let i = 0; i < m; i++) {
615
+ const seam = i === m - 1;
616
+ if (seam && !closed) {
617
+ push(out, E(i));
618
+ break;
619
+ }
620
+ const j = seam ? 0 : i + 1;
621
+ const p = run[i + 1].p;
622
+ const d0 = dirs[i];
623
+ const d1 = dirs[j];
624
+ const cross2 = d0[0] * d1[1] - d0[1] * d1[0];
625
+ const dot2 = d0[0] * d1[0] + d0[1] * d1[1];
626
+ const r0 = w[i + 1][k];
627
+ const r1 = w[j][k];
628
+ const e = E(i);
629
+ const st = S(j);
630
+ if (Math.abs(cross2) < 1e-12 && dot2 > 0) {
631
+ push(out, e);
632
+ if (!seam) push(out, st);
633
+ continue;
634
+ }
635
+ const uturn = Math.abs(cross2) < 1e-12;
636
+ const outer = uturn ? sigma > 0 : cross2 * sigma > 0;
637
+ if (!outer) {
638
+ const x = seam ? null : crossing(out[out.length - 1], e, st, E(j));
639
+ if (x) push(out, x);
640
+ else {
641
+ push(out, e);
642
+ push(out, p);
643
+ if (!seam) push(out, st);
644
+ }
645
+ continue;
646
+ }
647
+ push(out, e);
648
+ const theta = uturn ? Math.PI : Math.atan2(cross2, dot2);
649
+ const r = (r0 + r1) / 2;
650
+ const kind = run[i + 1].corner || seam && poly.corner[0] ? join : "round";
651
+ if (r > 1e-9) {
652
+ if (kind === "round") {
653
+ if (r * (1 - Math.cos(Math.abs(theta) / 2)) > tol * 0.25) {
654
+ for (const q of arcPoints(p, Math.atan2(e[1] - p[1], e[0] - p[0]), theta, r0, r1, tol)) push(out, q);
655
+ }
656
+ } else if (kind === "miter") {
657
+ const halfCos = Math.cos(theta / 2);
658
+ const bis = [normals[i][0] + normals[j][0], normals[i][1] + normals[j][1]];
659
+ const bl = Math.hypot(bis[0], bis[1]);
660
+ if (halfCos > 1e-6 && 1 / halfCos <= miterLimit && bl > 1e-9) push(out, along(p, bis, sigma * r / halfCos / bl));
661
+ }
662
+ }
663
+ if (!seam) push(out, st);
664
+ }
665
+ if (closed && out.length > 1 && dist(out[0], out[out.length - 1]) <= 1e-12) out.pop();
666
+ return out;
667
+ };
668
+ const left = side(1);
669
+ const right = side(-1);
670
+ if (closed) {
671
+ rings.push(left, right.reverse());
672
+ continue;
673
+ }
674
+ const capPoints = (atStart) => {
675
+ if (cap === "butt") return [];
676
+ const i = atStart ? 0 : m;
677
+ const d = atStart ? dirs[0] : dirs[m - 1];
678
+ const [l, r] = w[i];
679
+ if (!(l + r > 1e-9)) return [];
680
+ const p = run[i].p;
681
+ const n = leftNormal(d);
682
+ const L = along(p, n, l);
683
+ const R = along(p, n, -r);
684
+ if (cap === "round") {
685
+ const a0 = Math.atan2(d[1], d[0]) + (atStart ? Math.PI / 2 : -Math.PI / 2);
686
+ return arcPoints(p, a0, Math.PI, atStart ? r : l, atStart ? l : r, tol);
687
+ }
688
+ const ext = (l + r) / 2;
689
+ const o = atStart ? [-d[0], -d[1]] : d;
690
+ return atStart ? [along(R, o, ext), along(L, o, ext)] : [along(L, o, ext), along(R, o, ext)];
691
+ };
692
+ const ring = [];
693
+ for (const q of [...left, ...capPoints(false), ...right.reverse(), ...capPoints(true)]) push(ring, q);
694
+ if (ring.length > 2) rings.push(ring);
695
+ }
696
+ }
697
+ return rings;
698
+ }
699
+ function variableStrokeOutline(path, options) {
700
+ const pieces = variableStrokeRings(path, options);
701
+ if (!pieces.length) return [];
702
+ const tol = options.tolerance && options.tolerance > 0 ? options.tolerance : 0.05;
703
+ let polygons;
704
+ try {
705
+ polygons = polygonBoolean("union", [{ rings: pieces, fillRule: "nonzero" }]);
706
+ } catch {
707
+ return [];
708
+ }
709
+ const out = [];
710
+ for (const polygon2 of polygons) {
711
+ for (const ring of polygon2) {
712
+ if (ring.length < 3) continue;
713
+ const cubics = fitCubics(ring, tol, { closed: true, cornerAngleDeg: 25 });
714
+ if (!cubics.length) continue;
715
+ out.push({ type: "M", x: cubics[0].p0[0], y: cubics[0].p0[1] });
716
+ cubics.forEach((c, i) => {
717
+ if (isStraightCubic(c, tol * 0.02)) {
718
+ if (i < cubics.length - 1) out.push({ type: "L", x: c.p3[0], y: c.p3[1] });
719
+ } else out.push({ type: "C", x1: c.p1[0], y1: c.p1[1], x2: c.p2[0], y2: c.p2[1], x: c.p3[0], y: c.p3[1] });
720
+ });
721
+ out.push({ type: "Z" });
722
+ }
723
+ }
724
+ return out;
725
+ }
726
+ function nearestStrokePosition(path, p, only) {
727
+ let best = null;
728
+ pathToContours(path).forEach((c, ci) => {
729
+ if (!c.segments.length || only !== void 0 && ci !== only) return;
730
+ const lengths = c.segments.map((s) => segmentLength(s));
731
+ const total = lengths.reduce((a, b) => a + b, 0);
732
+ if (!(total > 0)) return;
733
+ let acc = 0;
734
+ c.segments.forEach((seg, si) => {
735
+ const near2 = segmentNearest(seg, p);
736
+ if (!best || near2.distance < best.distance) {
737
+ const s = acc + segmentLength(seg, 0, near2.t);
738
+ const tan = segmentTangent(seg, near2.t);
739
+ const pt2 = seg.length === 2 ? lerp(seg[0], seg[1], near2.t) : evalSeg(seg, near2.t);
740
+ const n = leftNormal([tan[0], tan[1]]);
741
+ const side = (p[0] - pt2[0]) * n[0] + (p[1] - pt2[1]) * n[1] >= 0 ? "left" : "right";
742
+ best = { contour: ci, t: Math.min(Math.max(s / total, 0), 1), distance: near2.distance, side, point: pt2, tangent: [tan[0], tan[1]], length: total };
743
+ }
744
+ acc += lengths[si];
745
+ });
746
+ });
747
+ return best;
748
+ }
749
+ function evalSeg(seg, t) {
750
+ let pts = seg.map((q) => [q[0], q[1]]);
751
+ while (pts.length > 1) pts = pts.slice(1).map((q, i) => lerp(pts[i], q, t));
752
+ return pts[0];
753
+ }
754
+ function strokePositionPoint(path, index, t) {
755
+ const c = pathToContours(path)[index];
756
+ if (!c?.segments.length) return null;
757
+ const lengths = c.segments.map((s) => segmentLength(s));
758
+ const total = lengths.reduce((a, b) => a + b, 0);
759
+ let want = Math.min(Math.max(t, 0), 1) * total;
760
+ for (let i = 0; i < c.segments.length; i++) {
761
+ const seg = c.segments[i];
762
+ if (want <= lengths[i] + 1e-12 || i === c.segments.length - 1) {
763
+ let lo = 0;
764
+ let hi = 1;
765
+ const target = Math.min(want, lengths[i]);
766
+ for (let k = 0; k < 40; k++) {
767
+ const mid = (lo + hi) / 2;
768
+ if (segmentLength(seg, 0, mid) < target) lo = mid;
769
+ else hi = mid;
770
+ }
771
+ const u = (lo + hi) / 2;
772
+ const tan = segmentTangent(seg, u);
773
+ return { point: evalSeg(seg, u), tangent: [tan[0], tan[1]] };
774
+ }
775
+ want -= lengths[i];
776
+ }
777
+ return null;
778
+ }
779
+
780
+ // src/vector/stroke.ts
781
+ var KAPPA = 0.5522847498307936;
782
+ var NOTCH = 0.7;
783
+ function isClosedBezierShape(path) {
784
+ const contours = pathToContours(path).filter((c) => c.segments.length > 0);
785
+ return contours.length > 0 && contours.every((c) => c.closed);
786
+ }
787
+ function effectiveStrokeAlign(style, closedShape) {
788
+ const a = style.strokeAlign ?? "center";
789
+ return closedShape ? a : "center";
790
+ }
791
+ function strokeWidthProfileOf(style) {
792
+ if (!style.strokeWidthProfile?.length) return null;
793
+ const p = sanitizeWidthProfile(style.strokeWidthProfile);
794
+ return isUniformWidthProfile(p) ? null : p;
795
+ }
796
+ function hasStrokeExtras(style) {
797
+ return (style.strokeAlign ?? "center") !== "center" || !!style.startArrow || !!style.endArrow || !!strokeWidthProfileOf(style);
798
+ }
799
+ function vectorStrokeLineArea(parts, style, tolerance) {
800
+ const profile = strokeWidthProfileOf(style);
801
+ const dash = style.strokeDasharray?.length ? { dash: style.strokeDasharray } : {};
802
+ const join = style.strokeLinejoin ?? "miter";
803
+ const cap = style.strokeLinecap ?? "butt";
804
+ if (profile) return variableStrokeOutline(parts.line, { width: parts.drawWidth, profile, join, cap, ...dash, ...tolerance ? { tolerance } : {} });
805
+ return outlineStroke(parts.line, { width: parts.drawWidth, join, cap, ...dash, ...tolerance ? { tolerance } : {} });
806
+ }
807
+ var capExtent = (style, width) => style.strokeLinecap === "round" || style.strokeLinecap === "square" ? width / 2 : 0;
808
+ function arrowheadWeight(style, end) {
809
+ const scale = end === "start" ? style.startArrowScale : style.endArrowScale;
810
+ const s = typeof scale === "number" && scale > 0 && Number.isFinite(scale) ? scale : 1;
811
+ return Math.max(0.25, (style.strokeWidth ?? 1) * s);
812
+ }
813
+ function arrowheadInset(kind, hw, cap) {
814
+ const l = 4 * hw;
815
+ const solid = (back) => Math.max(back / 2, cap);
816
+ switch (kind) {
817
+ case "triangle":
818
+ case "circle":
819
+ case "square":
820
+ return solid(l);
821
+ case "arrow":
822
+ return solid(NOTCH * l);
823
+ case "bar":
824
+ return solid(0.75 * hw);
825
+ case "open":
826
+ return Math.max(hw * Math.sqrt(l * l + l * l / 4) / (l / 2), cap);
827
+ }
828
+ }
829
+ var v = (x, y) => ({ x, y });
830
+ var plus = (a, b, k = 1) => ({ x: a.x + b.x * k, y: a.y + b.y * k });
831
+ var unit = (a) => {
832
+ const l = Math.hypot(a.x, a.y);
833
+ return l > 0 ? { x: a.x / l, y: a.y / l } : { x: 1, y: 0 };
834
+ };
835
+ function polygon(pts) {
836
+ 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" }];
837
+ }
838
+ function circle(c, r) {
839
+ const k = r * KAPPA;
840
+ const { x, y } = c;
841
+ return [
842
+ { type: "M", x: x + r, y },
843
+ { type: "C", x1: x + r, y1: y + k, x2: x + k, y2: y + r, x, y: y + r },
844
+ { type: "C", x1: x - k, y1: y + r, x2: x - r, y2: y + k, x: x - r, y },
845
+ { type: "C", x1: x - r, y1: y - k, x2: x - k, y2: y - r, x, y: y - r },
846
+ { type: "C", x1: x + k, y1: y - r, x2: x + r, y2: y - k, x: x + r, y },
847
+ { type: "Z" }
848
+ ];
849
+ }
850
+ function arrowheadPath(kind, tip, dir, hw) {
851
+ const n = v(-dir.y, dir.x);
852
+ const l = 4 * hw;
853
+ const half = l / 2;
854
+ const base = plus(tip, dir, -l);
855
+ switch (kind) {
856
+ case "triangle":
857
+ return polygon([tip, plus(base, n, half), plus(base, n, -half)]);
858
+ case "arrow":
859
+ return polygon([tip, plus(base, n, half), plus(tip, dir, -NOTCH * l), plus(base, n, -half)]);
860
+ case "open": {
861
+ const t = hw;
862
+ const a = plus(base, n, half);
863
+ const b = plus(base, n, -half);
864
+ const ua = unit(plus(a, tip, -1));
865
+ const ub = unit(plus(b, tip, -1));
866
+ const na = v(-ua.y, ua.x);
867
+ const nb = v(ub.y, -ub.x);
868
+ const inner = plus(tip, dir, -(t * Math.hypot(a.x - tip.x, a.y - tip.y)) / half);
869
+ return polygon([tip, a, plus(a, na, t), inner, plus(b, nb, t), b]);
870
+ }
871
+ case "circle":
872
+ return circle(plus(tip, dir, -half), half);
873
+ case "square":
874
+ return polygon([plus(tip, n, half), plus(base, n, half), plus(base, n, -half), plus(tip, n, -half)]);
875
+ case "bar": {
876
+ const t = 0.75 * hw;
877
+ return polygon([plus(tip, n, half), plus(plus(tip, n, half), dir, -t), plus(plus(tip, n, -half), dir, -t), plus(tip, n, -half)]);
878
+ }
879
+ }
880
+ }
881
+ var toVec = (p) => ({ x: p[0], y: p[1] });
882
+ var segLen = (s) => segmentLength(s);
883
+ function endOf(segs, atStart) {
884
+ const long = (s2) => segLen(s2) > 1e-9;
885
+ if (atStart) {
886
+ const s2 = segs.find(long) ?? segs[0];
887
+ const t2 = segmentTangent(s2, 0);
888
+ return { p: toVec(segs[0][0]), out: v(-t2[0], -t2[1]) };
889
+ }
890
+ const s = [...segs].reverse().find(long) ?? segs[segs.length - 1];
891
+ const t = segmentTangent(s, 1);
892
+ const last = segs[segs.length - 1];
893
+ return { p: toVec(last[last.length - 1]), out: v(t[0], t[1]) };
894
+ }
895
+ function pointAlong(segs, fromStart, dist4) {
896
+ const order = fromStart ? segs : [...segs].reverse();
897
+ let acc = 0;
898
+ for (const s of order) {
899
+ const l = segLen(s);
900
+ if (acc + l >= dist4) {
901
+ const want = fromStart ? dist4 - acc : l - (dist4 - acc);
902
+ const pts = trimSegments([s], 0, want);
903
+ const p = pts.length ? pts[0][pts[0].length - 1] : s[0];
904
+ return toVec(p);
905
+ }
906
+ acc += l;
907
+ }
908
+ return null;
909
+ }
910
+ function vectorStrokeParts(path, style) {
911
+ const width = style.strokeWidth ?? 1;
912
+ const contours = pathToContours(path).filter((c) => c.segments.length > 0);
913
+ const closedShape = contours.length > 0 && contours.every((c) => c.closed);
914
+ const align = effectiveStrokeAlign(style, closedShape);
915
+ const drawWidth = align === "center" ? width : width * 2;
916
+ const heads = [];
917
+ if (!contours.length || !style.startArrow && !style.endArrow) return { align, line: path, drawWidth, heads };
918
+ const first = contours[0];
919
+ const last = contours[contours.length - 1];
920
+ const cap = capExtent(style, width);
921
+ const tipMode = style.arrowAlign === "tip";
922
+ const trims = contours.map(() => [0, 0]);
923
+ const place = (end, contour, ci) => {
924
+ const kind = end === "start" ? style.startArrow : style.endArrow;
925
+ if (!kind || contour.closed) return;
926
+ const hw = arrowheadWeight(style, end);
927
+ const inset = arrowheadInset(kind, hw, cap);
928
+ const atStart = end === "start";
929
+ const { p, out } = endOf(contour.segments, atStart);
930
+ let tip;
931
+ let dir;
932
+ if (tipMode) {
933
+ const q = pointAlong(contour.segments, atStart, inset);
934
+ const d = q ? v(p.x - q.x, p.y - q.y) : null;
935
+ dir = d && Math.hypot(d.x, d.y) > 1e-9 ? unit(d) : out;
936
+ tip = p;
937
+ trims[ci][atStart ? 0 : 1] = inset;
938
+ } else {
939
+ tip = plus(p, out, inset);
940
+ dir = out;
941
+ }
942
+ heads.push({ end, kind, tip, dir, inset, path: arrowheadPath(kind, tip, dir, hw) });
943
+ };
944
+ place("start", first, 0);
945
+ place("end", last, contours.length - 1);
946
+ if (!tipMode || !heads.length) return { align, line: path, drawWidth, heads };
947
+ const trimmed = contours.map((c, i) => {
948
+ const [s0, s1] = trims[i];
949
+ if (!s0 && !s1) return c;
950
+ const lengths = c.segments.map(segLen);
951
+ const total = lengths.reduce((a, b) => a + b, 0);
952
+ return { closed: false, segments: total - s1 > s0 ? trimSegments(c.segments, s0, total - s1, lengths) : [] };
953
+ });
954
+ return { align, line: contoursToPath(trimmed), drawWidth, heads };
955
+ }
956
+ function vectorStrokeOutlinePath(path, style) {
957
+ const parts = vectorStrokeParts(path, style);
958
+ const stroke = vectorStrokeLineArea(parts, style);
959
+ const fill = { path, fillRule: style.fillRule ?? "nonzero" };
960
+ let area = stroke;
961
+ if (parts.align === "inside") area = pathBoolean("intersection", [stroke, fill]).path;
962
+ else if (parts.align === "outside") area = pathBoolean("difference", [stroke, fill]).path;
963
+ if (!parts.heads.length) return area;
964
+ return pathBoolean("union", [area, ...parts.heads.map((h) => h.path)]).path;
965
+ }
966
+ function hitTestVectorStroke(path, p, style, tolerance = 0) {
967
+ const parts = vectorStrokeParts(path, style);
968
+ const point = [p.x, p.y];
969
+ const width = style.strokeWidth ?? 1;
970
+ for (const h of parts.heads) {
971
+ if (pointInBezierPath(h.path, point) || tolerance > 0 && distanceToBezierPath(h.path, point) <= tolerance) return true;
972
+ }
973
+ if (!parts.line.length) return false;
974
+ const profile = strokeWidthProfileOf(style);
975
+ let d;
976
+ let reach = width / 2;
977
+ if (profile) {
978
+ const near2 = nearestStrokePosition(parts.line, point);
979
+ if (!near2) return false;
980
+ d = near2.distance;
981
+ const w = widthProfileAt(profile, near2.t);
982
+ reach = width / 2 * (near2.side === "left" ? w.left : w.right);
983
+ if (parts.align !== "center") reach *= 2;
984
+ const atEnd = near2.t <= 1e-9 || near2.t >= 1 - 1e-9;
985
+ if (atEnd && (style.strokeLinecap === "round" || style.strokeLinecap === "square") && d > reach) {
986
+ reach = Math.max(reach, width / 2 * (w.left + w.right) / 2);
987
+ }
988
+ } else {
989
+ d = distanceToBezierPath(parts.line, point);
990
+ if (parts.align !== "center") reach = width;
991
+ }
992
+ if (parts.align === "center") return d <= reach + tolerance;
993
+ if (d > reach + tolerance) return false;
994
+ if (d <= tolerance) return true;
995
+ const inside = pointInBezierPath(path, point, style.fillRule ?? "nonzero");
996
+ return parts.align === "inside" ? inside : !inside;
997
+ }
998
+ function vectorStrokeReach(style) {
999
+ const width = (style.strokeWidth ?? 1) * widthProfileMaxScale(strokeWidthProfileOf(style));
1000
+ const align = style.strokeAlign ?? "center";
1001
+ let reach = align === "center" ? width / 2 : align === "outside" ? width : 0;
1002
+ if (style.strokeLinejoin !== "round" && style.strokeLinejoin !== "bevel") reach *= 4;
1003
+ for (const end of ["start", "end"]) {
1004
+ if (end === "start" ? style.startArrow : style.endArrow) reach = Math.max(reach, 8 * arrowheadWeight(style, end) + width);
1005
+ }
1006
+ return reach;
1007
+ }
1008
+
1009
+ // src/vector/geometry.ts
1010
+ var EPS2 = 1e-9;
38
1011
  var cloneAnchor = (a) => ({
39
1012
  x: a.x,
40
1013
  y: a.y,
41
1014
  ...a.in ? { in: { x: a.in.x, y: a.in.y } } : {},
42
1015
  ...a.out ? { out: { x: a.out.x, y: a.out.y } } : {},
43
- ...a.type && a.type !== "corner" ? { type: a.type } : {}
1016
+ ...a.type && a.type !== "corner" ? { type: a.type } : {},
1017
+ ...a.corner ? { corner: { ...a.corner } } : {}
44
1018
  });
45
1019
  var cloneSubpath = (s) => ({ anchors: s.anchors.map(cloneAnchor), closed: s.closed });
46
1020
  function cloneVectorNode(node) {
@@ -58,7 +1032,7 @@ function hasPaint(paint) {
58
1032
  return true;
59
1033
  }
60
1034
  var handleOf = (a, side) => a[side] ?? { x: a.x, y: a.y };
61
- var isOnAnchor = (a, h) => !h || Math.abs(h.x - a.x) < EPS && Math.abs(h.y - a.y) < EPS;
1035
+ var isOnAnchor = (a, h) => !h || Math.abs(h.x - a.x) < EPS2 && Math.abs(h.y - a.y) < EPS2;
62
1036
  function subpathSegmentCount(sp) {
63
1037
  const n = sp.anchors.length;
64
1038
  if (n < 2) return 0;
@@ -72,9 +1046,9 @@ function subpathSegment(sp, i) {
72
1046
  const c2 = handleOf(b, "in");
73
1047
  return [[a.x, a.y], [c1.x, c1.y], [c2.x, c2.y], [b.x, b.y]];
74
1048
  }
75
- function subpathsToBezier(subpaths) {
1049
+ function subpathsToBezier(subpaths, options = {}) {
76
1050
  const out = [];
77
- for (const sp of subpaths) {
1051
+ for (const sp of options.corners === false ? subpaths : expandVectorCorners(subpaths)) {
78
1052
  if (sp.anchors.length === 0) continue;
79
1053
  const first = sp.anchors[0];
80
1054
  out.push({ type: "M", x: first.x, y: first.y });
@@ -104,9 +1078,9 @@ function inferAnchorType(a) {
104
1078
  const oy = a.out.y - a.y;
105
1079
  const li = Math.hypot(ix, iy);
106
1080
  const lo = Math.hypot(ox, oy);
107
- const cross = (ix * oy - iy * ox) / (li * lo);
108
- const dot = (ix * ox + iy * oy) / (li * lo);
109
- if (Math.abs(cross) > 1e-3 || dot > 0) return "corner";
1081
+ const cross2 = (ix * oy - iy * ox) / (li * lo);
1082
+ const dot2 = (ix * ox + iy * oy) / (li * lo);
1083
+ if (Math.abs(cross2) > 1e-3 || dot2 > 0) return "corner";
110
1084
  return Math.abs(li - lo) <= 1e-3 * Math.max(li, lo) ? "symmetric" : "smooth";
111
1085
  }
112
1086
  function bezierToSubpaths(input) {
@@ -249,8 +1223,12 @@ function hitTestVectorNode(node, p, tolerance = 0, parent = IDENTITY_MATRIX) {
249
1223
  if (path.length === 0) return false;
250
1224
  const point = [p.x, p.y];
251
1225
  if (hasPaint(node.fill) && pointInBezierPath(path, point, node.fillRule ?? "nonzero")) return true;
252
- const reach = (hasPaint(node.stroke) ? (node.strokeWidth ?? 1) / 2 : 0) + tolerance;
253
- return distanceToBezierPath(path, point) <= reach;
1226
+ if (hasPaint(node.stroke)) return hitTestVectorStroke(path, p, node, tolerance);
1227
+ return distanceToBezierPath(path, point) <= tolerance;
1228
+ }
1229
+ function vectorStrokeOutline(node, parent = IDENTITY_MATRIX) {
1230
+ if (!hasPaint(node.stroke) || !((node.strokeWidth ?? 1) > 0)) return [];
1231
+ return vectorStrokeOutlinePath(vectorPathWorldBezier(node, parent), node);
254
1232
  }
255
1233
  function hitTestVectorNodes(nodes, p, tolerance = 0) {
256
1234
  for (let i = nodes.length - 1; i >= 0; i--) {
@@ -268,8 +1246,8 @@ function nearestSegment(node, p) {
268
1246
  const seg = subpathSegment(sp, i);
269
1247
  const hit = segmentNearest(seg, [p.x, p.y]);
270
1248
  if (!best || hit.distance < best.distance) {
271
- const pt = segmentPoint(seg, hit.t);
272
- best = { subpath: s, segment: i, t: hit.t, point: { x: pt[0], y: pt[1] }, distance: hit.distance };
1249
+ const pt2 = segmentPoint(seg, hit.t);
1250
+ best = { subpath: s, segment: i, t: hit.t, point: { x: pt2[0], y: pt2[1] }, distance: hit.distance };
273
1251
  }
274
1252
  }
275
1253
  });
@@ -303,8 +1281,8 @@ function invertMatrix(m) {
303
1281
  if (!det || !Number.isFinite(det)) return IDENTITY_MATRIX;
304
1282
  return [d / det, -b / det, -c / det, a / det, (c * f - d * e) / det, (b * e - a * f) / det];
305
1283
  }
306
- function applyMatrixLinear(m, v) {
307
- return { x: m[0] * v.x + m[2] * v.y, y: m[1] * v.x + m[3] * v.y };
1284
+ function applyMatrixLinear(m, v2) {
1285
+ return { x: m[0] * v2.x + m[2] * v2.y, y: m[1] * v2.x + m[3] * v2.y };
308
1286
  }
309
1287
  function matrixRotation(m) {
310
1288
  const [a, b, c, d] = m;
@@ -328,8 +1306,10 @@ function bakeIntoGeometry(node, m) {
328
1306
  if (isIdentityMatrix(m)) return node;
329
1307
  if (node.type === "group") return { ...node, children: node.children.map((c) => transformVectorNode(c, m)) };
330
1308
  const path = node;
1309
+ const mirrored = m[0] * m[3] - m[1] * m[2] < 0 && path.strokeWidthProfile?.length;
331
1310
  return {
332
1311
  ...path,
1312
+ ...mirrored ? { strokeWidthProfile: mirrorWidthProfile(path.strokeWidthProfile) } : {},
333
1313
  subpaths: path.subpaths.map((sp) => ({
334
1314
  closed: sp.closed,
335
1315
  anchors: sp.anchors.map((a) => {
@@ -338,6 +1318,7 @@ function bakeIntoGeometry(node, m) {
338
1318
  if (a.in) next.in = applyMatrix(m, a.in);
339
1319
  if (a.out) next.out = applyMatrix(m, a.out);
340
1320
  if (a.type) next.type = a.type;
1321
+ if (a.corner) next.corner = { ...a.corner };
341
1322
  return next;
342
1323
  })
343
1324
  }))
@@ -389,12 +1370,12 @@ function moveAnchorHandle(a, side, pos, breakHandles = false) {
389
1370
  if (breakHandles) delete next.type;
390
1371
  const dx = pos.x - a.x;
391
1372
  const dy = pos.y - a.y;
392
- const len = Math.hypot(dx, dy);
1373
+ const len2 = Math.hypot(dx, dy);
393
1374
  if (type === "symmetric") next[other] = { x: a.x - dx, y: a.y - dy };
394
- else if (type === "smooth" && len > EPS) {
1375
+ else if (type === "smooth" && len2 > EPS2) {
395
1376
  const o = a[other];
396
- const olen = o ? Math.hypot(o.x - a.x, o.y - a.y) : len;
397
- next[other] = { x: a.x - dx / len * olen, y: a.y - dy / len * olen };
1377
+ const olen = o ? Math.hypot(o.x - a.x, o.y - a.y) : len2;
1378
+ next[other] = { x: a.x - dx / len2 * olen, y: a.y - dy / len2 * olen };
398
1379
  }
399
1380
  if (isOnAnchor(next, next.in)) delete next.in;
400
1381
  if (isOnAnchor(next, next.out)) delete next.out;
@@ -421,15 +1402,15 @@ function convertAnchor(sp, index, type) {
421
1402
  else if (prev && next) dir = { x: next.x - prev.x, y: next.y - prev.y };
422
1403
  else if (next) dir = { x: next.x - a.x, y: next.y - a.y };
423
1404
  else if (prev) dir = { x: a.x - prev.x, y: a.y - prev.y };
424
- if (!dir || Math.hypot(dir.x, dir.y) < EPS) return { anchors, closed: sp.closed };
1405
+ if (!dir || Math.hypot(dir.x, dir.y) < EPS2) return { anchors, closed: sp.closed };
425
1406
  const dl = Math.hypot(dir.x, dir.y);
426
1407
  const ux = dir.x / dl;
427
1408
  const uy = dir.y / dl;
428
1409
  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;
429
1410
  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;
430
1411
  if (type === "symmetric") lenIn = lenOut = (lenIn + lenOut) / 2 || Math.max(lenIn, lenOut);
431
- if (lenIn > EPS) a.in = { x: a.x - ux * lenIn, y: a.y - uy * lenIn };
432
- if (lenOut > EPS) a.out = { x: a.x + ux * lenOut, y: a.y + uy * lenOut };
1412
+ if (lenIn > EPS2) a.in = { x: a.x - ux * lenIn, y: a.y - uy * lenIn };
1413
+ if (lenOut > EPS2) a.out = { x: a.x + ux * lenOut, y: a.y + uy * lenOut };
433
1414
  a.type = type;
434
1415
  return { anchors, closed: sp.closed };
435
1416
  }
@@ -465,37 +1446,24 @@ function reverseSubpath(sp) {
465
1446
  if (a.out) next.in = { ...a.out };
466
1447
  if (a.in) next.out = { ...a.in };
467
1448
  if (a.type) next.type = a.type;
1449
+ if (a.corner) next.corner = { ...a.corner };
468
1450
  return next;
469
1451
  });
470
1452
  return { anchors, closed: sp.closed };
471
1453
  }
472
- var KAPPA = 0.5522847498307936;
473
- function rectSubpath(b, radius = 0) {
1454
+ var KAPPA2 = 0.5522847498307936;
1455
+ function rectSubpath(b, radius = 0, kind) {
474
1456
  const { x, y, width: w, height: h } = b;
475
- const r = Math.max(0, Math.min(radius, w / 2, h / 2));
476
- if (r <= EPS) {
477
- return { anchors: [{ x, y }, { x: x + w, y }, { x: x + w, y: y + h }, { x, y: y + h }], closed: true };
478
- }
479
- const k = r * KAPPA;
480
- const anchors = [
481
- { x: x + r, y, in: { x: x + r - k, y } },
482
- { x: x + w - r, y, out: { x: x + w - r + k, y } },
483
- { x: x + w, y: y + r, in: { x: x + w, y: y + r - k } },
484
- { x: x + w, y: y + h - r, out: { x: x + w, y: y + h - r + k } },
485
- { x: x + w - r, y: y + h, in: { x: x + w - r + k, y: y + h } },
486
- { x: x + r, y: y + h, out: { x: x + r - k, y: y + h } },
487
- { x, y: y + h - r, in: { x, y: y + h - r + k } },
488
- { x, y: y + r, out: { x, y: y + r - k } }
489
- ];
490
- return { anchors, closed: true };
1457
+ const sp = { anchors: [{ x, y }, { x: x + w, y }, { x: x + w, y: y + h }, { x, y: y + h }], closed: true };
1458
+ return radius > 0 ? setSubpathCorners(sp, radius, kind) : sp;
491
1459
  }
492
1460
  function ellipseSubpath(b) {
493
1461
  const rx = b.width / 2;
494
1462
  const ry = b.height / 2;
495
1463
  const cx = b.x + rx;
496
1464
  const cy = b.y + ry;
497
- const kx = rx * KAPPA;
498
- const ky = ry * KAPPA;
1465
+ const kx = rx * KAPPA2;
1466
+ const ky = ry * KAPPA2;
499
1467
  return {
500
1468
  closed: true,
501
1469
  anchors: [
@@ -543,15 +1511,80 @@ function roundSubpaths(subpaths, precision = 3) {
543
1511
  }
544
1512
 
545
1513
  // src/vector/svg.ts
1514
+ function vectorCornersAttribute(subpaths, precision = 3) {
1515
+ const out = [];
1516
+ subpaths.forEach(
1517
+ (sp, s) => sp.anchors.forEach((a, i) => {
1518
+ if (!a.corner || !(a.corner.radius > 0)) return;
1519
+ out.push(`${s}.${i}:${formatNumber(a.corner.radius, precision)}${a.corner.kind && a.corner.kind !== "round" ? `:${a.corner.kind}` : ""}`);
1520
+ })
1521
+ );
1522
+ return out.join(" ");
1523
+ }
1524
+ function applyVectorCornersAttribute(subpaths, value) {
1525
+ for (const item of value.trim().split(/\s+/)) {
1526
+ const m = /^(\d+)\.(\d+):([\d.eE+-]+)(?::([a-z]+))?$/.exec(item);
1527
+ if (!m) continue;
1528
+ const a = subpaths[+m[1]]?.anchors[+m[2]];
1529
+ const radius = parseFloat(m[3]);
1530
+ if (!a || !(radius > 0)) continue;
1531
+ const kind = VECTOR_CORNER_KINDS.includes(m[4]) ? m[4] : void 0;
1532
+ a.corner = { radius, ...kind && kind !== "round" ? { kind } : {} };
1533
+ }
1534
+ return subpaths;
1535
+ }
546
1536
  var escapeAttr = (value) => value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
547
1537
  function vectorPaintToSvg(paint, gradientId) {
548
1538
  if (!hasPaint(paint)) return { value: "none", gradient: null };
549
1539
  if (typeof paint === "string") return { value: paint, gradient: null };
550
1540
  return { value: `url(#${gradientId})`, gradient: gradientToSvgDef(paint, gradientId) };
551
1541
  }
1542
+ var partsCache = /* @__PURE__ */ new WeakMap();
1543
+ function vectorStrokeSvgParts(node, clipId, precision = 3) {
1544
+ if (!hasPaint(node.stroke) || !hasStrokeExtras(node)) return void 0;
1545
+ const hit = partsCache.get(node);
1546
+ if (hit && hit.clipId === clipId && hit.precision === precision) return hit.parts;
1547
+ const parts = computeStrokeSvgParts(node, clipId, precision);
1548
+ partsCache.set(node, { clipId, precision, parts });
1549
+ return parts;
1550
+ }
1551
+ function computeStrokeSvgParts(node, clipId, precision) {
1552
+ const path = subpathsToBezier(node.subpaths);
1553
+ const parts = vectorStrokeParts(path, node);
1554
+ const profiled = !!strokeWidthProfileOf(node);
1555
+ if (parts.align === "center" && !parts.heads.length && !profiled) return void 0;
1556
+ let clip = null;
1557
+ if (parts.align !== "center") {
1558
+ const b = bezierPathBounds(path);
1559
+ const pad = vectorStrokeReach(node) + 1;
1560
+ 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 };
1561
+ clip = { kind: parts.align === "inside" ? "clip" : "mask", id: clipId, bounds };
1562
+ }
1563
+ return {
1564
+ align: parts.align,
1565
+ d: bezierPathToSvg(parts.line, { precision }),
1566
+ width: parts.drawWidth,
1567
+ ...profiled && (node.strokeWidth ?? 1) > 0 ? { outline: bezierPathToSvg(vectorStrokeLineArea(parts, node), { precision }) } : {},
1568
+ clip,
1569
+ heads: parts.heads.map((h) => bezierPathToSvg(h.path, { precision }))
1570
+ };
1571
+ }
1572
+ function widthProfileToAttribute(points, precision = 4) {
1573
+ return points.map((p) => [p.t, p.left, p.right].map((n) => formatNumber(n, precision)).join(",")).join(";");
1574
+ }
1575
+ function widthProfileFromAttribute(value) {
1576
+ if (!value) return [];
1577
+ return sanitizeWidthProfile(
1578
+ value.split(";").map((part) => {
1579
+ const [t, left, right] = part.split(",").map((n) => parseFloat(n));
1580
+ return { t, left, right: Number.isFinite(right) ? right : left };
1581
+ })
1582
+ );
1583
+ }
1584
+ var round2 = (n) => Math.round(n * 100) / 100;
552
1585
  function gradientMarkup(def) {
553
1586
  const tag = def.type === "radial" ? "radialGradient" : "linearGradient";
554
- const attrs = Object.entries(def.attrs).map(([k, v]) => ` ${k}="${escapeAttr(String(v))}"`).join("");
1587
+ const attrs = Object.entries(def.attrs).map(([k, v2]) => ` ${k}="${escapeAttr(String(v2))}"`).join("");
555
1588
  const stops = def.stops.map((s) => `<stop offset="${s.offset}" stop-color="${escapeAttr(s.stopColor)}"${s.stopOpacity !== void 0 ? ` stop-opacity="${s.stopOpacity}"` : ""}/>`).join("");
556
1589
  return `<${tag} id="${escapeAttr(def.id)}"${attrs}>${stops}</${tag}>`;
557
1590
  }
@@ -582,15 +1615,61 @@ function vectorDocumentToSvg(doc, options = {}) {
582
1615
  const stroke = vectorPaintToSvg(node.stroke, `${prefix}stroke-${node.id}`);
583
1616
  if (fill.gradient) defs.push(gradientMarkup(fill.gradient));
584
1617
  if (stroke.gradient) defs.push(gradientMarkup(stroke.gradient));
585
- let attrs = `${common(node)} d="${vectorPathData(node, precision)}" fill="${escapeAttr(fill.value)}"`;
586
- if (node.fillRule === "evenodd") attrs += ` fill-rule="evenodd"`;
587
- if (stroke.value !== "none") {
588
- attrs += ` stroke="${escapeAttr(stroke.value)}" stroke-width="${num(node.strokeWidth ?? 1)}"`;
589
- if (node.strokeLinecap && node.strokeLinecap !== "butt") attrs += ` stroke-linecap="${node.strokeLinecap}"`;
590
- if (node.strokeLinejoin && node.strokeLinejoin !== "miter") attrs += ` stroke-linejoin="${node.strokeLinejoin}"`;
591
- if (node.strokeDasharray?.length) attrs += ` stroke-dasharray="${node.strokeDasharray.map(num).join(" ")}"`;
592
- }
593
- lines.push(`${indent(depth)}<path${attrs}/>`);
1618
+ const d = vectorPathData(node, precision);
1619
+ const corners2 = vectorCornersAttribute(node.subpaths, precision);
1620
+ const live = corners2 ? ` data-vector-d="${bezierPathToSvg(subpathsToBezier(node.subpaths, { corners: false }), { precision })}" data-vector-corners="${corners2}"` : "";
1621
+ const fillAttrs = ` fill="${escapeAttr(fill.value)}"${node.fillRule === "evenodd" ? ` fill-rule="evenodd"` : ""}`;
1622
+ const lineAttrs = (width) => {
1623
+ let out2 = ` stroke="${escapeAttr(stroke.value)}" stroke-width="${num(width)}"`;
1624
+ if (node.strokeLinecap && node.strokeLinecap !== "butt") out2 += ` stroke-linecap="${node.strokeLinecap}"`;
1625
+ if (node.strokeLinejoin && node.strokeLinejoin !== "miter") out2 += ` stroke-linejoin="${node.strokeLinejoin}"`;
1626
+ if (node.strokeDasharray?.length) out2 += ` stroke-dasharray="${node.strokeDasharray.map(num).join(" ")}"`;
1627
+ return out2;
1628
+ };
1629
+ const parts = stroke.value !== "none" ? vectorStrokeSvgParts(node, `${prefix}clip-${node.id}`, precision) : void 0;
1630
+ if (!parts) {
1631
+ lines.push(`${indent(depth)}<path${common(node)} d="${d}"${fillAttrs}${stroke.value !== "none" ? lineAttrs(node.strokeWidth ?? 1) : ""}${live}/>`);
1632
+ return;
1633
+ }
1634
+ let data = ` data-vector-path="" data-vector-stroke-width="${num(node.strokeWidth ?? 1)}"`;
1635
+ if (parts.align !== "center") data += ` data-vector-stroke-align="${parts.align}"`;
1636
+ if (node.startArrow) data += ` data-vector-start-arrow="${node.startArrow}"`;
1637
+ if (node.endArrow) data += ` data-vector-end-arrow="${node.endArrow}"`;
1638
+ if (node.arrowAlign && node.arrowAlign !== "extend") data += ` data-vector-arrow-align="${node.arrowAlign}"`;
1639
+ if (node.startArrowScale !== void 0 && node.startArrowScale !== 1) data += ` data-vector-start-arrow-scale="${num(node.startArrowScale)}"`;
1640
+ if (node.endArrowScale !== void 0 && node.endArrowScale !== 1) data += ` data-vector-end-arrow-scale="${num(node.endArrowScale)}"`;
1641
+ const profile = strokeWidthProfileOf(node);
1642
+ if (profile) {
1643
+ data += ` data-vector-width-profile="${widthProfileToAttribute(profile)}"`;
1644
+ if (node.strokeLinecap && node.strokeLinecap !== "butt") data += ` data-vector-stroke-linecap="${node.strokeLinecap}"`;
1645
+ if (node.strokeLinejoin && node.strokeLinejoin !== "miter") data += ` data-vector-stroke-linejoin="${node.strokeLinejoin}"`;
1646
+ if (node.strokeDasharray?.length) data += ` data-vector-stroke-dasharray="${node.strokeDasharray.map(num).join(" ")}"`;
1647
+ }
1648
+ lines.push(`${indent(depth)}<g${common(node)}${data}>`);
1649
+ lines.push(`${indent(depth + 1)}<path data-vector-part="fill" d="${d}"${fillAttrs}${live}/>`);
1650
+ if (options.strokes === "outline") {
1651
+ const area = bezierPathToSvg(vectorStrokeOutlinePath(subpathsToBezier(node.subpaths), node), { precision });
1652
+ lines.push(`${indent(depth + 1)}<path data-vector-part="stroke-outline" d="${area}" fill="${escapeAttr(stroke.value)}"/>`);
1653
+ } else {
1654
+ let clip = "";
1655
+ if (parts.clip) {
1656
+ const rule = node.fillRule === "evenodd" ? "evenodd" : "nonzero";
1657
+ const id = escapeAttr(parts.clip.id);
1658
+ if (parts.clip.kind === "clip") {
1659
+ defs.push(`<clipPath id="${id}"><path d="${d}" clip-rule="${rule}"/></clipPath>`);
1660
+ clip = ` clip-path="url(#${id})"`;
1661
+ } else {
1662
+ const b = parts.clip.bounds;
1663
+ const box = `x="${num(b.x)}" y="${num(b.y)}" width="${num(b.width)}" height="${num(b.height)}"`;
1664
+ defs.push(`<mask id="${id}" maskUnits="userSpaceOnUse" ${box}><rect ${box} fill="#fff"/><path d="${d}" fill="#000" fill-rule="${rule}"/></mask>`);
1665
+ clip = ` mask="url(#${id})"`;
1666
+ }
1667
+ }
1668
+ if (parts.outline !== void 0) lines.push(`${indent(depth + 1)}<path data-vector-part="stroke-outline" d="${parts.outline}" fill="${escapeAttr(stroke.value)}"${clip}/>`);
1669
+ else lines.push(`${indent(depth + 1)}<path data-vector-part="stroke" d="${parts.d}" fill="none"${lineAttrs(parts.width)}${clip}/>`);
1670
+ for (const h2 of parts.heads) lines.push(`${indent(depth + 1)}<path data-vector-part="head" d="${h2}" fill="${escapeAttr(stroke.value)}"/>`);
1671
+ }
1672
+ lines.push(`${indent(depth)}</g>`);
594
1673
  };
595
1674
  for (const node of doc.nodes) emit(node, 1);
596
1675
  const w = num(doc.width);
@@ -765,11 +1844,11 @@ function parseVectorSvg(svg, options = {}) {
765
1844
  const rawGradients = /* @__PURE__ */ new Map();
766
1845
  collectGradients(root, rawGradients);
767
1846
  const paint = (value) => {
768
- const v = value.trim();
769
- const url = /^url\(\s*['"]?#([^'")\s]+)['"]?\s*\)/.exec(v);
1847
+ const v2 = value.trim();
1848
+ const url = /^url\(\s*['"]?#([^'")\s]+)['"]?\s*\)/.exec(v2);
770
1849
  if (url) return resolveGradient(url[1], rawGradients) ?? null;
771
- if (v === "none" || v === "") return null;
772
- return v;
1850
+ if (v2 === "none" || v2 === "") return null;
1851
+ return v2;
773
1852
  };
774
1853
  const viewBox = numberList(root.attrs.viewBox);
775
1854
  const width = viewBox.length === 4 ? viewBox[2] : length(root.attrs.width, 300);
@@ -779,8 +1858,13 @@ function parseVectorSvg(svg, options = {}) {
779
1858
  const shapeOf = (el) => {
780
1859
  const a = el.attrs;
781
1860
  switch (el.name) {
782
- case "path":
1861
+ case "path": {
1862
+ if (a["data-vector-d"] && a["data-vector-corners"]) {
1863
+ const sharp = bezierToSubpaths(a["data-vector-d"]);
1864
+ if (sharp.length) return applyVectorCornersAttribute(sharp, a["data-vector-corners"]);
1865
+ }
783
1866
  return a.d ? bezierToSubpaths(a.d) : null;
1867
+ }
784
1868
  case "rect": {
785
1869
  const w = length(a.width);
786
1870
  const h = length(a.height);
@@ -814,6 +1898,65 @@ function parseVectorSvg(svg, options = {}) {
814
1898
  }
815
1899
  return null;
816
1900
  };
1901
+ const strokeStyle = (s) => ({
1902
+ ...s.strokeWidth ? { strokeWidth: length(s.strokeWidth, 1) } : {},
1903
+ ...s.strokeLinecap ? { strokeLinecap: s.strokeLinecap } : {},
1904
+ ...s.strokeLinejoin ? { strokeLinejoin: s.strokeLinejoin } : {},
1905
+ ...s.strokeDasharray && s.strokeDasharray !== "none" ? { strokeDasharray: numberList(s.strokeDasharray) } : {},
1906
+ ...s.fillRule === "evenodd" ? { fillRule: "evenodd" } : {}
1907
+ });
1908
+ const placed = (node, m) => {
1909
+ if (m === IDENTITY_MATRIX) return node;
1910
+ const next = transformVectorNode(node, m);
1911
+ const strokeScale = Math.sqrt(Math.abs(m[0] * m[3] - m[1] * m[2]));
1912
+ if (hasPaint(next.stroke) && strokeScale > 0 && Math.abs(strokeScale - 1) > 1e-9) next.strokeWidth = (next.strokeWidth ?? 1) * strokeScale;
1913
+ return next;
1914
+ };
1915
+ const mergedPath = (g, style, m, meta) => {
1916
+ const part = (name) => g.children.find((c) => c.name === "path" && c.attrs["data-vector-part"] === name);
1917
+ const fillEl = part("fill");
1918
+ const subpaths = fillEl ? shapeOf(fillEl) : null;
1919
+ if (!fillEl || !subpaths?.length) return null;
1920
+ const a = g.attrs;
1921
+ const fs = readStyle(fillEl, style);
1922
+ const strokeEl = part("stroke");
1923
+ const outlineEl = part("stroke-outline");
1924
+ const ss = strokeEl ? readStyle(strokeEl, style) : null;
1925
+ const width2 = length(a["data-vector-stroke-width"], ss ? length(ss.strokeWidth, 1) : 1);
1926
+ const arrow = (v2) => VECTOR_ARROWHEADS.includes(v2) ? v2 : void 0;
1927
+ const scale = (v2) => {
1928
+ const n = v2 === void 0 ? NaN : parseFloat(v2);
1929
+ return n > 0 ? n : void 0;
1930
+ };
1931
+ const align = a["data-vector-stroke-align"];
1932
+ const profile = widthProfileFromAttribute(a["data-vector-width-profile"]);
1933
+ const kept = (key, values) => values.includes(a[key]) ? a[key] : void 0;
1934
+ const keptCap = kept("data-vector-stroke-linecap", ["round", "square"]);
1935
+ const keptJoin = kept("data-vector-stroke-linejoin", ["round", "bevel"]);
1936
+ const keptDash = a["data-vector-stroke-dasharray"] ? numberList(a["data-vector-stroke-dasharray"]) : void 0;
1937
+ const node = {
1938
+ id: idFor(g),
1939
+ type: "path",
1940
+ ...meta,
1941
+ subpaths,
1942
+ fill: paint(fs.fill),
1943
+ stroke: ss ? paint(ss.stroke) : outlineEl ? paint(readStyle(outlineEl, style).fill) : null,
1944
+ ...ss ? strokeStyle(ss) : {},
1945
+ ...fs.fillRule === "evenodd" ? { fillRule: "evenodd" } : {},
1946
+ strokeWidth: width2,
1947
+ ...align === "inside" || align === "outside" ? { strokeAlign: align } : {},
1948
+ ...arrow(a["data-vector-start-arrow"]) ? { startArrow: arrow(a["data-vector-start-arrow"]) } : {},
1949
+ ...arrow(a["data-vector-end-arrow"]) ? { endArrow: arrow(a["data-vector-end-arrow"]) } : {},
1950
+ ...a["data-vector-arrow-align"] === "tip" ? { arrowAlign: "tip" } : {},
1951
+ ...scale(a["data-vector-start-arrow-scale"]) ? { startArrowScale: scale(a["data-vector-start-arrow-scale"]) } : {},
1952
+ ...scale(a["data-vector-end-arrow-scale"]) ? { endArrowScale: scale(a["data-vector-end-arrow-scale"]) } : {},
1953
+ ...profile.length ? { strokeWidthProfile: profile } : {},
1954
+ ...keptCap ? { strokeLinecap: keptCap } : {},
1955
+ ...keptJoin ? { strokeLinejoin: keptJoin } : {},
1956
+ ...keptDash?.length ? { strokeDasharray: keptDash } : {}
1957
+ };
1958
+ return placed(node, m);
1959
+ };
817
1960
  const visit = (el, style, matrix) => {
818
1961
  const out = [];
819
1962
  for (const child of el.children) {
@@ -827,6 +1970,11 @@ function parseVectorSvg(svg, options = {}) {
827
1970
  ...opacity !== void 0 && Number.isFinite(opacity) && opacity < 1 ? { opacity } : {},
828
1971
  ...hidden ? { visible: false } : {}
829
1972
  };
1973
+ if (child.name === "g" && child.attrs["data-vector-path"] !== void 0) {
1974
+ const node2 = mergedPath(child, s, m, meta);
1975
+ if (node2) out.push(node2);
1976
+ continue;
1977
+ }
830
1978
  if (child.name === "g" || child.name === "svg" || child.name === "a" || child.name === "switch") {
831
1979
  const children = visit(child, s, m);
832
1980
  if (!children.length) continue;
@@ -836,24 +1984,18 @@ function parseVectorSvg(svg, options = {}) {
836
1984
  }
837
1985
  const subpaths = shapeOf(child);
838
1986
  if (!subpaths || !subpaths.length) continue;
839
- let node = {
840
- id: idFor(child),
841
- type: "path",
842
- ...meta,
843
- subpaths,
844
- fill: child.name === "line" ? null : paint(s.fill),
845
- stroke: paint(s.stroke),
846
- ...s.strokeWidth ? { strokeWidth: length(s.strokeWidth, 1) } : {},
847
- ...s.strokeLinecap ? { strokeLinecap: s.strokeLinecap } : {},
848
- ...s.strokeLinejoin ? { strokeLinejoin: s.strokeLinejoin } : {},
849
- ...s.strokeDasharray && s.strokeDasharray !== "none" ? { strokeDasharray: numberList(s.strokeDasharray) } : {},
850
- ...s.fillRule === "evenodd" ? { fillRule: "evenodd" } : {}
851
- };
852
- if (m !== IDENTITY_MATRIX) {
853
- node = transformVectorNode(node, m);
854
- const strokeScale = Math.sqrt(Math.abs(m[0] * m[3] - m[1] * m[2]));
855
- if (hasPaint(node.stroke) && strokeScale > 0 && Math.abs(strokeScale - 1) > 1e-9) node.strokeWidth = (node.strokeWidth ?? 1) * strokeScale;
856
- }
1987
+ const node = placed(
1988
+ {
1989
+ id: idFor(child),
1990
+ type: "path",
1991
+ ...meta,
1992
+ subpaths,
1993
+ fill: child.name === "line" ? null : paint(s.fill),
1994
+ stroke: paint(s.stroke),
1995
+ ...strokeStyle(s)
1996
+ },
1997
+ m
1998
+ );
857
1999
  out.push(node);
858
2000
  }
859
2001
  return out;
@@ -876,9 +2018,9 @@ function constrainToLeave(leave, p) {
876
2018
  return { x: leave.at.x + Math.sign(dx || 1) * s, y: leave.at.y + Math.sign(dy || 1) * s };
877
2019
  }
878
2020
  default: {
879
- const len = Math.hypot(dx, dy);
2021
+ const len2 = Math.hypot(dx, dy);
880
2022
  const a = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);
881
- return { x: leave.at.x + Math.cos(a) * len, y: leave.at.y + Math.sin(a) * len };
2023
+ return { x: leave.at.x + Math.cos(a) * len2, y: leave.at.y + Math.sin(a) * len2 };
882
2024
  }
883
2025
  }
884
2026
  }
@@ -887,24 +2029,24 @@ var CROSSING_PAIRS = 32;
887
2029
  var CURVE_SAMPLES = 48;
888
2030
  var BISECT = 50;
889
2031
  var EQUAL = 1e-6;
890
- function bound(sorted, key, v, strict) {
2032
+ function bound(sorted, key, v2, strict) {
891
2033
  let lo = 0;
892
2034
  let hi = sorted.length;
893
2035
  while (lo < hi) {
894
2036
  const mid = lo + hi >>> 1;
895
2037
  const k = key(sorted[mid]);
896
- if (strict ? k <= v : k < v) lo = mid + 1;
2038
+ if (strict ? k <= v2 : k < v2) lo = mid + 1;
897
2039
  else hi = mid;
898
2040
  }
899
2041
  return lo;
900
2042
  }
901
- function within(sorted, key, v, tol) {
902
- return sorted.slice(bound(sorted, key, v - tol, false), bound(sorted, key, v + tol, true));
2043
+ function within(sorted, key, v2, tol) {
2044
+ return sorted.slice(bound(sorted, key, v2 - tol, false), bound(sorted, key, v2 + tol, true));
903
2045
  }
904
- var linesNear = (lines, v, tol) => within(lines, (l) => l.v, v, tol);
2046
+ var linesNear = (lines, v2, tol) => within(lines, (l) => l.v, v2, tol);
905
2047
  var distance = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
906
2048
  var toPoint = (p) => [p.x, p.y];
907
- var toVec = (p) => ({ x: p[0], y: p[1] });
2049
+ var toVec2 = (p) => ({ x: p[0], y: p[1] });
908
2050
  var LABELS = {
909
2051
  anchor: "anchor",
910
2052
  centre: "centre",
@@ -1022,7 +2164,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
1022
2164
  const count = subpathSegmentCount(sp);
1023
2165
  for (let i = 0; i < count && budget > 0; i++) {
1024
2166
  if (moving(i) || moving((i + 1) % n)) continue;
1025
- const seg = subpathSegment(sp, i).map((q) => toPoint(applyMatrix(m, toVec(q))));
2167
+ const seg = subpathSegment(sp, i).map((q) => toPoint(applyMatrix(m, toVec2(q))));
1026
2168
  let minX = Infinity;
1027
2169
  let minY = Infinity;
1028
2170
  let maxX = -Infinity;
@@ -1131,7 +2273,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
1131
2273
  */
1132
2274
  snapFrom(p, leave, tol) {
1133
2275
  const none = (q) => ({ point: q, kind: null, x: false, y: false, lines: [], labels: [] });
1134
- const at = (q, kind, lines2 = []) => ({
2276
+ const at2 = (q, kind, lines2 = []) => ({
1135
2277
  point: q,
1136
2278
  kind,
1137
2279
  x: true,
@@ -1145,16 +2287,16 @@ var VectorSnapTargets = class _VectorSnapTargets {
1145
2287
  return hit ? { point: hit.point, x: true, y: true, ...this.linedUp(hit.point, hit.from) } : none(q);
1146
2288
  }
1147
2289
  const point = this.nearestPoint(p, tol);
1148
- if (point) return at(point.p, point.kind);
2290
+ if (point) return at2(point.p, point.kind);
1149
2291
  const near2 = this.segmentsNear(p, tol);
1150
- const crossing = this.crossing(near2, p, tol);
1151
- if (crossing) return at(crossing, "intersect");
2292
+ const crossing2 = this.crossing(near2, p, tol);
2293
+ if (crossing2) return at2(crossing2, "intersect");
1152
2294
  if (leave && leave.curves !== false) {
1153
2295
  const foot = this.curvePoint(near2, leave.at, p, tol);
1154
- if (foot) return at(foot.point, foot.kind, [line("construction", leave.at, foot.point)]);
2296
+ if (foot) return at2(foot.point, foot.kind, [line("construction", leave.at, foot.point)]);
1155
2297
  }
1156
2298
  const on = this.onSegment(near2);
1157
- if (on) return at(on, "path");
2299
+ if (on) return at2(on, "path");
1158
2300
  if (leave?.angles?.length) {
1159
2301
  const c = construction(leave, p, tol);
1160
2302
  if (c) {
@@ -1166,9 +2308,9 @@ var VectorSnapTargets = class _VectorSnapTargets {
1166
2308
  return { point: c.point, kind: "construction", x: true, y: true, lines: [line("construction", leave.at, c.point)], labels: [] };
1167
2309
  }
1168
2310
  }
1169
- const nearest = (lines2, v) => {
2311
+ const nearest = (lines2, v2) => {
1170
2312
  let best = null;
1171
- for (const l of linesNear(lines2, v, tol)) if (!best || Math.abs(l.v - v) < Math.abs(best.v - v)) best = l;
2313
+ for (const l of linesNear(lines2, v2, tol)) if (!best || Math.abs(l.v - v2) < Math.abs(best.v - v2)) best = l;
1172
2314
  return best;
1173
2315
  };
1174
2316
  const bx = nearest(this.xs, p.x);
@@ -1201,7 +2343,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
1201
2343
  * the nearest target line crosses it, at most twice `tol` away: where it lands and the
1202
2344
  * target point that line comes from.
1203
2345
  */
1204
- slide(at, dir, q, tol, ray) {
2346
+ slide(at2, dir, q, tol, ray) {
1205
2347
  const len2 = dir.x * dir.x + dir.y * dir.y;
1206
2348
  if (!Number.isFinite(len2) || len2 <= 1e-18) return null;
1207
2349
  const reach = 2 * tol;
@@ -1209,10 +2351,10 @@ var VectorSnapTargets = class _VectorSnapTargets {
1209
2351
  for (const vertical of [true, false]) {
1210
2352
  const d = vertical ? dir.x : dir.y;
1211
2353
  if (Math.abs(d) <= 1e-9) continue;
1212
- const a = vertical ? at.x : at.y;
2354
+ const a = vertical ? at2.x : at2.y;
1213
2355
  for (const l of linesNear(vertical ? this.xs : this.ys, vertical ? q.x : q.y, reach)) {
1214
2356
  const s = (l.v - a) / d;
1215
- const hit = vertical ? { x: l.v, y: at.y + dir.y * s } : { x: at.x + dir.x * s, y: l.v };
2357
+ const hit = vertical ? { x: l.v, y: at2.y + dir.y * s } : { x: at2.x + dir.x * s, y: l.v };
1216
2358
  const moved = distance(hit, q);
1217
2359
  if (moved <= reach && (!ray || s > 0) && (!best || moved < best.moved)) best = { moved, point: hit, from: l.from };
1218
2360
  }
@@ -1228,16 +2370,16 @@ var VectorSnapTargets = class _VectorSnapTargets {
1228
2370
  /** Target segments whose control box reaches `p` and which pass within `tol` of it, nearest first. */
1229
2371
  segmentsNear(p, tol) {
1230
2372
  const out = [];
1231
- const pt = toPoint(p);
2373
+ const pt2 = toPoint(p);
1232
2374
  for (const s of this.segments) {
1233
2375
  if (p.x < s.minX - tol || p.x > s.maxX + tol || p.y < s.minY - tol || p.y > s.maxY + tol) continue;
1234
- const hit = segmentNearest(s.seg, pt);
2376
+ const hit = segmentNearest(s.seg, pt2);
1235
2377
  if (hit.distance <= tol) out.push({ seg: s.seg, t: hit.t, distance: hit.distance });
1236
2378
  }
1237
2379
  return out.sort((a, b) => a.distance - b.distance);
1238
2380
  }
1239
2381
  onSegment(near2) {
1240
- return near2.length ? toVec(segmentPoint(near2[0].seg, near2[0].t)) : null;
2382
+ return near2.length ? toVec2(segmentPoint(near2[0].seg, near2[0].t)) : null;
1241
2383
  }
1242
2384
  /** Where two of the target segments near `p` cross, the crossing nearest `p` within `tol`. */
1243
2385
  crossing(near2, p, tol) {
@@ -1247,7 +2389,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
1247
2389
  for (let i = 0; i < list.length; i++) {
1248
2390
  for (let j = i + 1; j < list.length; j++) {
1249
2391
  for (const hit of segmentIntersections(list[i].seg, list[j].seg)) {
1250
- const q = toVec(hit.point);
2392
+ const q = toVec2(hit.point);
1251
2393
  const d = distance(q, p);
1252
2394
  if (d <= tol && d < bestD) {
1253
2395
  best = q;
@@ -1288,7 +2430,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
1288
2430
  const axes = { x: options.axes?.x !== false, y: options.axes?.y !== false };
1289
2431
  const nearest = (lines, values) => {
1290
2432
  let best = null;
1291
- 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 };
2433
+ 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 };
1292
2434
  return best;
1293
2435
  };
1294
2436
  const bx = axes.x ? nearest(this.xs, [r.x, r.x + r.width / 2, r.x + r.width]) : null;
@@ -1331,7 +2473,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
1331
2473
  evenSpacing(r, horizontal, tol) {
1332
2474
  const span = (b, x) => x ? [b.x, b.x + b.width] : [b.y, b.y + b.height];
1333
2475
  const [r0, r1] = span(r, horizontal);
1334
- const len = r1 - r0;
2476
+ const len2 = r1 - r0;
1335
2477
  const [c0, c1] = span(r, !horizontal);
1336
2478
  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);
1337
2479
  if (!row.length) return null;
@@ -1351,20 +2493,20 @@ var VectorSnapTargets = class _VectorSnapTargets {
1351
2493
  if (e.a0 >= r1 - tol && (!after || e.a0 < after.a0)) after = e;
1352
2494
  }
1353
2495
  const starts = [];
1354
- if (before && after && after.a0 - before.a1 > len) starts.push({ start: (before.a1 + after.a0 - len) / 2, repeats: null });
2496
+ if (before && after && after.a0 - before.a1 > len2) starts.push({ start: (before.a1 + after.a0 - len2) / 2, repeats: null });
1355
2497
  for (const { g } of pairs) {
1356
2498
  if (before) starts.push({ start: before.a1 + g, repeats: g });
1357
- if (after) starts.push({ start: after.a0 - g - len, repeats: g });
2499
+ if (after) starts.push({ start: after.a0 - g - len2, repeats: g });
1358
2500
  }
1359
2501
  let pick = null;
1360
2502
  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;
1361
2503
  if (!pick) return null;
1362
2504
  const { start, repeats } = pick;
1363
- const at = horizontal ? { ...r, x: start } : { ...r, y: start };
1364
- const end = start + len;
2505
+ const at2 = horizontal ? { ...r, x: start } : { ...r, y: start };
2506
+ const end = start + len2;
1365
2507
  const gaps = [];
1366
- if (before && (repeats === null || Math.abs(start - before.a1 - repeats) <= EQUAL)) gaps.push([before.a1, start, mid(before.b, at)]);
1367
- if (after && (repeats === null || Math.abs(after.a0 - end - repeats) <= EQUAL)) gaps.push([end, after.a0, mid(after.b, at)]);
2508
+ if (before && (repeats === null || Math.abs(start - before.a1 - repeats) <= EQUAL)) gaps.push([before.a1, start, mid(before.b, at2)]);
2509
+ if (after && (repeats === null || Math.abs(after.a0 - end - repeats) <= EQUAL)) gaps.push([end, after.a0, mid(after.b, at2)]);
1368
2510
  if (repeats !== null) {
1369
2511
  for (const p of pairs) if (Math.abs(p.g - repeats) <= EQUAL) gaps.push(p.gap);
1370
2512
  }
@@ -1372,9 +2514,9 @@ var VectorSnapTargets = class _VectorSnapTargets {
1372
2514
  }
1373
2515
  /** A gap mark: a line across the gap with a tick at each end, and its length as a label. */
1374
2516
  gapMarks([u0, u1, c], horizontal, tick, out) {
1375
- const pt = (u, v) => horizontal ? { x: u, y: v } : { x: v, y: u };
1376
- 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)));
1377
- out.labels.push(this.label(pt((u0 + u1) / 2, c), "spacing", fmtLength(u1 - u0)));
2517
+ const pt2 = (u, v2) => horizontal ? { x: u, y: v2 } : { x: v2, y: u };
2518
+ 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)));
2519
+ out.labels.push(this.label(pt2((u0 + u1) / 2, c), "spacing", fmtLength(u1 - u0)));
1378
2520
  }
1379
2521
  /** The other objects' side length nearest `length`, within `tol`, or null. */
1380
2522
  matchSize(length2, tol) {
@@ -1401,9 +2543,9 @@ var VectorSnapTargets = class _VectorSnapTargets {
1401
2543
  const hp = handlePos(handle, frame);
1402
2544
  const d0 = { x: hp.x - origin.x, y: hp.y - origin.y };
1403
2545
  const valid = (n, s) => Number.isFinite(n) && n * s > 0;
1404
- const nearest = (v, lines) => {
2546
+ const nearest = (v2, lines) => {
1405
2547
  let best = null;
1406
- for (const l of linesNear(lines, v, tol)) if (!best || Math.abs(l.v - v) < Math.abs(best.v - v)) best = l;
2548
+ for (const l of linesNear(lines, v2, tol)) if (!best || Math.abs(l.v - v2) < Math.abs(best.v - v2)) best = l;
1407
2549
  return best;
1408
2550
  };
1409
2551
  let nx = sx;
@@ -1454,9 +2596,9 @@ var VectorSnapTargets = class _VectorSnapTargets {
1454
2596
  };
1455
2597
  for (const h of [handle, ...ends]) {
1456
2598
  const hv = handlePos(h, frame);
1457
- const v = { x: hv.x - origin.x, y: hv.y - origin.y };
1458
- const base = applyMatrix(toDoc, { x: origin.x + c.x * v.x, y: origin.y + c.y * v.y });
1459
- const dir = applyMatrix(toDoc, { x: e.x * v.x, y: e.y * v.y });
2599
+ const v2 = { x: hv.x - origin.x, y: hv.y - origin.y };
2600
+ const base = applyMatrix(toDoc, { x: origin.x + c.x * v2.x, y: origin.y + c.y * v2.y });
2601
+ const dir = applyMatrix(toDoc, { x: e.x * v2.x, y: e.y * v2.y });
1460
2602
  const p = { x: base.x + dir.x * s0, y: base.y + dir.y * s0 };
1461
2603
  const l2 = dir.x * dir.x + dir.y * dir.y;
1462
2604
  const on = l2 > 1e-18 ? this.nearestPoint(p, tol) : null;
@@ -1489,16 +2631,16 @@ var VectorSnapTargets = class _VectorSnapTargets {
1489
2631
  const sized = [];
1490
2632
  if (!proportional) {
1491
2633
  if (movesX && !snap.hx && !(frame.angle && snap.hy) && frame.width > 1e-9) {
1492
- const len = this.matchSize(Math.abs(out.sx) * frame.width, tol);
1493
- if (len !== null && len > 0) {
1494
- out.sx = Math.sign(out.sx || 1) * (len / frame.width);
2634
+ const len2 = this.matchSize(Math.abs(out.sx) * frame.width, tol);
2635
+ if (len2 !== null && len2 > 0) {
2636
+ out.sx = Math.sign(out.sx || 1) * (len2 / frame.width);
1495
2637
  sized.push("x");
1496
2638
  }
1497
2639
  }
1498
2640
  if (movesY && !snap.hy && !(frame.angle && snap.hx) && frame.height > 1e-9) {
1499
- const len = this.matchSize(Math.abs(out.sy) * frame.height, tol);
1500
- if (len !== null && len > 0) {
1501
- out.sy = Math.sign(out.sy || 1) * (len / frame.height);
2641
+ const len2 = this.matchSize(Math.abs(out.sy) * frame.height, tol);
2642
+ if (len2 !== null && len2 > 0) {
2643
+ out.sy = Math.sign(out.sy || 1) * (len2 / frame.height);
1502
2644
  sized.push("y");
1503
2645
  }
1504
2646
  }
@@ -1549,14 +2691,14 @@ function line(kind, a, b) {
1549
2691
  return { kind, x1: a.x, y1: a.y, x2: b.x, y2: b.y };
1550
2692
  }
1551
2693
  function construction(leave, p, tol) {
1552
- const v = { x: p.x - leave.at.x, y: p.y - leave.at.y };
1553
- if (Math.hypot(v.x, v.y) <= tol) return null;
2694
+ const v2 = { x: p.x - leave.at.x, y: p.y - leave.at.y };
2695
+ if (Math.hypot(v2.x, v2.y) <= tol) return null;
1554
2696
  let best = null;
1555
2697
  for (const a of leave.angles ?? []) {
1556
2698
  const r = a * Math.PI / 180;
1557
2699
  const exact = (n) => Math.abs(n) < 1e-12 ? 0 : n;
1558
2700
  const dir = { x: exact(Math.cos(r)), y: exact(-Math.sin(r)) };
1559
- const k = v.x * dir.x + v.y * dir.y;
2701
+ const k = v2.x * dir.x + v2.y * dir.y;
1560
2702
  const point = { x: leave.at.x + dir.x * k, y: leave.at.y + dir.y * k };
1561
2703
  const d = distance(point, p);
1562
2704
  if (d <= tol && (!best || d < best.d)) best = { dir, point, d };
@@ -1581,7 +2723,7 @@ function solveOnSegment(seg, base, cond) {
1581
2723
  vals.push(cond(r, d));
1582
2724
  scale = Math.max(scale, Math.hypot(r[0], r[1]) * Math.hypot(d[0], d[1]));
1583
2725
  }
1584
- if (!(scale > 0) || vals.every((v) => Math.abs(v) <= scale * 1e-12)) return [];
2726
+ if (!(scale > 0) || vals.every((v2) => Math.abs(v2) <= scale * 1e-12)) return [];
1585
2727
  const out = [];
1586
2728
  for (let i = 0; i < CURVE_SAMPLES; i++) {
1587
2729
  const ga = vals[i];
@@ -1614,8 +2756,8 @@ function solveOnSegment(seg, base, cond) {
1614
2756
  }
1615
2757
 
1616
2758
  // src/vector/pathEdit.ts
1617
- var EPS2 = 1e-9;
1618
- var unit = (x, y) => {
2759
+ var EPS3 = 1e-9;
2760
+ var unit2 = (x, y) => {
1619
2761
  const l = Math.hypot(x, y);
1620
2762
  return l > 1e-12 && Number.isFinite(l) ? [x / l, y / l] : null;
1621
2763
  };
@@ -1628,14 +2770,14 @@ function cubicOf(sp, i) {
1628
2770
  var isLine = (sp, i) => subpathSegment(sp, i).length === 2;
1629
2771
  function startTangent(c) {
1630
2772
  for (let k = 1; k < 4; k++) {
1631
- const u = unit(c[k][0] - c[0][0], c[k][1] - c[0][1]);
2773
+ const u = unit2(c[k][0] - c[0][0], c[k][1] - c[0][1]);
1632
2774
  if (u) return u;
1633
2775
  }
1634
2776
  return [1, 0];
1635
2777
  }
1636
2778
  function endTangent(c) {
1637
2779
  for (let k = 2; k >= 0; k--) {
1638
- const u = unit(c[3][0] - c[k][0], c[3][1] - c[k][1]);
2780
+ const u = unit2(c[3][0] - c[k][0], c[3][1] - c[k][1]);
1639
2781
  if (u) return u;
1640
2782
  }
1641
2783
  return [1, 0];
@@ -1643,11 +2785,11 @@ function endTangent(c) {
1643
2785
  function sample(c, n, out, withStart) {
1644
2786
  for (let i = withStart ? 0 : 1; i <= n; i++) out.push(segmentPoint(c, i / n));
1645
2787
  }
1646
- var onAnchor = (a, h) => !h || Math.abs(h.x - a.x) < EPS2 && Math.abs(h.y - a.y) < EPS2;
2788
+ var onAnchor2 = (a, h) => !h || Math.abs(h.x - a.x) < EPS3 && Math.abs(h.y - a.y) < EPS3;
1647
2789
  function tidy(a, keepType = true) {
1648
2790
  const next = cloneAnchor(a);
1649
- if (onAnchor(next, next.in)) delete next.in;
1650
- if (onAnchor(next, next.out)) delete next.out;
2791
+ if (onAnchor2(next, next.in)) delete next.in;
2792
+ if (onAnchor2(next, next.out)) delete next.out;
1651
2793
  const shown = inferAnchorType(next);
1652
2794
  const type = keepType && a.type ? a.type === "symmetric" && shown !== "symmetric" ? shown === "corner" ? "corner" : "smooth" : a.type === "smooth" && shown === "corner" ? "corner" : a.type : shown;
1653
2795
  if (type === "corner") delete next.type;
@@ -1755,8 +2897,8 @@ function curvatureMoveAnchor(sp, index, q) {
1755
2897
  const a = sp.anchors[index];
1756
2898
  if (!a) return { anchors, closed: sp.closed };
1757
2899
  const own = [
1758
- a.in && !onAnchor(a, a.in) ? { x: a.in.x - a.x, y: a.in.y - a.y } : null,
1759
- a.out && !onAnchor(a, a.out) ? { x: a.out.x - a.x, y: a.out.y - a.y } : null
2900
+ a.in && !onAnchor2(a, a.in) ? { x: a.in.x - a.x, y: a.in.y - a.y } : null,
2901
+ a.out && !onAnchor2(a, a.out) ? { x: a.out.x - a.x, y: a.out.y - a.y } : null
1760
2902
  ];
1761
2903
  const ends = [null, null];
1762
2904
  neighbours(sp, index).forEach((j, side) => {
@@ -1775,15 +2917,16 @@ function curvatureMoveAnchor(sp, index, q) {
1775
2917
  if (own[0]) moved.in = { x: q.x + own[0].x, y: q.y + own[0].y };
1776
2918
  if (own[1]) moved.out = { x: q.x + own[1].x, y: q.y + own[1].y };
1777
2919
  if (a.type) moved.type = a.type;
2920
+ if (a.corner) moved.corner = { ...a.corner };
1778
2921
  const [prev, next] = ends;
1779
2922
  if (a.type && a.type !== "corner" && prev && next) {
1780
2923
  const dx = next.x - prev.x;
1781
2924
  const dy = next.y - prev.y;
1782
2925
  const l = Math.hypot(dx, dy);
1783
2926
  if (l > 1e-9) {
1784
- const len = (h, to) => h ? Math.hypot(h.x, h.y) : Math.hypot(q.x - to.x, q.y - to.y) / 3;
1785
- let li = len(own[0], prev);
1786
- let lo = len(own[1], next);
2927
+ const len2 = (h, to) => h ? Math.hypot(h.x, h.y) : Math.hypot(q.x - to.x, q.y - to.y) / 3;
2928
+ let li = len2(own[0], prev);
2929
+ let lo = len2(own[1], next);
1787
2930
  if (a.type === "symmetric") li = lo = (li + lo) / 2;
1788
2931
  moved.in = { x: q.x - dx * li / l, y: q.y - dy * li / l };
1789
2932
  moved.out = { x: q.x + dx * lo / l, y: q.y + dy * lo / l };
@@ -1792,9 +2935,9 @@ function curvatureMoveAnchor(sp, index, q) {
1792
2935
  anchors[index] = moved;
1793
2936
  return { anchors, closed: sp.closed };
1794
2937
  }
1795
- function cutSubpathAt(sp, at) {
2938
+ function cutSubpathAt(sp, at2) {
1796
2939
  const n = sp.anchors.length;
1797
- const cuts = [...new Set(at)].filter((i) => Number.isInteger(i) && i >= 0 && i < n && (sp.closed || i > 0 && i < n - 1)).sort((a, b) => a - b);
2940
+ const cuts = [...new Set(at2)].filter((i) => Number.isInteger(i) && i >= 0 && i < n && (sp.closed || i > 0 && i < n - 1)).sort((a, b) => a - b);
1798
2941
  if (!cuts.length || n < 2) return [{ anchors: sp.anchors.map(cloneAnchor), closed: sp.closed }];
1799
2942
  const first = cuts[0];
1800
2943
  const run = sp.closed ? Array.from({ length: n + 1 }, (_, i) => sp.anchors[(first + i) % n]) : sp.anchors;
@@ -1824,8 +2967,8 @@ function splitSubpathAt(sp, segment, t, endTolerance = 1e-6) {
1824
2967
  const { subpath, index } = insertAnchor(sp, segment, t);
1825
2968
  return cutSubpathAt(subpath, [index]);
1826
2969
  }
1827
- var endOf = (sp, last) => sp.anchors[last ? sp.anchors.length - 1 : 0];
1828
- var dist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
2970
+ var endOf2 = (sp, last) => sp.anchors[last ? sp.anchors.length - 1 : 0];
2971
+ var dist2 = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
1829
2972
  function reverse(sp) {
1830
2973
  return {
1831
2974
  closed: sp.closed,
@@ -1834,6 +2977,7 @@ function reverse(sp) {
1834
2977
  if (a.out) next.in = { ...a.out };
1835
2978
  if (a.in) next.out = { ...a.in };
1836
2979
  if (a.type) next.type = a.type;
2980
+ if (a.corner) next.corner = { ...a.corner };
1837
2981
  return next;
1838
2982
  })
1839
2983
  };
@@ -1851,7 +2995,7 @@ function joinSubpaths(subpaths, tolerance) {
1851
2995
  const sp = open[0];
1852
2996
  const first = sp.anchors[0];
1853
2997
  const last = sp.anchors[sp.anchors.length - 1];
1854
- if (sp.anchors.length > 2 && dist(first, last) <= tolerance) {
2998
+ if (sp.anchors.length > 2 && dist2(first, last) <= tolerance) {
1855
2999
  sp.anchors.pop();
1856
3000
  sp.anchors[0] = tidy({ ...first, in: last.in ?? null }, false);
1857
3001
  } else {
@@ -1867,7 +3011,7 @@ function joinSubpaths(subpaths, tolerance) {
1867
3011
  for (let j = i + 1; j < open.length; j++) {
1868
3012
  for (const ei of [false, true]) {
1869
3013
  for (const ej of [false, true]) {
1870
- const d = dist(endOf(open[i], ei), endOf(open[j], ej));
3014
+ const d = dist2(endOf2(open[i], ei), endOf2(open[j], ej));
1871
3015
  if (d < best.d) best = { d, i, j, ei, ej };
1872
3016
  }
1873
3017
  }
@@ -1905,10 +3049,10 @@ function smoothSubpath(sp, amount = 1, only) {
1905
3049
  const prev = orig[(i + n - 1) % n];
1906
3050
  const next = orig[(i + 1) % n];
1907
3051
  const p = orig[i];
1908
- const dir = unit(next.x - prev.x, next.y - prev.y);
3052
+ const dir = unit2(next.x - prev.x, next.y - prev.y);
1909
3053
  if (!dir) continue;
1910
- const li = dist(p, prev) / 3;
1911
- const lo = dist(p, next) / 3;
3054
+ const li = dist2(p, prev) / 3;
3055
+ const lo = dist2(p, next) / 3;
1912
3056
  const hin = p.in ?? p;
1913
3057
  const hout = p.out ?? p;
1914
3058
  const target = (h, x, y) => ({ x: h.x + (x - h.x) * t, y: h.y + (y - h.y) * t });
@@ -1935,12 +3079,12 @@ function segsOf(sp) {
1935
3079
  function segsToSubpath(segs, closed, types) {
1936
3080
  if (!segs.length) return null;
1937
3081
  const anchors = [];
1938
- const at = (p) => ({ x: p[0], y: p[1] });
1939
- anchors.push(at(segs[0].c[0]));
3082
+ const at2 = (p) => ({ x: p[0], y: p[1] });
3083
+ anchors.push(at2(segs[0].c[0]));
1940
3084
  for (const s of segs) {
1941
3085
  const end = s.c[s.c.length - 1];
1942
3086
  if (!s.line) anchors[anchors.length - 1].out = { x: s.c[1][0], y: s.c[1][1] };
1943
- const next = at(end);
3087
+ const next = at2(end);
1944
3088
  if (!s.line) next.in = { x: s.c[2][0], y: s.c[2][1] };
1945
3089
  anchors.push(next);
1946
3090
  }
@@ -1962,12 +3106,12 @@ function mergePair(a, b, tol) {
1962
3106
  if (a.line && b.line) {
1963
3107
  const cx = b3[0] - a0[0];
1964
3108
  const cy = b3[1] - a0[1];
1965
- const len = Math.hypot(cx, cy);
1966
- if (len < 1e-12) return null;
3109
+ const len2 = Math.hypot(cx, cy);
3110
+ if (len2 < 1e-12) return null;
1967
3111
  const mx = a3[0] - a0[0];
1968
3112
  const my = a3[1] - a0[1];
1969
- const along = (mx * cx + my * cy) / len;
1970
- return Math.abs((mx * cy - my * cx) / len) <= tol && along >= 0 && along <= len ? { c: [a0, b3], line: true } : null;
3113
+ const along2 = (mx * cx + my * cy) / len2;
3114
+ return Math.abs((mx * cy - my * cx) / len2) <= tol && along2 >= 0 && along2 <= len2 ? { c: [a0, b3], line: true } : null;
1971
3115
  }
1972
3116
  const ca = a.line ? [a0, a0, a3, a3] : a.c;
1973
3117
  const cb = b.line ? [a3, a3, b3, b3] : b.c;
@@ -2091,7 +3235,7 @@ function lineHits(p, q, seg) {
2091
3235
  const dy = q[1] - p[1];
2092
3236
  const len2 = dx * dx + dy * dy;
2093
3237
  if (len2 < 1e-24) return [];
2094
- const h = (pt) => (pt[0] - p[0]) * -dy + (pt[1] - p[1]) * dx;
3238
+ const h = (pt2) => (pt2[0] - p[0]) * -dy + (pt2[1] - p[1]) * dx;
2095
3239
  let coeffs;
2096
3240
  if (seg.length === 2) coeffs = [0, 0, h(seg[1]) - h(seg[0]), h(seg[0])];
2097
3241
  else {
@@ -2099,7 +3243,7 @@ function lineHits(p, q, seg) {
2099
3243
  coeffs = [-h0 + 3 * h1 - 3 * h2 + h3, 3 * h0 - 6 * h1 + 3 * h2, -3 * h0 + 3 * h1, h0];
2100
3244
  }
2101
3245
  if (coeffs.every((c) => Math.abs(c) < 1e-12 * Math.max(1, len2))) return [];
2102
- const scale = Math.sqrt(len2) * Math.max(1, ...seg.map((pt) => Math.abs(pt[0]) + Math.abs(pt[1])));
3246
+ const scale = Math.sqrt(len2) * Math.max(1, ...seg.map((pt2) => Math.abs(pt2[0]) + Math.abs(pt2[1])));
2103
3247
  const out = [];
2104
3248
  for (const t of rootsInUnit(...coeffs, scale)) {
2105
3249
  const point = segmentPoint(seg, t);
@@ -2326,12 +3470,12 @@ function simplifyPolyline(points, tolerance) {
2326
3470
  const [ax, ay] = points[a];
2327
3471
  const dx = points[b][0] - ax;
2328
3472
  const dy = points[b][1] - ay;
2329
- const len = Math.hypot(dx, dy);
3473
+ const len2 = Math.hypot(dx, dy);
2330
3474
  let far = -1;
2331
3475
  let farD = 0;
2332
3476
  for (let i = a + 1; i < b; i++) {
2333
3477
  const [px, py] = points[i];
2334
- const d = len < 1e-12 ? Math.hypot(px - ax, py - ay) : Math.abs((px - ax) * dy - (py - ay) * dx) / len;
3478
+ const d = len2 < 1e-12 ? Math.hypot(px - ax, py - ay) : Math.abs((px - ax) * dy - (py - ay) * dx) / len2;
2335
3479
  if (d > farD) {
2336
3480
  farD = d;
2337
3481
  far = i;
@@ -2399,7 +3543,7 @@ function vectorInkShapeName(shape) {
2399
3543
  return shape.points.length === 3 ? "triangle" : "polygon";
2400
3544
  }
2401
3545
  }
2402
- var dist2 = (a, b) => Math.hypot(b[0] - a[0], b[1] - a[1]);
3546
+ var dist3 = (a, b) => Math.hypot(b[0] - a[0], b[1] - a[1]);
2403
3547
  function bbox(pts) {
2404
3548
  if (!pts.length) return [0, 0, 0, 0];
2405
3549
  let [x0, y0] = pts[0];
@@ -2417,7 +3561,7 @@ function segDistance(a, b, p) {
2417
3561
  const dy = b[1] - a[1];
2418
3562
  const len2 = dx * dx + dy * dy;
2419
3563
  const t = len2 > 0 ? Math.min(1, Math.max(0, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / len2)) : 0;
2420
- return dist2([a[0] + t * dx, a[1] + t * dy], p);
3564
+ return dist3([a[0] + t * dx, a[1] + t * dy], p);
2421
3565
  }
2422
3566
  function polygonDistance(c, p) {
2423
3567
  let best = Infinity;
@@ -2443,11 +3587,11 @@ function snapSquare(x, y, w, h) {
2443
3587
  }
2444
3588
  function turning(a, b, c) {
2445
3589
  const u = [b[0] - a[0], b[1] - a[1]];
2446
- const v = [c[0] - b[0], c[1] - b[1]];
3590
+ const v2 = [c[0] - b[0], c[1] - b[1]];
2447
3591
  const lu = Math.hypot(u[0], u[1]);
2448
- const lv = Math.hypot(v[0], v[1]);
3592
+ const lv = Math.hypot(v2[0], v2[1]);
2449
3593
  if (lu <= 0 || lv <= 0) return 0;
2450
- return Math.acos(Math.min(1, Math.max(-1, (u[0] * v[0] + u[1] * v[1]) / (lu * lv)))) * 180 / Math.PI;
3594
+ return Math.acos(Math.min(1, Math.max(-1, (u[0] * v2[0] + u[1] * v2[1]) / (lu * lv)))) * 180 / Math.PI;
2451
3595
  }
2452
3596
  function rdp(p, eps) {
2453
3597
  const n = p.length;
@@ -2479,7 +3623,7 @@ function corners(p, diag) {
2479
3623
  let far = 0;
2480
3624
  let farD = 0;
2481
3625
  p.forEach((q, i) => {
2482
- const d = dist2(start, q);
3626
+ const d = dist3(start, q);
2483
3627
  if (d > farD) {
2484
3628
  farD = d;
2485
3629
  far = i;
@@ -2506,7 +3650,7 @@ function corners(p, diag) {
2506
3650
  }
2507
3651
  let short = -1;
2508
3652
  for (let j = 0; j < n; j++) {
2509
- if (dist2(ring[j], ring[(j + 1) % n]) < 0.1 * diag) {
3653
+ if (dist3(ring[j], ring[(j + 1) % n]) < 0.1 * diag) {
2510
3654
  short = j;
2511
3655
  break;
2512
3656
  }
@@ -2546,8 +3690,8 @@ function recognizeInkShape(points, options = {}) {
2546
3690
  const diag = Math.hypot(w, h);
2547
3691
  if (size < minSize) return null;
2548
3692
  let length2 = 0;
2549
- for (let i = 1; i < p.length; i++) length2 += dist2(p[i - 1], p[i]);
2550
- const gap = dist2(first, last);
3693
+ for (let i = 1; i < p.length; i++) length2 += dist3(p[i - 1], p[i]);
3694
+ const gap = dist3(first, last);
2551
3695
  if (gap > 0.85 * length2 && gap >= minSize) {
2552
3696
  const off = Math.max(...p.map((q) => segDistance(first, last, q)));
2553
3697
  if (off <= Math.max(0.06 * gap, 1.5 * minSize / 6)) return { kind: "line", a: first, b: last };
@@ -2573,8 +3717,31 @@ function recognizeInkShape(points, options = {}) {
2573
3717
 
2574
3718
  // src/vector/controller.ts
2575
3719
  var DEFAULT_VECTOR_SNAP = { grid: 0, objects: true, guides: true, threshold: 5, angles: VECTOR_CONSTRUCTION_ANGLES };
2576
- var DEFAULT_VECTOR_PATH_EDIT = { keepShapeOnDelete: true, curvatureMoves: false, pencilSmoothing: 0, recognizeShapes: false, eraserSize: 10 };
3720
+ var DEFAULT_VECTOR_PATH_EDIT = {
3721
+ keepShapeOnDelete: true,
3722
+ curvatureMoves: false,
3723
+ pencilSmoothing: 0,
3724
+ recognizeShapes: false,
3725
+ eraserSize: 10,
3726
+ pencilPressure: "pen"
3727
+ };
2577
3728
  var VECTOR_PENCIL_STRING_PX = 100;
3729
+ var VECTOR_STYLE_KEYS = [
3730
+ "fill",
3731
+ "stroke",
3732
+ "strokeWidth",
3733
+ "strokeLinecap",
3734
+ "strokeLinejoin",
3735
+ "strokeDasharray",
3736
+ "fillRule",
3737
+ "strokeAlign",
3738
+ "startArrow",
3739
+ "endArrow",
3740
+ "arrowAlign",
3741
+ "startArrowScale",
3742
+ "endArrowScale",
3743
+ "strokeWidthProfile"
3744
+ ];
2578
3745
  var VECTOR_CLIPBOARD_TYPE = "pixerate/vector";
2579
3746
  var DEFAULT_DEFAULTS = {
2580
3747
  fill: "#d4d4d8",
@@ -2635,9 +3802,9 @@ function getVisibleHandles(state) {
2635
3802
  return out;
2636
3803
  }
2637
3804
  if (state.tool !== "direct") return out;
2638
- const editable = new Set(getEditablePathIds(state));
3805
+ const editable2 = new Set(getEditablePathIds(state));
2639
3806
  for (const ref of state.selectedAnchors) {
2640
- if (!editable.has(ref.nodeId)) continue;
3807
+ if (!editable2.has(ref.nodeId)) continue;
2641
3808
  const node = findVectorNode(state.document.nodes, ref.nodeId);
2642
3809
  const sp = node.subpaths[ref.subpath];
2643
3810
  if (!sp) continue;
@@ -2651,6 +3818,79 @@ function getVisibleHandles(state) {
2651
3818
  }
2652
3819
  return out;
2653
3820
  }
3821
+ var VECTOR_CORNER_WIDGET_INSET = 10;
3822
+ function getVectorCornerWidgets(state) {
3823
+ if (state.tool !== "direct" || state.pen) return [];
3824
+ const zoom = state.viewport.zoom;
3825
+ const out = [];
3826
+ for (const id of getEditablePathIds(state)) {
3827
+ const node = findVectorNode(state.document.nodes, id);
3828
+ const picked = state.selectedAnchors.filter((r) => r.nodeId === id);
3829
+ const m = vectorWorldMatrix(state.document.nodes, id) ?? IDENTITY_MATRIX;
3830
+ for (const c of vectorCorners(node.subpaths)) {
3831
+ if (picked.length && !picked.some((r) => r.subpath === c.subpath && r.index === c.index)) continue;
3832
+ const side = Math.min(c.lu, c.lv);
3833
+ if (side * zoom < 3 * VECTOR_CORNER_WIDGET_INSET) continue;
3834
+ const a = node.subpaths[c.subpath].anchors[c.index];
3835
+ const radius = a.corner?.radius ?? 0;
3836
+ const drawn = fitCornerRadius(c, radius);
3837
+ const k = Math.min(Math.max(drawn / c.sin, VECTOR_CORNER_WIDGET_INSET / (zoom * Math.sqrt(1 + c.cos))), side / 2);
3838
+ const p = applyMatrix(m, cornerBisectorPoint(c, k));
3839
+ 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 });
3840
+ }
3841
+ }
3842
+ return out;
3843
+ }
3844
+ function vectorStrokeWidthGeometry(node) {
3845
+ if (!hasPaint(node.stroke) || !((node.strokeWidth ?? 1) > 0)) return null;
3846
+ const parts = vectorStrokeParts(subpathsToBezier(node.subpaths), node);
3847
+ return parts.line.length ? { line: parts.line, unit: parts.drawWidth / 2 } : null;
3848
+ }
3849
+ function getVectorWidthHandles(state) {
3850
+ if (state.tool !== "width") return [];
3851
+ const ids = /* @__PURE__ */ new Set();
3852
+ for (const id of state.selection) {
3853
+ const n = findVectorNode(state.document.nodes, id);
3854
+ if (n) for (const p of collectPathNodes(n)) ids.add(p.id);
3855
+ }
3856
+ if (state.hover?.nodeId) ids.add(state.hover.nodeId);
3857
+ if (state.interaction?.type === "width") ids.add(state.interaction.ref.nodeId);
3858
+ if (state.selectedWidthPoint) ids.add(state.selectedWidthPoint.nodeId);
3859
+ const out = [];
3860
+ for (const id of ids) {
3861
+ const node = findVectorNode(state.document.nodes, id);
3862
+ if (node?.type !== "path" || node.visible === false || !node.strokeWidthProfile?.length) continue;
3863
+ const g = vectorStrokeWidthGeometry(node);
3864
+ if (!g) continue;
3865
+ const profile = sanitizeWidthProfile(node.strokeWidthProfile);
3866
+ const m = vectorWorldMatrix(state.document.nodes, id) ?? IDENTITY_MATRIX;
3867
+ const contours = node.subpaths.length;
3868
+ for (let sp = 0; sp < contours; sp++) {
3869
+ profile.forEach((w, index) => {
3870
+ const at2 = strokePositionPoint(g.line, sp, w.t);
3871
+ if (!at2) return;
3872
+ const n = { x: at2.tangent[1], y: -at2.tangent[0] };
3873
+ const c = { x: at2.point[0], y: at2.point[1] };
3874
+ const p = applyMatrix(m, c);
3875
+ const l = applyMatrix(m, { x: c.x + n.x * w.left * g.unit, y: c.y + n.y * w.left * g.unit });
3876
+ const r = applyMatrix(m, { x: c.x - n.x * w.right * g.unit, y: c.y - n.y * w.right * g.unit });
3877
+ const sel = state.selectedWidthPoint;
3878
+ out.push({
3879
+ ref: { nodeId: id, index, subpath: sp },
3880
+ x: p.x,
3881
+ y: p.y,
3882
+ left: l,
3883
+ right: r,
3884
+ t: w.t,
3885
+ leftScale: w.left,
3886
+ rightScale: w.right,
3887
+ selected: !!sel && sel.nodeId === id && sel.index === index
3888
+ });
3889
+ });
3890
+ }
3891
+ }
3892
+ return out;
3893
+ }
2654
3894
  function getAnchor(doc, ref) {
2655
3895
  const node = findVectorNode(doc.nodes, ref.nodeId);
2656
3896
  return node?.type === "path" ? node.subpaths[ref.subpath]?.anchors[ref.index] : void 0;
@@ -2721,9 +3961,10 @@ var TOOL_LABELS = {
2721
3961
  line: "Line",
2722
3962
  scissors: "Scissors",
2723
3963
  knife: "Knife",
2724
- eraser: "Eraser"
3964
+ eraser: "Eraser",
3965
+ width: "Width"
2725
3966
  };
2726
- var TOOL_KEYS = { v: "select", a: "direct", p: "pen", n: "pencil", r: "rect", o: "ellipse", y: "polygon", s: "star", l: "line", c: "scissors", k: "knife", e: "eraser" };
3967
+ 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", w: "width" };
2727
3968
  function vectorToolLabel(tool) {
2728
3969
  return TOOL_LABELS[tool];
2729
3970
  }
@@ -2739,7 +3980,8 @@ var VECTOR_TOOL_SHORTCUTS = {
2739
3980
  line: "L",
2740
3981
  scissors: "C",
2741
3982
  knife: "K",
2742
- eraser: "E"
3983
+ eraser: "E",
3984
+ width: "W"
2743
3985
  };
2744
3986
  var VECTOR_TOOL_GLYPHS = {
2745
3987
  select: "\u2196",
@@ -2753,7 +3995,8 @@ var VECTOR_TOOL_GLYPHS = {
2753
3995
  line: "\u2571",
2754
3996
  scissors: "\u2702",
2755
3997
  knife: "\u27CB",
2756
- eraser: "\u232B"
3998
+ eraser: "\u232B",
3999
+ width: "\u21F9"
2757
4000
  };
2758
4001
  var SHAPE_TOOLS = /* @__PURE__ */ new Set(["rect", "ellipse", "polygon", "star", "line"]);
2759
4002
  var BOOLEAN_OPS = {
@@ -2807,6 +4050,7 @@ var VectorEditorController = class {
2807
4050
  doc;
2808
4051
  selection = [];
2809
4052
  anchors = [];
4053
+ widthPoint = null;
2810
4054
  editingId = null;
2811
4055
  pen = null;
2812
4056
  tool;
@@ -2885,6 +4129,7 @@ var VectorEditorController = class {
2885
4129
  tool: this.tool,
2886
4130
  selection: this.selection,
2887
4131
  selectedAnchors: this.anchors,
4132
+ selectedWidthPoint: this.widthPoint,
2888
4133
  editingId: this.editingId,
2889
4134
  selectionBounds: this.boundsOf(this.selection),
2890
4135
  selectionFrame: this.frameOf(this.selectedRoots()),
@@ -2948,11 +4193,15 @@ var VectorEditorController = class {
2948
4193
  if (h?.transformHandle) return vectorResizeCursor(h.transformHandle, this.frameOf(this.selectedRoots())?.angle ?? 0);
2949
4194
  return h?.nodeId ? "move" : "default";
2950
4195
  case "direct":
2951
- if (h?.anchor || h?.handle) return "pointer";
4196
+ if (h?.anchor || h?.handle || h?.corner) return "pointer";
2952
4197
  if (h?.segment) return "copy";
2953
4198
  return h?.nodeId ? "move" : "default";
2954
4199
  case "pen":
2955
4200
  return h?.anchor ? "pointer" : "crosshair";
4201
+ case "width":
4202
+ if (this.drag?.kind === "width") return this.drag.mode === "move" ? "grabbing" : "ew-resize";
4203
+ if (h?.widthPoint) return h.widthPoint.part === "center" ? "grab" : "ew-resize";
4204
+ return h?.strokeSpot ? "copy" : "default";
2956
4205
  default:
2957
4206
  return "crosshair";
2958
4207
  }
@@ -3033,12 +4282,12 @@ var VectorEditorController = class {
3033
4282
  // Viewport
3034
4283
  // -------------------------------------------------------------------------
3035
4284
  screenToDocument(p) {
3036
- const v = this.viewport;
3037
- return { x: (p.x - v.x) / v.zoom, y: (p.y - v.y) / v.zoom };
4285
+ const v2 = this.viewport;
4286
+ return { x: (p.x - v2.x) / v2.zoom, y: (p.y - v2.y) / v2.zoom };
3038
4287
  }
3039
4288
  documentToScreen(p) {
3040
- const v = this.viewport;
3041
- return { x: p.x * v.zoom + v.x, y: p.y * v.zoom + v.y };
4289
+ const v2 = this.viewport;
4290
+ return { x: p.x * v2.zoom + v2.x, y: p.y * v2.zoom + v2.y };
3042
4291
  }
3043
4292
  setViewport(viewport) {
3044
4293
  const next = { ...this.viewport, ...viewport };
@@ -3078,6 +4327,7 @@ var VectorEditorController = class {
3078
4327
  this.tool = tool;
3079
4328
  this.hover = null;
3080
4329
  this.guides = null;
4330
+ this.widthPoint = null;
3081
4331
  if (tool !== "direct") {
3082
4332
  this.anchors = [];
3083
4333
  this.editingId = null;
@@ -3105,9 +4355,9 @@ var VectorEditorController = class {
3105
4355
  toggleSnapping(on = !this.snap.objects) {
3106
4356
  this.setSnap({ objects: on });
3107
4357
  }
3108
- gridValue(v) {
4358
+ gridValue(v2) {
3109
4359
  const g = this.snap.grid;
3110
- return g > 0 ? Math.round(v / g) * g : v;
4360
+ return g > 0 ? Math.round(v2 / g) * g : v2;
3111
4361
  }
3112
4362
  snapPoint(p) {
3113
4363
  return { x: this.gridValue(p.x), y: this.gridValue(p.y) };
@@ -3263,7 +4513,7 @@ var VectorEditorController = class {
3263
4513
  let subpath;
3264
4514
  switch (input.kind) {
3265
4515
  case "rect":
3266
- subpath = rectSubpath(b, input.cornerRadius ?? d.cornerRadius);
4516
+ subpath = rectSubpath(b);
3267
4517
  break;
3268
4518
  case "ellipse":
3269
4519
  subpath = ellipseSubpath(b);
@@ -3278,10 +4528,13 @@ var VectorEditorController = class {
3278
4528
  subpath = lineSubpath({ x: b.x, y: b.y }, { x: b.x + b.width, y: b.y + b.height });
3279
4529
  break;
3280
4530
  }
4531
+ const radius = input.cornerRadius ?? d.cornerRadius;
4532
+ const kind = input.cornerKind ?? d.cornerKind;
4533
+ if ((input.kind === "rect" || input.kind === "polygon" || input.kind === "star") && radius > 0) subpath = setSubpathCorners(subpath, radius, kind);
3281
4534
  const names = { rect: "Rectangle", ellipse: "Ellipse", polygon: "Polygon", star: "Star", line: "Line" };
3282
4535
  const style = this.styleFor(input.kind === "line");
3283
4536
  const own = {};
3284
- for (const key of ["fill", "stroke", "strokeWidth", "strokeLinecap", "strokeLinejoin", "strokeDasharray", "fillRule"]) {
4537
+ for (const key of VECTOR_STYLE_KEYS) {
3285
4538
  if (input[key] !== void 0) own[key] = input[key];
3286
4539
  }
3287
4540
  return { id: this.newId(), type: "path", name: input.name ?? names[input.kind], ...style, ...own, subpaths: [subpath] };
@@ -3304,6 +4557,9 @@ var VectorEditorController = class {
3304
4557
  if (d.strokeLinejoin) out.strokeLinejoin = d.strokeLinejoin;
3305
4558
  if (d.strokeDasharray) out.strokeDasharray = d.strokeDasharray;
3306
4559
  if (d.fillRule) out.fillRule = d.fillRule;
4560
+ if (d.strokeAlign && d.strokeAlign !== "center") out.strokeAlign = d.strokeAlign;
4561
+ for (const key of ["startArrow", "endArrow", "arrowAlign", "startArrowScale", "endArrowScale"]) if (d[key]) out[key] = d[key];
4562
+ if (d.strokeWidthProfile?.length) out.strokeWidthProfile = d.strokeWidthProfile.map((p) => ({ ...p }));
3307
4563
  return out;
3308
4564
  }
3309
4565
  /** Merges `patch` into a node (paths take style and geometry; groups name / visibility / opacity). */
@@ -3324,10 +4580,129 @@ var VectorEditorController = class {
3324
4580
  }
3325
4581
  if (!targets.size) return { ok: false, message: "Nothing is selected." };
3326
4582
  this.change(options.label ?? "Change Style", () => {
3327
- this.setDoc(mapTree(this.doc.nodes, (n) => targets.has(n.id) ? { ...n, ...style } : n));
4583
+ this.setDoc(mapTree(this.doc.nodes, (n) => targets.has(n.id) ? withStyle(n, style) : n));
4584
+ });
4585
+ return { ok: true };
4586
+ }
4587
+ /**
4588
+ * Sets the variable-width profile of the selected (or given) paths: a preset id
4589
+ * (`uniform` removes the profile), points, or a parametric taper / wave. Returns an error for
4590
+ * an unknown preset.
4591
+ */
4592
+ setWidthProfile(profile, options = {}) {
4593
+ let points;
4594
+ if (profile === null) points = [];
4595
+ else if (typeof profile === "string") {
4596
+ const preset = widthProfilePreset(profile);
4597
+ if (!preset) return { ok: false, message: `There is no width profile "${profile}".` };
4598
+ points = preset;
4599
+ } else if (Array.isArray(profile)) points = sanitizeWidthProfile(profile);
4600
+ else {
4601
+ const p = profile;
4602
+ points = p.taper || p.wave ? generateWidthProfile(p) : [];
4603
+ }
4604
+ this.widthPoint = null;
4605
+ return this.setStyle({ strokeWidthProfile: points.length ? points : null }, options.ids ?? this.selection, { label: points.length ? "Width Profile" : "Uniform Width" });
4606
+ }
4607
+ /** Removes one width point of a path's profile (the width tool's Delete). */
4608
+ removeWidthPoint(ref) {
4609
+ const node = this.getNode(ref.nodeId);
4610
+ if (node?.type !== "path" || !node.strokeWidthProfile?.[ref.index]) return { ok: false, message: "There is no such width point." };
4611
+ const rest = removeWidthPoints(sanitizeWidthProfile(node.strokeWidthProfile), [ref.index]);
4612
+ this.widthPoint = null;
4613
+ this.change("Delete Width Point", () => this.replaceNode(node.id, withStyle(node, { strokeWidthProfile: rest.length ? rest : null })));
4614
+ return { ok: true };
4615
+ }
4616
+ /**
4617
+ * Sets the live corner radius (and optionally the kind) of anchors: `refs`, else the selected
4618
+ * anchors (direct tool), else every anchor of the `ids` / selected paths. Radius 0 makes the
4619
+ * corners sharp again. Corners shrink to fit when drawn, so any radius is accepted.
4620
+ */
4621
+ setCornerRadius(radius, options = {}) {
4622
+ if (!Number.isFinite(radius) || radius < 0) return { ok: false, message: "The radius must be a number of at least 0." };
4623
+ const refs = this.cornerTargets(options);
4624
+ if (!refs.length) return { ok: false, message: "Nothing is selected." };
4625
+ this.change(radius > 0 ? "Corner Radius" : "Sharpen Corners", () => {
4626
+ this.doc = this.updateAnchors(this.doc, refs, (a) => setAnchorCorner(a, radius, options.kind));
4627
+ });
4628
+ return { ok: true };
4629
+ }
4630
+ /** Sets the live corner kind of the same anchors as `setCornerRadius`, keeping their radii. */
4631
+ setCornerKind(kind, options = {}) {
4632
+ const refs = this.cornerTargets(options);
4633
+ if (!refs.length) return { ok: false, message: "Nothing is selected." };
4634
+ this.change("Corner Kind", () => {
4635
+ this.doc = this.updateAnchors(this.doc, refs, (a) => setAnchorCorner(a, a.corner?.radius ?? 0, kind));
3328
4636
  });
3329
4637
  return { ok: true };
3330
4638
  }
4639
+ /** Anchors a corner change applies to (see `setCornerRadius`). */
4640
+ cornerTargets(options) {
4641
+ if (options.refs) return options.refs.filter((r) => getAnchor(this.doc, r));
4642
+ if (!options.ids && this.tool === "direct" && this.anchors.length) return this.anchors.slice();
4643
+ const out = [];
4644
+ for (const id of options.ids ?? this.selection) {
4645
+ const node = this.getNode(id);
4646
+ if (!node) continue;
4647
+ for (const p of collectPathNodes(node)) {
4648
+ if (p.locked) continue;
4649
+ p.subpaths.forEach((sp, s) => sp.anchors.forEach((_, i) => out.push({ nodeId: p.id, subpath: s, index: i })));
4650
+ }
4651
+ }
4652
+ return out;
4653
+ }
4654
+ /**
4655
+ * Outline Stroke: replaces each selected (or given) path's stroke with a filled path of the
4656
+ * area it paints (alignment, joins, caps, dashes and arrowheads included), placed above a copy
4657
+ * of the path that keeps the fill (left out when there is none). Returns the new paths' ids.
4658
+ */
4659
+ outlineStroke(options = {}) {
4660
+ const roots = options.ids ?? this.selectedRoots();
4661
+ const paths = [];
4662
+ for (const id of roots) {
4663
+ const node = this.getNode(id);
4664
+ if (node) {
4665
+ for (const p of collectPathNodes(node)) if (!p.locked && hasPaint(p.stroke) && (p.strokeWidth ?? 1) > 0) paths.push(p);
4666
+ }
4667
+ }
4668
+ if (!paths.length) return [];
4669
+ const created = [];
4670
+ const replace = /* @__PURE__ */ new Map();
4671
+ for (const p of paths) {
4672
+ const outline = bezierToSubpaths(vectorStrokeOutlinePath(subpathsToBezier(p.subpaths), p));
4673
+ if (!outline.length) continue;
4674
+ const {
4675
+ stroke,
4676
+ strokeWidth: _w,
4677
+ strokeLinecap: _c,
4678
+ strokeLinejoin: _j,
4679
+ strokeDasharray: _d,
4680
+ strokeAlign: _a,
4681
+ startArrow: _s,
4682
+ endArrow: _e,
4683
+ arrowAlign: _al,
4684
+ startArrowScale: _ss,
4685
+ endArrowScale: _es,
4686
+ strokeWidthProfile: _wp,
4687
+ ...rest
4688
+ } = cloneVectorNode(p);
4689
+ const id = this.newId();
4690
+ const strokeNode = { ...rest, id, name: `${p.name ?? "Path"} Stroke`, subpaths: outline, fill: stroke ?? null, stroke: null, fillRule: "nonzero" };
4691
+ const list = hasPaint(p.fill) ? [{ ...rest, stroke: null }, strokeNode] : [strokeNode];
4692
+ created.push(id);
4693
+ replace.set(p.id, list);
4694
+ }
4695
+ if (!created.length) return [];
4696
+ this.change("Outline Stroke", () => {
4697
+ let nodes = this.doc.nodes;
4698
+ for (const [id, list] of replace) nodes = spliceTree(nodes, id, list);
4699
+ this.setDoc(nodes);
4700
+ this.selection = created;
4701
+ this.anchors = [];
4702
+ this.editingId = null;
4703
+ });
4704
+ return created;
4705
+ }
3331
4706
  rename(id, name) {
3332
4707
  return this.updateNode(id, { name }, { label: "Rename" });
3333
4708
  }
@@ -3610,7 +4985,7 @@ var VectorEditorController = class {
3610
4985
  if (!groups.length) return { ok: false, message: "Select a group to ungroup." };
3611
4986
  const set = new Set(groups.map((g) => g.id));
3612
4987
  this.change("Ungroup", () => {
3613
- const flatten = (list) => {
4988
+ const flatten2 = (list) => {
3614
4989
  const out = [];
3615
4990
  let changed = false;
3616
4991
  for (const n of list) {
@@ -3622,14 +4997,14 @@ var VectorEditorController = class {
3622
4997
  ...n.children.map((c) => n.transform ? transformVectorNode(c, m) : c).map((c) => opacity !== void 0 && opacity < 1 ? { ...c, opacity: (c.opacity ?? 1) * opacity } : c)
3623
4998
  );
3624
4999
  } else if (n.type === "group") {
3625
- const children = flatten(n.children);
5000
+ const children = flatten2(n.children);
3626
5001
  if (children !== n.children) changed = true;
3627
5002
  out.push(children !== n.children ? { ...n, children } : n);
3628
5003
  } else out.push(n);
3629
5004
  }
3630
5005
  return changed ? out : list;
3631
5006
  };
3632
- this.setDoc(flatten(this.doc.nodes));
5007
+ this.setDoc(flatten2(this.doc.nodes));
3633
5008
  this.selection = groups.flatMap((g) => g.children.map((c) => c.id));
3634
5009
  });
3635
5010
  return { ok: true };
@@ -3899,7 +5274,12 @@ var VectorEditorController = class {
3899
5274
  const set = new Set(ids.flatMap((id) => this.getNode(id) ? collectPathNodes(this.getNode(id)) : []).map((p) => p.id));
3900
5275
  if (!set.size) return { ok: false, message: "Select a path." };
3901
5276
  this.change("Reverse Path", () => {
3902
- this.setDoc(mapTree(this.doc.nodes, (n) => n.type === "path" && set.has(n.id) ? { ...n, subpaths: n.subpaths.map(reverseSubpath) } : n));
5277
+ this.setDoc(
5278
+ mapTree(
5279
+ this.doc.nodes,
5280
+ (n) => n.type === "path" && set.has(n.id) ? { ...n, subpaths: n.subpaths.map(reverseSubpath), ...n.strokeWidthProfile?.length ? { strokeWidthProfile: reverseWidthProfile(n.strokeWidthProfile) } : {} } : n
5281
+ )
5282
+ );
3903
5283
  this.anchors = [];
3904
5284
  });
3905
5285
  return { ok: true };
@@ -3930,7 +5310,7 @@ var VectorEditorController = class {
3930
5310
  const map = (p) => applyMatrix(m, p);
3931
5311
  return from.subpaths.map((sp) => ({
3932
5312
  closed: sp.closed,
3933
- anchors: sp.anchors.map((a) => ({ ...map(a), ...a.in ? { in: map(a.in) } : {}, ...a.out ? { out: map(a.out) } : {}, ...a.type ? { type: a.type } : {} }))
5313
+ 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 } } : {} }))
3934
5314
  }));
3935
5315
  }
3936
5316
  /**
@@ -3980,9 +5360,9 @@ var VectorEditorController = class {
3980
5360
  const cx = points.reduce((s, p) => s + p.at.x, 0) / points.length;
3981
5361
  const cy = points.reduce((s, p) => s + p.at.y, 0) / points.length;
3982
5362
  this.change("Average", () => {
3983
- for (const { ref, at } of points) {
3984
- const dx = axis === "horizontal" ? 0 : cx - at.x;
3985
- const dy = axis === "vertical" ? 0 : cy - at.y;
5363
+ for (const { ref, at: at2 } of points) {
5364
+ const dx = axis === "horizontal" ? 0 : cx - at2.x;
5365
+ const dy = axis === "vertical" ? 0 : cy - at2.y;
3986
5366
  if (dx || dy) this.doc = this.movedAnchors(this.doc, [ref], dx, dy);
3987
5367
  }
3988
5368
  });
@@ -4066,8 +5446,8 @@ var VectorEditorController = class {
4066
5446
  const node = this.getNode(id);
4067
5447
  if (node.locked) continue;
4068
5448
  const ids = this.applyCuts(node, (sp, i) => {
4069
- const at = list.filter((r) => r.subpath === i).map((r) => r.index);
4070
- const pieces = at.length ? cutSubpathAt(sp, at) : null;
5449
+ const at2 = list.filter((r) => r.subpath === i).map((r) => r.index);
5450
+ const pieces = at2.length ? cutSubpathAt(sp, at2) : null;
4071
5451
  return pieces && !(pieces.length === 1 && pieces[0].closed === sp.closed) ? pieces : null;
4072
5452
  });
4073
5453
  if (ids) created.push(...ids);
@@ -4285,6 +5665,22 @@ var VectorEditorController = class {
4285
5665
  }
4286
5666
  return null;
4287
5667
  }
5668
+ cornerView() {
5669
+ return { ...this.view(), viewport: this.viewport };
5670
+ }
5671
+ hitCornerWidget(screen) {
5672
+ let best = null;
5673
+ let bestD = Infinity;
5674
+ for (const w of getVectorCornerWidgets(this.cornerView())) {
5675
+ const s = this.documentToScreen(w);
5676
+ const d = Math.hypot(s.x - screen.x, s.y - screen.y);
5677
+ if (d <= this.hitRadius && d < bestD) {
5678
+ best = w;
5679
+ bestD = d;
5680
+ }
5681
+ }
5682
+ return best;
5683
+ }
4288
5684
  hitAnchor(screen, refs) {
4289
5685
  let best = null;
4290
5686
  let bestD = Infinity;
@@ -4343,6 +5739,8 @@ var VectorEditorController = class {
4343
5739
  if (handle) return { nodeId: handle.nodeId, handle };
4344
5740
  const anchor = this.hitAnchor(screen, getVisibleAnchors(this.view()));
4345
5741
  if (anchor) return { nodeId: anchor.nodeId, anchor };
5742
+ const widget = this.hitCornerWidget(screen);
5743
+ if (widget) return { nodeId: widget.ref.nodeId, corner: widget.ref };
4346
5744
  for (const id of getEditablePathIds(this.view())) {
4347
5745
  const node = this.getNode(id);
4348
5746
  const seg = nearestSegment(node, this.toLocal(id, p));
@@ -4363,10 +5761,131 @@ var VectorEditorController = class {
4363
5761
  const seg = this.nearestOutline(p);
4364
5762
  return seg ? { nodeId: seg.nodeId, segment: seg } : null;
4365
5763
  }
5764
+ case "width": {
5765
+ const hit = this.hitWidthHandle(screen);
5766
+ if (hit) return { nodeId: hit.ref.nodeId, widthPoint: hit.ref };
5767
+ const spot = this.strokeSpotAt(p);
5768
+ return spot ? { nodeId: spot.nodeId, strokeSpot: spot } : null;
5769
+ }
4366
5770
  default:
4367
5771
  return null;
4368
5772
  }
4369
5773
  }
5774
+ widthView() {
5775
+ return { document: this.doc, selection: this.selection, tool: this.tool, hover: this.hover, interaction: this.interaction, selectedWidthPoint: this.widthPoint };
5776
+ }
5777
+ /** The width point centre or side end under `screen` (width tool). */
5778
+ hitWidthHandle(screen) {
5779
+ let best = null;
5780
+ let bestD = Infinity;
5781
+ for (const h of getVectorWidthHandles(this.widthView())) {
5782
+ for (const [part, q] of [["center", h], ["left", h.left], ["right", h.right]]) {
5783
+ const s = this.documentToScreen(q);
5784
+ const d = Math.hypot(s.x - screen.x, s.y - screen.y);
5785
+ if (d <= this.hitRadius && d < bestD - 1e-9) {
5786
+ best = { ref: { ...h.ref, part } };
5787
+ bestD = d;
5788
+ }
5789
+ }
5790
+ }
5791
+ return best;
5792
+ }
5793
+ /** The nearest stroked, unlocked visible path whose stroke is under document point `p` (width tool). */
5794
+ strokeSpotAt(p) {
5795
+ let best = null;
5796
+ const tol = this.tolerance();
5797
+ walkVectorNodes(this.doc.nodes, (n) => {
5798
+ if (n.type !== "path" || n.visible === false || n.locked) return;
5799
+ if (hiddenOrLocked(this.doc.nodes, n.id)) return;
5800
+ const g = vectorStrokeWidthGeometry(n);
5801
+ if (!g) return;
5802
+ const m = this.worldOf(n.id);
5803
+ const local = applyMatrix(invertMatrix(m), p);
5804
+ const near2 = nearestStrokePosition(g.line, [local.x, local.y]);
5805
+ if (!near2) return;
5806
+ const profile = strokeWidthProfileOf(n);
5807
+ const w = widthProfileAt(profile, near2.t);
5808
+ const reach = Math.max(near2.side === "left" ? w.left : w.right, 0) * g.unit + tol;
5809
+ if (near2.distance > reach || near2.distance > widthProfileMaxScale(profile) * g.unit + tol) return;
5810
+ if (best && near2.distance >= best.distance) return;
5811
+ const nx = near2.tangent[1];
5812
+ const ny = -near2.tangent[0];
5813
+ const [cx, cy] = near2.point;
5814
+ best = {
5815
+ nodeId: n.id,
5816
+ subpath: near2.contour,
5817
+ t: near2.t,
5818
+ point: applyMatrix(m, { x: cx, y: cy }),
5819
+ left: applyMatrix(m, { x: cx + nx * w.left * g.unit, y: cy + ny * w.left * g.unit }),
5820
+ right: applyMatrix(m, { x: cx - nx * w.right * g.unit, y: cy - ny * w.right * g.unit }),
5821
+ distance: near2.distance
5822
+ };
5823
+ });
5824
+ if (!best) return null;
5825
+ const { distance: _d, ...spot } = best;
5826
+ return spot;
5827
+ }
5828
+ downWidth(screen, p, input) {
5829
+ const hit = this.hitWidthHandle(screen);
5830
+ const spot = hit ? null : this.strokeSpotAt(p);
5831
+ const nodeId = hit?.ref.nodeId ?? spot?.nodeId;
5832
+ const node = nodeId ? this.getNode(nodeId) : void 0;
5833
+ const g = node?.type === "path" ? vectorStrokeWidthGeometry(node) : null;
5834
+ if (!node || node.type !== "path" || !g) {
5835
+ if (this.widthPoint) {
5836
+ this.widthPoint = null;
5837
+ this.notify();
5838
+ }
5839
+ return;
5840
+ }
5841
+ const before = this.snapshot();
5842
+ const local = this.toLocal(node.id, p);
5843
+ let profile;
5844
+ let ref;
5845
+ let mode = "width";
5846
+ let side = "both";
5847
+ let t;
5848
+ let subpath;
5849
+ if (hit) {
5850
+ profile = sanitizeWidthProfile(node.strokeWidthProfile);
5851
+ ref = { nodeId: node.id, index: hit.ref.index, subpath: hit.ref.subpath };
5852
+ t = profile[ref.index]?.t ?? 0;
5853
+ subpath = hit.ref.subpath;
5854
+ if (hit.ref.part === "center") mode = "move";
5855
+ else if (input.alt) side = hit.ref.part;
5856
+ } else {
5857
+ const near2 = nearestStrokePosition(g.line, [local.x, local.y]);
5858
+ const inserted = insertWidthPoint(strokeWidthProfileOf(node), near2.t);
5859
+ profile = inserted.points;
5860
+ subpath = near2.contour;
5861
+ t = near2.t;
5862
+ ref = { nodeId: node.id, index: inserted.index, subpath };
5863
+ if (input.alt) side = near2.side;
5864
+ }
5865
+ const at2 = strokePositionPoint(g.line, subpath, t);
5866
+ if (!at2) return;
5867
+ const base = { ...this.doc, nodes: mapTree(this.doc.nodes, (n) => n.id === node.id ? { ...n, strokeWidthProfile: profile } : n) };
5868
+ this.doc = base;
5869
+ this.widthPoint = { nodeId: node.id, index: ref.index, subpath };
5870
+ this.drag = {
5871
+ kind: "width",
5872
+ ref,
5873
+ mode,
5874
+ side,
5875
+ origin: { x: at2.point[0], y: at2.point[1] },
5876
+ normal: { x: at2.tangent[1], y: -at2.tangent[0] },
5877
+ unit: g.unit,
5878
+ profile,
5879
+ before,
5880
+ base,
5881
+ start: p,
5882
+ moved: false,
5883
+ created: !hit
5884
+ };
5885
+ const w = profile[ref.index];
5886
+ this.interaction = { type: "width", ref, mode, width: (w.left + w.right) * g.unit };
5887
+ this.notify(!hit);
5888
+ }
4370
5889
  /** The nearest outline point of any visible, unlocked path within the pick distance of document point `p`. */
4371
5890
  nearestOutline(p) {
4372
5891
  let best = null;
@@ -4414,7 +5933,9 @@ var VectorEditorController = class {
4414
5933
  case "pen":
4415
5934
  return this.downPen(screen, p, input);
4416
5935
  case "pencil":
4417
- return this.downPencil(p);
5936
+ return this.downPencil(p, input);
5937
+ case "width":
5938
+ return this.downWidth(screen, p, input);
4418
5939
  case "scissors": {
4419
5940
  const seg = this.nearestOutline(p);
4420
5941
  if (seg) this.cutPathAt(seg.nodeId, seg.point);
@@ -4500,6 +6021,13 @@ var VectorEditorController = class {
4500
6021
  this.notify();
4501
6022
  return;
4502
6023
  }
6024
+ const widget = this.hitCornerWidget(screen);
6025
+ if (widget) {
6026
+ const refs = getVectorCornerWidgets(this.cornerView()).map((w) => w.ref);
6027
+ this.drag = { kind: "corner", widget, refs, start: p, before: this.snapshot(), base: this.doc, moved: false, alt: !!input.alt, shift: !!input.shift };
6028
+ this.notify();
6029
+ return;
6030
+ }
4503
6031
  for (const id of getEditablePathIds(this.view())) {
4504
6032
  const node = this.getNode(id);
4505
6033
  const seg = nearestSegment(node, this.toLocal(id, p));
@@ -4557,10 +6085,10 @@ var VectorEditorController = class {
4557
6085
  }
4558
6086
  const last = getAnchorWorld(this.doc, { nodeId: pen.nodeId, subpath: pen.subpath, index: sp.anchors.length - 1 });
4559
6087
  const leave = { at: { x: last.x, y: last.y }, constrain: input.shift ? "angle" : null };
4560
- let pt2 = this.snapDrawn(p, leave, input, () => this.drawingTargets(pen.nodeId));
4561
- pt2 = this.toLocal(pen.nodeId, pt2);
6088
+ let pt3 = this.snapDrawn(p, leave, input, () => this.drawingTargets(pen.nodeId));
6089
+ pt3 = this.toLocal(pen.nodeId, pt3);
4562
6090
  const node2 = this.getNode(pen.nodeId);
4563
- const anchors = [...sp.anchors, { x: pt2.x, y: pt2.y }];
6091
+ const anchors = [...sp.anchors, { x: pt3.x, y: pt3.y }];
4564
6092
  this.replaceNode(pen.nodeId, { ...node2, subpaths: node2.subpaths.map((s, i) => i === pen.subpath ? { ...s, anchors } : s) });
4565
6093
  const ref2 = { nodeId: pen.nodeId, subpath: pen.subpath, index: anchors.length - 1 };
4566
6094
  this.anchors = [ref2];
@@ -4588,8 +6116,8 @@ var VectorEditorController = class {
4588
6116
  this.notify(this.doc !== before.doc);
4589
6117
  return;
4590
6118
  }
4591
- const pt = this.snapDrawn(p, null, input, () => this.drawingTargets(null));
4592
- const node = { id: this.newId(), type: "path", name: "Path", ...this.styleFor(true), subpaths: [{ anchors: [{ x: pt.x, y: pt.y }], closed: false }] };
6119
+ const pt2 = this.snapDrawn(p, null, input, () => this.drawingTargets(null));
6120
+ const node = { id: this.newId(), type: "path", name: "Path", ...this.styleFor(true), subpaths: [{ anchors: [{ x: pt2.x, y: pt2.y }], closed: false }] };
4593
6121
  this.setDoc([...this.doc.nodes, node]);
4594
6122
  this.pen = { nodeId: node.id, subpath: 0 };
4595
6123
  this.selection = [node.id];
@@ -4600,7 +6128,7 @@ var VectorEditorController = class {
4600
6128
  this.interaction = { type: "pen", nodeId: node.id };
4601
6129
  this.notify(true);
4602
6130
  }
4603
- downPencil(p) {
6131
+ downPencil(p, input = {}) {
4604
6132
  const before = this.snapshot();
4605
6133
  const node = {
4606
6134
  id: this.newId(),
@@ -4614,7 +6142,11 @@ var VectorEditorController = class {
4614
6142
  this.setDoc([...this.doc.nodes, node]);
4615
6143
  const lazy = new LazyMouse(this.editing.pencilSmoothing * VECTOR_PENCIL_STRING_PX / this.viewport.zoom);
4616
6144
  lazy.push([p.x, p.y]);
4617
- this.drag = { kind: "pencil", nodeId: node.id, points: [[p.x, p.y]], before, lazy };
6145
+ const mode = this.editing.pencilPressure;
6146
+ const hasPressure = typeof input.pressure === "number" && Number.isFinite(input.pressure);
6147
+ const record = hasPressure && (mode === "all" || mode === "pen" && input.pointerType === "pen");
6148
+ const pressure = hasPressure ? input.pressure : 0.5;
6149
+ this.drag = { kind: "pencil", nodeId: node.id, points: [[p.x, p.y]], before, lazy, pressures: record ? [pressure] : null, pressure };
4618
6150
  this.interaction = { type: "draw", tool: "pencil", nodeId: node.id, bounds: { x: p.x, y: p.y, width: 0, height: 0 } };
4619
6151
  this.notify(true);
4620
6152
  }
@@ -4737,13 +6269,25 @@ var VectorEditorController = class {
4737
6269
  case "handle": {
4738
6270
  const anchor = getAnchorWorld(drag.base, drag.ref);
4739
6271
  const leave = anchor ? { at: { x: anchor.x, y: anchor.y }, constrain: input.shift ? "angle" : null } : null;
4740
- const pt = this.snapDrawn(p, leave, input, () => drag.targets ??= VectorSnapTargets.forHandle(drag.base, drag.ref.nodeId));
4741
- const local = this.toLocal(drag.ref.nodeId, pt, drag.base);
6272
+ const pt2 = this.snapDrawn(p, leave, input, () => drag.targets ??= VectorSnapTargets.forHandle(drag.base, drag.ref.nodeId));
6273
+ const local = this.toLocal(drag.ref.nodeId, pt2, drag.base);
4742
6274
  this.doc = this.updateAnchors(drag.base, [drag.ref], (a) => moveAnchorHandle(a, drag.ref.handle, local, input.alt));
4743
6275
  this.interaction = { type: "handle", ref: drag.ref };
4744
6276
  this.notify(true);
4745
6277
  return;
4746
6278
  }
6279
+ case "corner": {
6280
+ if (!drag.moved && Math.hypot(p.x - drag.start.x, p.y - drag.start.y) * this.viewport.zoom < 3) return;
6281
+ drag.moved = true;
6282
+ const c = drag.widget.corner;
6283
+ const inv = invertMatrix(this.worldOf(drag.widget.ref.nodeId, drag.base));
6284
+ const d = applyMatrixLinear(inv, { x: p.x - drag.start.x, y: p.y - drag.start.y });
6285
+ const radius = Math.max(0, Math.min(c.maxRadius, drag.widget.drawnRadius + cornerRadiusChange(c, d)));
6286
+ this.doc = this.updateAnchors(drag.base, drag.refs, (a) => setAnchorCorner(a, radius));
6287
+ this.interaction = { type: "corner", ref: drag.widget.ref, radius };
6288
+ this.notify(true);
6289
+ return;
6290
+ }
4747
6291
  case "bend": {
4748
6292
  if (!drag.moved && Math.hypot(p.x - drag.start.x, p.y - drag.start.y) * this.viewport.zoom < 2) return;
4749
6293
  drag.moved = true;
@@ -4769,10 +6313,10 @@ var VectorEditorController = class {
4769
6313
  return;
4770
6314
  }
4771
6315
  this.guides = null;
4772
- let pt = p;
4773
- if (input.shift) pt = constrain45(anchor, pt);
6316
+ let pt2 = p;
6317
+ if (input.shift) pt2 = constrain45(anchor, pt2);
4774
6318
  const side = drag.closing ? "in" : "out";
4775
- const pos = this.toLocal(drag.ref.nodeId, drag.closing ? { x: 2 * anchor.x - pt.x, y: 2 * anchor.y - pt.y } : pt, drag.base);
6319
+ const pos = this.toLocal(drag.ref.nodeId, drag.closing ? { x: 2 * anchor.x - pt2.x, y: 2 * anchor.y - pt2.y } : pt2, drag.base);
4776
6320
  this.doc = this.updateAnchors(drag.base, [drag.ref], (a) => {
4777
6321
  if (drag.outOnly || drag.altBreaks && input.alt) return moveAnchorHandle(a, side, pos, true);
4778
6322
  return moveAnchorHandle({ ...a, type: "symmetric" }, side, pos);
@@ -4811,12 +6355,36 @@ var VectorEditorController = class {
4811
6355
  this.notify(true);
4812
6356
  return;
4813
6357
  }
6358
+ case "width": {
6359
+ if (!drag.moved && Math.hypot(p.x - drag.start.x, p.y - drag.start.y) * this.viewport.zoom < 2) return;
6360
+ drag.moved = true;
6361
+ const node = findVectorNode(drag.base.nodes, drag.ref.nodeId);
6362
+ const local = this.toLocal(node.id, p, drag.base);
6363
+ let profile;
6364
+ if (drag.mode === "move") {
6365
+ const g = vectorStrokeWidthGeometry(node);
6366
+ const near2 = g ? nearestStrokePosition(g.line, [local.x, local.y], drag.ref.subpath) : null;
6367
+ if (!near2) return;
6368
+ profile = setWidthPoint(drag.profile, drag.ref.index, { t: near2.t });
6369
+ } else {
6370
+ const off = ((local.x - drag.origin.x) * drag.normal.x + (local.y - drag.origin.y) * drag.normal.y) / drag.unit;
6371
+ const patch = drag.side === "both" ? { left: Math.abs(off), right: Math.abs(off) } : drag.side === "left" ? { left: Math.max(0, off) } : { right: Math.max(0, -off) };
6372
+ profile = setWidthPoint(drag.profile, drag.ref.index, patch);
6373
+ }
6374
+ this.doc = { ...drag.base, nodes: mapTree(drag.base.nodes, (n) => n.id === node.id ? { ...n, strokeWidthProfile: profile } : n) };
6375
+ const w = profile[drag.ref.index];
6376
+ this.interaction = { type: "width", ref: drag.ref, mode: drag.mode, width: (w.left + w.right) * drag.unit };
6377
+ this.notify(true);
6378
+ return;
6379
+ }
4814
6380
  case "pencil": {
4815
6381
  const last = drag.points[drag.points.length - 1];
4816
6382
  let moved = false;
6383
+ if (typeof input.pressure === "number" && Number.isFinite(input.pressure)) drag.pressure = input.pressure;
4817
6384
  for (const q of drag.lazy.push([p.x, p.y])) {
4818
6385
  if (Math.hypot(q[0] - last[0], q[1] - last[1]) * this.viewport.zoom < 1.5) continue;
4819
6386
  drag.points.push(q);
6387
+ drag.pressures?.push(drag.pressure);
4820
6388
  moved = true;
4821
6389
  }
4822
6390
  const string = drag.lazy.string();
@@ -4885,19 +6453,19 @@ var VectorEditorController = class {
4885
6453
  const guides = { lines: [], labels: [] };
4886
6454
  for (const axis of ["x", "y"]) {
4887
6455
  const d = end[axis] - start[axis];
4888
- const len = Math.abs(d) > 1e-9 ? targets.matchSize(Math.abs(d) * k, tol) : null;
4889
- if (len === null || len <= 0) continue;
4890
- out[axis] = start[axis] + Math.sign(d) * len / k;
6456
+ const len2 = Math.abs(d) > 1e-9 ? targets.matchSize(Math.abs(d) * k, tol) : null;
6457
+ if (len2 === null || len2 <= 0) continue;
6458
+ out[axis] = start[axis] + Math.sign(d) * len2 / k;
4891
6459
  const from = { ...out, [axis]: fromCentre ? 2 * start[axis] - out[axis] : start[axis] };
4892
6460
  guides.lines.push({ kind: "size", x1: from.x, y1: from.y, x2: out.x, y2: out.y });
4893
- 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}` });
6461
+ 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}` });
4894
6462
  }
4895
6463
  this.setGuides(guides);
4896
6464
  return out;
4897
6465
  }
4898
6466
  /** Primary button released. */
4899
6467
  pointerUp(screen, input = {}) {
4900
- if (screen && this.drag && this.drag.kind !== "pan") this.pointerMove(screen, input);
6468
+ if (screen && this.drag && this.drag.kind !== "pan") this.pointerMove(screen, { ...input, pressure: void 0 });
4901
6469
  const drag = this.drag;
4902
6470
  this.drag = null;
4903
6471
  this.interaction = null;
@@ -4935,6 +6503,29 @@ var VectorEditorController = class {
4935
6503
  return;
4936
6504
  }
4937
6505
  return this.commit("Bend Segment", drag.before);
6506
+ case "corner": {
6507
+ if (drag.moved) return this.commit("Corner Radius", drag.before);
6508
+ if (drag.alt) {
6509
+ const kind = nextVectorCornerKind(drag.widget.kind);
6510
+ this.doc = this.updateAnchors(drag.base, drag.refs, (a) => setAnchorCorner(a, a.corner?.radius ?? 0, kind));
6511
+ return this.commit("Corner Kind", drag.before);
6512
+ }
6513
+ const ref = drag.widget.ref;
6514
+ const has = this.anchors.some((a) => sameRef(a, ref));
6515
+ this.editingId = ref.nodeId;
6516
+ this.anchors = drag.shift ? has ? this.anchors.filter((a) => !sameRef(a, ref)) : [...this.anchors, ref] : [ref];
6517
+ this.notify();
6518
+ return;
6519
+ }
6520
+ case "width": {
6521
+ if (drag.moved) return this.commit(drag.mode === "move" ? "Move Width Point" : "Width Point", drag.before);
6522
+ if (drag.created) {
6523
+ this.doc = drag.before.doc;
6524
+ this.widthPoint = null;
6525
+ this.notify(true);
6526
+ } else this.notify();
6527
+ return;
6528
+ }
4938
6529
  case "stroke": {
4939
6530
  this.notify();
4940
6531
  if (drag.tool === "knife") this.knife(drag.points);
@@ -4965,7 +6556,10 @@ var VectorEditorController = class {
4965
6556
  this.notify(true);
4966
6557
  return;
4967
6558
  }
4968
- for (const q of drag.lazy.finish()) drag.points.push(q);
6559
+ for (const q of drag.lazy.finish()) {
6560
+ drag.points.push(q);
6561
+ drag.pressures?.push(drag.pressure);
6562
+ }
4969
6563
  const shape = this.editing.recognizeShapes ? recognizeInkShape(drag.points, { minSize: 6 / this.viewport.zoom }) : null;
4970
6564
  if (shape) {
4971
6565
  this.replaceNode(drag.nodeId, { ...node, name: inkShapeLabel(shape), subpaths: [inkShapeSubpath(shape)] });
@@ -4980,7 +6574,8 @@ var VectorEditorController = class {
4980
6574
  anchors.push({ x: c.p3[0], y: c.p3[1], in: { x: c.p2[0], y: c.p2[1] } });
4981
6575
  }
4982
6576
  const subpaths = anchors.length > 1 ? bezierToSubpaths(subpathsToBezier([{ anchors, closed: false }])) : node.subpaths;
4983
- this.replaceNode(drag.nodeId, { ...node, subpaths });
6577
+ const profile = drag.pressures ? pencilPressureProfile(drag.points, drag.pressures) : [];
6578
+ this.replaceNode(drag.nodeId, { ...node, subpaths, ...profile.length ? { strokeWidthProfile: profile } : {} });
4984
6579
  this.selection = [drag.nodeId];
4985
6580
  return this.commit("Draw", drag.before);
4986
6581
  }
@@ -5102,7 +6697,10 @@ var VectorEditorController = class {
5102
6697
  switch (key) {
5103
6698
  case "Escape":
5104
6699
  if (this.drag) this.cancelDrag();
5105
- else if (this.pen) this.finishPath();
6700
+ else if (this.tool === "width" && this.widthPoint) {
6701
+ this.widthPoint = null;
6702
+ this.notify();
6703
+ } else if (this.pen) this.finishPath();
5106
6704
  else if (this.tool === "direct" && this.anchors.length) {
5107
6705
  this.anchors = [];
5108
6706
  this.notify();
@@ -5123,6 +6721,7 @@ var VectorEditorController = class {
5123
6721
  return false;
5124
6722
  case "Delete":
5125
6723
  case "Backspace":
6724
+ if (this.tool === "width" && this.widthPoint) return this.removeWidthPoint(this.widthPoint).ok;
5126
6725
  if (this.pen) {
5127
6726
  const node = this.getNode(this.pen.nodeId);
5128
6727
  const sp = node.subpaths[this.pen.subpath];
@@ -5179,9 +6778,9 @@ function inkShapeSubpath(shape) {
5179
6778
  function constrain45(origin, p) {
5180
6779
  const dx = p.x - origin.x;
5181
6780
  const dy = p.y - origin.y;
5182
- const len = Math.hypot(dx, dy);
6781
+ const len2 = Math.hypot(dx, dy);
5183
6782
  const a = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);
5184
- return { x: origin.x + Math.cos(a) * len, y: origin.y + Math.sin(a) * len };
6783
+ return { x: origin.x + Math.cos(a) * len2, y: origin.y + Math.sin(a) * len2 };
5185
6784
  }
5186
6785
  function scaleFor(handle, b, p, mods = {}) {
5187
6786
  const west = handle.includes("w");
@@ -5204,6 +6803,31 @@ function scaleFor(handle, b, p, mods = {}) {
5204
6803
  }
5205
6804
  return { sx, sy, origin };
5206
6805
  }
6806
+ function withStyle(node, style) {
6807
+ const next = { ...node, ...style };
6808
+ if ("strokeWidthProfile" in style) {
6809
+ const p = style.strokeWidthProfile ? sanitizeWidthProfile(style.strokeWidthProfile) : [];
6810
+ if (p.length) next.strokeWidthProfile = p;
6811
+ else delete next.strokeWidthProfile;
6812
+ }
6813
+ return next;
6814
+ }
6815
+ function hiddenOrLocked(nodes, id) {
6816
+ const loc = locateVectorNode(nodes, id);
6817
+ if (!loc) return false;
6818
+ const n = loc.list[loc.index];
6819
+ if (n.locked || n.visible === false) return true;
6820
+ return loc.parent ? hiddenOrLocked(nodes, loc.parent.id) : false;
6821
+ }
6822
+ function pencilPressureProfile(points, pressures) {
6823
+ const n = Math.min(points.length, pressures.length);
6824
+ if (n < 2) return [];
6825
+ const s = [0];
6826
+ for (let i = 1; i < n; i++) s.push(s[i - 1] + Math.hypot(points[i][0] - points[i - 1][0], points[i][1] - points[i - 1][1]));
6827
+ const total = s[n - 1];
6828
+ if (!(total > 0)) return [];
6829
+ return pressureWidthProfile(s.map((d, i) => ({ t: d / total, pressure: pressures[i] })));
6830
+ }
5207
6831
 
5208
6832
  // src/vector/render.ts
5209
6833
  var fmt = (n) => Math.round(n * 100) / 100;
@@ -5218,10 +6842,12 @@ function renderNode(node, defs, idPrefix) {
5218
6842
  const stroke = vectorPaintToSvg(node.stroke, `${idPrefix}stroke-${node.id}`);
5219
6843
  if (fill.gradient) defs.push(fill.gradient);
5220
6844
  if (stroke.gradient) defs.push(stroke.gradient);
6845
+ const strokeParts = stroke.value !== "none" ? vectorStrokeSvgParts(node, `${idPrefix}clip-${node.id}`) : void 0;
5221
6846
  return {
5222
6847
  kind: "path",
5223
6848
  id: node.id,
5224
6849
  ...node.transform ? { transform: fmtMatrix(node.transform) } : {},
6850
+ ...strokeParts ? { strokeParts } : {},
5225
6851
  d: vectorPathData(node),
5226
6852
  fill: fill.value,
5227
6853
  stroke: stroke.value,
@@ -5237,9 +6863,9 @@ function getVectorSceneRenderData(state, options = {}) {
5237
6863
  const defs = [];
5238
6864
  const prefix = options.idPrefix ?? "ve-";
5239
6865
  const nodes = state.document.nodes.map((n) => renderNode(n, defs, prefix)).filter((n) => !!n);
5240
- const v = state.viewport;
6866
+ const v2 = state.viewport;
5241
6867
  return {
5242
- transform: `translate(${fmt(v.x)} ${fmt(v.y)}) scale(${v.zoom})`,
6868
+ transform: `translate(${fmt(v2.x)} ${fmt(v2.y)}) scale(${v2.zoom})`,
5243
6869
  width: state.document.width,
5244
6870
  height: state.document.height,
5245
6871
  ...state.document.background ? { background: state.document.background } : {},
@@ -5249,8 +6875,8 @@ function getVectorSceneRenderData(state, options = {}) {
5249
6875
  }
5250
6876
  var toScreen = (state, p) => ({ x: p.x * state.viewport.zoom + state.viewport.x, y: p.y * state.viewport.zoom + state.viewport.y });
5251
6877
  function screenOutline(state, node) {
5252
- const v = state.viewport;
5253
- const view = [v.zoom, 0, 0, v.zoom, v.x, v.y];
6878
+ const v2 = state.viewport;
6879
+ const view = [v2.zoom, 0, 0, v2.zoom, v2.x, v2.y];
5254
6880
  const parent = vectorParentMatrix(state.document.nodes, node.id) ?? IDENTITY_MATRIX;
5255
6881
  return collectWithMatrix(node, parent).map(({ path, parent: m }) => bezierPathToSvg(transformBezierPath(vectorPathWorldBezier(path, m), view), { precision: 2 })).join(" ");
5256
6882
  }
@@ -5359,8 +6985,28 @@ function getVectorOverlayRenderData(state) {
5359
6985
  const b = toScreen(state, it.string[1]);
5360
6986
  pencilString = { x1: fmt(a.x), y1: fmt(a.y), x2: fmt(b.x), y2: fmt(b.y) };
5361
6987
  }
6988
+ const dragged = it?.type === "corner" ? it.ref : null;
6989
+ const cornerWidgets = getVectorCornerWidgets(state).map((w) => {
6990
+ const s = toScreen(state, w);
6991
+ 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 };
6992
+ });
6993
+ if (it?.type === "corner") status = `R ${fmt(it.radius)}`;
5362
6994
  const cutPoint = state.tool === "scissors" && state.hover?.segment && !it ? (({ x, y }) => ({ x: fmt(x), y: fmt(y) }))(toScreen(state, state.hover.segment.point)) : null;
5363
- return { outlines, selectionBox, anchors, handles, marquee, penPreview, artboard, guides, status, toolStroke, pencilString, cutPoint };
6995
+ const sc = (p) => {
6996
+ const q = toScreen(state, p);
6997
+ return { x: fmt(q.x), y: fmt(q.y) };
6998
+ };
6999
+ const sameWidth = (a, b) => !!a && a.nodeId === b.nodeId && a.index === b.index;
7000
+ const widthPoints = getVectorWidthHandles(state).map((h) => {
7001
+ const c = sc(h);
7002
+ const hw = state.hover?.widthPoint;
7003
+ const hovered = hw && sameWidth(hw, h.ref) && hw.subpath === h.ref.subpath ? hw.part : null;
7004
+ return { ref: h.ref, x: c.x, y: c.y, left: sc(h.left), right: sc(h.right), selected: h.selected, hovered, active: it?.type === "width" && sameWidth(it.ref, h.ref) };
7005
+ });
7006
+ const spot = state.tool === "width" && !it ? state.hover?.strokeSpot : null;
7007
+ const widthSpot = spot ? { ...sc(spot.point), left: sc(spot.left), right: sc(spot.right) } : null;
7008
+ if (it?.type === "width") status = it.mode === "move" ? "Move width point" : `W ${fmt(it.width)}`;
7009
+ return { outlines, selectionBox, anchors, handles, marquee, penPreview, artboard, guides, status, toolStroke, pencilString, cutPoint, cornerWidgets, widthPoints, widthSpot };
5364
7010
  }
5365
7011
  var VECTOR_OVERLAY_THEME = {
5366
7012
  accent: "#0ea5e9",
@@ -5372,13 +7018,25 @@ var VECTOR_OVERLAY_THEME = {
5372
7018
  eraserFill: "rgba(239, 68, 68, 0.25)",
5373
7019
  transformHandleSize: 8,
5374
7020
  anchorSize: 7,
5375
- handleRadius: 3.5
7021
+ handleRadius: 3.5,
7022
+ /** Radius of a live corner widget (a ring with a dot). */
7023
+ cornerWidgetRadius: 4,
7024
+ /** Half the diagonal of a width point's diamond. */
7025
+ widthPointSize: 5
5376
7026
  };
7027
+ function vectorDiamondPath(x, y, r = VECTOR_OVERLAY_THEME.widthPointSize) {
7028
+ return `M${fmt(x)} ${fmt(y - r)}L${fmt(x + r)} ${fmt(y)}L${fmt(x)} ${fmt(y + r)}L${fmt(x - r)} ${fmt(y)}Z`;
7029
+ }
5377
7030
  function toVectorModifiers(event) {
5378
7031
  return { shift: !!event.shiftKey, alt: !!event.altKey, mod: !!(event.metaKey || event.ctrlKey) };
5379
7032
  }
5380
7033
  function toVectorPointerInput(event) {
5381
- return { ...toVectorModifiers(event), button: event.button };
7034
+ return {
7035
+ ...toVectorModifiers(event),
7036
+ button: event.button,
7037
+ ...typeof event.pressure === "number" ? { pressure: event.pressure } : {},
7038
+ ...event.pointerType ? { pointerType: event.pointerType } : {}
7039
+ };
5382
7040
  }
5383
7041
  function toVectorKeyInput(event) {
5384
7042
  return { key: event.key, ...toVectorModifiers(event) };
@@ -5396,18 +7054,75 @@ function getVectorSelectionStyle(state) {
5396
7054
  const node = findVectorNode(state.document.nodes, id);
5397
7055
  if (node) paths.push(...collect(node));
5398
7056
  }
7057
+ const anchorMode = state.tool === "direct" && !!state.selectedAnchors?.length;
7058
+ if (anchorMode) {
7059
+ for (const r of state.selectedAnchors) {
7060
+ if (paths.some((p) => p.id === r.nodeId)) continue;
7061
+ const node = findVectorNode(state.document.nodes, r.nodeId);
7062
+ if (node?.type === "path") paths.push(node);
7063
+ }
7064
+ }
5399
7065
  if (!paths.length) return null;
5400
7066
  const first = paths[0];
5401
7067
  const same = (get) => paths.every((p) => JSON.stringify(get(p) ?? null) === JSON.stringify(get(first) ?? null));
7068
+ const corners2 = [];
7069
+ for (const p of paths) {
7070
+ for (const c of vectorCorners(p.subpaths)) {
7071
+ if (anchorMode && !state.selectedAnchors.some((r) => r.nodeId === p.id && r.subpath === c.subpath && r.index === c.index)) continue;
7072
+ const corner = p.subpaths[c.subpath].anchors[c.index].corner;
7073
+ corners2.push({ radius: corner?.radius ?? 0, kind: corner?.kind ?? "round" });
7074
+ }
7075
+ }
7076
+ const c0 = corners2[0] ?? { radius: 0, kind: "round" };
5402
7077
  return {
5403
7078
  fill: first.fill ?? null,
5404
7079
  stroke: first.stroke ?? null,
5405
7080
  strokeWidth: first.strokeWidth ?? 1,
5406
7081
  opacity: first.opacity ?? 1,
5407
- mixed: { fill: !same((p) => p.fill), stroke: !same((p) => p.stroke), strokeWidth: !same((p) => p.strokeWidth ?? 1), opacity: !same((p) => p.opacity ?? 1) },
7082
+ strokeAlign: first.strokeAlign ?? "center",
7083
+ startArrow: first.startArrow ?? null,
7084
+ endArrow: first.endArrow ?? null,
7085
+ arrowAlign: first.arrowAlign ?? "extend",
7086
+ startArrowScale: first.startArrowScale ?? 1,
7087
+ endArrowScale: first.endArrowScale ?? 1,
7088
+ strokeWidthProfile: profileOf(first),
7089
+ widthProfile: widthProfilePresetId(profileOf(first)),
7090
+ cornerRadius: c0.radius,
7091
+ cornerKind: c0.kind,
7092
+ corners: corners2.length,
7093
+ hasOpenPath: paths.some((p) => p.subpaths.some((sp) => !sp.closed && sp.anchors.length > 1)),
7094
+ mixed: {
7095
+ fill: !same((p) => p.fill),
7096
+ stroke: !same((p) => p.stroke),
7097
+ strokeWidth: !same((p) => p.strokeWidth ?? 1),
7098
+ opacity: !same((p) => p.opacity ?? 1),
7099
+ strokeAlign: !same((p) => p.strokeAlign ?? "center"),
7100
+ startArrow: !same((p) => p.startArrow ?? null),
7101
+ endArrow: !same((p) => p.endArrow ?? null),
7102
+ arrowAlign: !same((p) => p.arrowAlign ?? "extend"),
7103
+ startArrowScale: !same((p) => p.startArrowScale ?? 1),
7104
+ endArrowScale: !same((p) => p.endArrowScale ?? 1),
7105
+ widthProfile: !same(profileOf),
7106
+ cornerRadius: corners2.some((c) => c.radius !== c0.radius),
7107
+ cornerKind: corners2.some((c) => c.kind !== c0.kind)
7108
+ },
5408
7109
  pathIds: paths.map((p) => p.id)
5409
7110
  };
5410
7111
  }
7112
+ function profileOf(p) {
7113
+ const points = sanitizeWidthProfile(p.strokeWidthProfile);
7114
+ return points.length && widthProfilePresetId(points) !== "uniform" ? points : null;
7115
+ }
7116
+ var VECTOR_STROKE_ALIGN_LABELS = { center: "Center", inside: "Inside", outside: "Outside" };
7117
+ var VECTOR_CORNER_KIND_LABELS = { round: "Round", inverted: "Inverted", chamfer: "Chamfer" };
7118
+ var VECTOR_ARROWHEAD_LABELS = {
7119
+ triangle: "Triangle",
7120
+ arrow: "Arrow",
7121
+ open: "Open arrow",
7122
+ circle: "Circle",
7123
+ square: "Square",
7124
+ bar: "Bar"
7125
+ };
5411
7126
  function vectorPaintToHex(paint) {
5412
7127
  if (typeof paint !== "string") return null;
5413
7128
  const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(paint.trim());
@@ -5420,7 +7135,8 @@ var VECTOR_PATH_ACTIONS = [
5420
7135
  { id: "average", label: "\u22B9", title: "Average Points", shortcut: "Alt+J", enabled: (_s, p) => p > 1, run: (c) => c.averageAnchors("both") },
5421
7136
  { id: "cut", label: "\u2702\xB7", title: "Cut at Points", enabled: (_s, p) => p > 0, run: (c) => c.cutAtAnchors() },
5422
7137
  { id: "clean", label: "\u2301", title: "Remove Redundant Points", enabled: (s, p) => s + p > 0, run: (c) => c.removeRedundantPoints() },
5423
- { id: "smooth", label: "\u223F", title: "Smooth", enabled: (s, p) => s + p > 0, run: (c) => c.smoothPaths() }
7138
+ { id: "smooth", label: "\u223F", title: "Smooth", enabled: (s, p) => s + p > 0, run: (c) => c.smoothPaths() },
7139
+ { id: "outline", label: "\u22A1", title: "Outline Stroke", enabled: (s) => s > 0, run: (c) => c.outlineStroke() }
5424
7140
  ];
5425
7141
  var VECTOR_EDIT_TOGGLES = [
5426
7142
  { key: "keepShapeOnDelete", label: "Keep", title: "Keep shape when deleting points" },
@@ -5448,6 +7164,19 @@ var POINTS = {
5448
7164
  minItems: 1,
5449
7165
  description: "Polyline [[x, y], \u2026] in document units."
5450
7166
  };
7167
+ var WIDTH_POINTS = {
7168
+ type: ["array", "null"],
7169
+ items: {
7170
+ type: "object",
7171
+ properties: {
7172
+ t: { type: "number", minimum: 0, maximum: 1, description: "Position along each subpath by length (0 start, 1 end)." },
7173
+ left: { type: "number", minimum: 0, description: "Left side as a multiple of half the stroke width (1 = uniform)." },
7174
+ right: { type: "number", minimum: 0, description: "Right side (looking along the path, y down)." }
7175
+ },
7176
+ required: ["t", "left", "right"]
7177
+ },
7178
+ description: "Variable-width profile points, interpolated smoothly (monotone cubic); two points at one `t` make a step. `null` or `[]` strokes at a uniform width."
7179
+ };
5451
7180
  var PAINT = {
5452
7181
  description: 'A CSS colour, `null` for none, or a gradient `{ type: "linear", angle, stops: [{ offset, color }] }`.'
5453
7182
  };
@@ -5458,7 +7187,14 @@ var STYLE_PROPS = {
5458
7187
  strokeLinecap: { type: "string", enum: ["butt", "round", "square"] },
5459
7188
  strokeLinejoin: { type: "string", enum: ["miter", "round", "bevel"] },
5460
7189
  strokeDasharray: { type: "array", items: { type: "number" } },
5461
- fillRule: { type: "string", enum: ["nonzero", "evenodd"] }
7190
+ fillRule: { type: "string", enum: ["nonzero", "evenodd"] },
7191
+ strokeAlign: { type: "string", enum: ["center", "inside", "outside"], description: "Where the stroke sits on the outline (closed shapes only; open paths stroke centred)." },
7192
+ startArrow: { type: ["string", "null"], enum: [...VECTOR_ARROWHEADS, null], description: "Head at the start of an open path, or null for none." },
7193
+ endArrow: { type: ["string", "null"], enum: [...VECTOR_ARROWHEADS, null], description: "Head at the end of an open path, or null for none." },
7194
+ arrowAlign: { type: "string", enum: ["extend", "tip"], description: "`extend`: the tip sits past the end point; `tip`: on it, the stroke shortened under the head." },
7195
+ startArrowScale: { type: "number", exclusiveMinimum: 0, description: "Start head size as a multiple of the stroke width (default 1)." },
7196
+ endArrowScale: { type: "number", exclusiveMinimum: 0, description: "End head size as a multiple of the stroke width (default 1)." },
7197
+ strokeWidthProfile: WIDTH_POINTS
5462
7198
  };
5463
7199
  var pickStyle = (params) => {
5464
7200
  const out = {};
@@ -5551,7 +7287,8 @@ function createVectorCommands(controller, options = {}) {
5551
7287
  y: { type: "number" },
5552
7288
  width: { type: "number", description: "For a line: the x offset of its end." },
5553
7289
  height: { type: "number", description: "For a line: the y offset of its end." },
5554
- cornerRadius: { type: "number", minimum: 0 },
7290
+ cornerRadius: { type: "number", minimum: 0, description: "Live corner radius of a rect, polygon or star." },
7291
+ cornerKind: { type: "string", enum: VECTOR_CORNER_KINDS },
5555
7292
  sides: { type: "integer", minimum: 3 },
5556
7293
  points: { type: "integer", minimum: 3 },
5557
7294
  innerRatio: { type: "number", minimum: 0, maximum: 1 },
@@ -5597,6 +7334,76 @@ function createVectorCommands(controller, options = {}) {
5597
7334
  return { ok: true };
5598
7335
  }
5599
7336
  },
7337
+ {
7338
+ ...base,
7339
+ id: `${prefix}setCorners`,
7340
+ label: "Corner Radius\u2026",
7341
+ keywords: ["round", "rounded", "corners", "radius", "fillet", "chamfer", "live corners"],
7342
+ 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.",
7343
+ params: { type: "object", properties: { radius: { type: "number", minimum: 0 }, kind: { type: "string", enum: VECTOR_CORNER_KINDS }, ids: IDS, anchors: ANCHORS } },
7344
+ undoable: true,
7345
+ run: (_ctx, params) => {
7346
+ const refs = params?.anchors ? anchorsOf(params.anchors) : void 0;
7347
+ const target = { refs, ids: params?.ids ? ids(params) : void 0 };
7348
+ if (params?.radius !== void 0) check(controller.setCornerRadius(params.radius, { ...target, kind: params.kind }));
7349
+ else if (params?.kind) check(controller.setCornerKind(params.kind, target));
7350
+ else fail("Pass `radius` and / or `kind`.");
7351
+ return { ok: true };
7352
+ }
7353
+ },
7354
+ {
7355
+ ...base,
7356
+ id: `${prefix}setWidthProfile`,
7357
+ label: "Width Profile\u2026",
7358
+ keywords: ["variable width", "taper", "tapered", "pressure", "profile", "brush", "calligraphy", "bulge", "thick", "thin"],
7359
+ description: "Gives the stroke of paths a variable width: a `preset` (`uniform` removes the profile), explicit `points` ({ t, left, right }: side scales along each subpath), or a parametric `taper` (startLength / endLength as fractions of the path, startWidth / endWidth 0\u20131, startEase / endEase 0\u20131) and / or `wave` (amount 0\u20131, cycles, phase in degrees). Profiled strokes draw as filled outlines; Outline Stroke bakes them.",
7360
+ params: {
7361
+ type: "object",
7362
+ properties: {
7363
+ ids: IDS,
7364
+ preset: { type: "string", enum: VECTOR_WIDTH_PRESET_IDS },
7365
+ points: WIDTH_POINTS,
7366
+ taper: {
7367
+ type: "object",
7368
+ properties: {
7369
+ startLength: { type: "number", minimum: 0, maximum: 1 },
7370
+ endLength: { type: "number", minimum: 0, maximum: 1 },
7371
+ startWidth: { type: "number", minimum: 0, maximum: 1 },
7372
+ endWidth: { type: "number", minimum: 0, maximum: 1 },
7373
+ startEase: { type: "number", minimum: 0, maximum: 1 },
7374
+ endEase: { type: "number", minimum: 0, maximum: 1 }
7375
+ }
7376
+ },
7377
+ wave: { type: "object", properties: { amount: { type: "number", minimum: 0, maximum: 1 }, cycles: { type: "number", exclusiveMinimum: 0 }, phase: { type: "number" } } }
7378
+ }
7379
+ },
7380
+ enabled: enabledFor,
7381
+ undoable: true,
7382
+ run: (_ctx, params) => {
7383
+ const target = { ids: ids(params) };
7384
+ if (params?.preset) check(controller.setWidthProfile(params.preset, target));
7385
+ else if (params?.points !== void 0) check(controller.setWidthProfile(params.points ?? null, target));
7386
+ else if (params?.taper || params?.wave) check(controller.setWidthProfile({ taper: params.taper, wave: params.wave }, target));
7387
+ else fail("Pass `preset`, `points`, `taper` or `wave`.");
7388
+ const node = controller.getNode(target.ids[0]);
7389
+ return { ok: true, points: node?.type === "path" ? node.strokeWidthProfile ?? [] : [] };
7390
+ }
7391
+ },
7392
+ {
7393
+ ...base,
7394
+ id: `${prefix}outlineStroke`,
7395
+ label: "Outline Stroke",
7396
+ keywords: ["expand", "stroke to path", "outline", "convert stroke"],
7397
+ 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.",
7398
+ params: { type: "object", properties: { ids: IDS } },
7399
+ enabled: enabledFor,
7400
+ undoable: true,
7401
+ run: (_ctx, params) => {
7402
+ const created = controller.outlineStroke({ ids: params?.ids ? ids(params) : void 0 });
7403
+ if (!created.length) fail("No selected path has a stroke.");
7404
+ return { ids: created };
7405
+ }
7406
+ },
5600
7407
  {
5601
7408
  ...base,
5602
7409
  id: `${prefix}transform`,
@@ -5888,7 +7695,7 @@ function createVectorCommands(controller, options = {}) {
5888
7695
  id: `${prefix}setEditing`,
5889
7696
  label: "Path Editing Options\u2026",
5890
7697
  keywords: ["settings", "curvature", "pencil", "smoothing", "lazy mouse", "shapes", "eraser"],
5891
- description: "Sets how editing behaves: `keepShapeOnDelete`, `curvatureMoves` (anchor drags bend like the Curvature tool), `pencilSmoothing` (lazy mouse, 0\u20131), `recognizeShapes` (pencil scribbles become lines, rectangles, ellipses, polygons) and `eraserSize`.",
7698
+ 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), `eraserSize` and `pencilPressure` (`pen`: record pen pressure on pencil strokes as a width profile, `all`: any pointer reporting pressure, `off`).",
5892
7699
  params: {
5893
7700
  type: "object",
5894
7701
  properties: {
@@ -5896,12 +7703,13 @@ function createVectorCommands(controller, options = {}) {
5896
7703
  curvatureMoves: { type: "boolean" },
5897
7704
  pencilSmoothing: { type: "number", minimum: 0, maximum: 1 },
5898
7705
  recognizeShapes: { type: "boolean" },
5899
- eraserSize: { type: "number", exclusiveMinimum: 0 }
7706
+ eraserSize: { type: "number", exclusiveMinimum: 0 },
7707
+ pencilPressure: { type: "string", enum: ["pen", "all", "off"] }
5900
7708
  }
5901
7709
  },
5902
7710
  run: (_ctx, params) => {
5903
7711
  const patch = {};
5904
- for (const key of ["keepShapeOnDelete", "curvatureMoves", "pencilSmoothing", "recognizeShapes", "eraserSize"]) if (params?.[key] !== void 0) patch[key] = params[key];
7712
+ for (const key of ["keepShapeOnDelete", "curvatureMoves", "pencilSmoothing", "recognizeShapes", "eraserSize", "pencilPressure"]) if (params?.[key] !== void 0) patch[key] = params[key];
5905
7713
  controller.setEditing(patch);
5906
7714
  return { editing: controller.getState().editing };
5907
7715
  }
@@ -5947,24 +7755,41 @@ export {
5947
7755
  DEFAULT_VECTOR_PATH_EDIT,
5948
7756
  DEFAULT_VECTOR_SNAP,
5949
7757
  IDENTITY_MATRIX,
5950
- KAPPA,
7758
+ KAPPA2 as KAPPA,
5951
7759
  LazyMouse,
7760
+ VECTOR_ARROWHEADS,
7761
+ VECTOR_ARROWHEAD_LABELS,
5952
7762
  VECTOR_CLIPBOARD_TYPE,
5953
7763
  VECTOR_CONSTRUCTION_ANGLES,
7764
+ VECTOR_CORNER_KINDS,
7765
+ VECTOR_CORNER_KIND_LABELS,
7766
+ VECTOR_CORNER_WIDGET_INSET,
5954
7767
  VECTOR_EDIT_TOGGLES,
5955
7768
  VECTOR_INK_MAX_CORNERS,
5956
7769
  VECTOR_OVERLAY_THEME,
5957
7770
  VECTOR_PATH_ACTIONS,
5958
7771
  VECTOR_PENCIL_STRING_PX,
7772
+ VECTOR_PRESSURE_NOMINAL,
5959
7773
  VECTOR_ROTATE_HANDLE_OFFSET,
7774
+ VECTOR_STROKE_ALIGN_LABELS,
7775
+ VECTOR_STYLE_KEYS,
5960
7776
  VECTOR_TOOLS,
5961
7777
  VECTOR_TOOL_GLYPHS,
5962
7778
  VECTOR_TOOL_SHORTCUTS,
5963
7779
  VECTOR_TRANSFORM_HANDLES,
7780
+ VECTOR_WIDTH_PRESETS,
7781
+ VECTOR_WIDTH_PRESET_IDS,
7782
+ VECTOR_WIDTH_PROFILE_MAX_POINTS,
7783
+ VECTOR_WIDTH_PROFILE_MAX_SCALE,
7784
+ VECTOR_WIDTH_SAME_T,
5964
7785
  VectorEditorController,
5965
7786
  VectorSnapTargets,
5966
7787
  applyMatrix,
5967
7788
  applyMatrixLinear,
7789
+ applyVectorCornersAttribute,
7790
+ arrowheadInset,
7791
+ arrowheadPath,
7792
+ arrowheadWeight,
5968
7793
  bendSegment,
5969
7794
  bezierToSubpaths,
5970
7795
  boundsFromPoints,
@@ -5976,67 +7801,97 @@ export {
5976
7801
  collectPathNodes,
5977
7802
  constrainToLeave,
5978
7803
  convertAnchor,
7804
+ cornerBisectorPoint,
7805
+ cornerRadiusChange,
7806
+ cornerSetback,
5979
7807
  createVectorCommands,
5980
7808
  curvatureMoveAnchor,
5981
7809
  cutSubpathAt,
7810
+ dashIntervals,
5982
7811
  decomposeRigid,
7812
+ effectiveStrokeAlign,
5983
7813
  ellipseSubpath,
5984
7814
  eraseFromFill,
5985
7815
  eraseFromOutline,
5986
7816
  eraserRegion,
7817
+ expandVectorCorners,
7818
+ expandVectorCornersOf,
5987
7819
  findVectorNode,
7820
+ fitCornerRadius,
7821
+ generateWidthProfile,
5988
7822
  getAnchor,
5989
7823
  getAnchorWorld,
5990
7824
  getEditablePathIds,
5991
7825
  getTransformHandlePositions,
7826
+ getVectorCornerWidgets,
5992
7827
  getVectorOverlayRenderData,
5993
7828
  getVectorSceneRenderData,
5994
7829
  getVectorSelectionBoxGeometry,
5995
7830
  getVectorSelectionFrame,
5996
7831
  getVectorSelectionStyle,
7832
+ getVectorWidthHandles,
5997
7833
  getVisibleAnchors,
5998
7834
  getVisibleHandles,
5999
7835
  hasPaint,
7836
+ hasStrokeExtras,
7837
+ hasVectorCorners,
6000
7838
  hitTestVectorNode,
6001
7839
  hitTestVectorNodes,
7840
+ hitTestVectorStroke,
6002
7841
  inferAnchorType,
6003
7842
  insertAnchor,
7843
+ insertWidthPoint,
6004
7844
  invertMatrix,
7845
+ isClosedBezierShape,
6005
7846
  isIdentityMatrix,
7847
+ isUniformWidthProfile,
6006
7848
  isVectorTextTarget,
6007
7849
  joinSubpaths,
6008
7850
  knifePath,
6009
7851
  lineSubpath,
6010
7852
  locateVectorNode,
6011
7853
  matrixRotation,
7854
+ mirrorWidthProfile,
6012
7855
  moveAnchorHandle,
6013
7856
  multiplyMatrices,
6014
7857
  nearestSegment,
7858
+ nearestStrokePosition,
7859
+ nextVectorCornerKind,
6015
7860
  nodeMatrix,
6016
7861
  parseSvgTransform,
6017
7862
  parseVectorSvg,
6018
7863
  polygonSubpath,
6019
7864
  polylineBounds,
7865
+ pressureWidthProfile,
6020
7866
  recognizeInkShape,
6021
7867
  rectSubpath,
6022
7868
  removeAnchorKeepingShape,
6023
7869
  removeAnchors,
6024
7870
  removeAnchorsKeepingShape,
6025
7871
  removeRedundantPoints,
7872
+ removeWidthPoints,
6026
7873
  reverseSubpath,
7874
+ reverseWidthProfile,
6027
7875
  rotateMatrix,
6028
7876
  roundSubpaths,
7877
+ sanitizeWidthProfile,
6029
7878
  scaleFor,
6030
7879
  scaleMatrix,
7880
+ setAnchorCorner,
7881
+ setSubpathCorners,
7882
+ setWidthPoint,
6031
7883
  simplifyPolyline,
6032
7884
  smoothSubpath,
6033
7885
  splitIntoFaces,
6034
7886
  splitSubpathAt,
6035
7887
  starSubpath,
7888
+ strokePositionPoint,
7889
+ strokeWidthProfileOf,
6036
7890
  subpathPointAt,
6037
7891
  subpathSegment,
6038
7892
  subpathSegmentCount,
6039
7893
  subpathsToBezier,
7894
+ taperWaveFactor,
6040
7895
  toVectorKeyInput,
6041
7896
  toVectorModifiers,
6042
7897
  toVectorPointerInput,
@@ -6044,6 +7899,12 @@ export {
6044
7899
  translateAnchor,
6045
7900
  translateMatrix,
6046
7901
  unionBounds,
7902
+ variableStrokeOutline,
7903
+ variableStrokeRings,
7904
+ vectorCornerAt,
7905
+ vectorCorners,
7906
+ vectorCornersAttribute,
7907
+ vectorDiamondPath,
6047
7908
  vectorDocumentToSvg,
6048
7909
  vectorInkShapeName,
6049
7910
  vectorNodeBounds,
@@ -6055,8 +7916,21 @@ export {
6055
7916
  vectorPathData,
6056
7917
  vectorPathWorldBezier,
6057
7918
  vectorResizeCursor,
7919
+ vectorStrokeLineArea,
7920
+ vectorStrokeOutline,
7921
+ vectorStrokeOutlinePath,
7922
+ vectorStrokeParts,
7923
+ vectorStrokeReach,
7924
+ vectorStrokeSvgParts,
7925
+ vectorStrokeWidthGeometry,
6058
7926
  vectorToolLabel,
6059
7927
  vectorWorldMatrix,
6060
- walkVectorNodes
7928
+ walkVectorNodes,
7929
+ widthProfileAt,
7930
+ widthProfileFromAttribute,
7931
+ widthProfileMaxScale,
7932
+ widthProfilePreset,
7933
+ widthProfilePresetId,
7934
+ widthProfileToAttribute
6061
7935
  };
6062
7936
  //# sourceMappingURL=index.js.map