@pixerate/editor 0.36.0 → 0.38.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.
@@ -2,6 +2,7 @@ import {
2
2
  HistoryManager
3
3
  } from "../chunk-QVZZGT4X.js";
4
4
  import {
5
+ PlanarMap,
5
6
  bezierPathBounds,
6
7
  bezierPathToSvg,
7
8
  closeContour,
@@ -10,13 +11,17 @@ import {
10
11
  distanceToBezierPath,
11
12
  fitCubicWithTangents,
12
13
  fitCubics,
14
+ flattenChain,
13
15
  formatNumber,
14
16
  gradientToSvgDef,
17
+ isStraightCubic,
15
18
  normalizeBezierPath,
16
19
  outlineStroke,
17
20
  pathBoolean,
18
21
  pathToContours,
22
+ pathfinder,
19
23
  pointInBezierPath,
24
+ polygonBoolean,
20
25
  segmentBounds,
21
26
  segmentDerivative,
22
27
  segmentIntersections,
@@ -31,13 +36,30 @@ import {
31
36
  transformBezierPath,
32
37
  trimSegments,
33
38
  windingNumber
34
- } from "../chunk-EGPIVUEF.js";
39
+ } from "../chunk-ZCCU3VNI.js";
35
40
 
36
41
  // src/vector/types.ts
37
42
  var VECTOR_CORNER_KINDS = ["round", "inverted", "chamfer"];
38
43
  var VECTOR_ARROWHEADS = ["triangle", "arrow", "open", "circle", "square", "bar"];
39
- var VECTOR_TOOLS = ["select", "direct", "pen", "pencil", "rect", "ellipse", "polygon", "star", "line", "scissors", "knife", "eraser"];
44
+ var VECTOR_TOOLS = [
45
+ "select",
46
+ "direct",
47
+ "pen",
48
+ "pencil",
49
+ "rect",
50
+ "ellipse",
51
+ "polygon",
52
+ "star",
53
+ "line",
54
+ "scissors",
55
+ "knife",
56
+ "eraser",
57
+ "width",
58
+ "shapeBuilder",
59
+ "paintBucket"
60
+ ];
40
61
  var VECTOR_TRANSFORM_HANDLES = ["nw", "n", "ne", "e", "se", "s", "sw", "w", "rotate"];
62
+ var VECTOR_PATHFINDER_OPERATIONS = ["divide", "trim", "merge", "crop", "outline", "minusBack"];
41
63
 
42
64
  // src/vector/corners.ts
43
65
  var EPS = 1e-9;
@@ -245,6 +267,535 @@ function setSubpathCorners(sp, radius, kind) {
245
267
  return { closed: sp.closed, anchors: sp.anchors.map((a) => setAnchorCorner(a, radius, kind)) };
246
268
  }
247
269
 
270
+ // src/vector/width.ts
271
+ var VECTOR_WIDTH_PROFILE_MAX_POINTS = 64;
272
+ var VECTOR_WIDTH_PROFILE_MAX_SCALE = 1e3;
273
+ var VECTOR_WIDTH_SAME_T = 1e-6;
274
+ var sym = (t, s) => ({ t, left: s, right: s });
275
+ var VECTOR_WIDTH_PRESETS = [
276
+ { id: "uniform", label: "Uniform", points: [] },
277
+ { id: "taperStart", label: "Taper start", points: [sym(0, 0), sym(1, 1)] },
278
+ { id: "taperEnd", label: "Taper end", points: [sym(0, 1), sym(1, 0)] },
279
+ { id: "taperBoth", label: "Taper both", points: [sym(0, 0), sym(0.5, 1), sym(1, 0)] },
280
+ // Full width at the ends, swelling to twice the width in the middle.
281
+ { id: "bulge", label: "Bulge", points: [sym(0, 1), sym(0.5, 2), sym(1, 1)] },
282
+ // Full width at the ends, pinched to a quarter in the middle.
283
+ { id: "pinch", label: "Pinch", points: [sym(0, 1), sym(0.5, 0.25), sym(1, 1)] },
284
+ // A round head a fifth of the way along, then a long taper to the end.
285
+ { id: "teardrop", label: "Teardrop", points: [sym(0, 0), sym(0.2, 1), sym(1, 0)] },
286
+ // Two swells between narrow necks.
287
+ { 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)] },
288
+ // A pen stroke: a quick press, a slow lift.
289
+ { 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)] }
290
+ ];
291
+ var VECTOR_WIDTH_PRESET_IDS = VECTOR_WIDTH_PRESETS.map((p) => p.id);
292
+ var clean = (v2, max) => typeof v2 === "number" && Number.isFinite(v2) ? Math.min(Math.max(v2, 0), max) : null;
293
+ function sanitizeWidthProfile(points) {
294
+ if (!Array.isArray(points)) return [];
295
+ const out = [];
296
+ for (const p of points) {
297
+ if (!p || typeof p !== "object") continue;
298
+ const t = clean(p.t, 1);
299
+ const l = clean(p.left, VECTOR_WIDTH_PROFILE_MAX_SCALE);
300
+ const r = clean(p.right, VECTOR_WIDTH_PROFILE_MAX_SCALE);
301
+ if (t === null || l === null && r === null) continue;
302
+ out.push({ t, left: l ?? r, right: r ?? l });
303
+ if (out.length >= VECTOR_WIDTH_PROFILE_MAX_POINTS) break;
304
+ }
305
+ return out.map((p, i) => [p, i]).sort((a, b) => a[0].t - b[0].t || a[1] - b[1]).map(([p]) => p);
306
+ }
307
+ function isUniformWidthProfile(points) {
308
+ return !points?.length || points.every((p) => p.left === 1 && p.right === 1);
309
+ }
310
+ function secant(pts, i, k) {
311
+ const a = pts[i];
312
+ const b = pts[i + 1];
313
+ return a && b && b.t - a.t > 1e-12 ? (b[k] - a[k]) / (b.t - a.t) : 0;
314
+ }
315
+ function tangent(pts, i, k) {
316
+ const last = pts.length - 1;
317
+ if (i <= 0) return secant(pts, 0, k);
318
+ if (i >= last) return secant(pts, last - 1, k);
319
+ const d0 = secant(pts, i - 1, k);
320
+ const d1 = secant(pts, i, k);
321
+ return d0 * d1 <= 0 ? 0 : 2 * d0 * d1 / (d0 + d1);
322
+ }
323
+ function interp(pts, t, k) {
324
+ const first = pts[0];
325
+ const last = pts[pts.length - 1];
326
+ if (!(t > first.t)) return first[k];
327
+ if (t >= last.t) return last[k];
328
+ let i = 0;
329
+ while (i + 1 < pts.length && pts[i + 1].t <= t) i++;
330
+ const a = pts[i];
331
+ const b = pts[i + 1];
332
+ const h = b.t - a.t;
333
+ if (h <= 1e-12) return b[k];
334
+ const u = (t - a.t) / h;
335
+ const u2 = u * u;
336
+ const u3 = u2 * u;
337
+ 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);
338
+ return Math.max(0, w);
339
+ }
340
+ function widthProfileAt(points, t) {
341
+ if (!points?.length) return { left: 1, right: 1 };
342
+ return { left: interp(points, t, "left"), right: interp(points, t, "right") };
343
+ }
344
+ function widthProfileMaxScale(points) {
345
+ if (!points?.length) return 1;
346
+ return points.reduce((m, p) => Math.max(m, p.left, p.right), 0);
347
+ }
348
+ function widthProfilePreset(id) {
349
+ const p = VECTOR_WIDTH_PRESETS.find((q) => q.id === id);
350
+ return p ? p.points.map((q) => ({ ...q })) : null;
351
+ }
352
+ function widthProfilePresetId(points) {
353
+ if (isUniformWidthProfile(points)) return "uniform";
354
+ 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);
355
+ return VECTOR_WIDTH_PRESETS.find((p) => p.points.length && same(points, p.points))?.id ?? "custom";
356
+ }
357
+ function reverseWidthProfile(points) {
358
+ return points.map((p) => ({ t: 1 - p.t, left: p.right, right: p.left })).reverse();
359
+ }
360
+ function mirrorWidthProfile(points) {
361
+ return points.map((p) => ({ t: p.t, left: p.right, right: p.left }));
362
+ }
363
+ function editable(points) {
364
+ return points?.length ? points.map((p) => ({ ...p })) : [sym(0, 1), sym(1, 1)];
365
+ }
366
+ function insertWidthPoint(points, t, sides2 = {}, options = {}) {
367
+ const pts = editable(points);
368
+ const tt = Math.min(Math.max(Number.isFinite(t) ? t : 0, 0), 1);
369
+ const here = widthProfileAt(points?.length ? points : null, tt);
370
+ const p = {
371
+ t: tt,
372
+ left: clean(sides2.left, VECTOR_WIDTH_PROFILE_MAX_SCALE) ?? here.left,
373
+ right: clean(sides2.right, VECTOR_WIDTH_PROFILE_MAX_SCALE) ?? here.right
374
+ };
375
+ const same = pts.findIndex((q) => Math.abs(q.t - tt) <= VECTOR_WIDTH_SAME_T);
376
+ if (same >= 0 && !options.step) {
377
+ pts[same] = { ...p, t: pts[same].t };
378
+ return { points: pts, index: same };
379
+ }
380
+ let i = 0;
381
+ while (i < pts.length && pts[i].t <= tt) i++;
382
+ if (pts.length >= VECTOR_WIDTH_PROFILE_MAX_POINTS) return { points: pts, index: Math.max(0, i - 1) };
383
+ pts.splice(i, 0, p);
384
+ return { points: pts, index: i };
385
+ }
386
+ function setWidthPoint(points, index, patch) {
387
+ const pts = points.map((p2) => ({ ...p2 }));
388
+ const p = pts[index];
389
+ if (!p) return pts;
390
+ if (typeof patch.t === "number" && Number.isFinite(patch.t)) {
391
+ const lo = index > 0 ? pts[index - 1].t : 0;
392
+ const hi = index < pts.length - 1 ? pts[index + 1].t : 1;
393
+ p.t = Math.min(Math.max(patch.t, lo), Math.max(hi, lo));
394
+ }
395
+ const l = clean(patch.left, VECTOR_WIDTH_PROFILE_MAX_SCALE);
396
+ const r = clean(patch.right, VECTOR_WIDTH_PROFILE_MAX_SCALE);
397
+ if (l !== null) p.left = l;
398
+ if (r !== null) p.right = r;
399
+ return pts;
400
+ }
401
+ function removeWidthPoints(points, indices) {
402
+ const drop = new Set(indices);
403
+ return points.filter((_, i) => !drop.has(i)).map((p) => ({ ...p }));
404
+ }
405
+ function ease(u, e) {
406
+ const x = Math.min(Math.max(u, 0), 1);
407
+ const round = Math.sqrt(Math.max(0, 1 - (1 - x) * (1 - x)));
408
+ return x + (round - x) * Math.min(Math.max(e, 0), 1);
409
+ }
410
+ function taperWaveFactor(t, taper, wave) {
411
+ let k = 1;
412
+ if (taper) {
413
+ let ls = Math.max(0, taper.startLength ?? 0);
414
+ let le = Math.max(0, taper.endLength ?? 0);
415
+ if (ls + le > 1) {
416
+ const f = 1 / (ls + le);
417
+ ls *= f;
418
+ le *= f;
419
+ }
420
+ const sw = Math.min(Math.max(taper.startWidth ?? 0, 0), 1);
421
+ const ew = Math.min(Math.max(taper.endWidth ?? 0, 0), 1);
422
+ if (ls > 0 && t < ls) k = Math.min(k, sw + (1 - sw) * ease(t / ls, taper.startEase ?? 0));
423
+ if (le > 0 && t > 1 - le) k = Math.min(k, ew + (1 - ew) * ease((1 - t) / le, taper.endEase ?? 0));
424
+ }
425
+ if (wave) {
426
+ const cycles = wave.cycles ?? 4;
427
+ const amount = Math.min(Math.max(wave.amount ?? 0.5, 0), 1);
428
+ if (cycles > 0 && amount > 0) {
429
+ const ph = 2 * Math.PI * t * cycles + (wave.phase ?? 0) * Math.PI / 180;
430
+ k *= 1 - amount * (0.5 - 0.5 * Math.cos(ph));
431
+ }
432
+ }
433
+ return k;
434
+ }
435
+ function generateWidthProfile(options) {
436
+ const cycles = options.wave ? Math.max(0, options.wave.cycles ?? 4) : 0;
437
+ const n = Math.max(8, Math.min(VECTOR_WIDTH_PROFILE_MAX_POINTS * 4, options.samples ?? Math.ceil(32 + cycles * 16)));
438
+ const raw = [];
439
+ for (let i = 0; i <= n; i++) raw.push([i / n, taperWaveFactor(i / n, options.taper, options.wave)]);
440
+ const ts = [options.taper?.startLength, options.taper?.endLength !== void 0 ? 1 - options.taper.endLength : void 0];
441
+ for (const t of ts) if (typeof t === "number" && t > 0 && t < 1) raw.push([t, taperWaveFactor(t, options.taper, options.wave)]);
442
+ raw.sort((a, b) => a[0] - b[0]);
443
+ const kept = simplifySamples(raw, options.tolerance ?? 0.01, VECTOR_WIDTH_PROFILE_MAX_POINTS);
444
+ return kept.map(([t, k]) => sym(t, k));
445
+ }
446
+ function simplifySamples(samples, tolerance, max) {
447
+ if (samples.length <= 2) return samples.slice();
448
+ const keep = new Array(samples.length).fill(false);
449
+ keep[0] = keep[samples.length - 1] = true;
450
+ const stack = [[0, samples.length - 1]];
451
+ while (stack.length) {
452
+ const [a, b] = stack.pop();
453
+ const [ta, va] = samples[a];
454
+ const [tb, vb] = samples[b];
455
+ let worst = -1;
456
+ let err = tolerance;
457
+ for (let i = a + 1; i < b; i++) {
458
+ const [t, v2] = samples[i];
459
+ const lin = tb - ta > 1e-12 ? va + (vb - va) * (t - ta) / (tb - ta) : va;
460
+ const e = Math.abs(v2 - lin);
461
+ if (e > err) {
462
+ err = e;
463
+ worst = i;
464
+ }
465
+ }
466
+ if (worst >= 0) {
467
+ keep[worst] = true;
468
+ stack.push([a, worst], [worst, b]);
469
+ }
470
+ }
471
+ let out = samples.filter((_, i) => keep[i]);
472
+ while (out.length > max) out = out.filter((_, i) => i === 0 || i === out.length - 1 || i % 2 === 0);
473
+ return out;
474
+ }
475
+ var VECTOR_PRESSURE_NOMINAL = 0.5;
476
+ function pressureWidthProfile(samples, options = {}) {
477
+ 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)]);
478
+ if (raw.length < 2) return [];
479
+ if (raw.every((s) => Math.abs(s[1] - raw[0][1]) < 1e-6)) return [];
480
+ return simplifySamples(raw, options.tolerance ?? 0.02, VECTOR_WIDTH_PROFILE_MAX_POINTS).map(([t, k]) => sym(t, Math.round(k * 1e3) / 1e3));
481
+ }
482
+
483
+ // src/vector/varstroke.ts
484
+ var SAMPLES_PER_SUBPATH = 128;
485
+ var MAX_DASHES = 5e3;
486
+ var sub2 = (a, b) => [a[0] - b[0], a[1] - b[1]];
487
+ var along = (p, v2, k) => [p[0] + v2[0] * k, p[1] + v2[1] * k];
488
+ var lerp = (a, b, u) => [a[0] + (b[0] - a[0]) * u, a[1] + (b[1] - a[1]) * u];
489
+ var dist = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1]);
490
+ var unitOf = (v2) => {
491
+ const l = Math.hypot(v2[0], v2[1]);
492
+ return l > 0 ? [v2[0] / l, v2[1] / l] : [1, 0];
493
+ };
494
+ var leftNormal = (d) => [d[1], -d[0]];
495
+ function flatten(segments, closed, tol) {
496
+ const pts = [];
497
+ const corner = [];
498
+ const push = (p, isCorner) => {
499
+ const last = pts[pts.length - 1];
500
+ if (last && dist(last, p) <= 1e-9) {
501
+ corner[corner.length - 1] ||= isCorner;
502
+ return;
503
+ }
504
+ pts.push(p);
505
+ corner.push(isCorner);
506
+ };
507
+ for (const seg of segments) {
508
+ const flat = flattenChain([seg], tol);
509
+ if (dist(seg[0], seg[seg.length - 1]) <= 1e-12 && flat.length > 1) flat.push(seg[seg.length - 1]);
510
+ flat.forEach((p, i) => push(p, i === 0 || i === flat.length - 1));
511
+ }
512
+ if (closed && pts.length > 2 && dist(pts[0], pts[pts.length - 1]) > 1e-9) push(pts[0], true);
513
+ if (pts.length < 2) return null;
514
+ let total = 0;
515
+ for (let i = 1; i < pts.length; i++) total += dist(pts[i - 1], pts[i]);
516
+ if (!(total > 0)) return null;
517
+ const step = Math.max(total / SAMPLES_PER_SUBPATH, tol);
518
+ const fine = [pts[0]];
519
+ const fineCorner = [corner[0]];
520
+ for (let i = 1; i < pts.length; i++) {
521
+ const n = Math.max(1, Math.ceil(dist(pts[i - 1], pts[i]) / step));
522
+ for (let k = 1; k <= n; k++) {
523
+ fine.push(k === n ? pts[i] : lerp(pts[i - 1], pts[i], k / n));
524
+ fineCorner.push(k === n ? corner[i] : false);
525
+ }
526
+ }
527
+ const s = [0];
528
+ for (let i = 1; i < fine.length; i++) s.push(s[i - 1] + dist(fine[i - 1], fine[i]));
529
+ return { pts: fine, s, corner: fineCorner, closed };
530
+ }
531
+ function dashIntervals(pattern, offset, len2) {
532
+ let pat = pattern.map((v2) => Number.isFinite(v2) ? Math.max(0, v2) : 0);
533
+ if (pat.length % 2 === 1) pat = [...pat, ...pat];
534
+ const period = pat.reduce((a, b) => a + b, 0);
535
+ if (pat.length < 2 || !(period > 1e-9) || !(len2 > 0)) return [[0, len2]];
536
+ let pos = ((Number.isFinite(offset) ? offset : 0) % period + period) % period;
537
+ let idx = 0;
538
+ while (pos >= pat[idx]) {
539
+ pos -= pat[idx];
540
+ idx = (idx + 1) % pat.length;
541
+ }
542
+ const out = [];
543
+ let s = 0;
544
+ let guard = 0;
545
+ while (s < len2 && guard++ < MAX_DASHES * pat.length) {
546
+ const e = Math.min(s + pat[idx] - pos, len2);
547
+ if (idx % 2 === 0 && e >= s) out.push([s, e]);
548
+ s += pat[idx] - pos;
549
+ pos = 0;
550
+ idx = (idx + 1) % pat.length;
551
+ }
552
+ return out;
553
+ }
554
+ function at(poly, t) {
555
+ let i = 1;
556
+ while (i < poly.pts.length - 1 && poly.s[i] <= t) i++;
557
+ const a = poly.s[i - 1];
558
+ const b = poly.s[i];
559
+ const u = b > a ? Math.min(Math.max((t - a) / (b - a), 0), 1) : 0;
560
+ return [lerp(poly.pts[i - 1], poly.pts[i], u), i];
561
+ }
562
+ function arcPoints(c, a0, sweep, r0, r1, tol) {
563
+ const r = Math.max(r0, r1);
564
+ if (!(r > tol * 0.5) || !Number.isFinite(sweep)) return [];
565
+ const stepAngle = 2 * Math.acos(Math.min(Math.max(1 - tol / r, -1), 1));
566
+ const n = Math.min(128, Math.max(2, Math.ceil(Math.abs(sweep) / Math.max(stepAngle, 1e-3))));
567
+ const out = [];
568
+ for (let k = 1; k < n; k++) {
569
+ const f = k / n;
570
+ const a = a0 + sweep * f;
571
+ const rr = r0 + (r1 - r0) * f;
572
+ out.push([c[0] + Math.cos(a) * rr, c[1] + Math.sin(a) * rr]);
573
+ }
574
+ return out;
575
+ }
576
+ function crossing(a0, a1, b0, b1) {
577
+ const r = sub2(a1, a0);
578
+ const q = sub2(b1, b0);
579
+ const den = r[0] * q[1] - r[1] * q[0];
580
+ if (Math.abs(den) < 1e-12) return null;
581
+ const w = sub2(b0, a0);
582
+ const t = (w[0] * q[1] - w[1] * q[0]) / den;
583
+ const u = (w[0] * r[1] - w[1] * r[0]) / den;
584
+ return t > 1e-9 && t <= 1 && u >= 0 && u < 1 - 1e-9 ? [a0[0] + r[0] * t, a0[1] + r[1] * t] : null;
585
+ }
586
+ function variableStrokeRings(path, options) {
587
+ const half = options.width / 2;
588
+ if (!(half > 0) || !Number.isFinite(half)) return [];
589
+ const tol = options.tolerance && options.tolerance > 0 ? options.tolerance : 0.05;
590
+ const join = options.join ?? "miter";
591
+ const cap = options.cap ?? "butt";
592
+ const miterLimit = options.miterLimit && options.miterLimit >= 1 ? options.miterLimit : 4;
593
+ const rings = [];
594
+ const push = (out, p) => {
595
+ const last = out[out.length - 1];
596
+ if (!last || dist(last, p) > 1e-12) out.push(p);
597
+ };
598
+ for (const contour of pathToContours(path)) {
599
+ if (!contour.segments.length) continue;
600
+ const poly = flatten(contour.segments, contour.closed, Math.min(tol, half) / 4);
601
+ if (!poly) continue;
602
+ const len2 = poly.s[poly.s.length - 1];
603
+ const sides2 = (s) => {
604
+ const w = widthProfileAt(options.profile, s / len2);
605
+ return [w.left * half, w.right * half];
606
+ };
607
+ const intervals = options.dash?.some((v2) => v2 > 0) ? dashIntervals(options.dash, options.dashOffset ?? 0, len2) : [[0, len2]];
608
+ const whole = intervals.length === 1 && intervals[0][0] <= 0 && intervals[0][1] >= len2;
609
+ for (const [a, b] of intervals) {
610
+ const [pa, ia] = at(poly, a);
611
+ const [pb, ib] = at(poly, b);
612
+ const pts = [{ p: pa, s: a, corner: poly.corner[0] && a <= 0 }];
613
+ for (let i = ia; i < ib; i++) pts.push({ p: poly.pts[i], s: poly.s[i], corner: poly.corner[i] });
614
+ pts.push({ p: pb, s: b, corner: false });
615
+ const run = pts.filter((q, i) => i === 0 || dist(q.p, pts[i - 1].p) > 1e-9);
616
+ if (run.length < 2) {
617
+ const [l, r] = sides2(a);
618
+ const w2 = (l + r) / 2;
619
+ if (cap === "round" && w2 > 0) rings.push([[pa[0] + w2, pa[1]], ...arcPoints(pa, 0, 2 * Math.PI, w2, w2, tol)]);
620
+ continue;
621
+ }
622
+ const closed = whole && poly.closed;
623
+ const m = run.length - 1;
624
+ const dirs = [];
625
+ for (let i = 0; i < m; i++) dirs.push(unitOf(sub2(run[i + 1].p, run[i].p)));
626
+ const normals = dirs.map(leftNormal);
627
+ const w = run.map((q) => sides2(q.s));
628
+ const side = (sigma) => {
629
+ const k = sigma > 0 ? 0 : 1;
630
+ const S = (i) => along(run[i].p, normals[i], sigma * w[i][k]);
631
+ const E = (i) => along(run[i + 1].p, normals[i], sigma * w[i + 1][k]);
632
+ const out = [S(0)];
633
+ for (let i = 0; i < m; i++) {
634
+ const seam = i === m - 1;
635
+ if (seam && !closed) {
636
+ push(out, E(i));
637
+ break;
638
+ }
639
+ const j = seam ? 0 : i + 1;
640
+ const p = run[i + 1].p;
641
+ const d0 = dirs[i];
642
+ const d1 = dirs[j];
643
+ const cross2 = d0[0] * d1[1] - d0[1] * d1[0];
644
+ const dot2 = d0[0] * d1[0] + d0[1] * d1[1];
645
+ const r0 = w[i + 1][k];
646
+ const r1 = w[j][k];
647
+ const e = E(i);
648
+ const st = S(j);
649
+ if (Math.abs(cross2) < 1e-12 && dot2 > 0) {
650
+ push(out, e);
651
+ if (!seam) push(out, st);
652
+ continue;
653
+ }
654
+ const uturn = Math.abs(cross2) < 1e-12;
655
+ const outer = uturn ? sigma > 0 : cross2 * sigma > 0;
656
+ if (!outer) {
657
+ const x = seam ? null : crossing(out[out.length - 1], e, st, E(j));
658
+ if (x) push(out, x);
659
+ else {
660
+ push(out, e);
661
+ push(out, p);
662
+ if (!seam) push(out, st);
663
+ }
664
+ continue;
665
+ }
666
+ push(out, e);
667
+ const theta = uturn ? Math.PI : Math.atan2(cross2, dot2);
668
+ const r = (r0 + r1) / 2;
669
+ const kind = run[i + 1].corner || seam && poly.corner[0] ? join : "round";
670
+ if (r > 1e-9) {
671
+ if (kind === "round") {
672
+ if (r * (1 - Math.cos(Math.abs(theta) / 2)) > tol * 0.25) {
673
+ for (const q of arcPoints(p, Math.atan2(e[1] - p[1], e[0] - p[0]), theta, r0, r1, tol)) push(out, q);
674
+ }
675
+ } else if (kind === "miter") {
676
+ const halfCos = Math.cos(theta / 2);
677
+ const bis = [normals[i][0] + normals[j][0], normals[i][1] + normals[j][1]];
678
+ const bl = Math.hypot(bis[0], bis[1]);
679
+ if (halfCos > 1e-6 && 1 / halfCos <= miterLimit && bl > 1e-9) push(out, along(p, bis, sigma * r / halfCos / bl));
680
+ }
681
+ }
682
+ if (!seam) push(out, st);
683
+ }
684
+ if (closed && out.length > 1 && dist(out[0], out[out.length - 1]) <= 1e-12) out.pop();
685
+ return out;
686
+ };
687
+ const left = side(1);
688
+ const right = side(-1);
689
+ if (closed) {
690
+ rings.push(left, right.reverse());
691
+ continue;
692
+ }
693
+ const capPoints = (atStart) => {
694
+ if (cap === "butt") return [];
695
+ const i = atStart ? 0 : m;
696
+ const d = atStart ? dirs[0] : dirs[m - 1];
697
+ const [l, r] = w[i];
698
+ if (!(l + r > 1e-9)) return [];
699
+ const p = run[i].p;
700
+ const n = leftNormal(d);
701
+ const L = along(p, n, l);
702
+ const R = along(p, n, -r);
703
+ if (cap === "round") {
704
+ const a0 = Math.atan2(d[1], d[0]) + (atStart ? Math.PI / 2 : -Math.PI / 2);
705
+ return arcPoints(p, a0, Math.PI, atStart ? r : l, atStart ? l : r, tol);
706
+ }
707
+ const ext = (l + r) / 2;
708
+ const o = atStart ? [-d[0], -d[1]] : d;
709
+ return atStart ? [along(R, o, ext), along(L, o, ext)] : [along(L, o, ext), along(R, o, ext)];
710
+ };
711
+ const ring = [];
712
+ for (const q of [...left, ...capPoints(false), ...right.reverse(), ...capPoints(true)]) push(ring, q);
713
+ if (ring.length > 2) rings.push(ring);
714
+ }
715
+ }
716
+ return rings;
717
+ }
718
+ function variableStrokeOutline(path, options) {
719
+ const pieces = variableStrokeRings(path, options);
720
+ if (!pieces.length) return [];
721
+ const tol = options.tolerance && options.tolerance > 0 ? options.tolerance : 0.05;
722
+ let polygons;
723
+ try {
724
+ polygons = polygonBoolean("union", [{ rings: pieces, fillRule: "nonzero" }]);
725
+ } catch {
726
+ return [];
727
+ }
728
+ const out = [];
729
+ for (const polygon2 of polygons) {
730
+ for (const ring of polygon2) {
731
+ if (ring.length < 3) continue;
732
+ const cubics = fitCubics(ring, tol, { closed: true, cornerAngleDeg: 25 });
733
+ if (!cubics.length) continue;
734
+ out.push({ type: "M", x: cubics[0].p0[0], y: cubics[0].p0[1] });
735
+ cubics.forEach((c, i) => {
736
+ if (isStraightCubic(c, tol * 0.02)) {
737
+ if (i < cubics.length - 1) out.push({ type: "L", x: c.p3[0], y: c.p3[1] });
738
+ } 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] });
739
+ });
740
+ out.push({ type: "Z" });
741
+ }
742
+ }
743
+ return out;
744
+ }
745
+ function nearestStrokePosition(path, p, only) {
746
+ let best = null;
747
+ pathToContours(path).forEach((c, ci) => {
748
+ if (!c.segments.length || only !== void 0 && ci !== only) return;
749
+ const lengths = c.segments.map((s) => segmentLength(s));
750
+ const total = lengths.reduce((a, b) => a + b, 0);
751
+ if (!(total > 0)) return;
752
+ let acc = 0;
753
+ c.segments.forEach((seg, si) => {
754
+ const near2 = segmentNearest(seg, p);
755
+ if (!best || near2.distance < best.distance) {
756
+ const s = acc + segmentLength(seg, 0, near2.t);
757
+ const tan = segmentTangent(seg, near2.t);
758
+ const pt2 = seg.length === 2 ? lerp(seg[0], seg[1], near2.t) : evalSeg(seg, near2.t);
759
+ const n = leftNormal([tan[0], tan[1]]);
760
+ const side = (p[0] - pt2[0]) * n[0] + (p[1] - pt2[1]) * n[1] >= 0 ? "left" : "right";
761
+ 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 };
762
+ }
763
+ acc += lengths[si];
764
+ });
765
+ });
766
+ return best;
767
+ }
768
+ function evalSeg(seg, t) {
769
+ let pts = seg.map((q) => [q[0], q[1]]);
770
+ while (pts.length > 1) pts = pts.slice(1).map((q, i) => lerp(pts[i], q, t));
771
+ return pts[0];
772
+ }
773
+ function strokePositionPoint(path, index, t) {
774
+ const c = pathToContours(path)[index];
775
+ if (!c?.segments.length) return null;
776
+ const lengths = c.segments.map((s) => segmentLength(s));
777
+ const total = lengths.reduce((a, b) => a + b, 0);
778
+ let want = Math.min(Math.max(t, 0), 1) * total;
779
+ for (let i = 0; i < c.segments.length; i++) {
780
+ const seg = c.segments[i];
781
+ if (want <= lengths[i] + 1e-12 || i === c.segments.length - 1) {
782
+ let lo = 0;
783
+ let hi = 1;
784
+ const target = Math.min(want, lengths[i]);
785
+ for (let k = 0; k < 40; k++) {
786
+ const mid = (lo + hi) / 2;
787
+ if (segmentLength(seg, 0, mid) < target) lo = mid;
788
+ else hi = mid;
789
+ }
790
+ const u = (lo + hi) / 2;
791
+ const tan = segmentTangent(seg, u);
792
+ return { point: evalSeg(seg, u), tangent: [tan[0], tan[1]] };
793
+ }
794
+ want -= lengths[i];
795
+ }
796
+ return null;
797
+ }
798
+
248
799
  // src/vector/stroke.ts
249
800
  var KAPPA = 0.5522847498307936;
250
801
  var NOTCH = 0.7;
@@ -256,8 +807,21 @@ function effectiveStrokeAlign(style, closedShape) {
256
807
  const a = style.strokeAlign ?? "center";
257
808
  return closedShape ? a : "center";
258
809
  }
810
+ function strokeWidthProfileOf(style) {
811
+ if (!style.strokeWidthProfile?.length) return null;
812
+ const p = sanitizeWidthProfile(style.strokeWidthProfile);
813
+ return isUniformWidthProfile(p) ? null : p;
814
+ }
259
815
  function hasStrokeExtras(style) {
260
- return (style.strokeAlign ?? "center") !== "center" || !!style.startArrow || !!style.endArrow;
816
+ return (style.strokeAlign ?? "center") !== "center" || !!style.startArrow || !!style.endArrow || !!strokeWidthProfileOf(style);
817
+ }
818
+ function vectorStrokeLineArea(parts, style, tolerance) {
819
+ const profile = strokeWidthProfileOf(style);
820
+ const dash = style.strokeDasharray?.length ? { dash: style.strokeDasharray } : {};
821
+ const join = style.strokeLinejoin ?? "miter";
822
+ const cap = style.strokeLinecap ?? "butt";
823
+ if (profile) return variableStrokeOutline(parts.line, { width: parts.drawWidth, profile, join, cap, ...dash, ...tolerance ? { tolerance } : {} });
824
+ return outlineStroke(parts.line, { width: parts.drawWidth, join, cap, ...dash, ...tolerance ? { tolerance } : {} });
261
825
  }
262
826
  var capExtent = (style, width) => style.strokeLinecap === "round" || style.strokeLinecap === "square" ? width / 2 : 0;
263
827
  function arrowheadWeight(style, end) {
@@ -347,13 +911,13 @@ function endOf(segs, atStart) {
347
911
  const last = segs[segs.length - 1];
348
912
  return { p: toVec(last[last.length - 1]), out: v(t[0], t[1]) };
349
913
  }
350
- function pointAlong(segs, fromStart, dist3) {
914
+ function pointAlong(segs, fromStart, dist4) {
351
915
  const order = fromStart ? segs : [...segs].reverse();
352
916
  let acc = 0;
353
917
  for (const s of order) {
354
918
  const l = segLen(s);
355
- if (acc + l >= dist3) {
356
- const want = fromStart ? dist3 - acc : l - (dist3 - acc);
919
+ if (acc + l >= dist4) {
920
+ const want = fromStart ? dist4 - acc : l - (dist4 - acc);
357
921
  const pts = trimSegments([s], 0, want);
358
922
  const p = pts.length ? pts[0][pts[0].length - 1] : s[0];
359
923
  return toVec(p);
@@ -410,12 +974,7 @@ function vectorStrokeParts(path, style) {
410
974
  }
411
975
  function vectorStrokeOutlinePath(path, style) {
412
976
  const parts = vectorStrokeParts(path, style);
413
- const stroke = outlineStroke(parts.line, {
414
- width: parts.drawWidth,
415
- join: style.strokeLinejoin ?? "miter",
416
- cap: style.strokeLinecap ?? "butt",
417
- ...style.strokeDasharray?.length ? { dash: style.strokeDasharray } : {}
418
- });
977
+ const stroke = vectorStrokeLineArea(parts, style);
419
978
  const fill = { path, fillRule: style.fillRule ?? "nonzero" };
420
979
  let area = stroke;
421
980
  if (parts.align === "inside") area = pathBoolean("intersection", [stroke, fill]).path;
@@ -431,15 +990,32 @@ function hitTestVectorStroke(path, p, style, tolerance = 0) {
431
990
  if (pointInBezierPath(h.path, point) || tolerance > 0 && distanceToBezierPath(h.path, point) <= tolerance) return true;
432
991
  }
433
992
  if (!parts.line.length) return false;
434
- const d = distanceToBezierPath(parts.line, point);
435
- if (parts.align === "center") return d <= width / 2 + tolerance;
436
- if (d > width + tolerance) return false;
993
+ const profile = strokeWidthProfileOf(style);
994
+ let d;
995
+ let reach = width / 2;
996
+ if (profile) {
997
+ const near2 = nearestStrokePosition(parts.line, point);
998
+ if (!near2) return false;
999
+ d = near2.distance;
1000
+ const w = widthProfileAt(profile, near2.t);
1001
+ reach = width / 2 * (near2.side === "left" ? w.left : w.right);
1002
+ if (parts.align !== "center") reach *= 2;
1003
+ const atEnd = near2.t <= 1e-9 || near2.t >= 1 - 1e-9;
1004
+ if (atEnd && (style.strokeLinecap === "round" || style.strokeLinecap === "square") && d > reach) {
1005
+ reach = Math.max(reach, width / 2 * (w.left + w.right) / 2);
1006
+ }
1007
+ } else {
1008
+ d = distanceToBezierPath(parts.line, point);
1009
+ if (parts.align !== "center") reach = width;
1010
+ }
1011
+ if (parts.align === "center") return d <= reach + tolerance;
1012
+ if (d > reach + tolerance) return false;
437
1013
  if (d <= tolerance) return true;
438
1014
  const inside = pointInBezierPath(path, point, style.fillRule ?? "nonzero");
439
1015
  return parts.align === "inside" ? inside : !inside;
440
1016
  }
441
1017
  function vectorStrokeReach(style) {
442
- const width = style.strokeWidth ?? 1;
1018
+ const width = (style.strokeWidth ?? 1) * widthProfileMaxScale(strokeWidthProfileOf(style));
443
1019
  const align = style.strokeAlign ?? "center";
444
1020
  let reach = align === "center" ? width / 2 : align === "outside" ? width : 0;
445
1021
  if (style.strokeLinejoin !== "round" && style.strokeLinejoin !== "bevel") reach *= 4;
@@ -749,8 +1325,10 @@ function bakeIntoGeometry(node, m) {
749
1325
  if (isIdentityMatrix(m)) return node;
750
1326
  if (node.type === "group") return { ...node, children: node.children.map((c) => transformVectorNode(c, m)) };
751
1327
  const path = node;
1328
+ const mirrored = m[0] * m[3] - m[1] * m[2] < 0 && path.strokeWidthProfile?.length;
752
1329
  return {
753
1330
  ...path,
1331
+ ...mirrored ? { strokeWidthProfile: mirrorWidthProfile(path.strokeWidthProfile) } : {},
754
1332
  subpaths: path.subpaths.map((sp) => ({
755
1333
  closed: sp.closed,
756
1334
  anchors: sp.anchors.map((a) => {
@@ -915,8 +1493,8 @@ function ellipseSubpath(b) {
915
1493
  ]
916
1494
  };
917
1495
  }
918
- function polygonSubpath(b, sides = 6) {
919
- const n = Math.max(3, Math.round(sides));
1496
+ function polygonSubpath(b, sides2 = 6) {
1497
+ const n = Math.max(3, Math.round(sides2));
920
1498
  const cx = b.x + b.width / 2;
921
1499
  const cy = b.y + b.height / 2;
922
1500
  const anchors = [];
@@ -980,11 +1558,20 @@ function vectorPaintToSvg(paint, gradientId) {
980
1558
  if (typeof paint === "string") return { value: paint, gradient: null };
981
1559
  return { value: `url(#${gradientId})`, gradient: gradientToSvgDef(paint, gradientId) };
982
1560
  }
1561
+ var partsCache = /* @__PURE__ */ new WeakMap();
983
1562
  function vectorStrokeSvgParts(node, clipId, precision = 3) {
984
1563
  if (!hasPaint(node.stroke) || !hasStrokeExtras(node)) return void 0;
1564
+ const hit = partsCache.get(node);
1565
+ if (hit && hit.clipId === clipId && hit.precision === precision) return hit.parts;
1566
+ const parts = computeStrokeSvgParts(node, clipId, precision);
1567
+ partsCache.set(node, { clipId, precision, parts });
1568
+ return parts;
1569
+ }
1570
+ function computeStrokeSvgParts(node, clipId, precision) {
985
1571
  const path = subpathsToBezier(node.subpaths);
986
1572
  const parts = vectorStrokeParts(path, node);
987
- if (parts.align === "center" && !parts.heads.length) return void 0;
1573
+ const profiled = !!strokeWidthProfileOf(node);
1574
+ if (parts.align === "center" && !parts.heads.length && !profiled) return void 0;
988
1575
  let clip = null;
989
1576
  if (parts.align !== "center") {
990
1577
  const b = bezierPathBounds(path);
@@ -992,7 +1579,26 @@ function vectorStrokeSvgParts(node, clipId, precision = 3) {
992
1579
  const bounds = b ? { x: round2(b.minX - pad), y: round2(b.minY - pad), width: round2(b.maxX - b.minX + 2 * pad), height: round2(b.maxY - b.minY + 2 * pad) } : { x: 0, y: 0, width: 0, height: 0 };
993
1580
  clip = { kind: parts.align === "inside" ? "clip" : "mask", id: clipId, bounds };
994
1581
  }
995
- return { align: parts.align, d: bezierPathToSvg(parts.line, { precision }), width: parts.drawWidth, clip, heads: parts.heads.map((h) => bezierPathToSvg(h.path, { precision })) };
1582
+ return {
1583
+ align: parts.align,
1584
+ d: bezierPathToSvg(parts.line, { precision }),
1585
+ width: parts.drawWidth,
1586
+ ...profiled && (node.strokeWidth ?? 1) > 0 ? { outline: bezierPathToSvg(vectorStrokeLineArea(parts, node), { precision }) } : {},
1587
+ clip,
1588
+ heads: parts.heads.map((h) => bezierPathToSvg(h.path, { precision }))
1589
+ };
1590
+ }
1591
+ function widthProfileToAttribute(points, precision = 4) {
1592
+ return points.map((p) => [p.t, p.left, p.right].map((n) => formatNumber(n, precision)).join(",")).join(";");
1593
+ }
1594
+ function widthProfileFromAttribute(value) {
1595
+ if (!value) return [];
1596
+ return sanitizeWidthProfile(
1597
+ value.split(";").map((part) => {
1598
+ const [t, left, right] = part.split(",").map((n) => parseFloat(n));
1599
+ return { t, left, right: Number.isFinite(right) ? right : left };
1600
+ })
1601
+ );
996
1602
  }
997
1603
  var round2 = (n) => Math.round(n * 100) / 100;
998
1604
  function gradientMarkup(def) {
@@ -1051,6 +1657,13 @@ function vectorDocumentToSvg(doc, options = {}) {
1051
1657
  if (node.arrowAlign && node.arrowAlign !== "extend") data += ` data-vector-arrow-align="${node.arrowAlign}"`;
1052
1658
  if (node.startArrowScale !== void 0 && node.startArrowScale !== 1) data += ` data-vector-start-arrow-scale="${num(node.startArrowScale)}"`;
1053
1659
  if (node.endArrowScale !== void 0 && node.endArrowScale !== 1) data += ` data-vector-end-arrow-scale="${num(node.endArrowScale)}"`;
1660
+ const profile = strokeWidthProfileOf(node);
1661
+ if (profile) {
1662
+ data += ` data-vector-width-profile="${widthProfileToAttribute(profile)}"`;
1663
+ if (node.strokeLinecap && node.strokeLinecap !== "butt") data += ` data-vector-stroke-linecap="${node.strokeLinecap}"`;
1664
+ if (node.strokeLinejoin && node.strokeLinejoin !== "miter") data += ` data-vector-stroke-linejoin="${node.strokeLinejoin}"`;
1665
+ if (node.strokeDasharray?.length) data += ` data-vector-stroke-dasharray="${node.strokeDasharray.map(num).join(" ")}"`;
1666
+ }
1054
1667
  lines.push(`${indent(depth)}<g${common(node)}${data}>`);
1055
1668
  lines.push(`${indent(depth + 1)}<path data-vector-part="fill" d="${d}"${fillAttrs}${live}/>`);
1056
1669
  if (options.strokes === "outline") {
@@ -1071,7 +1684,8 @@ function vectorDocumentToSvg(doc, options = {}) {
1071
1684
  clip = ` mask="url(#${id})"`;
1072
1685
  }
1073
1686
  }
1074
- lines.push(`${indent(depth + 1)}<path data-vector-part="stroke" d="${parts.d}" fill="none"${lineAttrs(parts.width)}${clip}/>`);
1687
+ if (parts.outline !== void 0) lines.push(`${indent(depth + 1)}<path data-vector-part="stroke-outline" d="${parts.outline}" fill="${escapeAttr(stroke.value)}"${clip}/>`);
1688
+ else lines.push(`${indent(depth + 1)}<path data-vector-part="stroke" d="${parts.d}" fill="none"${lineAttrs(parts.width)}${clip}/>`);
1075
1689
  for (const h2 of parts.heads) lines.push(`${indent(depth + 1)}<path data-vector-part="head" d="${h2}" fill="${escapeAttr(stroke.value)}"/>`);
1076
1690
  }
1077
1691
  lines.push(`${indent(depth)}</g>`);
@@ -1334,6 +1948,11 @@ function parseVectorSvg(svg, options = {}) {
1334
1948
  return n > 0 ? n : void 0;
1335
1949
  };
1336
1950
  const align = a["data-vector-stroke-align"];
1951
+ const profile = widthProfileFromAttribute(a["data-vector-width-profile"]);
1952
+ const kept = (key, values) => values.includes(a[key]) ? a[key] : void 0;
1953
+ const keptCap = kept("data-vector-stroke-linecap", ["round", "square"]);
1954
+ const keptJoin = kept("data-vector-stroke-linejoin", ["round", "bevel"]);
1955
+ const keptDash = a["data-vector-stroke-dasharray"] ? numberList(a["data-vector-stroke-dasharray"]) : void 0;
1337
1956
  const node = {
1338
1957
  id: idFor(g),
1339
1958
  type: "path",
@@ -1349,7 +1968,11 @@ function parseVectorSvg(svg, options = {}) {
1349
1968
  ...arrow(a["data-vector-end-arrow"]) ? { endArrow: arrow(a["data-vector-end-arrow"]) } : {},
1350
1969
  ...a["data-vector-arrow-align"] === "tip" ? { arrowAlign: "tip" } : {},
1351
1970
  ...scale(a["data-vector-start-arrow-scale"]) ? { startArrowScale: scale(a["data-vector-start-arrow-scale"]) } : {},
1352
- ...scale(a["data-vector-end-arrow-scale"]) ? { endArrowScale: scale(a["data-vector-end-arrow-scale"]) } : {}
1971
+ ...scale(a["data-vector-end-arrow-scale"]) ? { endArrowScale: scale(a["data-vector-end-arrow-scale"]) } : {},
1972
+ ...profile.length ? { strokeWidthProfile: profile } : {},
1973
+ ...keptCap ? { strokeLinecap: keptCap } : {},
1974
+ ...keptJoin ? { strokeLinejoin: keptJoin } : {},
1975
+ ...keptDash?.length ? { strokeDasharray: keptDash } : {}
1353
1976
  };
1354
1977
  return placed(node, m);
1355
1978
  };
@@ -1669,7 +2292,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
1669
2292
  */
1670
2293
  snapFrom(p, leave, tol) {
1671
2294
  const none = (q) => ({ point: q, kind: null, x: false, y: false, lines: [], labels: [] });
1672
- const at = (q, kind, lines2 = []) => ({
2295
+ const at2 = (q, kind, lines2 = []) => ({
1673
2296
  point: q,
1674
2297
  kind,
1675
2298
  x: true,
@@ -1683,16 +2306,16 @@ var VectorSnapTargets = class _VectorSnapTargets {
1683
2306
  return hit ? { point: hit.point, x: true, y: true, ...this.linedUp(hit.point, hit.from) } : none(q);
1684
2307
  }
1685
2308
  const point = this.nearestPoint(p, tol);
1686
- if (point) return at(point.p, point.kind);
2309
+ if (point) return at2(point.p, point.kind);
1687
2310
  const near2 = this.segmentsNear(p, tol);
1688
- const crossing = this.crossing(near2, p, tol);
1689
- if (crossing) return at(crossing, "intersect");
2311
+ const crossing2 = this.crossing(near2, p, tol);
2312
+ if (crossing2) return at2(crossing2, "intersect");
1690
2313
  if (leave && leave.curves !== false) {
1691
2314
  const foot = this.curvePoint(near2, leave.at, p, tol);
1692
- if (foot) return at(foot.point, foot.kind, [line("construction", leave.at, foot.point)]);
2315
+ if (foot) return at2(foot.point, foot.kind, [line("construction", leave.at, foot.point)]);
1693
2316
  }
1694
2317
  const on = this.onSegment(near2);
1695
- if (on) return at(on, "path");
2318
+ if (on) return at2(on, "path");
1696
2319
  if (leave?.angles?.length) {
1697
2320
  const c = construction(leave, p, tol);
1698
2321
  if (c) {
@@ -1739,7 +2362,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
1739
2362
  * the nearest target line crosses it, at most twice `tol` away: where it lands and the
1740
2363
  * target point that line comes from.
1741
2364
  */
1742
- slide(at, dir, q, tol, ray) {
2365
+ slide(at2, dir, q, tol, ray) {
1743
2366
  const len2 = dir.x * dir.x + dir.y * dir.y;
1744
2367
  if (!Number.isFinite(len2) || len2 <= 1e-18) return null;
1745
2368
  const reach = 2 * tol;
@@ -1747,10 +2370,10 @@ var VectorSnapTargets = class _VectorSnapTargets {
1747
2370
  for (const vertical of [true, false]) {
1748
2371
  const d = vertical ? dir.x : dir.y;
1749
2372
  if (Math.abs(d) <= 1e-9) continue;
1750
- const a = vertical ? at.x : at.y;
2373
+ const a = vertical ? at2.x : at2.y;
1751
2374
  for (const l of linesNear(vertical ? this.xs : this.ys, vertical ? q.x : q.y, reach)) {
1752
2375
  const s = (l.v - a) / d;
1753
- const hit = vertical ? { x: l.v, y: at.y + dir.y * s } : { x: at.x + dir.x * s, y: l.v };
2376
+ const hit = vertical ? { x: l.v, y: at2.y + dir.y * s } : { x: at2.x + dir.x * s, y: l.v };
1754
2377
  const moved = distance(hit, q);
1755
2378
  if (moved <= reach && (!ray || s > 0) && (!best || moved < best.moved)) best = { moved, point: hit, from: l.from };
1756
2379
  }
@@ -1898,11 +2521,11 @@ var VectorSnapTargets = class _VectorSnapTargets {
1898
2521
  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;
1899
2522
  if (!pick) return null;
1900
2523
  const { start, repeats } = pick;
1901
- const at = horizontal ? { ...r, x: start } : { ...r, y: start };
2524
+ const at2 = horizontal ? { ...r, x: start } : { ...r, y: start };
1902
2525
  const end = start + len2;
1903
2526
  const gaps = [];
1904
- if (before && (repeats === null || Math.abs(start - before.a1 - repeats) <= EQUAL)) gaps.push([before.a1, start, mid(before.b, at)]);
1905
- if (after && (repeats === null || Math.abs(after.a0 - end - repeats) <= EQUAL)) gaps.push([end, after.a0, mid(after.b, at)]);
2527
+ if (before && (repeats === null || Math.abs(start - before.a1 - repeats) <= EQUAL)) gaps.push([before.a1, start, mid(before.b, at2)]);
2528
+ if (after && (repeats === null || Math.abs(after.a0 - end - repeats) <= EQUAL)) gaps.push([end, after.a0, mid(after.b, at2)]);
1906
2529
  if (repeats !== null) {
1907
2530
  for (const p of pairs) if (Math.abs(p.g - repeats) <= EQUAL) gaps.push(p.gap);
1908
2531
  }
@@ -2331,9 +2954,9 @@ function curvatureMoveAnchor(sp, index, q) {
2331
2954
  anchors[index] = moved;
2332
2955
  return { anchors, closed: sp.closed };
2333
2956
  }
2334
- function cutSubpathAt(sp, at) {
2957
+ function cutSubpathAt(sp, at2) {
2335
2958
  const n = sp.anchors.length;
2336
- const cuts = [...new Set(at)].filter((i) => Number.isInteger(i) && i >= 0 && i < n && (sp.closed || i > 0 && i < n - 1)).sort((a, b) => a - b);
2959
+ 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);
2337
2960
  if (!cuts.length || n < 2) return [{ anchors: sp.anchors.map(cloneAnchor), closed: sp.closed }];
2338
2961
  const first = cuts[0];
2339
2962
  const run = sp.closed ? Array.from({ length: n + 1 }, (_, i) => sp.anchors[(first + i) % n]) : sp.anchors;
@@ -2364,7 +2987,7 @@ function splitSubpathAt(sp, segment, t, endTolerance = 1e-6) {
2364
2987
  return cutSubpathAt(subpath, [index]);
2365
2988
  }
2366
2989
  var endOf2 = (sp, last) => sp.anchors[last ? sp.anchors.length - 1 : 0];
2367
- var dist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
2990
+ var dist2 = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
2368
2991
  function reverse(sp) {
2369
2992
  return {
2370
2993
  closed: sp.closed,
@@ -2391,7 +3014,7 @@ function joinSubpaths(subpaths, tolerance) {
2391
3014
  const sp = open[0];
2392
3015
  const first = sp.anchors[0];
2393
3016
  const last = sp.anchors[sp.anchors.length - 1];
2394
- if (sp.anchors.length > 2 && dist(first, last) <= tolerance) {
3017
+ if (sp.anchors.length > 2 && dist2(first, last) <= tolerance) {
2395
3018
  sp.anchors.pop();
2396
3019
  sp.anchors[0] = tidy({ ...first, in: last.in ?? null }, false);
2397
3020
  } else {
@@ -2407,7 +3030,7 @@ function joinSubpaths(subpaths, tolerance) {
2407
3030
  for (let j = i + 1; j < open.length; j++) {
2408
3031
  for (const ei of [false, true]) {
2409
3032
  for (const ej of [false, true]) {
2410
- const d = dist(endOf2(open[i], ei), endOf2(open[j], ej));
3033
+ const d = dist2(endOf2(open[i], ei), endOf2(open[j], ej));
2411
3034
  if (d < best.d) best = { d, i, j, ei, ej };
2412
3035
  }
2413
3036
  }
@@ -2447,8 +3070,8 @@ function smoothSubpath(sp, amount = 1, only) {
2447
3070
  const p = orig[i];
2448
3071
  const dir = unit2(next.x - prev.x, next.y - prev.y);
2449
3072
  if (!dir) continue;
2450
- const li = dist(p, prev) / 3;
2451
- const lo = dist(p, next) / 3;
3073
+ const li = dist2(p, prev) / 3;
3074
+ const lo = dist2(p, next) / 3;
2452
3075
  const hin = p.in ?? p;
2453
3076
  const hout = p.out ?? p;
2454
3077
  const target = (h, x, y) => ({ x: h.x + (x - h.x) * t, y: h.y + (y - h.y) * t });
@@ -2475,12 +3098,12 @@ function segsOf(sp) {
2475
3098
  function segsToSubpath(segs, closed, types) {
2476
3099
  if (!segs.length) return null;
2477
3100
  const anchors = [];
2478
- const at = (p) => ({ x: p[0], y: p[1] });
2479
- anchors.push(at(segs[0].c[0]));
3101
+ const at2 = (p) => ({ x: p[0], y: p[1] });
3102
+ anchors.push(at2(segs[0].c[0]));
2480
3103
  for (const s of segs) {
2481
3104
  const end = s.c[s.c.length - 1];
2482
3105
  if (!s.line) anchors[anchors.length - 1].out = { x: s.c[1][0], y: s.c[1][1] };
2483
- const next = at(end);
3106
+ const next = at2(end);
2484
3107
  if (!s.line) next.in = { x: s.c[2][0], y: s.c[2][1] };
2485
3108
  anchors.push(next);
2486
3109
  }
@@ -2506,8 +3129,8 @@ function mergePair(a, b, tol) {
2506
3129
  if (len2 < 1e-12) return null;
2507
3130
  const mx = a3[0] - a0[0];
2508
3131
  const my = a3[1] - a0[1];
2509
- const along = (mx * cx + my * cy) / len2;
2510
- return Math.abs((mx * cy - my * cx) / len2) <= tol && along >= 0 && along <= len2 ? { c: [a0, b3], line: true } : null;
3132
+ const along2 = (mx * cx + my * cy) / len2;
3133
+ return Math.abs((mx * cy - my * cx) / len2) <= tol && along2 >= 0 && along2 <= len2 ? { c: [a0, b3], line: true } : null;
2511
3134
  }
2512
3135
  const ca = a.line ? [a0, a0, a3, a3] : a.c;
2513
3136
  const cb = b.line ? [a3, a3, b3, b3] : b.c;
@@ -2553,13 +3176,13 @@ function loopSample(l) {
2553
3176
  }
2554
3177
  var contains2 = (l, p) => windingNumber([l.segs], p) !== 0;
2555
3178
  function groupFaces(loops) {
2556
- const usable = loops.filter((l) => l.segs.length && Math.abs(loopArea(l)) > 1e-12);
2557
- if (!usable.length) return [];
2558
- const largest = usable.reduce((a, b) => Math.abs(loopArea(b)) > Math.abs(loopArea(a)) ? b : a);
3179
+ const usable2 = loops.filter((l) => l.segs.length && Math.abs(loopArea(l)) > 1e-12);
3180
+ if (!usable2.length) return [];
3181
+ const largest = usable2.reduce((a, b) => Math.abs(loopArea(b)) > Math.abs(loopArea(a)) ? b : a);
2559
3182
  const sign = Math.sign(loopArea(largest));
2560
- const outers = usable.filter((l) => Math.sign(loopArea(l)) === sign).sort((a, b) => Math.abs(loopArea(a)) - Math.abs(loopArea(b)));
3183
+ const outers = usable2.filter((l) => Math.sign(loopArea(l)) === sign).sort((a, b) => Math.abs(loopArea(a)) - Math.abs(loopArea(b)));
2561
3184
  const faces = new Map(outers.map((o) => [o, { loops: [o] }]));
2562
- for (const hole of usable.filter((l) => Math.sign(loopArea(l)) !== sign)) {
3185
+ for (const hole of usable2.filter((l) => Math.sign(loopArea(l)) !== sign)) {
2563
3186
  const p = loopSample(hole);
2564
3187
  const parent = outers.find((o) => Math.abs(loopArea(o)) > Math.abs(loopArea(hole)) && contains2(o, p));
2565
3188
  if (parent) faces.get(parent).loops.push(hole);
@@ -2939,7 +3562,7 @@ function vectorInkShapeName(shape) {
2939
3562
  return shape.points.length === 3 ? "triangle" : "polygon";
2940
3563
  }
2941
3564
  }
2942
- var dist2 = (a, b) => Math.hypot(b[0] - a[0], b[1] - a[1]);
3565
+ var dist3 = (a, b) => Math.hypot(b[0] - a[0], b[1] - a[1]);
2943
3566
  function bbox(pts) {
2944
3567
  if (!pts.length) return [0, 0, 0, 0];
2945
3568
  let [x0, y0] = pts[0];
@@ -2957,7 +3580,7 @@ function segDistance(a, b, p) {
2957
3580
  const dy = b[1] - a[1];
2958
3581
  const len2 = dx * dx + dy * dy;
2959
3582
  const t = len2 > 0 ? Math.min(1, Math.max(0, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / len2)) : 0;
2960
- return dist2([a[0] + t * dx, a[1] + t * dy], p);
3583
+ return dist3([a[0] + t * dx, a[1] + t * dy], p);
2961
3584
  }
2962
3585
  function polygonDistance(c, p) {
2963
3586
  let best = Infinity;
@@ -3019,7 +3642,7 @@ function corners(p, diag) {
3019
3642
  let far = 0;
3020
3643
  let farD = 0;
3021
3644
  p.forEach((q, i) => {
3022
- const d = dist2(start, q);
3645
+ const d = dist3(start, q);
3023
3646
  if (d > farD) {
3024
3647
  farD = d;
3025
3648
  far = i;
@@ -3046,7 +3669,7 @@ function corners(p, diag) {
3046
3669
  }
3047
3670
  let short = -1;
3048
3671
  for (let j = 0; j < n; j++) {
3049
- if (dist2(ring[j], ring[(j + 1) % n]) < 0.1 * diag) {
3672
+ if (dist3(ring[j], ring[(j + 1) % n]) < 0.1 * diag) {
3050
3673
  short = j;
3051
3674
  break;
3052
3675
  }
@@ -3086,8 +3709,8 @@ function recognizeInkShape(points, options = {}) {
3086
3709
  const diag = Math.hypot(w, h);
3087
3710
  if (size < minSize) return null;
3088
3711
  let length2 = 0;
3089
- for (let i = 1; i < p.length; i++) length2 += dist2(p[i - 1], p[i]);
3090
- const gap = dist2(first, last);
3712
+ for (let i = 1; i < p.length; i++) length2 += dist3(p[i - 1], p[i]);
3713
+ const gap = dist3(first, last);
3091
3714
  if (gap > 0.85 * length2 && gap >= minSize) {
3092
3715
  const off = Math.max(...p.map((q) => segDistance(first, last, q)));
3093
3716
  if (off <= Math.max(0.06 * gap, 1.5 * minSize / 6)) return { kind: "line", a: first, b: last };
@@ -3111,9 +3734,188 @@ function recognizeInkShape(points, options = {}) {
3111
3734
  return roundish && rms < 0.1 ? ellipse() : null;
3112
3735
  }
3113
3736
 
3737
+ // src/vector/builder.ts
3738
+ function vectorPathEntries(doc, roots, options = {}) {
3739
+ const out = [];
3740
+ const visit = (node, parent, inside) => {
3741
+ if (node.visible === false || node.locked && !options.locked) return;
3742
+ const picked = inside || roots === null || roots.includes(node.id);
3743
+ if (node.type === "group") {
3744
+ const m = multiplyMatrices(nodeMatrix(node), parent);
3745
+ for (const c of node.children) visit(c, m, picked);
3746
+ } else if (picked) out.push({ node, parent });
3747
+ };
3748
+ for (const n of doc.nodes) visit(n, IDENTITY_MATRIX, false);
3749
+ return out;
3750
+ }
3751
+ var usable = (sp) => sp.anchors.length > 1;
3752
+ function worldOf(entry, subpaths) {
3753
+ return vectorPathWorldBezier({ ...entry.node, subpaths }, entry.parent);
3754
+ }
3755
+ function subpathEnclosesArea(sp) {
3756
+ const contour = pathToContours(subpathsToBezier([{ ...sp, closed: true }]))[0];
3757
+ if (!contour) return false;
3758
+ const segs = closeContour(contour, true);
3759
+ const b = bezierPathBounds(contoursToPath([{ segments: segs, closed: true }]));
3760
+ if (!b) return false;
3761
+ const w = b.maxX - b.minX;
3762
+ const h = b.maxY - b.minY;
3763
+ return Math.abs(contourSignedArea(segs)) > 1e-9 * (w * w + h * h);
3764
+ }
3765
+ var paintKey = (paint) => typeof paint === "string" ? paint.trim().toLowerCase() : JSON.stringify(paint ?? null);
3766
+ function vectorPathfinderShape(entry, op) {
3767
+ const { node } = entry;
3768
+ const subpaths = node.subpaths.filter(usable);
3769
+ const closed = subpaths.map((s) => ({ ...s, closed: true }));
3770
+ const fillRule = node.fillRule ?? "nonzero";
3771
+ if (hasPaint(node.fill)) return { path: worldOf(entry, closed), fillRule, key: `fill:${paintKey(node.fill)}`, paint: "fill" };
3772
+ const open = subpaths.length > 0 && subpaths.every((s) => !s.closed);
3773
+ if (op === "divide" && open) return { path: worldOf(entry, subpaths), line: true, paint: "line" };
3774
+ if (hasPaint(node.stroke) && (node.strokeWidth ?? 1) > 0) {
3775
+ const path = vectorStrokeOutline({ ...node, subpaths }, entry.parent);
3776
+ return { path, fillRule: "nonzero", key: `fill:${paintKey(node.stroke)}`, paint: "stroke" };
3777
+ }
3778
+ return { path: worldOf(entry, closed), fillRule, key: "none", paint: "outline" };
3779
+ }
3780
+ var STROKE_KEYS = [
3781
+ "stroke",
3782
+ "strokeWidth",
3783
+ "strokeLinecap",
3784
+ "strokeLinejoin",
3785
+ "strokeDasharray",
3786
+ "strokeAlign",
3787
+ "startArrow",
3788
+ "endArrow",
3789
+ "arrowAlign",
3790
+ "startArrowScale",
3791
+ "endArrowScale",
3792
+ "strokeWidthProfile"
3793
+ ];
3794
+ function vectorResultStyle(node, paint, edge = false) {
3795
+ const { id: _id, subpaths: _s, transform: _t, ...rest } = node;
3796
+ const base = { ...rest, fillRule: "nonzero" };
3797
+ if (edge) {
3798
+ for (const k of STROKE_KEYS) delete base[k];
3799
+ const stroke = paint === "stroke" || !hasPaint(node.fill) ? node.stroke : node.fill;
3800
+ return { ...base, fill: null, stroke: stroke ?? "#000000", strokeWidth: node.strokeWidth ?? 1 };
3801
+ }
3802
+ if (paint === "stroke") {
3803
+ for (const k of STROKE_KEYS) delete base[k];
3804
+ return { ...base, fill: node.stroke ?? null, stroke: null };
3805
+ }
3806
+ return base;
3807
+ }
3808
+ function vectorShapeBuilderMap(entries) {
3809
+ const lines = [];
3810
+ const inputs = entries.map((entry) => {
3811
+ const filled = hasPaint(entry.node.fill);
3812
+ const subpaths = entry.node.subpaths.filter(usable).map((sp) => !sp.closed && filled && subpathEnclosesArea(sp) ? { ...sp, closed: true } : sp);
3813
+ lines.push(!subpaths.some((s) => s.closed));
3814
+ return { path: worldOf(entry, subpaths), fillRule: entry.node.fillRule ?? "nonzero" };
3815
+ });
3816
+ const map = PlanarMap.build(inputs);
3817
+ if (!map) return null;
3818
+ const pieces = map.chains().filter((c) => lines[c.source] && !c.closed).map((c) => {
3819
+ const path = contoursToPath([{ segments: c.segments, closed: false }]);
3820
+ return { path, segments: c.segments, source: c.source, bounds: bezierPathBounds(path) };
3821
+ });
3822
+ return { entries: [...entries], map, lines, pieces };
3823
+ }
3824
+ function vectorShapeBuilderHit(m, p, lineTolerance = null) {
3825
+ const point = [p.x, p.y];
3826
+ if (lineTolerance !== null && lineTolerance > 0) {
3827
+ let best = -1;
3828
+ let bestD = lineTolerance;
3829
+ m.pieces.forEach((piece, i) => {
3830
+ const b = piece.bounds;
3831
+ if (p.x < b.minX - bestD || p.x > b.maxX + bestD || p.y < b.minY - bestD || p.y > b.maxY + bestD) return;
3832
+ const d = distanceToBezierPath(piece.path, point);
3833
+ if (d <= bestD) {
3834
+ best = i;
3835
+ bestD = d;
3836
+ }
3837
+ });
3838
+ if (best >= 0) return { line: best };
3839
+ }
3840
+ const face = m.map.faceAt(point);
3841
+ return face >= 0 ? { face } : null;
3842
+ }
3843
+ function samplePolyline(points, step) {
3844
+ const out = [];
3845
+ const s = Math.max(step, 1e-3);
3846
+ points.forEach((p, i) => {
3847
+ if (i > 0) {
3848
+ const a = points[i - 1];
3849
+ const n = Math.min(4e3, Math.ceil(Math.hypot(p.x - a.x, p.y - a.y) / s));
3850
+ for (let k = 1; k < n; k++) out.push({ x: a.x + (p.x - a.x) * k / n, y: a.y + (p.y - a.y) * k / n });
3851
+ }
3852
+ out.push(p);
3853
+ });
3854
+ return out;
3855
+ }
3856
+ function addBuilderHit(touched, hit) {
3857
+ if (!hit) return false;
3858
+ const [list, i] = "face" in hit ? [touched.faces, hit.face] : [touched.lines, hit.line];
3859
+ if (list.includes(i)) return false;
3860
+ list.push(i);
3861
+ return true;
3862
+ }
3863
+ function sides(m, piece) {
3864
+ const seg = piece.segments[Math.floor(piece.segments.length / 2)];
3865
+ const mid = segmentPoint(seg, 0.5);
3866
+ const t = segmentTangent(seg, 0.5);
3867
+ const chord = Math.hypot(seg[seg.length - 1][0] - seg[0][0], seg[seg.length - 1][1] - seg[0][1]);
3868
+ const off = Math.max(1e-6, Math.min(0.05, 0.05 * chord));
3869
+ return [m.map.faceAt([mid[0] - t[1] * off, mid[1] + t[0] * off]), m.map.faceAt([mid[0] + t[1] * off, mid[1] - t[0] * off])];
3870
+ }
3871
+ function planShapeBuilder(m, touched, erase) {
3872
+ const replace = /* @__PURE__ */ new Map();
3873
+ const picked = new Set(touched.faces);
3874
+ const faces = m.map.faces;
3875
+ const sourcesOf = faces.map((_, i) => m.map.faceSources(i));
3876
+ m.entries.forEach((_, i) => {
3877
+ if (m.lines[i]) {
3878
+ const own = m.pieces.flatMap((piece, k) => piece.source === i ? [k] : []);
3879
+ const keep = own.filter((k) => !touched.lines.includes(k) && !sides(m, m.pieces[k]).every((f) => f >= 0 && picked.has(f)));
3880
+ if (keep.length === own.length) return;
3881
+ replace.set(i, keep.length ? { kind: "pieces", paths: keep.map((k) => m.pieces[k].path) } : null);
3882
+ return;
3883
+ }
3884
+ if (!touched.faces.some((f) => sourcesOf[f].includes(i))) return;
3885
+ const kept = faces.flatMap((_2, f) => sourcesOf[f].includes(i) && !picked.has(f) ? [f] : []);
3886
+ const rest = kept.length ? m.map.merge(kept) : [];
3887
+ replace.set(i, rest.length ? { kind: "fill", path: rest } : null);
3888
+ });
3889
+ let merged = null;
3890
+ if (!erase && touched.faces.length) {
3891
+ const path = m.map.merge(touched.faces);
3892
+ const covered = touched.faces.map((f) => sourcesOf[f]).find((s) => s.length);
3893
+ const lastShape = m.lines.lastIndexOf(false);
3894
+ const style = covered ? covered[covered.length - 1] : lastShape >= 0 ? lastShape : m.entries.length - 1;
3895
+ if (path.length) merged = { path, style, currentFill: !covered };
3896
+ }
3897
+ return { replace, merged };
3898
+ }
3899
+ function vectorFillMap(doc, gap = 0) {
3900
+ const entries = vectorPathEntries(doc, null, { locked: true });
3901
+ return PlanarMap.build(
3902
+ entries.map((e) => ({ path: worldOf(e, e.node.subpaths.filter(usable)), fills: false })),
3903
+ { gap, maxSegments: 2e4 }
3904
+ );
3905
+ }
3906
+
3114
3907
  // src/vector/controller.ts
3115
3908
  var DEFAULT_VECTOR_SNAP = { grid: 0, objects: true, guides: true, threshold: 5, angles: VECTOR_CONSTRUCTION_ANGLES };
3116
- var DEFAULT_VECTOR_PATH_EDIT = { keepShapeOnDelete: true, curvatureMoves: false, pencilSmoothing: 0, recognizeShapes: false, eraserSize: 10 };
3909
+ var VECTOR_FILL_GAP = 6;
3910
+ var DEFAULT_VECTOR_PATH_EDIT = {
3911
+ keepShapeOnDelete: true,
3912
+ curvatureMoves: false,
3913
+ pencilSmoothing: 0,
3914
+ recognizeShapes: false,
3915
+ eraserSize: 10,
3916
+ pencilPressure: "pen",
3917
+ fillGap: 0
3918
+ };
3117
3919
  var VECTOR_PENCIL_STRING_PX = 100;
3118
3920
  var VECTOR_STYLE_KEYS = [
3119
3921
  "fill",
@@ -3128,7 +3930,8 @@ var VECTOR_STYLE_KEYS = [
3128
3930
  "endArrow",
3129
3931
  "arrowAlign",
3130
3932
  "startArrowScale",
3131
- "endArrowScale"
3933
+ "endArrowScale",
3934
+ "strokeWidthProfile"
3132
3935
  ];
3133
3936
  var VECTOR_CLIPBOARD_TYPE = "pixerate/vector";
3134
3937
  var DEFAULT_DEFAULTS = {
@@ -3190,9 +3993,9 @@ function getVisibleHandles(state) {
3190
3993
  return out;
3191
3994
  }
3192
3995
  if (state.tool !== "direct") return out;
3193
- const editable = new Set(getEditablePathIds(state));
3996
+ const editable2 = new Set(getEditablePathIds(state));
3194
3997
  for (const ref of state.selectedAnchors) {
3195
- if (!editable.has(ref.nodeId)) continue;
3998
+ if (!editable2.has(ref.nodeId)) continue;
3196
3999
  const node = findVectorNode(state.document.nodes, ref.nodeId);
3197
4000
  const sp = node.subpaths[ref.subpath];
3198
4001
  if (!sp) continue;
@@ -3229,6 +4032,56 @@ function getVectorCornerWidgets(state) {
3229
4032
  }
3230
4033
  return out;
3231
4034
  }
4035
+ function vectorStrokeWidthGeometry(node) {
4036
+ if (!hasPaint(node.stroke) || !((node.strokeWidth ?? 1) > 0)) return null;
4037
+ const parts = vectorStrokeParts(subpathsToBezier(node.subpaths), node);
4038
+ return parts.line.length ? { line: parts.line, unit: parts.drawWidth / 2 } : null;
4039
+ }
4040
+ function getVectorWidthHandles(state) {
4041
+ if (state.tool !== "width") return [];
4042
+ const ids = /* @__PURE__ */ new Set();
4043
+ for (const id of state.selection) {
4044
+ const n = findVectorNode(state.document.nodes, id);
4045
+ if (n) for (const p of collectPathNodes(n)) ids.add(p.id);
4046
+ }
4047
+ if (state.hover?.nodeId) ids.add(state.hover.nodeId);
4048
+ if (state.interaction?.type === "width") ids.add(state.interaction.ref.nodeId);
4049
+ if (state.selectedWidthPoint) ids.add(state.selectedWidthPoint.nodeId);
4050
+ const out = [];
4051
+ for (const id of ids) {
4052
+ const node = findVectorNode(state.document.nodes, id);
4053
+ if (node?.type !== "path" || node.visible === false || !node.strokeWidthProfile?.length) continue;
4054
+ const g = vectorStrokeWidthGeometry(node);
4055
+ if (!g) continue;
4056
+ const profile = sanitizeWidthProfile(node.strokeWidthProfile);
4057
+ const m = vectorWorldMatrix(state.document.nodes, id) ?? IDENTITY_MATRIX;
4058
+ const contours = node.subpaths.length;
4059
+ for (let sp = 0; sp < contours; sp++) {
4060
+ profile.forEach((w, index) => {
4061
+ const at2 = strokePositionPoint(g.line, sp, w.t);
4062
+ if (!at2) return;
4063
+ const n = { x: at2.tangent[1], y: -at2.tangent[0] };
4064
+ const c = { x: at2.point[0], y: at2.point[1] };
4065
+ const p = applyMatrix(m, c);
4066
+ const l = applyMatrix(m, { x: c.x + n.x * w.left * g.unit, y: c.y + n.y * w.left * g.unit });
4067
+ const r = applyMatrix(m, { x: c.x - n.x * w.right * g.unit, y: c.y - n.y * w.right * g.unit });
4068
+ const sel = state.selectedWidthPoint;
4069
+ out.push({
4070
+ ref: { nodeId: id, index, subpath: sp },
4071
+ x: p.x,
4072
+ y: p.y,
4073
+ left: l,
4074
+ right: r,
4075
+ t: w.t,
4076
+ leftScale: w.left,
4077
+ rightScale: w.right,
4078
+ selected: !!sel && sel.nodeId === id && sel.index === index
4079
+ });
4080
+ });
4081
+ }
4082
+ }
4083
+ return out;
4084
+ }
3232
4085
  function getAnchor(doc, ref) {
3233
4086
  const node = findVectorNode(doc.nodes, ref.nodeId);
3234
4087
  return node?.type === "path" ? node.subpaths[ref.subpath]?.anchors[ref.index] : void 0;
@@ -3299,9 +4152,28 @@ var TOOL_LABELS = {
3299
4152
  line: "Line",
3300
4153
  scissors: "Scissors",
3301
4154
  knife: "Knife",
3302
- eraser: "Eraser"
4155
+ eraser: "Eraser",
4156
+ width: "Width",
4157
+ shapeBuilder: "Shape Builder",
4158
+ paintBucket: "Paint Bucket"
4159
+ };
4160
+ var TOOL_KEYS = {
4161
+ v: "select",
4162
+ a: "direct",
4163
+ p: "pen",
4164
+ n: "pencil",
4165
+ r: "rect",
4166
+ o: "ellipse",
4167
+ y: "polygon",
4168
+ s: "star",
4169
+ l: "line",
4170
+ c: "scissors",
4171
+ k: "knife",
4172
+ e: "eraser",
4173
+ w: "width",
4174
+ m: "shapeBuilder",
4175
+ b: "paintBucket"
3303
4176
  };
3304
- var TOOL_KEYS = { v: "select", a: "direct", p: "pen", n: "pencil", r: "rect", o: "ellipse", y: "polygon", s: "star", l: "line", c: "scissors", k: "knife", e: "eraser" };
3305
4177
  function vectorToolLabel(tool) {
3306
4178
  return TOOL_LABELS[tool];
3307
4179
  }
@@ -3317,7 +4189,10 @@ var VECTOR_TOOL_SHORTCUTS = {
3317
4189
  line: "L",
3318
4190
  scissors: "C",
3319
4191
  knife: "K",
3320
- eraser: "E"
4192
+ eraser: "E",
4193
+ width: "W",
4194
+ shapeBuilder: "M",
4195
+ paintBucket: "B"
3321
4196
  };
3322
4197
  var VECTOR_TOOL_GLYPHS = {
3323
4198
  select: "\u2196",
@@ -3331,7 +4206,10 @@ var VECTOR_TOOL_GLYPHS = {
3331
4206
  line: "\u2571",
3332
4207
  scissors: "\u2702",
3333
4208
  knife: "\u27CB",
3334
- eraser: "\u232B"
4209
+ eraser: "\u232B",
4210
+ width: "\u21F9",
4211
+ shapeBuilder: "\u2295",
4212
+ paintBucket: "\u25CD"
3335
4213
  };
3336
4214
  var SHAPE_TOOLS = /* @__PURE__ */ new Set(["rect", "ellipse", "polygon", "star", "line"]);
3337
4215
  var BOOLEAN_OPS = {
@@ -3341,6 +4219,14 @@ var BOOLEAN_OPS = {
3341
4219
  exclude: "xor"
3342
4220
  };
3343
4221
  var BOOLEAN_LABELS = { union: "Union", subtract: "Subtract", intersect: "Intersect", exclude: "Exclude" };
4222
+ var VECTOR_PATHFINDER_LABELS = {
4223
+ divide: "Divide",
4224
+ trim: "Trim",
4225
+ merge: "Merge",
4226
+ crop: "Crop",
4227
+ outline: "Outline",
4228
+ minusBack: "Minus Back"
4229
+ };
3344
4230
  function mapTree(nodes, fn) {
3345
4231
  let changed = false;
3346
4232
  const out = [];
@@ -3385,6 +4271,7 @@ var VectorEditorController = class {
3385
4271
  doc;
3386
4272
  selection = [];
3387
4273
  anchors = [];
4274
+ widthPoint = null;
3388
4275
  editingId = null;
3389
4276
  pen = null;
3390
4277
  tool;
@@ -3404,6 +4291,13 @@ var VectorEditorController = class {
3404
4291
  hoverTargets = null;
3405
4292
  spaceHeld = false;
3406
4293
  clipboard = null;
4294
+ /** Shape Builder map of the selection, kept while the document and the selection stay the same. */
4295
+ builderCache = null;
4296
+ /** Paint bucket map of the visible outlines. */
4297
+ fillCache = null;
4298
+ builderPreview = null;
4299
+ /** What the highlight shows, to skip notifying when the pointer stays over the same face. */
4300
+ builderHoverKey = null;
3407
4301
  createId;
3408
4302
  onChange;
3409
4303
  listeners = /* @__PURE__ */ new Set();
@@ -3463,6 +4357,7 @@ var VectorEditorController = class {
3463
4357
  tool: this.tool,
3464
4358
  selection: this.selection,
3465
4359
  selectedAnchors: this.anchors,
4360
+ selectedWidthPoint: this.widthPoint,
3466
4361
  editingId: this.editingId,
3467
4362
  selectionBounds: this.boundsOf(this.selection),
3468
4363
  selectionFrame: this.frameOf(this.selectedRoots()),
@@ -3477,6 +4372,7 @@ var VectorEditorController = class {
3477
4372
  interaction: this.interaction,
3478
4373
  pen: this.pen,
3479
4374
  pointer: this.pointer,
4375
+ builder: this.builderPreview,
3480
4376
  canUndo: h.canUndo,
3481
4377
  canRedo: h.canRedo,
3482
4378
  undoLabel: h.undoName,
@@ -3531,6 +4427,15 @@ var VectorEditorController = class {
3531
4427
  return h?.nodeId ? "move" : "default";
3532
4428
  case "pen":
3533
4429
  return h?.anchor ? "pointer" : "crosshair";
4430
+ case "width":
4431
+ if (this.drag?.kind === "width") return this.drag.mode === "move" ? "grabbing" : "ew-resize";
4432
+ if (h?.widthPoint) return h.widthPoint.part === "center" ? "grab" : "ew-resize";
4433
+ return h?.strokeSpot ? "copy" : "default";
4434
+ case "shapeBuilder":
4435
+ if (!this.builderPreview?.faces.length && !this.builderPreview?.lines.length) return "crosshair";
4436
+ return this.builderPreview.mode === "erase" ? "not-allowed" : "cell";
4437
+ case "paintBucket":
4438
+ return this.builderPreview ? "cell" : "crosshair";
3534
4439
  default:
3535
4440
  return "crosshair";
3536
4441
  }
@@ -3656,6 +4561,9 @@ var VectorEditorController = class {
3656
4561
  this.tool = tool;
3657
4562
  this.hover = null;
3658
4563
  this.guides = null;
4564
+ this.widthPoint = null;
4565
+ this.builderPreview = null;
4566
+ this.builderHoverKey = null;
3659
4567
  if (tool !== "direct") {
3660
4568
  this.anchors = [];
3661
4569
  this.editingId = null;
@@ -3887,6 +4795,7 @@ var VectorEditorController = class {
3887
4795
  if (d.fillRule) out.fillRule = d.fillRule;
3888
4796
  if (d.strokeAlign && d.strokeAlign !== "center") out.strokeAlign = d.strokeAlign;
3889
4797
  for (const key of ["startArrow", "endArrow", "arrowAlign", "startArrowScale", "endArrowScale"]) if (d[key]) out[key] = d[key];
4798
+ if (d.strokeWidthProfile?.length) out.strokeWidthProfile = d.strokeWidthProfile.map((p) => ({ ...p }));
3890
4799
  return out;
3891
4800
  }
3892
4801
  /** Merges `patch` into a node (paths take style and geometry; groups name / visibility / opacity). */
@@ -3907,10 +4816,39 @@ var VectorEditorController = class {
3907
4816
  }
3908
4817
  if (!targets.size) return { ok: false, message: "Nothing is selected." };
3909
4818
  this.change(options.label ?? "Change Style", () => {
3910
- this.setDoc(mapTree(this.doc.nodes, (n) => targets.has(n.id) ? { ...n, ...style } : n));
4819
+ this.setDoc(mapTree(this.doc.nodes, (n) => targets.has(n.id) ? withStyle(n, style) : n));
3911
4820
  });
3912
4821
  return { ok: true };
3913
4822
  }
4823
+ /**
4824
+ * Sets the variable-width profile of the selected (or given) paths: a preset id
4825
+ * (`uniform` removes the profile), points, or a parametric taper / wave. Returns an error for
4826
+ * an unknown preset.
4827
+ */
4828
+ setWidthProfile(profile, options = {}) {
4829
+ let points;
4830
+ if (profile === null) points = [];
4831
+ else if (typeof profile === "string") {
4832
+ const preset = widthProfilePreset(profile);
4833
+ if (!preset) return { ok: false, message: `There is no width profile "${profile}".` };
4834
+ points = preset;
4835
+ } else if (Array.isArray(profile)) points = sanitizeWidthProfile(profile);
4836
+ else {
4837
+ const p = profile;
4838
+ points = p.taper || p.wave ? generateWidthProfile(p) : [];
4839
+ }
4840
+ this.widthPoint = null;
4841
+ return this.setStyle({ strokeWidthProfile: points.length ? points : null }, options.ids ?? this.selection, { label: points.length ? "Width Profile" : "Uniform Width" });
4842
+ }
4843
+ /** Removes one width point of a path's profile (the width tool's Delete). */
4844
+ removeWidthPoint(ref) {
4845
+ const node = this.getNode(ref.nodeId);
4846
+ if (node?.type !== "path" || !node.strokeWidthProfile?.[ref.index]) return { ok: false, message: "There is no such width point." };
4847
+ const rest = removeWidthPoints(sanitizeWidthProfile(node.strokeWidthProfile), [ref.index]);
4848
+ this.widthPoint = null;
4849
+ this.change("Delete Width Point", () => this.replaceNode(node.id, withStyle(node, { strokeWidthProfile: rest.length ? rest : null })));
4850
+ return { ok: true };
4851
+ }
3914
4852
  /**
3915
4853
  * Sets the live corner radius (and optionally the kind) of anchors: `refs`, else the selected
3916
4854
  * anchors (direct tool), else every anchor of the `ids` / selected paths. Radius 0 makes the
@@ -3981,6 +4919,7 @@ var VectorEditorController = class {
3981
4919
  arrowAlign: _al,
3982
4920
  startArrowScale: _ss,
3983
4921
  endArrowScale: _es,
4922
+ strokeWidthProfile: _wp,
3984
4923
  ...rest
3985
4924
  } = cloneVectorNode(p);
3986
4925
  const id = this.newId();
@@ -4282,7 +5221,7 @@ var VectorEditorController = class {
4282
5221
  if (!groups.length) return { ok: false, message: "Select a group to ungroup." };
4283
5222
  const set = new Set(groups.map((g) => g.id));
4284
5223
  this.change("Ungroup", () => {
4285
- const flatten = (list) => {
5224
+ const flatten2 = (list) => {
4286
5225
  const out = [];
4287
5226
  let changed = false;
4288
5227
  for (const n of list) {
@@ -4294,14 +5233,14 @@ var VectorEditorController = class {
4294
5233
  ...n.children.map((c) => n.transform ? transformVectorNode(c, m) : c).map((c) => opacity !== void 0 && opacity < 1 ? { ...c, opacity: (c.opacity ?? 1) * opacity } : c)
4295
5234
  );
4296
5235
  } else if (n.type === "group") {
4297
- const children = flatten(n.children);
5236
+ const children = flatten2(n.children);
4298
5237
  if (children !== n.children) changed = true;
4299
5238
  out.push(children !== n.children ? { ...n, children } : n);
4300
5239
  } else out.push(n);
4301
5240
  }
4302
5241
  return changed ? out : list;
4303
5242
  };
4304
- this.setDoc(flatten(this.doc.nodes));
5243
+ this.setDoc(flatten2(this.doc.nodes));
4305
5244
  this.selection = groups.flatMap((g) => g.children.map((c) => c.id));
4306
5245
  });
4307
5246
  return { ok: true };
@@ -4412,6 +5351,248 @@ var VectorEditorController = class {
4412
5351
  return { ok: true };
4413
5352
  }
4414
5353
  // -------------------------------------------------------------------------
5354
+ // Pathfinder, Shape Builder and paint bucket
5355
+ // -------------------------------------------------------------------------
5356
+ /**
5357
+ * Pathfinder: Divide, Trim, Merge, Crop, Outline or Minus Back over the selected (or given)
5358
+ * paths, in document space (rotated nodes and groups included, live corners cut). Unfilled
5359
+ * stroked paths take part as the area their stroke paints (Divide: open ones cut instead).
5360
+ * The results replace the operands at the place of the top-most one, grouped when there are
5361
+ * several; each keeps the style of the operand whose area it is. Returns the new path ids,
5362
+ * or an empty list when nothing came out.
5363
+ */
5364
+ pathfinder(op, options = {}) {
5365
+ const roots = options.ids ?? this.selectedRoots();
5366
+ const entries = vectorPathEntries(this.doc, roots);
5367
+ if (entries.length < (op === "divide" || op === "outline" ? 1 : 2)) return [];
5368
+ const shapes = entries.map((e) => vectorPathfinderShape(e, op));
5369
+ const result = pathfinder(op, shapes);
5370
+ if (result.error || !result.pieces.length) return [];
5371
+ const last = entries[entries.length - 1].node.id;
5372
+ const topRoot = [...roots].reverse().find((r) => {
5373
+ const n = this.getNode(r);
5374
+ return !!n && !!findVectorNode([n], last);
5375
+ }) ?? last;
5376
+ const nodes = [];
5377
+ for (const piece of result.pieces) {
5378
+ const subpaths = bezierToSubpaths(this.intoParentSpace(topRoot, piece.path)).map((sp) => piece.open ? { ...sp, closed: false } : sp);
5379
+ if (!subpaths.length) continue;
5380
+ const source = entries[piece.source].node;
5381
+ const style = vectorResultStyle(source, shapes[piece.source].paint, op === "outline");
5382
+ nodes.push({ ...style, type: "path", id: this.newId(), name: source.name ?? "Path", subpaths });
5383
+ }
5384
+ if (!nodes.length) return [];
5385
+ const label = VECTOR_PATHFINDER_LABELS[op];
5386
+ const container = nodes.length > 1 ? { id: this.newId(), type: "group", name: label, children: nodes } : nodes[0];
5387
+ const drop = /* @__PURE__ */ new Set([...entries.map((e) => e.node.id), ...roots]);
5388
+ this.change(label, () => {
5389
+ this.setDoc(mapTree(this.doc.nodes, (n) => n.id === topRoot ? container : drop.has(n.id) ? null : n));
5390
+ this.selection = [container.id];
5391
+ this.anchors = [];
5392
+ this.editingId = null;
5393
+ });
5394
+ return nodes.map((n) => n.id);
5395
+ }
5396
+ /** The Shape Builder map of the selected (or given) paths, cached per document and selection. */
5397
+ shapeBuilderMap(ids) {
5398
+ const roots = ids ?? this.selectedRoots();
5399
+ const key = roots.join("\0");
5400
+ if (this.builderCache?.doc === this.doc && this.builderCache.key === key) return this.builderCache.map;
5401
+ const entries = roots.length ? vectorPathEntries(this.doc, roots) : [];
5402
+ const map = entries.length ? vectorShapeBuilderMap(entries) : null;
5403
+ this.builderCache = { doc: this.doc, key, map };
5404
+ return map;
5405
+ }
5406
+ /** The faces the Shape Builder offers for the selected (or given) paths (document units). */
5407
+ getShapeBuilderFaces(options = {}) {
5408
+ const m = this.shapeBuilderMap(options.ids);
5409
+ if (!m) return [];
5410
+ return m.map.faces.map((f, index) => {
5411
+ const p = m.map.faceInterior(index);
5412
+ return {
5413
+ index,
5414
+ d: bezierPathToSvg(f.path, { precision: 3 }),
5415
+ area: f.area,
5416
+ sources: m.map.faceSources(index).map((i) => m.entries[i].node.id),
5417
+ point: p ? { x: p[0], y: p[1] } : null
5418
+ };
5419
+ });
5420
+ }
5421
+ /** Pick distance of the Shape Builder's erase for line pieces (document units). */
5422
+ builderLineTolerance() {
5423
+ return Math.max(this.hitRadius, 4) / this.viewport.zoom;
5424
+ }
5425
+ /**
5426
+ * Shape Builder gesture along `points` (document units; one point is a click) over the faces
5427
+ * the selected (or given) paths make. Merges the faces touched into one shape styled like the
5428
+ * top-most shape over the first face (the current fill for a face only outlines enclose);
5429
+ * with `erase` deletes them, and the pieces of unfilled open paths within `tolerance`. Every
5430
+ * shape covering a touched face loses that area. One undo step.
5431
+ */
5432
+ shapeBuilder(points, options = {}) {
5433
+ const none = (message) => ({ ok: false, message, merged: null, faces: 0, lines: 0 });
5434
+ if (!points.length || !points.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))) return none("Give the points of the gesture.");
5435
+ const m = this.shapeBuilderMap(options.ids);
5436
+ if (!m) return none("Select paths that make faces.");
5437
+ const erase = !!options.erase;
5438
+ const tol = erase ? options.tolerance ?? this.builderLineTolerance() : null;
5439
+ const touched = { faces: [], lines: [] };
5440
+ const size = m.map.faces.reduce((s, f) => Math.max(s, f.bounds.maxX - f.bounds.minX, f.bounds.maxY - f.bounds.minY), 1);
5441
+ for (const p of samplePolyline(points, Math.max(size / 2e3, 0.25 / this.viewport.zoom))) addBuilderHit(touched, vectorShapeBuilderHit(m, p, tol));
5442
+ if (!touched.faces.length && !touched.lines.length) return none("There is no face under the pointer.");
5443
+ return this.applyShapeBuilder(m, touched, erase);
5444
+ }
5445
+ applyShapeBuilder(m, touched, erase) {
5446
+ const plan = planShapeBuilder(m, touched, erase);
5447
+ let mergedId = null;
5448
+ const created = /* @__PURE__ */ new Map();
5449
+ this.change(erase ? "Shape Builder Delete" : "Shape Builder", () => {
5450
+ let nodes = this.doc.nodes;
5451
+ m.entries.forEach((entry, i) => {
5452
+ const node = entry.node;
5453
+ const toLocal = (path) => transformBezierPath(path, invertMatrix(multiplyMatrices(nodeMatrix(node), entry.parent)));
5454
+ const list = [];
5455
+ const r = plan.replace.get(i);
5456
+ if (r === void 0) list.push(node);
5457
+ else if (r?.kind === "fill") list.push({ ...node, fillRule: "nonzero", subpaths: bezierToSubpaths(toLocal(r.path)) });
5458
+ else if (r?.kind === "pieces") {
5459
+ r.paths.forEach(
5460
+ (path, k) => list.push({ ...node, id: k === 0 ? node.id : this.newId(), subpaths: bezierToSubpaths(toLocal(path)).map((sp) => ({ ...sp, closed: false })) })
5461
+ );
5462
+ }
5463
+ if (plan.merged && plan.merged.style === i) {
5464
+ mergedId = this.newId();
5465
+ const merged = {
5466
+ ...vectorResultStyle(node, "fill"),
5467
+ type: "path",
5468
+ id: mergedId,
5469
+ name: "Shape",
5470
+ ...node.transform ? { transform: node.transform } : {},
5471
+ subpaths: bezierToSubpaths(toLocal(plan.merged.path))
5472
+ };
5473
+ if (plan.merged.currentFill || !hasPaint(merged.fill)) merged.fill = hasPaint(this.defaults.fill) ? this.defaults.fill : "#808080";
5474
+ list.push(merged);
5475
+ }
5476
+ if (r === void 0 && list.length === 1) return;
5477
+ created.set(node.id, list.map((n) => n.id));
5478
+ nodes = spliceTree(nodes, node.id, list);
5479
+ });
5480
+ this.setDoc(nodes);
5481
+ const selection = this.selection.flatMap((id) => created.get(id) ?? [id]);
5482
+ if (mergedId && !selection.includes(mergedId)) selection.push(mergedId);
5483
+ this.selection = selection.filter((id, k) => !!this.getNode(id) && selection.indexOf(id) === k);
5484
+ this.anchors = [];
5485
+ this.editingId = null;
5486
+ });
5487
+ this.builderCache = null;
5488
+ return { ok: true, merged: mergedId, faces: touched.faces.length, lines: touched.lines.length };
5489
+ }
5490
+ /** The paint bucket's map of the visible outlines, cached per document and gap. */
5491
+ fillMap(gap) {
5492
+ if (this.fillCache?.doc === this.doc && this.fillCache.gap === gap) return this.fillCache.map;
5493
+ const map = vectorFillMap(this.doc, gap);
5494
+ this.fillCache = { doc: this.doc, gap, map };
5495
+ return map;
5496
+ }
5497
+ /**
5498
+ * Paint bucket: the region the visible outlines enclose around `point` (document units),
5499
+ * crossings split and islands left as holes, becomes a new path filled with the current fill
5500
+ * default, placed just below the selection (or on top when nothing is selected). `gap`
5501
+ * (default `editing.fillGap`) closes gaps up to that size between loose ends. Returns the new
5502
+ * path's id, or null when nothing encloses the point.
5503
+ */
5504
+ fillRegion(point, options = {}) {
5505
+ if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) return null;
5506
+ const region = this.fillMap(options.gap ?? this.editing.fillGap)?.regionAt([point.x, point.y]);
5507
+ if (!region?.length) return null;
5508
+ let below = null;
5509
+ const roots = new Set(this.selectedRoots());
5510
+ walkVectorNodes(this.doc.nodes, (n) => {
5511
+ if (!below && roots.has(n.id)) below = n.id;
5512
+ });
5513
+ const subpaths = bezierToSubpaths(below ? this.intoParentSpace(below, region) : region);
5514
+ if (!subpaths.length) return null;
5515
+ const id = this.newId();
5516
+ const node = { id, type: "path", name: "Fill", ...this.styleFor(false), stroke: null, fillRule: "nonzero", subpaths };
5517
+ if (!hasPaint(node.fill)) node.fill = "#808080";
5518
+ const target = below;
5519
+ this.change("Fill Region", () => {
5520
+ if (target) {
5521
+ const current = this.getNode(target);
5522
+ this.setDoc(spliceTree(this.doc.nodes, target, [node, current]));
5523
+ } else this.setDoc([...this.doc.nodes, node]);
5524
+ if (!target) this.selection = [id];
5525
+ this.anchors = [];
5526
+ this.editingId = null;
5527
+ });
5528
+ return id;
5529
+ }
5530
+ /** Updates the Shape Builder / paint bucket highlight for the pointer at `p`; true when it changed. */
5531
+ hoverBuilder(p, alt) {
5532
+ if (this.tool === "paintBucket") {
5533
+ const region = this.fillMap(this.editing.fillGap)?.regionAt([p.x, p.y]) ?? null;
5534
+ const key2 = region ? bezierPathToSvg(region, { precision: 2 }) : null;
5535
+ if (key2 === this.builderHoverKey) return false;
5536
+ this.builderHoverKey = key2;
5537
+ this.builderPreview = region ? { mode: "fill", faces: [region], lines: [], trail: null } : null;
5538
+ return true;
5539
+ }
5540
+ const m = this.selectedRoots().length ? this.shapeBuilderMap() : null;
5541
+ const hit = m ? vectorShapeBuilderHit(m, p, alt ? this.builderLineTolerance() : null) : null;
5542
+ const key = hit ? `${alt ? "erase" : "merge"}:${JSON.stringify(hit)}` : null;
5543
+ if (key === this.builderHoverKey) return false;
5544
+ this.builderHoverKey = key;
5545
+ this.builderPreview = hit && m ? this.builderPreviewOf(m, "face" in hit ? { faces: [hit.face], lines: [] } : { faces: [], lines: [hit.line] }, alt, null) : null;
5546
+ return true;
5547
+ }
5548
+ builderPreviewOf(m, touched, erase, trail) {
5549
+ const faces = m.map.faces;
5550
+ return {
5551
+ mode: erase ? "erase" : "merge",
5552
+ faces: touched.faces.map((f) => faces[f].path),
5553
+ lines: touched.lines.map((k) => m.pieces[k].path),
5554
+ trail
5555
+ };
5556
+ }
5557
+ downBuilder(p, input) {
5558
+ this.builderHoverKey = null;
5559
+ if (this.tool === "paintBucket") {
5560
+ this.fillRegion(p);
5561
+ this.builderPreview = null;
5562
+ this.hoverBuilder(p, false);
5563
+ this.notify();
5564
+ return;
5565
+ }
5566
+ const m = this.selectedRoots().length ? this.shapeBuilderMap() : null;
5567
+ if (!m) return;
5568
+ const erase = !!input.alt;
5569
+ const touched = { faces: [], lines: [] };
5570
+ addBuilderHit(touched, vectorShapeBuilderHit(m, p, erase ? this.builderLineTolerance() : null));
5571
+ this.drag = { kind: "builder", erase, points: [p], touched, map: m };
5572
+ this.interaction = { type: "builder", mode: erase ? "erase" : "merge", points: [p] };
5573
+ this.builderPreview = this.builderPreviewOf(m, touched, erase, [p]);
5574
+ this.notify();
5575
+ }
5576
+ moveBuilder(drag, p) {
5577
+ const last = drag.points[drag.points.length - 1];
5578
+ if (Math.hypot(p.x - last.x, p.y - last.y) * this.viewport.zoom < 1) return;
5579
+ const tol = drag.erase ? this.builderLineTolerance() : null;
5580
+ for (const q of samplePolyline([last, p], 2 / this.viewport.zoom)) addBuilderHit(drag.touched, vectorShapeBuilderHit(drag.map, q, tol));
5581
+ drag.points.push(p);
5582
+ this.interaction = { type: "builder", mode: drag.erase ? "erase" : "merge", points: drag.points.slice() };
5583
+ this.builderPreview = this.builderPreviewOf(drag.map, drag.touched, drag.erase, drag.points.slice());
5584
+ this.notify();
5585
+ }
5586
+ upBuilder(drag) {
5587
+ this.builderPreview = null;
5588
+ this.builderHoverKey = null;
5589
+ if (!drag.touched.faces.length && !drag.touched.lines.length) {
5590
+ this.notify();
5591
+ return;
5592
+ }
5593
+ this.applyShapeBuilder(drag.map, drag.touched, drag.erase);
5594
+ }
5595
+ // -------------------------------------------------------------------------
4415
5596
  // Anchor editing
4416
5597
  // -------------------------------------------------------------------------
4417
5598
  updateAnchors(doc, refs, fn) {
@@ -4571,7 +5752,12 @@ var VectorEditorController = class {
4571
5752
  const set = new Set(ids.flatMap((id) => this.getNode(id) ? collectPathNodes(this.getNode(id)) : []).map((p) => p.id));
4572
5753
  if (!set.size) return { ok: false, message: "Select a path." };
4573
5754
  this.change("Reverse Path", () => {
4574
- this.setDoc(mapTree(this.doc.nodes, (n) => n.type === "path" && set.has(n.id) ? { ...n, subpaths: n.subpaths.map(reverseSubpath) } : n));
5755
+ this.setDoc(
5756
+ mapTree(
5757
+ this.doc.nodes,
5758
+ (n) => n.type === "path" && set.has(n.id) ? { ...n, subpaths: n.subpaths.map(reverseSubpath), ...n.strokeWidthProfile?.length ? { strokeWidthProfile: reverseWidthProfile(n.strokeWidthProfile) } : {} } : n
5759
+ )
5760
+ );
4575
5761
  this.anchors = [];
4576
5762
  });
4577
5763
  return { ok: true };
@@ -4652,9 +5838,9 @@ var VectorEditorController = class {
4652
5838
  const cx = points.reduce((s, p) => s + p.at.x, 0) / points.length;
4653
5839
  const cy = points.reduce((s, p) => s + p.at.y, 0) / points.length;
4654
5840
  this.change("Average", () => {
4655
- for (const { ref, at } of points) {
4656
- const dx = axis === "horizontal" ? 0 : cx - at.x;
4657
- const dy = axis === "vertical" ? 0 : cy - at.y;
5841
+ for (const { ref, at: at2 } of points) {
5842
+ const dx = axis === "horizontal" ? 0 : cx - at2.x;
5843
+ const dy = axis === "vertical" ? 0 : cy - at2.y;
4658
5844
  if (dx || dy) this.doc = this.movedAnchors(this.doc, [ref], dx, dy);
4659
5845
  }
4660
5846
  });
@@ -4738,8 +5924,8 @@ var VectorEditorController = class {
4738
5924
  const node = this.getNode(id);
4739
5925
  if (node.locked) continue;
4740
5926
  const ids = this.applyCuts(node, (sp, i) => {
4741
- const at = list.filter((r) => r.subpath === i).map((r) => r.index);
4742
- const pieces = at.length ? cutSubpathAt(sp, at) : null;
5927
+ const at2 = list.filter((r) => r.subpath === i).map((r) => r.index);
5928
+ const pieces = at2.length ? cutSubpathAt(sp, at2) : null;
4743
5929
  return pieces && !(pieces.length === 1 && pieces[0].closed === sp.closed) ? pieces : null;
4744
5930
  });
4745
5931
  if (ids) created.push(...ids);
@@ -4943,8 +6129,8 @@ var VectorEditorController = class {
4943
6129
  if (!frame) return null;
4944
6130
  const { box, handles } = getVectorSelectionBoxGeometry(frame, this.viewport);
4945
6131
  const r = this.hitRadius + 2;
4946
- const usable = box.width < 2 * r || box.height < 2 * r ? handles.filter((h) => h.handle.length === 2 || h.handle === "rotate") : handles;
4947
- for (const h of usable) if (Math.hypot(h.screen.x - screen.x, h.screen.y - screen.y) <= r * Math.SQRT2) return h.handle;
6132
+ const usable2 = box.width < 2 * r || box.height < 2 * r ? handles.filter((h) => h.handle.length === 2 || h.handle === "rotate") : handles;
6133
+ for (const h of usable2) if (Math.hypot(h.screen.x - screen.x, h.screen.y - screen.y) <= r * Math.SQRT2) return h.handle;
4948
6134
  return null;
4949
6135
  }
4950
6136
  hitHandle(screen) {
@@ -5053,10 +6239,131 @@ var VectorEditorController = class {
5053
6239
  const seg = this.nearestOutline(p);
5054
6240
  return seg ? { nodeId: seg.nodeId, segment: seg } : null;
5055
6241
  }
6242
+ case "width": {
6243
+ const hit = this.hitWidthHandle(screen);
6244
+ if (hit) return { nodeId: hit.ref.nodeId, widthPoint: hit.ref };
6245
+ const spot = this.strokeSpotAt(p);
6246
+ return spot ? { nodeId: spot.nodeId, strokeSpot: spot } : null;
6247
+ }
5056
6248
  default:
5057
6249
  return null;
5058
6250
  }
5059
6251
  }
6252
+ widthView() {
6253
+ return { document: this.doc, selection: this.selection, tool: this.tool, hover: this.hover, interaction: this.interaction, selectedWidthPoint: this.widthPoint };
6254
+ }
6255
+ /** The width point centre or side end under `screen` (width tool). */
6256
+ hitWidthHandle(screen) {
6257
+ let best = null;
6258
+ let bestD = Infinity;
6259
+ for (const h of getVectorWidthHandles(this.widthView())) {
6260
+ for (const [part, q] of [["center", h], ["left", h.left], ["right", h.right]]) {
6261
+ const s = this.documentToScreen(q);
6262
+ const d = Math.hypot(s.x - screen.x, s.y - screen.y);
6263
+ if (d <= this.hitRadius && d < bestD - 1e-9) {
6264
+ best = { ref: { ...h.ref, part } };
6265
+ bestD = d;
6266
+ }
6267
+ }
6268
+ }
6269
+ return best;
6270
+ }
6271
+ /** The nearest stroked, unlocked visible path whose stroke is under document point `p` (width tool). */
6272
+ strokeSpotAt(p) {
6273
+ let best = null;
6274
+ const tol = this.tolerance();
6275
+ walkVectorNodes(this.doc.nodes, (n) => {
6276
+ if (n.type !== "path" || n.visible === false || n.locked) return;
6277
+ if (hiddenOrLocked(this.doc.nodes, n.id)) return;
6278
+ const g = vectorStrokeWidthGeometry(n);
6279
+ if (!g) return;
6280
+ const m = this.worldOf(n.id);
6281
+ const local = applyMatrix(invertMatrix(m), p);
6282
+ const near2 = nearestStrokePosition(g.line, [local.x, local.y]);
6283
+ if (!near2) return;
6284
+ const profile = strokeWidthProfileOf(n);
6285
+ const w = widthProfileAt(profile, near2.t);
6286
+ const reach = Math.max(near2.side === "left" ? w.left : w.right, 0) * g.unit + tol;
6287
+ if (near2.distance > reach || near2.distance > widthProfileMaxScale(profile) * g.unit + tol) return;
6288
+ if (best && near2.distance >= best.distance) return;
6289
+ const nx = near2.tangent[1];
6290
+ const ny = -near2.tangent[0];
6291
+ const [cx, cy] = near2.point;
6292
+ best = {
6293
+ nodeId: n.id,
6294
+ subpath: near2.contour,
6295
+ t: near2.t,
6296
+ point: applyMatrix(m, { x: cx, y: cy }),
6297
+ left: applyMatrix(m, { x: cx + nx * w.left * g.unit, y: cy + ny * w.left * g.unit }),
6298
+ right: applyMatrix(m, { x: cx - nx * w.right * g.unit, y: cy - ny * w.right * g.unit }),
6299
+ distance: near2.distance
6300
+ };
6301
+ });
6302
+ if (!best) return null;
6303
+ const { distance: _d, ...spot } = best;
6304
+ return spot;
6305
+ }
6306
+ downWidth(screen, p, input) {
6307
+ const hit = this.hitWidthHandle(screen);
6308
+ const spot = hit ? null : this.strokeSpotAt(p);
6309
+ const nodeId = hit?.ref.nodeId ?? spot?.nodeId;
6310
+ const node = nodeId ? this.getNode(nodeId) : void 0;
6311
+ const g = node?.type === "path" ? vectorStrokeWidthGeometry(node) : null;
6312
+ if (!node || node.type !== "path" || !g) {
6313
+ if (this.widthPoint) {
6314
+ this.widthPoint = null;
6315
+ this.notify();
6316
+ }
6317
+ return;
6318
+ }
6319
+ const before = this.snapshot();
6320
+ const local = this.toLocal(node.id, p);
6321
+ let profile;
6322
+ let ref;
6323
+ let mode = "width";
6324
+ let side = "both";
6325
+ let t;
6326
+ let subpath;
6327
+ if (hit) {
6328
+ profile = sanitizeWidthProfile(node.strokeWidthProfile);
6329
+ ref = { nodeId: node.id, index: hit.ref.index, subpath: hit.ref.subpath };
6330
+ t = profile[ref.index]?.t ?? 0;
6331
+ subpath = hit.ref.subpath;
6332
+ if (hit.ref.part === "center") mode = "move";
6333
+ else if (input.alt) side = hit.ref.part;
6334
+ } else {
6335
+ const near2 = nearestStrokePosition(g.line, [local.x, local.y]);
6336
+ const inserted = insertWidthPoint(strokeWidthProfileOf(node), near2.t);
6337
+ profile = inserted.points;
6338
+ subpath = near2.contour;
6339
+ t = near2.t;
6340
+ ref = { nodeId: node.id, index: inserted.index, subpath };
6341
+ if (input.alt) side = near2.side;
6342
+ }
6343
+ const at2 = strokePositionPoint(g.line, subpath, t);
6344
+ if (!at2) return;
6345
+ const base = { ...this.doc, nodes: mapTree(this.doc.nodes, (n) => n.id === node.id ? { ...n, strokeWidthProfile: profile } : n) };
6346
+ this.doc = base;
6347
+ this.widthPoint = { nodeId: node.id, index: ref.index, subpath };
6348
+ this.drag = {
6349
+ kind: "width",
6350
+ ref,
6351
+ mode,
6352
+ side,
6353
+ origin: { x: at2.point[0], y: at2.point[1] },
6354
+ normal: { x: at2.tangent[1], y: -at2.tangent[0] },
6355
+ unit: g.unit,
6356
+ profile,
6357
+ before,
6358
+ base,
6359
+ start: p,
6360
+ moved: false,
6361
+ created: !hit
6362
+ };
6363
+ const w = profile[ref.index];
6364
+ this.interaction = { type: "width", ref, mode, width: (w.left + w.right) * g.unit };
6365
+ this.notify(!hit);
6366
+ }
5060
6367
  /** The nearest outline point of any visible, unlocked path within the pick distance of document point `p`. */
5061
6368
  nearestOutline(p) {
5062
6369
  let best = null;
@@ -5104,7 +6411,12 @@ var VectorEditorController = class {
5104
6411
  case "pen":
5105
6412
  return this.downPen(screen, p, input);
5106
6413
  case "pencil":
5107
- return this.downPencil(p);
6414
+ return this.downPencil(p, input);
6415
+ case "width":
6416
+ return this.downWidth(screen, p, input);
6417
+ case "shapeBuilder":
6418
+ case "paintBucket":
6419
+ return this.downBuilder(p, input);
5108
6420
  case "scissors": {
5109
6421
  const seg = this.nearestOutline(p);
5110
6422
  if (seg) this.cutPathAt(seg.nodeId, seg.point);
@@ -5297,7 +6609,7 @@ var VectorEditorController = class {
5297
6609
  this.interaction = { type: "pen", nodeId: node.id };
5298
6610
  this.notify(true);
5299
6611
  }
5300
- downPencil(p) {
6612
+ downPencil(p, input = {}) {
5301
6613
  const before = this.snapshot();
5302
6614
  const node = {
5303
6615
  id: this.newId(),
@@ -5311,7 +6623,11 @@ var VectorEditorController = class {
5311
6623
  this.setDoc([...this.doc.nodes, node]);
5312
6624
  const lazy = new LazyMouse(this.editing.pencilSmoothing * VECTOR_PENCIL_STRING_PX / this.viewport.zoom);
5313
6625
  lazy.push([p.x, p.y]);
5314
- this.drag = { kind: "pencil", nodeId: node.id, points: [[p.x, p.y]], before, lazy };
6626
+ const mode = this.editing.pencilPressure;
6627
+ const hasPressure = typeof input.pressure === "number" && Number.isFinite(input.pressure);
6628
+ const record = hasPressure && (mode === "all" || mode === "pen" && input.pointerType === "pen");
6629
+ const pressure = hasPressure ? input.pressure : 0.5;
6630
+ this.drag = { kind: "pencil", nodeId: node.id, points: [[p.x, p.y]], before, lazy, pressures: record ? [pressure] : null, pressure };
5315
6631
  this.interaction = { type: "draw", tool: "pencil", nodeId: node.id, bounds: { x: p.x, y: p.y, width: 0, height: 0 } };
5316
6632
  this.notify(true);
5317
6633
  }
@@ -5330,6 +6646,11 @@ var VectorEditorController = class {
5330
6646
  pointerMove(screen, input = {}) {
5331
6647
  const p = this.screenToDocument(screen);
5332
6648
  const drag = this.drag;
6649
+ if (!drag && (this.tool === "shapeBuilder" || this.tool === "paintBucket")) {
6650
+ this.pointer = p;
6651
+ if (this.hoverBuilder(p, !!input.alt)) this.notify();
6652
+ return;
6653
+ }
5333
6654
  if (!drag) {
5334
6655
  const hover = this.computeHover(screen);
5335
6656
  const pointerMatters = this.tool === "pen" && !!this.pen;
@@ -5461,6 +6782,8 @@ var VectorEditorController = class {
5461
6782
  this.notify(true);
5462
6783
  return;
5463
6784
  }
6785
+ case "builder":
6786
+ return this.moveBuilder(drag, p);
5464
6787
  case "stroke": {
5465
6788
  const last = drag.points[drag.points.length - 1];
5466
6789
  if (Math.hypot(p.x - last.x, p.y - last.y) * this.viewport.zoom < 1.5) return;
@@ -5520,12 +6843,36 @@ var VectorEditorController = class {
5520
6843
  this.notify(true);
5521
6844
  return;
5522
6845
  }
6846
+ case "width": {
6847
+ if (!drag.moved && Math.hypot(p.x - drag.start.x, p.y - drag.start.y) * this.viewport.zoom < 2) return;
6848
+ drag.moved = true;
6849
+ const node = findVectorNode(drag.base.nodes, drag.ref.nodeId);
6850
+ const local = this.toLocal(node.id, p, drag.base);
6851
+ let profile;
6852
+ if (drag.mode === "move") {
6853
+ const g = vectorStrokeWidthGeometry(node);
6854
+ const near2 = g ? nearestStrokePosition(g.line, [local.x, local.y], drag.ref.subpath) : null;
6855
+ if (!near2) return;
6856
+ profile = setWidthPoint(drag.profile, drag.ref.index, { t: near2.t });
6857
+ } else {
6858
+ const off = ((local.x - drag.origin.x) * drag.normal.x + (local.y - drag.origin.y) * drag.normal.y) / drag.unit;
6859
+ 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) };
6860
+ profile = setWidthPoint(drag.profile, drag.ref.index, patch);
6861
+ }
6862
+ this.doc = { ...drag.base, nodes: mapTree(drag.base.nodes, (n) => n.id === node.id ? { ...n, strokeWidthProfile: profile } : n) };
6863
+ const w = profile[drag.ref.index];
6864
+ this.interaction = { type: "width", ref: drag.ref, mode: drag.mode, width: (w.left + w.right) * drag.unit };
6865
+ this.notify(true);
6866
+ return;
6867
+ }
5523
6868
  case "pencil": {
5524
6869
  const last = drag.points[drag.points.length - 1];
5525
6870
  let moved = false;
6871
+ if (typeof input.pressure === "number" && Number.isFinite(input.pressure)) drag.pressure = input.pressure;
5526
6872
  for (const q of drag.lazy.push([p.x, p.y])) {
5527
6873
  if (Math.hypot(q[0] - last[0], q[1] - last[1]) * this.viewport.zoom < 1.5) continue;
5528
6874
  drag.points.push(q);
6875
+ drag.pressures?.push(drag.pressure);
5529
6876
  moved = true;
5530
6877
  }
5531
6878
  const string = drag.lazy.string();
@@ -5606,7 +6953,7 @@ var VectorEditorController = class {
5606
6953
  }
5607
6954
  /** Primary button released. */
5608
6955
  pointerUp(screen, input = {}) {
5609
- if (screen && this.drag && this.drag.kind !== "pan") this.pointerMove(screen, input);
6956
+ if (screen && this.drag && this.drag.kind !== "pan") this.pointerMove(screen, { ...input, pressure: void 0 });
5610
6957
  const drag = this.drag;
5611
6958
  this.drag = null;
5612
6959
  this.interaction = null;
@@ -5658,6 +7005,17 @@ var VectorEditorController = class {
5658
7005
  this.notify();
5659
7006
  return;
5660
7007
  }
7008
+ case "width": {
7009
+ if (drag.moved) return this.commit(drag.mode === "move" ? "Move Width Point" : "Width Point", drag.before);
7010
+ if (drag.created) {
7011
+ this.doc = drag.before.doc;
7012
+ this.widthPoint = null;
7013
+ this.notify(true);
7014
+ } else this.notify();
7015
+ return;
7016
+ }
7017
+ case "builder":
7018
+ return this.upBuilder(drag);
5661
7019
  case "stroke": {
5662
7020
  this.notify();
5663
7021
  if (drag.tool === "knife") this.knife(drag.points);
@@ -5688,7 +7046,10 @@ var VectorEditorController = class {
5688
7046
  this.notify(true);
5689
7047
  return;
5690
7048
  }
5691
- for (const q of drag.lazy.finish()) drag.points.push(q);
7049
+ for (const q of drag.lazy.finish()) {
7050
+ drag.points.push(q);
7051
+ drag.pressures?.push(drag.pressure);
7052
+ }
5692
7053
  const shape = this.editing.recognizeShapes ? recognizeInkShape(drag.points, { minSize: 6 / this.viewport.zoom }) : null;
5693
7054
  if (shape) {
5694
7055
  this.replaceNode(drag.nodeId, { ...node, name: inkShapeLabel(shape), subpaths: [inkShapeSubpath(shape)] });
@@ -5703,7 +7064,8 @@ var VectorEditorController = class {
5703
7064
  anchors.push({ x: c.p3[0], y: c.p3[1], in: { x: c.p2[0], y: c.p2[1] } });
5704
7065
  }
5705
7066
  const subpaths = anchors.length > 1 ? bezierToSubpaths(subpathsToBezier([{ anchors, closed: false }])) : node.subpaths;
5706
- this.replaceNode(drag.nodeId, { ...node, subpaths });
7067
+ const profile = drag.pressures ? pencilPressureProfile(drag.points, drag.pressures) : [];
7068
+ this.replaceNode(drag.nodeId, { ...node, subpaths, ...profile.length ? { strokeWidthProfile: profile } : {} });
5707
7069
  this.selection = [drag.nodeId];
5708
7070
  return this.commit("Draw", drag.before);
5709
7071
  }
@@ -5725,7 +7087,8 @@ var VectorEditorController = class {
5725
7087
  this.drag = null;
5726
7088
  this.interaction = null;
5727
7089
  this.guides = null;
5728
- if (drag.kind === "pan") {
7090
+ if (drag.kind === "pan" || drag.kind === "builder") {
7091
+ if (drag.kind === "builder") this.builderPreview = null;
5729
7092
  this.notify();
5730
7093
  return;
5731
7094
  }
@@ -5825,7 +7188,10 @@ var VectorEditorController = class {
5825
7188
  switch (key) {
5826
7189
  case "Escape":
5827
7190
  if (this.drag) this.cancelDrag();
5828
- else if (this.pen) this.finishPath();
7191
+ else if (this.tool === "width" && this.widthPoint) {
7192
+ this.widthPoint = null;
7193
+ this.notify();
7194
+ } else if (this.pen) this.finishPath();
5829
7195
  else if (this.tool === "direct" && this.anchors.length) {
5830
7196
  this.anchors = [];
5831
7197
  this.notify();
@@ -5846,6 +7212,7 @@ var VectorEditorController = class {
5846
7212
  return false;
5847
7213
  case "Delete":
5848
7214
  case "Backspace":
7215
+ if (this.tool === "width" && this.widthPoint) return this.removeWidthPoint(this.widthPoint).ok;
5849
7216
  if (this.pen) {
5850
7217
  const node = this.getNode(this.pen.nodeId);
5851
7218
  const sp = node.subpaths[this.pen.subpath];
@@ -5927,6 +7294,31 @@ function scaleFor(handle, b, p, mods = {}) {
5927
7294
  }
5928
7295
  return { sx, sy, origin };
5929
7296
  }
7297
+ function withStyle(node, style) {
7298
+ const next = { ...node, ...style };
7299
+ if ("strokeWidthProfile" in style) {
7300
+ const p = style.strokeWidthProfile ? sanitizeWidthProfile(style.strokeWidthProfile) : [];
7301
+ if (p.length) next.strokeWidthProfile = p;
7302
+ else delete next.strokeWidthProfile;
7303
+ }
7304
+ return next;
7305
+ }
7306
+ function hiddenOrLocked(nodes, id) {
7307
+ const loc = locateVectorNode(nodes, id);
7308
+ if (!loc) return false;
7309
+ const n = loc.list[loc.index];
7310
+ if (n.locked || n.visible === false) return true;
7311
+ return loc.parent ? hiddenOrLocked(nodes, loc.parent.id) : false;
7312
+ }
7313
+ function pencilPressureProfile(points, pressures) {
7314
+ const n = Math.min(points.length, pressures.length);
7315
+ if (n < 2) return [];
7316
+ const s = [0];
7317
+ 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]));
7318
+ const total = s[n - 1];
7319
+ if (!(total > 0)) return [];
7320
+ return pressureWidthProfile(s.map((d, i) => ({ t: d / total, pressure: pressures[i] })));
7321
+ }
5930
7322
 
5931
7323
  // src/vector/render.ts
5932
7324
  var fmt = (n) => Math.round(n * 100) / 100;
@@ -6091,7 +7483,33 @@ function getVectorOverlayRenderData(state) {
6091
7483
  });
6092
7484
  if (it?.type === "corner") status = `R ${fmt(it.radius)}`;
6093
7485
  const cutPoint = state.tool === "scissors" && state.hover?.segment && !it ? (({ x, y }) => ({ x: fmt(x), y: fmt(y) }))(toScreen(state, state.hover.segment.point)) : null;
6094
- return { outlines, selectionBox, anchors, handles, marquee, penPreview, artboard, guides, status, toolStroke, pencilString, cutPoint, cornerWidgets };
7486
+ const sc = (p) => {
7487
+ const q = toScreen(state, p);
7488
+ return { x: fmt(q.x), y: fmt(q.y) };
7489
+ };
7490
+ const sameWidth = (a, b) => !!a && a.nodeId === b.nodeId && a.index === b.index;
7491
+ const widthPoints = getVectorWidthHandles(state).map((h) => {
7492
+ const c = sc(h);
7493
+ const hw = state.hover?.widthPoint;
7494
+ const hovered = hw && sameWidth(hw, h.ref) && hw.subpath === h.ref.subpath ? hw.part : null;
7495
+ 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) };
7496
+ });
7497
+ const spot = state.tool === "width" && !it ? state.hover?.strokeSpot : null;
7498
+ const widthSpot = spot ? { ...sc(spot.point), left: sc(spot.left), right: sc(spot.right) } : null;
7499
+ if (it?.type === "width") status = it.mode === "move" ? "Move width point" : `W ${fmt(it.width)}`;
7500
+ let builder = null;
7501
+ if (state.builder) {
7502
+ const v2 = state.viewport;
7503
+ const view = [v2.zoom, 0, 0, v2.zoom, v2.x, v2.y];
7504
+ const d = (path) => bezierPathToSvg(transformBezierPath(path, view), { precision: 2 });
7505
+ const trail = state.builder.trail && state.builder.trail.length > 1 ? state.builder.trail.map((q, i) => `${i ? "L" : "M"}${sc(q).x} ${sc(q).y}`).join("") : null;
7506
+ builder = { mode: state.builder.mode, faces: state.builder.faces.map(d), lines: state.builder.lines.map(d), trail };
7507
+ if (state.builder.mode !== "fill" && it?.type === "builder") {
7508
+ const n = state.builder.faces.length;
7509
+ status = state.builder.mode === "erase" ? `Delete ${n} ${n === 1 ? "face" : "faces"}` : `Merge ${n} ${n === 1 ? "face" : "faces"}`;
7510
+ }
7511
+ }
7512
+ return { outlines, selectionBox, anchors, handles, marquee, penPreview, artboard, guides, status, toolStroke, pencilString, cutPoint, cornerWidgets, widthPoints, widthSpot, builder };
6095
7513
  }
6096
7514
  var VECTOR_OVERLAY_THEME = {
6097
7515
  accent: "#0ea5e9",
@@ -6105,13 +7523,28 @@ var VECTOR_OVERLAY_THEME = {
6105
7523
  anchorSize: 7,
6106
7524
  handleRadius: 3.5,
6107
7525
  /** Radius of a live corner widget (a ring with a dot). */
6108
- cornerWidgetRadius: 4
7526
+ cornerWidgetRadius: 4,
7527
+ /** Half the diagonal of a width point's diamond. */
7528
+ widthPointSize: 5,
7529
+ /** Shape Builder faces to merge and paint bucket regions. */
7530
+ builderFill: "rgba(14, 165, 233, 0.22)",
7531
+ /** Shape Builder faces and line pieces an Alt gesture deletes. */
7532
+ builderErase: "#ef4444",
7533
+ builderEraseFill: "rgba(239, 68, 68, 0.22)"
6109
7534
  };
7535
+ function vectorDiamondPath(x, y, r = VECTOR_OVERLAY_THEME.widthPointSize) {
7536
+ 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`;
7537
+ }
6110
7538
  function toVectorModifiers(event) {
6111
7539
  return { shift: !!event.shiftKey, alt: !!event.altKey, mod: !!(event.metaKey || event.ctrlKey) };
6112
7540
  }
6113
7541
  function toVectorPointerInput(event) {
6114
- return { ...toVectorModifiers(event), button: event.button };
7542
+ return {
7543
+ ...toVectorModifiers(event),
7544
+ button: event.button,
7545
+ ...typeof event.pressure === "number" ? { pressure: event.pressure } : {},
7546
+ ...event.pointerType ? { pointerType: event.pointerType } : {}
7547
+ };
6115
7548
  }
6116
7549
  function toVectorKeyInput(event) {
6117
7550
  return { key: event.key, ...toVectorModifiers(event) };
@@ -6160,6 +7593,8 @@ function getVectorSelectionStyle(state) {
6160
7593
  arrowAlign: first.arrowAlign ?? "extend",
6161
7594
  startArrowScale: first.startArrowScale ?? 1,
6162
7595
  endArrowScale: first.endArrowScale ?? 1,
7596
+ strokeWidthProfile: profileOf(first),
7597
+ widthProfile: widthProfilePresetId(profileOf(first)),
6163
7598
  cornerRadius: c0.radius,
6164
7599
  cornerKind: c0.kind,
6165
7600
  corners: corners2.length,
@@ -6175,12 +7610,17 @@ function getVectorSelectionStyle(state) {
6175
7610
  arrowAlign: !same((p) => p.arrowAlign ?? "extend"),
6176
7611
  startArrowScale: !same((p) => p.startArrowScale ?? 1),
6177
7612
  endArrowScale: !same((p) => p.endArrowScale ?? 1),
7613
+ widthProfile: !same(profileOf),
6178
7614
  cornerRadius: corners2.some((c) => c.radius !== c0.radius),
6179
7615
  cornerKind: corners2.some((c) => c.kind !== c0.kind)
6180
7616
  },
6181
7617
  pathIds: paths.map((p) => p.id)
6182
7618
  };
6183
7619
  }
7620
+ function profileOf(p) {
7621
+ const points = sanitizeWidthProfile(p.strokeWidthProfile);
7622
+ return points.length && widthProfilePresetId(points) !== "uniform" ? points : null;
7623
+ }
6184
7624
  var VECTOR_STROKE_ALIGN_LABELS = { center: "Center", inside: "Inside", outside: "Outside" };
6185
7625
  var VECTOR_CORNER_KIND_LABELS = { round: "Round", inverted: "Inverted", chamfer: "Chamfer" };
6186
7626
  var VECTOR_ARROWHEAD_LABELS = {
@@ -6206,6 +7646,14 @@ var VECTOR_PATH_ACTIONS = [
6206
7646
  { id: "smooth", label: "\u223F", title: "Smooth", enabled: (s, p) => s + p > 0, run: (c) => c.smoothPaths() },
6207
7647
  { id: "outline", label: "\u22A1", title: "Outline Stroke", enabled: (s) => s > 0, run: (c) => c.outlineStroke() }
6208
7648
  ];
7649
+ var VECTOR_PATHFINDER_ACTIONS = [
7650
+ { id: "divide", label: "\u229E", title: "Divide", min: 1 },
7651
+ { id: "trim", label: "\u29C9", title: "Trim", min: 2 },
7652
+ { id: "merge", label: "\u29C8", title: "Merge", min: 2 },
7653
+ { id: "crop", label: "\u2317", title: "Crop", min: 2 },
7654
+ { id: "outline", label: "\u2B1A", title: "Outline", min: 1 },
7655
+ { id: "minusBack", label: "\u2296", title: "Minus Back", min: 2 }
7656
+ ];
6209
7657
  var VECTOR_EDIT_TOGGLES = [
6210
7658
  { key: "keepShapeOnDelete", label: "Keep", title: "Keep shape when deleting points" },
6211
7659
  { key: "curvatureMoves", label: "Curv", title: "Curvature point moves (Alt flips while dragging)" },
@@ -6232,6 +7680,19 @@ var POINTS = {
6232
7680
  minItems: 1,
6233
7681
  description: "Polyline [[x, y], \u2026] in document units."
6234
7682
  };
7683
+ var WIDTH_POINTS = {
7684
+ type: ["array", "null"],
7685
+ items: {
7686
+ type: "object",
7687
+ properties: {
7688
+ t: { type: "number", minimum: 0, maximum: 1, description: "Position along each subpath by length (0 start, 1 end)." },
7689
+ left: { type: "number", minimum: 0, description: "Left side as a multiple of half the stroke width (1 = uniform)." },
7690
+ right: { type: "number", minimum: 0, description: "Right side (looking along the path, y down)." }
7691
+ },
7692
+ required: ["t", "left", "right"]
7693
+ },
7694
+ description: "Variable-width profile points, interpolated smoothly (monotone cubic); two points at one `t` make a step. `null` or `[]` strokes at a uniform width."
7695
+ };
6235
7696
  var PAINT = {
6236
7697
  description: 'A CSS colour, `null` for none, or a gradient `{ type: "linear", angle, stops: [{ offset, color }] }`.'
6237
7698
  };
@@ -6248,7 +7709,8 @@ var STYLE_PROPS = {
6248
7709
  endArrow: { type: ["string", "null"], enum: [...VECTOR_ARROWHEADS, null], description: "Head at the end of an open path, or null for none." },
6249
7710
  arrowAlign: { type: "string", enum: ["extend", "tip"], description: "`extend`: the tip sits past the end point; `tip`: on it, the stroke shortened under the head." },
6250
7711
  startArrowScale: { type: "number", exclusiveMinimum: 0, description: "Start head size as a multiple of the stroke width (default 1)." },
6251
- endArrowScale: { type: "number", exclusiveMinimum: 0, description: "End head size as a multiple of the stroke width (default 1)." }
7712
+ endArrowScale: { type: "number", exclusiveMinimum: 0, description: "End head size as a multiple of the stroke width (default 1)." },
7713
+ strokeWidthProfile: WIDTH_POINTS
6252
7714
  };
6253
7715
  var pickStyle = (params) => {
6254
7716
  const out = {};
@@ -6405,6 +7867,44 @@ function createVectorCommands(controller, options = {}) {
6405
7867
  return { ok: true };
6406
7868
  }
6407
7869
  },
7870
+ {
7871
+ ...base,
7872
+ id: `${prefix}setWidthProfile`,
7873
+ label: "Width Profile\u2026",
7874
+ keywords: ["variable width", "taper", "tapered", "pressure", "profile", "brush", "calligraphy", "bulge", "thick", "thin"],
7875
+ 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.",
7876
+ params: {
7877
+ type: "object",
7878
+ properties: {
7879
+ ids: IDS,
7880
+ preset: { type: "string", enum: VECTOR_WIDTH_PRESET_IDS },
7881
+ points: WIDTH_POINTS,
7882
+ taper: {
7883
+ type: "object",
7884
+ properties: {
7885
+ startLength: { type: "number", minimum: 0, maximum: 1 },
7886
+ endLength: { type: "number", minimum: 0, maximum: 1 },
7887
+ startWidth: { type: "number", minimum: 0, maximum: 1 },
7888
+ endWidth: { type: "number", minimum: 0, maximum: 1 },
7889
+ startEase: { type: "number", minimum: 0, maximum: 1 },
7890
+ endEase: { type: "number", minimum: 0, maximum: 1 }
7891
+ }
7892
+ },
7893
+ wave: { type: "object", properties: { amount: { type: "number", minimum: 0, maximum: 1 }, cycles: { type: "number", exclusiveMinimum: 0 }, phase: { type: "number" } } }
7894
+ }
7895
+ },
7896
+ enabled: enabledFor,
7897
+ undoable: true,
7898
+ run: (_ctx, params) => {
7899
+ const target = { ids: ids(params) };
7900
+ if (params?.preset) check(controller.setWidthProfile(params.preset, target));
7901
+ else if (params?.points !== void 0) check(controller.setWidthProfile(params.points ?? null, target));
7902
+ else if (params?.taper || params?.wave) check(controller.setWidthProfile({ taper: params.taper, wave: params.wave }, target));
7903
+ else fail("Pass `preset`, `points`, `taper` or `wave`.");
7904
+ const node = controller.getNode(target.ids[0]);
7905
+ return { ok: true, points: node?.type === "path" ? node.strokeWidthProfile ?? [] : [] };
7906
+ }
7907
+ },
6408
7908
  {
6409
7909
  ...base,
6410
7910
  id: `${prefix}outlineStroke`,
@@ -6477,6 +7977,62 @@ function createVectorCommands(controller, options = {}) {
6477
7977
  return { id };
6478
7978
  }
6479
7979
  },
7980
+ {
7981
+ ...base,
7982
+ id: `${prefix}pathfinder`,
7983
+ label: "Pathfinder\u2026",
7984
+ keywords: ["divide", "trim", "merge", "crop", "outline", "minus back", "split", "faces"],
7985
+ description: "Pathfinder over the given paths (bottom to top): divide (every face its own shape, painted like the top-most shape over it; open unfilled paths cut), trim (hidden parts removed), merge (trim, then touching parts of one fill joined), crop (the top shape crops the others), outline (outlines cut into open strokes where they meet) or minusBack (the top shape minus everything below). Results replace the paths, grouped when there are several.",
7986
+ params: { type: "object", properties: { operation: { type: "string", enum: [...VECTOR_PATHFINDER_OPERATIONS] }, ids: IDS }, required: ["operation"] },
7987
+ undoable: true,
7988
+ run: (_ctx, params) => {
7989
+ if (!VECTOR_PATHFINDER_OPERATIONS.includes(params.operation)) fail(`Unknown Pathfinder operation "${params.operation}".`);
7990
+ const created = controller.pathfinder(params.operation, { ids: ids(params) });
7991
+ if (!created.length) fail("Nothing came out: select paths that overlap (two or more, except for divide and outline).");
7992
+ return { ids: created, selection: [...controller.getState().selection] };
7993
+ }
7994
+ },
7995
+ {
7996
+ ...base,
7997
+ id: `${prefix}shapeBuilderFaces`,
7998
+ label: "List Shape Builder Faces",
7999
+ keywords: ["shape builder", "regions", "faces", "planar"],
8000
+ description: "Lists the faces the given paths divide the plane into: path data, area, a point inside and the ids of the paths filling each (empty when only outlines enclose it).",
8001
+ params: { type: "object", properties: { ids: IDS } },
8002
+ run: (_ctx, params) => ({ faces: controller.getShapeBuilderFaces({ ids: ids(params) }) })
8003
+ },
8004
+ {
8005
+ ...base,
8006
+ id: `${prefix}shapeBuilder`,
8007
+ label: "Shape Builder",
8008
+ keywords: ["merge faces", "build shape", "combine regions", "delete region"],
8009
+ description: "Shape Builder gesture over the faces of the given paths: `points` is the pointer path in document units (one point is a click; [[x, y], \u2026]; use the points from shapeBuilderFaces). Merges the faces it touches into one shape, or with `erase: true` deletes them and the pieces of unfilled open paths within `tolerance`. Shapes covering those faces lose that area.",
8010
+ params: {
8011
+ type: "object",
8012
+ properties: { points: POINTS, erase: { type: "boolean" }, tolerance: { type: "number", minimum: 0 }, ids: IDS },
8013
+ required: ["points"]
8014
+ },
8015
+ undoable: true,
8016
+ run: (_ctx, params) => {
8017
+ const result = controller.shapeBuilder(params.points.map(([x, y]) => ({ x, y })), { ids: ids(params), erase: params.erase, tolerance: params.tolerance });
8018
+ check(result);
8019
+ return { merged: result.merged, faces: result.faces, lines: result.lines };
8020
+ }
8021
+ },
8022
+ {
8023
+ ...base,
8024
+ id: `${prefix}fillRegion`,
8025
+ label: "Fill Region",
8026
+ keywords: ["paint bucket", "live paint", "fill area", "enclosed"],
8027
+ description: "Paint bucket: fills the area the visible outlines enclose around `point` (crossings split, nested shapes left as holes) with a new path in the current fill, below the selection or on top. `gap` closes gaps up to that size between loose ends.",
8028
+ params: { type: "object", properties: { point: { type: "array", items: { type: "number" }, minItems: 2, maxItems: 2, description: "[x, y] in document units." }, gap: { type: "number", minimum: 0 } }, required: ["point"] },
8029
+ undoable: true,
8030
+ run: (_ctx, params) => {
8031
+ const id = controller.fillRegion({ x: params.point[0], y: params.point[1] }, { gap: params.gap });
8032
+ if (!id) fail("No closed region surrounds that point.");
8033
+ return { id };
8034
+ }
8035
+ },
6480
8036
  {
6481
8037
  ...base,
6482
8038
  id: `${prefix}align`,
@@ -6711,7 +8267,7 @@ function createVectorCommands(controller, options = {}) {
6711
8267
  id: `${prefix}setEditing`,
6712
8268
  label: "Path Editing Options\u2026",
6713
8269
  keywords: ["settings", "curvature", "pencil", "smoothing", "lazy mouse", "shapes", "eraser"],
6714
- description: "Sets how editing behaves: `keepShapeOnDelete`, `curvatureMoves` (anchor drags bend like the Curvature tool), `pencilSmoothing` (lazy mouse, 0\u20131), `recognizeShapes` (pencil scribbles become lines, rectangles, ellipses, polygons) and `eraserSize`.",
8270
+ 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`).",
6715
8271
  params: {
6716
8272
  type: "object",
6717
8273
  properties: {
@@ -6719,12 +8275,13 @@ function createVectorCommands(controller, options = {}) {
6719
8275
  curvatureMoves: { type: "boolean" },
6720
8276
  pencilSmoothing: { type: "number", minimum: 0, maximum: 1 },
6721
8277
  recognizeShapes: { type: "boolean" },
6722
- eraserSize: { type: "number", exclusiveMinimum: 0 }
8278
+ eraserSize: { type: "number", exclusiveMinimum: 0 },
8279
+ pencilPressure: { type: "string", enum: ["pen", "all", "off"] }
6723
8280
  }
6724
8281
  },
6725
8282
  run: (_ctx, params) => {
6726
8283
  const patch = {};
6727
- for (const key of ["keepShapeOnDelete", "curvatureMoves", "pencilSmoothing", "recognizeShapes", "eraserSize"]) if (params?.[key] !== void 0) patch[key] = params[key];
8284
+ for (const key of ["keepShapeOnDelete", "curvatureMoves", "pencilSmoothing", "recognizeShapes", "eraserSize", "pencilPressure"]) if (params?.[key] !== void 0) patch[key] = params[key];
6728
8285
  controller.setEditing(patch);
6729
8286
  return { editing: controller.getState().editing };
6730
8287
  }
@@ -6780,10 +8337,15 @@ export {
6780
8337
  VECTOR_CORNER_KIND_LABELS,
6781
8338
  VECTOR_CORNER_WIDGET_INSET,
6782
8339
  VECTOR_EDIT_TOGGLES,
8340
+ VECTOR_FILL_GAP,
6783
8341
  VECTOR_INK_MAX_CORNERS,
6784
8342
  VECTOR_OVERLAY_THEME,
8343
+ VECTOR_PATHFINDER_ACTIONS,
8344
+ VECTOR_PATHFINDER_LABELS,
8345
+ VECTOR_PATHFINDER_OPERATIONS,
6785
8346
  VECTOR_PATH_ACTIONS,
6786
8347
  VECTOR_PENCIL_STRING_PX,
8348
+ VECTOR_PRESSURE_NOMINAL,
6787
8349
  VECTOR_ROTATE_HANDLE_OFFSET,
6788
8350
  VECTOR_STROKE_ALIGN_LABELS,
6789
8351
  VECTOR_STYLE_KEYS,
@@ -6791,8 +8353,14 @@ export {
6791
8353
  VECTOR_TOOL_GLYPHS,
6792
8354
  VECTOR_TOOL_SHORTCUTS,
6793
8355
  VECTOR_TRANSFORM_HANDLES,
8356
+ VECTOR_WIDTH_PRESETS,
8357
+ VECTOR_WIDTH_PRESET_IDS,
8358
+ VECTOR_WIDTH_PROFILE_MAX_POINTS,
8359
+ VECTOR_WIDTH_PROFILE_MAX_SCALE,
8360
+ VECTOR_WIDTH_SAME_T,
6794
8361
  VectorEditorController,
6795
8362
  VectorSnapTargets,
8363
+ addBuilderHit,
6796
8364
  applyMatrix,
6797
8365
  applyMatrixLinear,
6798
8366
  applyVectorCornersAttribute,
@@ -6816,6 +8384,7 @@ export {
6816
8384
  createVectorCommands,
6817
8385
  curvatureMoveAnchor,
6818
8386
  cutSubpathAt,
8387
+ dashIntervals,
6819
8388
  decomposeRigid,
6820
8389
  effectiveStrokeAlign,
6821
8390
  ellipseSubpath,
@@ -6826,6 +8395,7 @@ export {
6826
8395
  expandVectorCornersOf,
6827
8396
  findVectorNode,
6828
8397
  fitCornerRadius,
8398
+ generateWidthProfile,
6829
8399
  getAnchor,
6830
8400
  getAnchorWorld,
6831
8401
  getEditablePathIds,
@@ -6836,6 +8406,7 @@ export {
6836
8406
  getVectorSelectionBoxGeometry,
6837
8407
  getVectorSelectionFrame,
6838
8408
  getVectorSelectionStyle,
8409
+ getVectorWidthHandles,
6839
8410
  getVisibleAnchors,
6840
8411
  getVisibleHandles,
6841
8412
  hasPaint,
@@ -6846,46 +8417,61 @@ export {
6846
8417
  hitTestVectorStroke,
6847
8418
  inferAnchorType,
6848
8419
  insertAnchor,
8420
+ insertWidthPoint,
6849
8421
  invertMatrix,
6850
8422
  isClosedBezierShape,
6851
8423
  isIdentityMatrix,
8424
+ isUniformWidthProfile,
6852
8425
  isVectorTextTarget,
6853
8426
  joinSubpaths,
6854
8427
  knifePath,
6855
8428
  lineSubpath,
6856
8429
  locateVectorNode,
6857
8430
  matrixRotation,
8431
+ mirrorWidthProfile,
6858
8432
  moveAnchorHandle,
6859
8433
  multiplyMatrices,
6860
8434
  nearestSegment,
8435
+ nearestStrokePosition,
6861
8436
  nextVectorCornerKind,
6862
8437
  nodeMatrix,
6863
8438
  parseSvgTransform,
6864
8439
  parseVectorSvg,
8440
+ planShapeBuilder,
6865
8441
  polygonSubpath,
6866
8442
  polylineBounds,
8443
+ pressureWidthProfile,
6867
8444
  recognizeInkShape,
6868
8445
  rectSubpath,
6869
8446
  removeAnchorKeepingShape,
6870
8447
  removeAnchors,
6871
8448
  removeAnchorsKeepingShape,
6872
8449
  removeRedundantPoints,
8450
+ removeWidthPoints,
6873
8451
  reverseSubpath,
8452
+ reverseWidthProfile,
6874
8453
  rotateMatrix,
6875
8454
  roundSubpaths,
8455
+ samplePolyline,
8456
+ sanitizeWidthProfile,
6876
8457
  scaleFor,
6877
8458
  scaleMatrix,
6878
8459
  setAnchorCorner,
6879
8460
  setSubpathCorners,
8461
+ setWidthPoint,
6880
8462
  simplifyPolyline,
6881
8463
  smoothSubpath,
6882
8464
  splitIntoFaces,
6883
8465
  splitSubpathAt,
6884
8466
  starSubpath,
8467
+ strokePositionPoint,
8468
+ strokeWidthProfileOf,
8469
+ subpathEnclosesArea,
6885
8470
  subpathPointAt,
6886
8471
  subpathSegment,
6887
8472
  subpathSegmentCount,
6888
8473
  subpathsToBezier,
8474
+ taperWaveFactor,
6889
8475
  toVectorKeyInput,
6890
8476
  toVectorModifiers,
6891
8477
  toVectorPointerInput,
@@ -6893,10 +8479,14 @@ export {
6893
8479
  translateAnchor,
6894
8480
  translateMatrix,
6895
8481
  unionBounds,
8482
+ variableStrokeOutline,
8483
+ variableStrokeRings,
6896
8484
  vectorCornerAt,
6897
8485
  vectorCorners,
6898
8486
  vectorCornersAttribute,
8487
+ vectorDiamondPath,
6899
8488
  vectorDocumentToSvg,
8489
+ vectorFillMap,
6900
8490
  vectorInkShapeName,
6901
8491
  vectorNodeBounds,
6902
8492
  vectorNodeRotation,
@@ -6905,15 +8495,28 @@ export {
6905
8495
  vectorPaintToSvg,
6906
8496
  vectorParentMatrix,
6907
8497
  vectorPathData,
8498
+ vectorPathEntries,
6908
8499
  vectorPathWorldBezier,
8500
+ vectorPathfinderShape,
6909
8501
  vectorResizeCursor,
8502
+ vectorResultStyle,
8503
+ vectorShapeBuilderHit,
8504
+ vectorShapeBuilderMap,
8505
+ vectorStrokeLineArea,
6910
8506
  vectorStrokeOutline,
6911
8507
  vectorStrokeOutlinePath,
6912
8508
  vectorStrokeParts,
6913
8509
  vectorStrokeReach,
6914
8510
  vectorStrokeSvgParts,
8511
+ vectorStrokeWidthGeometry,
6915
8512
  vectorToolLabel,
6916
8513
  vectorWorldMatrix,
6917
- walkVectorNodes
8514
+ walkVectorNodes,
8515
+ widthProfileAt,
8516
+ widthProfileFromAttribute,
8517
+ widthProfileMaxScale,
8518
+ widthProfilePreset,
8519
+ widthProfilePresetId,
8520
+ widthProfileToAttribute
6918
8521
  };
6919
8522
  //# sourceMappingURL=index.js.map