@pixerate/editor 0.36.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,13 +10,16 @@ import {
10
10
  distanceToBezierPath,
11
11
  fitCubicWithTangents,
12
12
  fitCubics,
13
+ flattenChain,
13
14
  formatNumber,
14
15
  gradientToSvgDef,
16
+ isStraightCubic,
15
17
  normalizeBezierPath,
16
18
  outlineStroke,
17
19
  pathBoolean,
18
20
  pathToContours,
19
21
  pointInBezierPath,
22
+ polygonBoolean,
20
23
  segmentBounds,
21
24
  segmentDerivative,
22
25
  segmentIntersections,
@@ -31,12 +34,12 @@ import {
31
34
  transformBezierPath,
32
35
  trimSegments,
33
36
  windingNumber
34
- } from "../chunk-EGPIVUEF.js";
37
+ } from "../chunk-HPEKB352.js";
35
38
 
36
39
  // src/vector/types.ts
37
40
  var VECTOR_CORNER_KINDS = ["round", "inverted", "chamfer"];
38
41
  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"];
42
+ var VECTOR_TOOLS = ["select", "direct", "pen", "pencil", "rect", "ellipse", "polygon", "star", "line", "scissors", "knife", "eraser", "width"];
40
43
  var VECTOR_TRANSFORM_HANDLES = ["nw", "n", "ne", "e", "se", "s", "sw", "w", "rotate"];
41
44
 
42
45
  // src/vector/corners.ts
@@ -245,6 +248,535 @@ function setSubpathCorners(sp, radius, kind) {
245
248
  return { closed: sp.closed, anchors: sp.anchors.map((a) => setAnchorCorner(a, radius, kind)) };
246
249
  }
247
250
 
251
+ // src/vector/width.ts
252
+ var VECTOR_WIDTH_PROFILE_MAX_POINTS = 64;
253
+ var VECTOR_WIDTH_PROFILE_MAX_SCALE = 1e3;
254
+ var VECTOR_WIDTH_SAME_T = 1e-6;
255
+ var sym = (t, s) => ({ t, left: s, right: s });
256
+ var VECTOR_WIDTH_PRESETS = [
257
+ { id: "uniform", label: "Uniform", points: [] },
258
+ { id: "taperStart", label: "Taper start", points: [sym(0, 0), sym(1, 1)] },
259
+ { id: "taperEnd", label: "Taper end", points: [sym(0, 1), sym(1, 0)] },
260
+ { id: "taperBoth", label: "Taper both", points: [sym(0, 0), sym(0.5, 1), sym(1, 0)] },
261
+ // Full width at the ends, swelling to twice the width in the middle.
262
+ { id: "bulge", label: "Bulge", points: [sym(0, 1), sym(0.5, 2), sym(1, 1)] },
263
+ // Full width at the ends, pinched to a quarter in the middle.
264
+ { id: "pinch", label: "Pinch", points: [sym(0, 1), sym(0.5, 0.25), sym(1, 1)] },
265
+ // A round head a fifth of the way along, then a long taper to the end.
266
+ { id: "teardrop", label: "Teardrop", points: [sym(0, 0), sym(0.2, 1), sym(1, 0)] },
267
+ // Two swells between narrow necks.
268
+ { id: "wave", label: "Wave", points: [sym(0, 0.3), sym(0.25, 1), sym(0.5, 0.3), sym(0.75, 1), sym(1, 0.3)] },
269
+ // A pen stroke: a quick press, a slow lift.
270
+ { id: "pressure", label: "Pressure", points: [sym(0, 0.15), sym(0.12, 0.85), sym(0.45, 1), sym(0.8, 0.7), sym(1, 0.05)] }
271
+ ];
272
+ var VECTOR_WIDTH_PRESET_IDS = VECTOR_WIDTH_PRESETS.map((p) => p.id);
273
+ var clean = (v2, max) => typeof v2 === "number" && Number.isFinite(v2) ? Math.min(Math.max(v2, 0), max) : null;
274
+ function sanitizeWidthProfile(points) {
275
+ if (!Array.isArray(points)) return [];
276
+ const out = [];
277
+ for (const p of points) {
278
+ if (!p || typeof p !== "object") continue;
279
+ const t = clean(p.t, 1);
280
+ const l = clean(p.left, VECTOR_WIDTH_PROFILE_MAX_SCALE);
281
+ const r = clean(p.right, VECTOR_WIDTH_PROFILE_MAX_SCALE);
282
+ if (t === null || l === null && r === null) continue;
283
+ out.push({ t, left: l ?? r, right: r ?? l });
284
+ if (out.length >= VECTOR_WIDTH_PROFILE_MAX_POINTS) break;
285
+ }
286
+ return out.map((p, i) => [p, i]).sort((a, b) => a[0].t - b[0].t || a[1] - b[1]).map(([p]) => p);
287
+ }
288
+ function isUniformWidthProfile(points) {
289
+ return !points?.length || points.every((p) => p.left === 1 && p.right === 1);
290
+ }
291
+ function secant(pts, i, k) {
292
+ const a = pts[i];
293
+ const b = pts[i + 1];
294
+ return a && b && b.t - a.t > 1e-12 ? (b[k] - a[k]) / (b.t - a.t) : 0;
295
+ }
296
+ function tangent(pts, i, k) {
297
+ const last = pts.length - 1;
298
+ if (i <= 0) return secant(pts, 0, k);
299
+ if (i >= last) return secant(pts, last - 1, k);
300
+ const d0 = secant(pts, i - 1, k);
301
+ const d1 = secant(pts, i, k);
302
+ return d0 * d1 <= 0 ? 0 : 2 * d0 * d1 / (d0 + d1);
303
+ }
304
+ function interp(pts, t, k) {
305
+ const first = pts[0];
306
+ const last = pts[pts.length - 1];
307
+ if (!(t > first.t)) return first[k];
308
+ if (t >= last.t) return last[k];
309
+ let i = 0;
310
+ while (i + 1 < pts.length && pts[i + 1].t <= t) i++;
311
+ const a = pts[i];
312
+ const b = pts[i + 1];
313
+ const h = b.t - a.t;
314
+ if (h <= 1e-12) return b[k];
315
+ const u = (t - a.t) / h;
316
+ const u2 = u * u;
317
+ const u3 = u2 * u;
318
+ const w = (2 * u3 - 3 * u2 + 1) * a[k] + (u3 - 2 * u2 + u) * h * tangent(pts, i, k) + (3 * u2 - 2 * u3) * b[k] + (u3 - u2) * h * tangent(pts, i + 1, k);
319
+ return Math.max(0, w);
320
+ }
321
+ function widthProfileAt(points, t) {
322
+ if (!points?.length) return { left: 1, right: 1 };
323
+ return { left: interp(points, t, "left"), right: interp(points, t, "right") };
324
+ }
325
+ function widthProfileMaxScale(points) {
326
+ if (!points?.length) return 1;
327
+ return points.reduce((m, p) => Math.max(m, p.left, p.right), 0);
328
+ }
329
+ function widthProfilePreset(id) {
330
+ const p = VECTOR_WIDTH_PRESETS.find((q) => q.id === id);
331
+ return p ? p.points.map((q) => ({ ...q })) : null;
332
+ }
333
+ function widthProfilePresetId(points) {
334
+ if (isUniformWidthProfile(points)) return "uniform";
335
+ const same = (a, b) => a.length === b.length && a.every((p, i) => Math.abs(p.t - b[i].t) < 1e-9 && Math.abs(p.left - b[i].left) < 1e-9 && Math.abs(p.right - b[i].right) < 1e-9);
336
+ return VECTOR_WIDTH_PRESETS.find((p) => p.points.length && same(points, p.points))?.id ?? "custom";
337
+ }
338
+ function reverseWidthProfile(points) {
339
+ return points.map((p) => ({ t: 1 - p.t, left: p.right, right: p.left })).reverse();
340
+ }
341
+ function mirrorWidthProfile(points) {
342
+ return points.map((p) => ({ t: p.t, left: p.right, right: p.left }));
343
+ }
344
+ function editable(points) {
345
+ return points?.length ? points.map((p) => ({ ...p })) : [sym(0, 1), sym(1, 1)];
346
+ }
347
+ function insertWidthPoint(points, t, sides = {}, options = {}) {
348
+ const pts = editable(points);
349
+ const tt = Math.min(Math.max(Number.isFinite(t) ? t : 0, 0), 1);
350
+ const here = widthProfileAt(points?.length ? points : null, tt);
351
+ const p = {
352
+ t: tt,
353
+ left: clean(sides.left, VECTOR_WIDTH_PROFILE_MAX_SCALE) ?? here.left,
354
+ right: clean(sides.right, VECTOR_WIDTH_PROFILE_MAX_SCALE) ?? here.right
355
+ };
356
+ const same = pts.findIndex((q) => Math.abs(q.t - tt) <= VECTOR_WIDTH_SAME_T);
357
+ if (same >= 0 && !options.step) {
358
+ pts[same] = { ...p, t: pts[same].t };
359
+ return { points: pts, index: same };
360
+ }
361
+ let i = 0;
362
+ while (i < pts.length && pts[i].t <= tt) i++;
363
+ if (pts.length >= VECTOR_WIDTH_PROFILE_MAX_POINTS) return { points: pts, index: Math.max(0, i - 1) };
364
+ pts.splice(i, 0, p);
365
+ return { points: pts, index: i };
366
+ }
367
+ function setWidthPoint(points, index, patch) {
368
+ const pts = points.map((p2) => ({ ...p2 }));
369
+ const p = pts[index];
370
+ if (!p) return pts;
371
+ if (typeof patch.t === "number" && Number.isFinite(patch.t)) {
372
+ const lo = index > 0 ? pts[index - 1].t : 0;
373
+ const hi = index < pts.length - 1 ? pts[index + 1].t : 1;
374
+ p.t = Math.min(Math.max(patch.t, lo), Math.max(hi, lo));
375
+ }
376
+ const l = clean(patch.left, VECTOR_WIDTH_PROFILE_MAX_SCALE);
377
+ const r = clean(patch.right, VECTOR_WIDTH_PROFILE_MAX_SCALE);
378
+ if (l !== null) p.left = l;
379
+ if (r !== null) p.right = r;
380
+ return pts;
381
+ }
382
+ function removeWidthPoints(points, indices) {
383
+ const drop = new Set(indices);
384
+ return points.filter((_, i) => !drop.has(i)).map((p) => ({ ...p }));
385
+ }
386
+ function ease(u, e) {
387
+ const x = Math.min(Math.max(u, 0), 1);
388
+ const round = Math.sqrt(Math.max(0, 1 - (1 - x) * (1 - x)));
389
+ return x + (round - x) * Math.min(Math.max(e, 0), 1);
390
+ }
391
+ function taperWaveFactor(t, taper, wave) {
392
+ let k = 1;
393
+ if (taper) {
394
+ let ls = Math.max(0, taper.startLength ?? 0);
395
+ let le = Math.max(0, taper.endLength ?? 0);
396
+ if (ls + le > 1) {
397
+ const f = 1 / (ls + le);
398
+ ls *= f;
399
+ le *= f;
400
+ }
401
+ const sw = Math.min(Math.max(taper.startWidth ?? 0, 0), 1);
402
+ const ew = Math.min(Math.max(taper.endWidth ?? 0, 0), 1);
403
+ if (ls > 0 && t < ls) k = Math.min(k, sw + (1 - sw) * ease(t / ls, taper.startEase ?? 0));
404
+ if (le > 0 && t > 1 - le) k = Math.min(k, ew + (1 - ew) * ease((1 - t) / le, taper.endEase ?? 0));
405
+ }
406
+ if (wave) {
407
+ const cycles = wave.cycles ?? 4;
408
+ const amount = Math.min(Math.max(wave.amount ?? 0.5, 0), 1);
409
+ if (cycles > 0 && amount > 0) {
410
+ const ph = 2 * Math.PI * t * cycles + (wave.phase ?? 0) * Math.PI / 180;
411
+ k *= 1 - amount * (0.5 - 0.5 * Math.cos(ph));
412
+ }
413
+ }
414
+ return k;
415
+ }
416
+ function generateWidthProfile(options) {
417
+ const cycles = options.wave ? Math.max(0, options.wave.cycles ?? 4) : 0;
418
+ const n = Math.max(8, Math.min(VECTOR_WIDTH_PROFILE_MAX_POINTS * 4, options.samples ?? Math.ceil(32 + cycles * 16)));
419
+ const raw = [];
420
+ for (let i = 0; i <= n; i++) raw.push([i / n, taperWaveFactor(i / n, options.taper, options.wave)]);
421
+ const ts = [options.taper?.startLength, options.taper?.endLength !== void 0 ? 1 - options.taper.endLength : void 0];
422
+ for (const t of ts) if (typeof t === "number" && t > 0 && t < 1) raw.push([t, taperWaveFactor(t, options.taper, options.wave)]);
423
+ raw.sort((a, b) => a[0] - b[0]);
424
+ const kept = simplifySamples(raw, options.tolerance ?? 0.01, VECTOR_WIDTH_PROFILE_MAX_POINTS);
425
+ return kept.map(([t, k]) => sym(t, k));
426
+ }
427
+ function simplifySamples(samples, tolerance, max) {
428
+ if (samples.length <= 2) return samples.slice();
429
+ const keep = new Array(samples.length).fill(false);
430
+ keep[0] = keep[samples.length - 1] = true;
431
+ const stack = [[0, samples.length - 1]];
432
+ while (stack.length) {
433
+ const [a, b] = stack.pop();
434
+ const [ta, va] = samples[a];
435
+ const [tb, vb] = samples[b];
436
+ let worst = -1;
437
+ let err = tolerance;
438
+ for (let i = a + 1; i < b; i++) {
439
+ const [t, v2] = samples[i];
440
+ const lin = tb - ta > 1e-12 ? va + (vb - va) * (t - ta) / (tb - ta) : va;
441
+ const e = Math.abs(v2 - lin);
442
+ if (e > err) {
443
+ err = e;
444
+ worst = i;
445
+ }
446
+ }
447
+ if (worst >= 0) {
448
+ keep[worst] = true;
449
+ stack.push([a, worst], [worst, b]);
450
+ }
451
+ }
452
+ let out = samples.filter((_, i) => keep[i]);
453
+ while (out.length > max) out = out.filter((_, i) => i === 0 || i === out.length - 1 || i % 2 === 0);
454
+ return out;
455
+ }
456
+ var VECTOR_PRESSURE_NOMINAL = 0.5;
457
+ function pressureWidthProfile(samples, options = {}) {
458
+ const raw = samples.filter((s) => Number.isFinite(s.t) && Number.isFinite(s.pressure)).map((s) => [Math.min(Math.max(s.t, 0), 1), Math.max(0.05, Math.min(1, Math.max(0, s.pressure)) / VECTOR_PRESSURE_NOMINAL)]);
459
+ if (raw.length < 2) return [];
460
+ if (raw.every((s) => Math.abs(s[1] - raw[0][1]) < 1e-6)) return [];
461
+ return simplifySamples(raw, options.tolerance ?? 0.02, VECTOR_WIDTH_PROFILE_MAX_POINTS).map(([t, k]) => sym(t, Math.round(k * 1e3) / 1e3));
462
+ }
463
+
464
+ // src/vector/varstroke.ts
465
+ var SAMPLES_PER_SUBPATH = 128;
466
+ var MAX_DASHES = 5e3;
467
+ var sub2 = (a, b) => [a[0] - b[0], a[1] - b[1]];
468
+ var along = (p, v2, k) => [p[0] + v2[0] * k, p[1] + v2[1] * k];
469
+ var lerp = (a, b, u) => [a[0] + (b[0] - a[0]) * u, a[1] + (b[1] - a[1]) * u];
470
+ var dist = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1]);
471
+ var unitOf = (v2) => {
472
+ const l = Math.hypot(v2[0], v2[1]);
473
+ return l > 0 ? [v2[0] / l, v2[1] / l] : [1, 0];
474
+ };
475
+ var leftNormal = (d) => [d[1], -d[0]];
476
+ function flatten(segments, closed, tol) {
477
+ const pts = [];
478
+ const corner = [];
479
+ const push = (p, isCorner) => {
480
+ const last = pts[pts.length - 1];
481
+ if (last && dist(last, p) <= 1e-9) {
482
+ corner[corner.length - 1] ||= isCorner;
483
+ return;
484
+ }
485
+ pts.push(p);
486
+ corner.push(isCorner);
487
+ };
488
+ for (const seg of segments) {
489
+ const flat = flattenChain([seg], tol);
490
+ if (dist(seg[0], seg[seg.length - 1]) <= 1e-12 && flat.length > 1) flat.push(seg[seg.length - 1]);
491
+ flat.forEach((p, i) => push(p, i === 0 || i === flat.length - 1));
492
+ }
493
+ if (closed && pts.length > 2 && dist(pts[0], pts[pts.length - 1]) > 1e-9) push(pts[0], true);
494
+ if (pts.length < 2) return null;
495
+ let total = 0;
496
+ for (let i = 1; i < pts.length; i++) total += dist(pts[i - 1], pts[i]);
497
+ if (!(total > 0)) return null;
498
+ const step = Math.max(total / SAMPLES_PER_SUBPATH, tol);
499
+ const fine = [pts[0]];
500
+ const fineCorner = [corner[0]];
501
+ for (let i = 1; i < pts.length; i++) {
502
+ const n = Math.max(1, Math.ceil(dist(pts[i - 1], pts[i]) / step));
503
+ for (let k = 1; k <= n; k++) {
504
+ fine.push(k === n ? pts[i] : lerp(pts[i - 1], pts[i], k / n));
505
+ fineCorner.push(k === n ? corner[i] : false);
506
+ }
507
+ }
508
+ const s = [0];
509
+ for (let i = 1; i < fine.length; i++) s.push(s[i - 1] + dist(fine[i - 1], fine[i]));
510
+ return { pts: fine, s, corner: fineCorner, closed };
511
+ }
512
+ function dashIntervals(pattern, offset, len2) {
513
+ let pat = pattern.map((v2) => Number.isFinite(v2) ? Math.max(0, v2) : 0);
514
+ if (pat.length % 2 === 1) pat = [...pat, ...pat];
515
+ const period = pat.reduce((a, b) => a + b, 0);
516
+ if (pat.length < 2 || !(period > 1e-9) || !(len2 > 0)) return [[0, len2]];
517
+ let pos = ((Number.isFinite(offset) ? offset : 0) % period + period) % period;
518
+ let idx = 0;
519
+ while (pos >= pat[idx]) {
520
+ pos -= pat[idx];
521
+ idx = (idx + 1) % pat.length;
522
+ }
523
+ const out = [];
524
+ let s = 0;
525
+ let guard = 0;
526
+ while (s < len2 && guard++ < MAX_DASHES * pat.length) {
527
+ const e = Math.min(s + pat[idx] - pos, len2);
528
+ if (idx % 2 === 0 && e >= s) out.push([s, e]);
529
+ s += pat[idx] - pos;
530
+ pos = 0;
531
+ idx = (idx + 1) % pat.length;
532
+ }
533
+ return out;
534
+ }
535
+ function at(poly, t) {
536
+ let i = 1;
537
+ while (i < poly.pts.length - 1 && poly.s[i] <= t) i++;
538
+ const a = poly.s[i - 1];
539
+ const b = poly.s[i];
540
+ const u = b > a ? Math.min(Math.max((t - a) / (b - a), 0), 1) : 0;
541
+ return [lerp(poly.pts[i - 1], poly.pts[i], u), i];
542
+ }
543
+ function arcPoints(c, a0, sweep, r0, r1, tol) {
544
+ const r = Math.max(r0, r1);
545
+ if (!(r > tol * 0.5) || !Number.isFinite(sweep)) return [];
546
+ const stepAngle = 2 * Math.acos(Math.min(Math.max(1 - tol / r, -1), 1));
547
+ const n = Math.min(128, Math.max(2, Math.ceil(Math.abs(sweep) / Math.max(stepAngle, 1e-3))));
548
+ const out = [];
549
+ for (let k = 1; k < n; k++) {
550
+ const f = k / n;
551
+ const a = a0 + sweep * f;
552
+ const rr = r0 + (r1 - r0) * f;
553
+ out.push([c[0] + Math.cos(a) * rr, c[1] + Math.sin(a) * rr]);
554
+ }
555
+ return out;
556
+ }
557
+ function crossing(a0, a1, b0, b1) {
558
+ const r = sub2(a1, a0);
559
+ const q = sub2(b1, b0);
560
+ const den = r[0] * q[1] - r[1] * q[0];
561
+ if (Math.abs(den) < 1e-12) return null;
562
+ const w = sub2(b0, a0);
563
+ const t = (w[0] * q[1] - w[1] * q[0]) / den;
564
+ const u = (w[0] * r[1] - w[1] * r[0]) / den;
565
+ return t > 1e-9 && t <= 1 && u >= 0 && u < 1 - 1e-9 ? [a0[0] + r[0] * t, a0[1] + r[1] * t] : null;
566
+ }
567
+ function variableStrokeRings(path, options) {
568
+ const half = options.width / 2;
569
+ if (!(half > 0) || !Number.isFinite(half)) return [];
570
+ const tol = options.tolerance && options.tolerance > 0 ? options.tolerance : 0.05;
571
+ const join = options.join ?? "miter";
572
+ const cap = options.cap ?? "butt";
573
+ const miterLimit = options.miterLimit && options.miterLimit >= 1 ? options.miterLimit : 4;
574
+ const rings = [];
575
+ const push = (out, p) => {
576
+ const last = out[out.length - 1];
577
+ if (!last || dist(last, p) > 1e-12) out.push(p);
578
+ };
579
+ for (const contour of pathToContours(path)) {
580
+ if (!contour.segments.length) continue;
581
+ const poly = flatten(contour.segments, contour.closed, Math.min(tol, half) / 4);
582
+ if (!poly) continue;
583
+ const len2 = poly.s[poly.s.length - 1];
584
+ const sides = (s) => {
585
+ const w = widthProfileAt(options.profile, s / len2);
586
+ return [w.left * half, w.right * half];
587
+ };
588
+ const intervals = options.dash?.some((v2) => v2 > 0) ? dashIntervals(options.dash, options.dashOffset ?? 0, len2) : [[0, len2]];
589
+ const whole = intervals.length === 1 && intervals[0][0] <= 0 && intervals[0][1] >= len2;
590
+ for (const [a, b] of intervals) {
591
+ const [pa, ia] = at(poly, a);
592
+ const [pb, ib] = at(poly, b);
593
+ const pts = [{ p: pa, s: a, corner: poly.corner[0] && a <= 0 }];
594
+ for (let i = ia; i < ib; i++) pts.push({ p: poly.pts[i], s: poly.s[i], corner: poly.corner[i] });
595
+ pts.push({ p: pb, s: b, corner: false });
596
+ const run = pts.filter((q, i) => i === 0 || dist(q.p, pts[i - 1].p) > 1e-9);
597
+ if (run.length < 2) {
598
+ const [l, r] = sides(a);
599
+ const w2 = (l + r) / 2;
600
+ if (cap === "round" && w2 > 0) rings.push([[pa[0] + w2, pa[1]], ...arcPoints(pa, 0, 2 * Math.PI, w2, w2, tol)]);
601
+ continue;
602
+ }
603
+ const closed = whole && poly.closed;
604
+ const m = run.length - 1;
605
+ const dirs = [];
606
+ for (let i = 0; i < m; i++) dirs.push(unitOf(sub2(run[i + 1].p, run[i].p)));
607
+ const normals = dirs.map(leftNormal);
608
+ const w = run.map((q) => sides(q.s));
609
+ const side = (sigma) => {
610
+ const k = sigma > 0 ? 0 : 1;
611
+ const S = (i) => along(run[i].p, normals[i], sigma * w[i][k]);
612
+ const E = (i) => along(run[i + 1].p, normals[i], sigma * w[i + 1][k]);
613
+ const out = [S(0)];
614
+ for (let i = 0; i < m; i++) {
615
+ const seam = i === m - 1;
616
+ if (seam && !closed) {
617
+ push(out, E(i));
618
+ break;
619
+ }
620
+ const j = seam ? 0 : i + 1;
621
+ const p = run[i + 1].p;
622
+ const d0 = dirs[i];
623
+ const d1 = dirs[j];
624
+ const cross2 = d0[0] * d1[1] - d0[1] * d1[0];
625
+ const dot2 = d0[0] * d1[0] + d0[1] * d1[1];
626
+ const r0 = w[i + 1][k];
627
+ const r1 = w[j][k];
628
+ const e = E(i);
629
+ const st = S(j);
630
+ if (Math.abs(cross2) < 1e-12 && dot2 > 0) {
631
+ push(out, e);
632
+ if (!seam) push(out, st);
633
+ continue;
634
+ }
635
+ const uturn = Math.abs(cross2) < 1e-12;
636
+ const outer = uturn ? sigma > 0 : cross2 * sigma > 0;
637
+ if (!outer) {
638
+ const x = seam ? null : crossing(out[out.length - 1], e, st, E(j));
639
+ if (x) push(out, x);
640
+ else {
641
+ push(out, e);
642
+ push(out, p);
643
+ if (!seam) push(out, st);
644
+ }
645
+ continue;
646
+ }
647
+ push(out, e);
648
+ const theta = uturn ? Math.PI : Math.atan2(cross2, dot2);
649
+ const r = (r0 + r1) / 2;
650
+ const kind = run[i + 1].corner || seam && poly.corner[0] ? join : "round";
651
+ if (r > 1e-9) {
652
+ if (kind === "round") {
653
+ if (r * (1 - Math.cos(Math.abs(theta) / 2)) > tol * 0.25) {
654
+ for (const q of arcPoints(p, Math.atan2(e[1] - p[1], e[0] - p[0]), theta, r0, r1, tol)) push(out, q);
655
+ }
656
+ } else if (kind === "miter") {
657
+ const halfCos = Math.cos(theta / 2);
658
+ const bis = [normals[i][0] + normals[j][0], normals[i][1] + normals[j][1]];
659
+ const bl = Math.hypot(bis[0], bis[1]);
660
+ if (halfCos > 1e-6 && 1 / halfCos <= miterLimit && bl > 1e-9) push(out, along(p, bis, sigma * r / halfCos / bl));
661
+ }
662
+ }
663
+ if (!seam) push(out, st);
664
+ }
665
+ if (closed && out.length > 1 && dist(out[0], out[out.length - 1]) <= 1e-12) out.pop();
666
+ return out;
667
+ };
668
+ const left = side(1);
669
+ const right = side(-1);
670
+ if (closed) {
671
+ rings.push(left, right.reverse());
672
+ continue;
673
+ }
674
+ const capPoints = (atStart) => {
675
+ if (cap === "butt") return [];
676
+ const i = atStart ? 0 : m;
677
+ const d = atStart ? dirs[0] : dirs[m - 1];
678
+ const [l, r] = w[i];
679
+ if (!(l + r > 1e-9)) return [];
680
+ const p = run[i].p;
681
+ const n = leftNormal(d);
682
+ const L = along(p, n, l);
683
+ const R = along(p, n, -r);
684
+ if (cap === "round") {
685
+ const a0 = Math.atan2(d[1], d[0]) + (atStart ? Math.PI / 2 : -Math.PI / 2);
686
+ return arcPoints(p, a0, Math.PI, atStart ? r : l, atStart ? l : r, tol);
687
+ }
688
+ const ext = (l + r) / 2;
689
+ const o = atStart ? [-d[0], -d[1]] : d;
690
+ return atStart ? [along(R, o, ext), along(L, o, ext)] : [along(L, o, ext), along(R, o, ext)];
691
+ };
692
+ const ring = [];
693
+ for (const q of [...left, ...capPoints(false), ...right.reverse(), ...capPoints(true)]) push(ring, q);
694
+ if (ring.length > 2) rings.push(ring);
695
+ }
696
+ }
697
+ return rings;
698
+ }
699
+ function variableStrokeOutline(path, options) {
700
+ const pieces = variableStrokeRings(path, options);
701
+ if (!pieces.length) return [];
702
+ const tol = options.tolerance && options.tolerance > 0 ? options.tolerance : 0.05;
703
+ let polygons;
704
+ try {
705
+ polygons = polygonBoolean("union", [{ rings: pieces, fillRule: "nonzero" }]);
706
+ } catch {
707
+ return [];
708
+ }
709
+ const out = [];
710
+ for (const polygon2 of polygons) {
711
+ for (const ring of polygon2) {
712
+ if (ring.length < 3) continue;
713
+ const cubics = fitCubics(ring, tol, { closed: true, cornerAngleDeg: 25 });
714
+ if (!cubics.length) continue;
715
+ out.push({ type: "M", x: cubics[0].p0[0], y: cubics[0].p0[1] });
716
+ cubics.forEach((c, i) => {
717
+ if (isStraightCubic(c, tol * 0.02)) {
718
+ if (i < cubics.length - 1) out.push({ type: "L", x: c.p3[0], y: c.p3[1] });
719
+ } else out.push({ type: "C", x1: c.p1[0], y1: c.p1[1], x2: c.p2[0], y2: c.p2[1], x: c.p3[0], y: c.p3[1] });
720
+ });
721
+ out.push({ type: "Z" });
722
+ }
723
+ }
724
+ return out;
725
+ }
726
+ function nearestStrokePosition(path, p, only) {
727
+ let best = null;
728
+ pathToContours(path).forEach((c, ci) => {
729
+ if (!c.segments.length || only !== void 0 && ci !== only) return;
730
+ const lengths = c.segments.map((s) => segmentLength(s));
731
+ const total = lengths.reduce((a, b) => a + b, 0);
732
+ if (!(total > 0)) return;
733
+ let acc = 0;
734
+ c.segments.forEach((seg, si) => {
735
+ const near2 = segmentNearest(seg, p);
736
+ if (!best || near2.distance < best.distance) {
737
+ const s = acc + segmentLength(seg, 0, near2.t);
738
+ const tan = segmentTangent(seg, near2.t);
739
+ const pt2 = seg.length === 2 ? lerp(seg[0], seg[1], near2.t) : evalSeg(seg, near2.t);
740
+ const n = leftNormal([tan[0], tan[1]]);
741
+ const side = (p[0] - pt2[0]) * n[0] + (p[1] - pt2[1]) * n[1] >= 0 ? "left" : "right";
742
+ best = { contour: ci, t: Math.min(Math.max(s / total, 0), 1), distance: near2.distance, side, point: pt2, tangent: [tan[0], tan[1]], length: total };
743
+ }
744
+ acc += lengths[si];
745
+ });
746
+ });
747
+ return best;
748
+ }
749
+ function evalSeg(seg, t) {
750
+ let pts = seg.map((q) => [q[0], q[1]]);
751
+ while (pts.length > 1) pts = pts.slice(1).map((q, i) => lerp(pts[i], q, t));
752
+ return pts[0];
753
+ }
754
+ function strokePositionPoint(path, index, t) {
755
+ const c = pathToContours(path)[index];
756
+ if (!c?.segments.length) return null;
757
+ const lengths = c.segments.map((s) => segmentLength(s));
758
+ const total = lengths.reduce((a, b) => a + b, 0);
759
+ let want = Math.min(Math.max(t, 0), 1) * total;
760
+ for (let i = 0; i < c.segments.length; i++) {
761
+ const seg = c.segments[i];
762
+ if (want <= lengths[i] + 1e-12 || i === c.segments.length - 1) {
763
+ let lo = 0;
764
+ let hi = 1;
765
+ const target = Math.min(want, lengths[i]);
766
+ for (let k = 0; k < 40; k++) {
767
+ const mid = (lo + hi) / 2;
768
+ if (segmentLength(seg, 0, mid) < target) lo = mid;
769
+ else hi = mid;
770
+ }
771
+ const u = (lo + hi) / 2;
772
+ const tan = segmentTangent(seg, u);
773
+ return { point: evalSeg(seg, u), tangent: [tan[0], tan[1]] };
774
+ }
775
+ want -= lengths[i];
776
+ }
777
+ return null;
778
+ }
779
+
248
780
  // src/vector/stroke.ts
249
781
  var KAPPA = 0.5522847498307936;
250
782
  var NOTCH = 0.7;
@@ -256,8 +788,21 @@ function effectiveStrokeAlign(style, closedShape) {
256
788
  const a = style.strokeAlign ?? "center";
257
789
  return closedShape ? a : "center";
258
790
  }
791
+ function strokeWidthProfileOf(style) {
792
+ if (!style.strokeWidthProfile?.length) return null;
793
+ const p = sanitizeWidthProfile(style.strokeWidthProfile);
794
+ return isUniformWidthProfile(p) ? null : p;
795
+ }
259
796
  function hasStrokeExtras(style) {
260
- return (style.strokeAlign ?? "center") !== "center" || !!style.startArrow || !!style.endArrow;
797
+ return (style.strokeAlign ?? "center") !== "center" || !!style.startArrow || !!style.endArrow || !!strokeWidthProfileOf(style);
798
+ }
799
+ function vectorStrokeLineArea(parts, style, tolerance) {
800
+ const profile = strokeWidthProfileOf(style);
801
+ const dash = style.strokeDasharray?.length ? { dash: style.strokeDasharray } : {};
802
+ const join = style.strokeLinejoin ?? "miter";
803
+ const cap = style.strokeLinecap ?? "butt";
804
+ if (profile) return variableStrokeOutline(parts.line, { width: parts.drawWidth, profile, join, cap, ...dash, ...tolerance ? { tolerance } : {} });
805
+ return outlineStroke(parts.line, { width: parts.drawWidth, join, cap, ...dash, ...tolerance ? { tolerance } : {} });
261
806
  }
262
807
  var capExtent = (style, width) => style.strokeLinecap === "round" || style.strokeLinecap === "square" ? width / 2 : 0;
263
808
  function arrowheadWeight(style, end) {
@@ -347,13 +892,13 @@ function endOf(segs, atStart) {
347
892
  const last = segs[segs.length - 1];
348
893
  return { p: toVec(last[last.length - 1]), out: v(t[0], t[1]) };
349
894
  }
350
- function pointAlong(segs, fromStart, dist3) {
895
+ function pointAlong(segs, fromStart, dist4) {
351
896
  const order = fromStart ? segs : [...segs].reverse();
352
897
  let acc = 0;
353
898
  for (const s of order) {
354
899
  const l = segLen(s);
355
- if (acc + l >= dist3) {
356
- const want = fromStart ? dist3 - acc : l - (dist3 - acc);
900
+ if (acc + l >= dist4) {
901
+ const want = fromStart ? dist4 - acc : l - (dist4 - acc);
357
902
  const pts = trimSegments([s], 0, want);
358
903
  const p = pts.length ? pts[0][pts[0].length - 1] : s[0];
359
904
  return toVec(p);
@@ -410,12 +955,7 @@ function vectorStrokeParts(path, style) {
410
955
  }
411
956
  function vectorStrokeOutlinePath(path, style) {
412
957
  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
- });
958
+ const stroke = vectorStrokeLineArea(parts, style);
419
959
  const fill = { path, fillRule: style.fillRule ?? "nonzero" };
420
960
  let area = stroke;
421
961
  if (parts.align === "inside") area = pathBoolean("intersection", [stroke, fill]).path;
@@ -431,15 +971,32 @@ function hitTestVectorStroke(path, p, style, tolerance = 0) {
431
971
  if (pointInBezierPath(h.path, point) || tolerance > 0 && distanceToBezierPath(h.path, point) <= tolerance) return true;
432
972
  }
433
973
  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;
974
+ const profile = strokeWidthProfileOf(style);
975
+ let d;
976
+ let reach = width / 2;
977
+ if (profile) {
978
+ const near2 = nearestStrokePosition(parts.line, point);
979
+ if (!near2) return false;
980
+ d = near2.distance;
981
+ const w = widthProfileAt(profile, near2.t);
982
+ reach = width / 2 * (near2.side === "left" ? w.left : w.right);
983
+ if (parts.align !== "center") reach *= 2;
984
+ const atEnd = near2.t <= 1e-9 || near2.t >= 1 - 1e-9;
985
+ if (atEnd && (style.strokeLinecap === "round" || style.strokeLinecap === "square") && d > reach) {
986
+ reach = Math.max(reach, width / 2 * (w.left + w.right) / 2);
987
+ }
988
+ } else {
989
+ d = distanceToBezierPath(parts.line, point);
990
+ if (parts.align !== "center") reach = width;
991
+ }
992
+ if (parts.align === "center") return d <= reach + tolerance;
993
+ if (d > reach + tolerance) return false;
437
994
  if (d <= tolerance) return true;
438
995
  const inside = pointInBezierPath(path, point, style.fillRule ?? "nonzero");
439
996
  return parts.align === "inside" ? inside : !inside;
440
997
  }
441
998
  function vectorStrokeReach(style) {
442
- const width = style.strokeWidth ?? 1;
999
+ const width = (style.strokeWidth ?? 1) * widthProfileMaxScale(strokeWidthProfileOf(style));
443
1000
  const align = style.strokeAlign ?? "center";
444
1001
  let reach = align === "center" ? width / 2 : align === "outside" ? width : 0;
445
1002
  if (style.strokeLinejoin !== "round" && style.strokeLinejoin !== "bevel") reach *= 4;
@@ -749,8 +1306,10 @@ function bakeIntoGeometry(node, m) {
749
1306
  if (isIdentityMatrix(m)) return node;
750
1307
  if (node.type === "group") return { ...node, children: node.children.map((c) => transformVectorNode(c, m)) };
751
1308
  const path = node;
1309
+ const mirrored = m[0] * m[3] - m[1] * m[2] < 0 && path.strokeWidthProfile?.length;
752
1310
  return {
753
1311
  ...path,
1312
+ ...mirrored ? { strokeWidthProfile: mirrorWidthProfile(path.strokeWidthProfile) } : {},
754
1313
  subpaths: path.subpaths.map((sp) => ({
755
1314
  closed: sp.closed,
756
1315
  anchors: sp.anchors.map((a) => {
@@ -980,11 +1539,20 @@ function vectorPaintToSvg(paint, gradientId) {
980
1539
  if (typeof paint === "string") return { value: paint, gradient: null };
981
1540
  return { value: `url(#${gradientId})`, gradient: gradientToSvgDef(paint, gradientId) };
982
1541
  }
1542
+ var partsCache = /* @__PURE__ */ new WeakMap();
983
1543
  function vectorStrokeSvgParts(node, clipId, precision = 3) {
984
1544
  if (!hasPaint(node.stroke) || !hasStrokeExtras(node)) return void 0;
1545
+ const hit = partsCache.get(node);
1546
+ if (hit && hit.clipId === clipId && hit.precision === precision) return hit.parts;
1547
+ const parts = computeStrokeSvgParts(node, clipId, precision);
1548
+ partsCache.set(node, { clipId, precision, parts });
1549
+ return parts;
1550
+ }
1551
+ function computeStrokeSvgParts(node, clipId, precision) {
985
1552
  const path = subpathsToBezier(node.subpaths);
986
1553
  const parts = vectorStrokeParts(path, node);
987
- if (parts.align === "center" && !parts.heads.length) return void 0;
1554
+ const profiled = !!strokeWidthProfileOf(node);
1555
+ if (parts.align === "center" && !parts.heads.length && !profiled) return void 0;
988
1556
  let clip = null;
989
1557
  if (parts.align !== "center") {
990
1558
  const b = bezierPathBounds(path);
@@ -992,7 +1560,26 @@ function vectorStrokeSvgParts(node, clipId, precision = 3) {
992
1560
  const bounds = b ? { x: round2(b.minX - pad), y: round2(b.minY - pad), width: round2(b.maxX - b.minX + 2 * pad), height: round2(b.maxY - b.minY + 2 * pad) } : { x: 0, y: 0, width: 0, height: 0 };
993
1561
  clip = { kind: parts.align === "inside" ? "clip" : "mask", id: clipId, bounds };
994
1562
  }
995
- return { align: parts.align, d: bezierPathToSvg(parts.line, { precision }), width: parts.drawWidth, clip, heads: parts.heads.map((h) => bezierPathToSvg(h.path, { precision })) };
1563
+ return {
1564
+ align: parts.align,
1565
+ d: bezierPathToSvg(parts.line, { precision }),
1566
+ width: parts.drawWidth,
1567
+ ...profiled && (node.strokeWidth ?? 1) > 0 ? { outline: bezierPathToSvg(vectorStrokeLineArea(parts, node), { precision }) } : {},
1568
+ clip,
1569
+ heads: parts.heads.map((h) => bezierPathToSvg(h.path, { precision }))
1570
+ };
1571
+ }
1572
+ function widthProfileToAttribute(points, precision = 4) {
1573
+ return points.map((p) => [p.t, p.left, p.right].map((n) => formatNumber(n, precision)).join(",")).join(";");
1574
+ }
1575
+ function widthProfileFromAttribute(value) {
1576
+ if (!value) return [];
1577
+ return sanitizeWidthProfile(
1578
+ value.split(";").map((part) => {
1579
+ const [t, left, right] = part.split(",").map((n) => parseFloat(n));
1580
+ return { t, left, right: Number.isFinite(right) ? right : left };
1581
+ })
1582
+ );
996
1583
  }
997
1584
  var round2 = (n) => Math.round(n * 100) / 100;
998
1585
  function gradientMarkup(def) {
@@ -1051,6 +1638,13 @@ function vectorDocumentToSvg(doc, options = {}) {
1051
1638
  if (node.arrowAlign && node.arrowAlign !== "extend") data += ` data-vector-arrow-align="${node.arrowAlign}"`;
1052
1639
  if (node.startArrowScale !== void 0 && node.startArrowScale !== 1) data += ` data-vector-start-arrow-scale="${num(node.startArrowScale)}"`;
1053
1640
  if (node.endArrowScale !== void 0 && node.endArrowScale !== 1) data += ` data-vector-end-arrow-scale="${num(node.endArrowScale)}"`;
1641
+ const profile = strokeWidthProfileOf(node);
1642
+ if (profile) {
1643
+ data += ` data-vector-width-profile="${widthProfileToAttribute(profile)}"`;
1644
+ if (node.strokeLinecap && node.strokeLinecap !== "butt") data += ` data-vector-stroke-linecap="${node.strokeLinecap}"`;
1645
+ if (node.strokeLinejoin && node.strokeLinejoin !== "miter") data += ` data-vector-stroke-linejoin="${node.strokeLinejoin}"`;
1646
+ if (node.strokeDasharray?.length) data += ` data-vector-stroke-dasharray="${node.strokeDasharray.map(num).join(" ")}"`;
1647
+ }
1054
1648
  lines.push(`${indent(depth)}<g${common(node)}${data}>`);
1055
1649
  lines.push(`${indent(depth + 1)}<path data-vector-part="fill" d="${d}"${fillAttrs}${live}/>`);
1056
1650
  if (options.strokes === "outline") {
@@ -1071,7 +1665,8 @@ function vectorDocumentToSvg(doc, options = {}) {
1071
1665
  clip = ` mask="url(#${id})"`;
1072
1666
  }
1073
1667
  }
1074
- lines.push(`${indent(depth + 1)}<path data-vector-part="stroke" d="${parts.d}" fill="none"${lineAttrs(parts.width)}${clip}/>`);
1668
+ if (parts.outline !== void 0) lines.push(`${indent(depth + 1)}<path data-vector-part="stroke-outline" d="${parts.outline}" fill="${escapeAttr(stroke.value)}"${clip}/>`);
1669
+ else lines.push(`${indent(depth + 1)}<path data-vector-part="stroke" d="${parts.d}" fill="none"${lineAttrs(parts.width)}${clip}/>`);
1075
1670
  for (const h2 of parts.heads) lines.push(`${indent(depth + 1)}<path data-vector-part="head" d="${h2}" fill="${escapeAttr(stroke.value)}"/>`);
1076
1671
  }
1077
1672
  lines.push(`${indent(depth)}</g>`);
@@ -1334,6 +1929,11 @@ function parseVectorSvg(svg, options = {}) {
1334
1929
  return n > 0 ? n : void 0;
1335
1930
  };
1336
1931
  const align = a["data-vector-stroke-align"];
1932
+ const profile = widthProfileFromAttribute(a["data-vector-width-profile"]);
1933
+ const kept = (key, values) => values.includes(a[key]) ? a[key] : void 0;
1934
+ const keptCap = kept("data-vector-stroke-linecap", ["round", "square"]);
1935
+ const keptJoin = kept("data-vector-stroke-linejoin", ["round", "bevel"]);
1936
+ const keptDash = a["data-vector-stroke-dasharray"] ? numberList(a["data-vector-stroke-dasharray"]) : void 0;
1337
1937
  const node = {
1338
1938
  id: idFor(g),
1339
1939
  type: "path",
@@ -1349,7 +1949,11 @@ function parseVectorSvg(svg, options = {}) {
1349
1949
  ...arrow(a["data-vector-end-arrow"]) ? { endArrow: arrow(a["data-vector-end-arrow"]) } : {},
1350
1950
  ...a["data-vector-arrow-align"] === "tip" ? { arrowAlign: "tip" } : {},
1351
1951
  ...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"]) } : {}
1952
+ ...scale(a["data-vector-end-arrow-scale"]) ? { endArrowScale: scale(a["data-vector-end-arrow-scale"]) } : {},
1953
+ ...profile.length ? { strokeWidthProfile: profile } : {},
1954
+ ...keptCap ? { strokeLinecap: keptCap } : {},
1955
+ ...keptJoin ? { strokeLinejoin: keptJoin } : {},
1956
+ ...keptDash?.length ? { strokeDasharray: keptDash } : {}
1353
1957
  };
1354
1958
  return placed(node, m);
1355
1959
  };
@@ -1669,7 +2273,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
1669
2273
  */
1670
2274
  snapFrom(p, leave, tol) {
1671
2275
  const none = (q) => ({ point: q, kind: null, x: false, y: false, lines: [], labels: [] });
1672
- const at = (q, kind, lines2 = []) => ({
2276
+ const at2 = (q, kind, lines2 = []) => ({
1673
2277
  point: q,
1674
2278
  kind,
1675
2279
  x: true,
@@ -1683,16 +2287,16 @@ var VectorSnapTargets = class _VectorSnapTargets {
1683
2287
  return hit ? { point: hit.point, x: true, y: true, ...this.linedUp(hit.point, hit.from) } : none(q);
1684
2288
  }
1685
2289
  const point = this.nearestPoint(p, tol);
1686
- if (point) return at(point.p, point.kind);
2290
+ if (point) return at2(point.p, point.kind);
1687
2291
  const near2 = this.segmentsNear(p, tol);
1688
- const crossing = this.crossing(near2, p, tol);
1689
- if (crossing) return at(crossing, "intersect");
2292
+ const crossing2 = this.crossing(near2, p, tol);
2293
+ if (crossing2) return at2(crossing2, "intersect");
1690
2294
  if (leave && leave.curves !== false) {
1691
2295
  const foot = this.curvePoint(near2, leave.at, p, tol);
1692
- if (foot) return at(foot.point, foot.kind, [line("construction", leave.at, foot.point)]);
2296
+ if (foot) return at2(foot.point, foot.kind, [line("construction", leave.at, foot.point)]);
1693
2297
  }
1694
2298
  const on = this.onSegment(near2);
1695
- if (on) return at(on, "path");
2299
+ if (on) return at2(on, "path");
1696
2300
  if (leave?.angles?.length) {
1697
2301
  const c = construction(leave, p, tol);
1698
2302
  if (c) {
@@ -1739,7 +2343,7 @@ var VectorSnapTargets = class _VectorSnapTargets {
1739
2343
  * the nearest target line crosses it, at most twice `tol` away: where it lands and the
1740
2344
  * target point that line comes from.
1741
2345
  */
1742
- slide(at, dir, q, tol, ray) {
2346
+ slide(at2, dir, q, tol, ray) {
1743
2347
  const len2 = dir.x * dir.x + dir.y * dir.y;
1744
2348
  if (!Number.isFinite(len2) || len2 <= 1e-18) return null;
1745
2349
  const reach = 2 * tol;
@@ -1747,10 +2351,10 @@ var VectorSnapTargets = class _VectorSnapTargets {
1747
2351
  for (const vertical of [true, false]) {
1748
2352
  const d = vertical ? dir.x : dir.y;
1749
2353
  if (Math.abs(d) <= 1e-9) continue;
1750
- const a = vertical ? at.x : at.y;
2354
+ const a = vertical ? at2.x : at2.y;
1751
2355
  for (const l of linesNear(vertical ? this.xs : this.ys, vertical ? q.x : q.y, reach)) {
1752
2356
  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 };
2357
+ const hit = vertical ? { x: l.v, y: at2.y + dir.y * s } : { x: at2.x + dir.x * s, y: l.v };
1754
2358
  const moved = distance(hit, q);
1755
2359
  if (moved <= reach && (!ray || s > 0) && (!best || moved < best.moved)) best = { moved, point: hit, from: l.from };
1756
2360
  }
@@ -1898,11 +2502,11 @@ var VectorSnapTargets = class _VectorSnapTargets {
1898
2502
  for (const s of starts) if (Math.abs(s.start - r0) <= tol && (!pick || Math.abs(s.start - r0) < Math.abs(pick.start - r0))) pick = s;
1899
2503
  if (!pick) return null;
1900
2504
  const { start, repeats } = pick;
1901
- const at = horizontal ? { ...r, x: start } : { ...r, y: start };
2505
+ const at2 = horizontal ? { ...r, x: start } : { ...r, y: start };
1902
2506
  const end = start + len2;
1903
2507
  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)]);
2508
+ if (before && (repeats === null || Math.abs(start - before.a1 - repeats) <= EQUAL)) gaps.push([before.a1, start, mid(before.b, at2)]);
2509
+ if (after && (repeats === null || Math.abs(after.a0 - end - repeats) <= EQUAL)) gaps.push([end, after.a0, mid(after.b, at2)]);
1906
2510
  if (repeats !== null) {
1907
2511
  for (const p of pairs) if (Math.abs(p.g - repeats) <= EQUAL) gaps.push(p.gap);
1908
2512
  }
@@ -2331,9 +2935,9 @@ function curvatureMoveAnchor(sp, index, q) {
2331
2935
  anchors[index] = moved;
2332
2936
  return { anchors, closed: sp.closed };
2333
2937
  }
2334
- function cutSubpathAt(sp, at) {
2938
+ function cutSubpathAt(sp, at2) {
2335
2939
  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);
2940
+ const cuts = [...new Set(at2)].filter((i) => Number.isInteger(i) && i >= 0 && i < n && (sp.closed || i > 0 && i < n - 1)).sort((a, b) => a - b);
2337
2941
  if (!cuts.length || n < 2) return [{ anchors: sp.anchors.map(cloneAnchor), closed: sp.closed }];
2338
2942
  const first = cuts[0];
2339
2943
  const run = sp.closed ? Array.from({ length: n + 1 }, (_, i) => sp.anchors[(first + i) % n]) : sp.anchors;
@@ -2364,7 +2968,7 @@ function splitSubpathAt(sp, segment, t, endTolerance = 1e-6) {
2364
2968
  return cutSubpathAt(subpath, [index]);
2365
2969
  }
2366
2970
  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);
2971
+ var dist2 = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
2368
2972
  function reverse(sp) {
2369
2973
  return {
2370
2974
  closed: sp.closed,
@@ -2391,7 +2995,7 @@ function joinSubpaths(subpaths, tolerance) {
2391
2995
  const sp = open[0];
2392
2996
  const first = sp.anchors[0];
2393
2997
  const last = sp.anchors[sp.anchors.length - 1];
2394
- if (sp.anchors.length > 2 && dist(first, last) <= tolerance) {
2998
+ if (sp.anchors.length > 2 && dist2(first, last) <= tolerance) {
2395
2999
  sp.anchors.pop();
2396
3000
  sp.anchors[0] = tidy({ ...first, in: last.in ?? null }, false);
2397
3001
  } else {
@@ -2407,7 +3011,7 @@ function joinSubpaths(subpaths, tolerance) {
2407
3011
  for (let j = i + 1; j < open.length; j++) {
2408
3012
  for (const ei of [false, true]) {
2409
3013
  for (const ej of [false, true]) {
2410
- const d = dist(endOf2(open[i], ei), endOf2(open[j], ej));
3014
+ const d = dist2(endOf2(open[i], ei), endOf2(open[j], ej));
2411
3015
  if (d < best.d) best = { d, i, j, ei, ej };
2412
3016
  }
2413
3017
  }
@@ -2447,8 +3051,8 @@ function smoothSubpath(sp, amount = 1, only) {
2447
3051
  const p = orig[i];
2448
3052
  const dir = unit2(next.x - prev.x, next.y - prev.y);
2449
3053
  if (!dir) continue;
2450
- const li = dist(p, prev) / 3;
2451
- const lo = dist(p, next) / 3;
3054
+ const li = dist2(p, prev) / 3;
3055
+ const lo = dist2(p, next) / 3;
2452
3056
  const hin = p.in ?? p;
2453
3057
  const hout = p.out ?? p;
2454
3058
  const target = (h, x, y) => ({ x: h.x + (x - h.x) * t, y: h.y + (y - h.y) * t });
@@ -2475,12 +3079,12 @@ function segsOf(sp) {
2475
3079
  function segsToSubpath(segs, closed, types) {
2476
3080
  if (!segs.length) return null;
2477
3081
  const anchors = [];
2478
- const at = (p) => ({ x: p[0], y: p[1] });
2479
- anchors.push(at(segs[0].c[0]));
3082
+ const at2 = (p) => ({ x: p[0], y: p[1] });
3083
+ anchors.push(at2(segs[0].c[0]));
2480
3084
  for (const s of segs) {
2481
3085
  const end = s.c[s.c.length - 1];
2482
3086
  if (!s.line) anchors[anchors.length - 1].out = { x: s.c[1][0], y: s.c[1][1] };
2483
- const next = at(end);
3087
+ const next = at2(end);
2484
3088
  if (!s.line) next.in = { x: s.c[2][0], y: s.c[2][1] };
2485
3089
  anchors.push(next);
2486
3090
  }
@@ -2506,8 +3110,8 @@ function mergePair(a, b, tol) {
2506
3110
  if (len2 < 1e-12) return null;
2507
3111
  const mx = a3[0] - a0[0];
2508
3112
  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;
3113
+ const along2 = (mx * cx + my * cy) / len2;
3114
+ return Math.abs((mx * cy - my * cx) / len2) <= tol && along2 >= 0 && along2 <= len2 ? { c: [a0, b3], line: true } : null;
2511
3115
  }
2512
3116
  const ca = a.line ? [a0, a0, a3, a3] : a.c;
2513
3117
  const cb = b.line ? [a3, a3, b3, b3] : b.c;
@@ -2939,7 +3543,7 @@ function vectorInkShapeName(shape) {
2939
3543
  return shape.points.length === 3 ? "triangle" : "polygon";
2940
3544
  }
2941
3545
  }
2942
- var dist2 = (a, b) => Math.hypot(b[0] - a[0], b[1] - a[1]);
3546
+ var dist3 = (a, b) => Math.hypot(b[0] - a[0], b[1] - a[1]);
2943
3547
  function bbox(pts) {
2944
3548
  if (!pts.length) return [0, 0, 0, 0];
2945
3549
  let [x0, y0] = pts[0];
@@ -2957,7 +3561,7 @@ function segDistance(a, b, p) {
2957
3561
  const dy = b[1] - a[1];
2958
3562
  const len2 = dx * dx + dy * dy;
2959
3563
  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);
3564
+ return dist3([a[0] + t * dx, a[1] + t * dy], p);
2961
3565
  }
2962
3566
  function polygonDistance(c, p) {
2963
3567
  let best = Infinity;
@@ -3019,7 +3623,7 @@ function corners(p, diag) {
3019
3623
  let far = 0;
3020
3624
  let farD = 0;
3021
3625
  p.forEach((q, i) => {
3022
- const d = dist2(start, q);
3626
+ const d = dist3(start, q);
3023
3627
  if (d > farD) {
3024
3628
  farD = d;
3025
3629
  far = i;
@@ -3046,7 +3650,7 @@ function corners(p, diag) {
3046
3650
  }
3047
3651
  let short = -1;
3048
3652
  for (let j = 0; j < n; j++) {
3049
- if (dist2(ring[j], ring[(j + 1) % n]) < 0.1 * diag) {
3653
+ if (dist3(ring[j], ring[(j + 1) % n]) < 0.1 * diag) {
3050
3654
  short = j;
3051
3655
  break;
3052
3656
  }
@@ -3086,8 +3690,8 @@ function recognizeInkShape(points, options = {}) {
3086
3690
  const diag = Math.hypot(w, h);
3087
3691
  if (size < minSize) return null;
3088
3692
  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);
3693
+ for (let i = 1; i < p.length; i++) length2 += dist3(p[i - 1], p[i]);
3694
+ const gap = dist3(first, last);
3091
3695
  if (gap > 0.85 * length2 && gap >= minSize) {
3092
3696
  const off = Math.max(...p.map((q) => segDistance(first, last, q)));
3093
3697
  if (off <= Math.max(0.06 * gap, 1.5 * minSize / 6)) return { kind: "line", a: first, b: last };
@@ -3113,7 +3717,14 @@ function recognizeInkShape(points, options = {}) {
3113
3717
 
3114
3718
  // src/vector/controller.ts
3115
3719
  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 };
3720
+ var DEFAULT_VECTOR_PATH_EDIT = {
3721
+ keepShapeOnDelete: true,
3722
+ curvatureMoves: false,
3723
+ pencilSmoothing: 0,
3724
+ recognizeShapes: false,
3725
+ eraserSize: 10,
3726
+ pencilPressure: "pen"
3727
+ };
3117
3728
  var VECTOR_PENCIL_STRING_PX = 100;
3118
3729
  var VECTOR_STYLE_KEYS = [
3119
3730
  "fill",
@@ -3128,7 +3739,8 @@ var VECTOR_STYLE_KEYS = [
3128
3739
  "endArrow",
3129
3740
  "arrowAlign",
3130
3741
  "startArrowScale",
3131
- "endArrowScale"
3742
+ "endArrowScale",
3743
+ "strokeWidthProfile"
3132
3744
  ];
3133
3745
  var VECTOR_CLIPBOARD_TYPE = "pixerate/vector";
3134
3746
  var DEFAULT_DEFAULTS = {
@@ -3190,9 +3802,9 @@ function getVisibleHandles(state) {
3190
3802
  return out;
3191
3803
  }
3192
3804
  if (state.tool !== "direct") return out;
3193
- const editable = new Set(getEditablePathIds(state));
3805
+ const editable2 = new Set(getEditablePathIds(state));
3194
3806
  for (const ref of state.selectedAnchors) {
3195
- if (!editable.has(ref.nodeId)) continue;
3807
+ if (!editable2.has(ref.nodeId)) continue;
3196
3808
  const node = findVectorNode(state.document.nodes, ref.nodeId);
3197
3809
  const sp = node.subpaths[ref.subpath];
3198
3810
  if (!sp) continue;
@@ -3229,6 +3841,56 @@ function getVectorCornerWidgets(state) {
3229
3841
  }
3230
3842
  return out;
3231
3843
  }
3844
+ function vectorStrokeWidthGeometry(node) {
3845
+ if (!hasPaint(node.stroke) || !((node.strokeWidth ?? 1) > 0)) return null;
3846
+ const parts = vectorStrokeParts(subpathsToBezier(node.subpaths), node);
3847
+ return parts.line.length ? { line: parts.line, unit: parts.drawWidth / 2 } : null;
3848
+ }
3849
+ function getVectorWidthHandles(state) {
3850
+ if (state.tool !== "width") return [];
3851
+ const ids = /* @__PURE__ */ new Set();
3852
+ for (const id of state.selection) {
3853
+ const n = findVectorNode(state.document.nodes, id);
3854
+ if (n) for (const p of collectPathNodes(n)) ids.add(p.id);
3855
+ }
3856
+ if (state.hover?.nodeId) ids.add(state.hover.nodeId);
3857
+ if (state.interaction?.type === "width") ids.add(state.interaction.ref.nodeId);
3858
+ if (state.selectedWidthPoint) ids.add(state.selectedWidthPoint.nodeId);
3859
+ const out = [];
3860
+ for (const id of ids) {
3861
+ const node = findVectorNode(state.document.nodes, id);
3862
+ if (node?.type !== "path" || node.visible === false || !node.strokeWidthProfile?.length) continue;
3863
+ const g = vectorStrokeWidthGeometry(node);
3864
+ if (!g) continue;
3865
+ const profile = sanitizeWidthProfile(node.strokeWidthProfile);
3866
+ const m = vectorWorldMatrix(state.document.nodes, id) ?? IDENTITY_MATRIX;
3867
+ const contours = node.subpaths.length;
3868
+ for (let sp = 0; sp < contours; sp++) {
3869
+ profile.forEach((w, index) => {
3870
+ const at2 = strokePositionPoint(g.line, sp, w.t);
3871
+ if (!at2) return;
3872
+ const n = { x: at2.tangent[1], y: -at2.tangent[0] };
3873
+ const c = { x: at2.point[0], y: at2.point[1] };
3874
+ const p = applyMatrix(m, c);
3875
+ const l = applyMatrix(m, { x: c.x + n.x * w.left * g.unit, y: c.y + n.y * w.left * g.unit });
3876
+ const r = applyMatrix(m, { x: c.x - n.x * w.right * g.unit, y: c.y - n.y * w.right * g.unit });
3877
+ const sel = state.selectedWidthPoint;
3878
+ out.push({
3879
+ ref: { nodeId: id, index, subpath: sp },
3880
+ x: p.x,
3881
+ y: p.y,
3882
+ left: l,
3883
+ right: r,
3884
+ t: w.t,
3885
+ leftScale: w.left,
3886
+ rightScale: w.right,
3887
+ selected: !!sel && sel.nodeId === id && sel.index === index
3888
+ });
3889
+ });
3890
+ }
3891
+ }
3892
+ return out;
3893
+ }
3232
3894
  function getAnchor(doc, ref) {
3233
3895
  const node = findVectorNode(doc.nodes, ref.nodeId);
3234
3896
  return node?.type === "path" ? node.subpaths[ref.subpath]?.anchors[ref.index] : void 0;
@@ -3299,9 +3961,10 @@ var TOOL_LABELS = {
3299
3961
  line: "Line",
3300
3962
  scissors: "Scissors",
3301
3963
  knife: "Knife",
3302
- eraser: "Eraser"
3964
+ eraser: "Eraser",
3965
+ width: "Width"
3303
3966
  };
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" };
3967
+ var TOOL_KEYS = { v: "select", a: "direct", p: "pen", n: "pencil", r: "rect", o: "ellipse", y: "polygon", s: "star", l: "line", c: "scissors", k: "knife", e: "eraser", w: "width" };
3305
3968
  function vectorToolLabel(tool) {
3306
3969
  return TOOL_LABELS[tool];
3307
3970
  }
@@ -3317,7 +3980,8 @@ var VECTOR_TOOL_SHORTCUTS = {
3317
3980
  line: "L",
3318
3981
  scissors: "C",
3319
3982
  knife: "K",
3320
- eraser: "E"
3983
+ eraser: "E",
3984
+ width: "W"
3321
3985
  };
3322
3986
  var VECTOR_TOOL_GLYPHS = {
3323
3987
  select: "\u2196",
@@ -3331,7 +3995,8 @@ var VECTOR_TOOL_GLYPHS = {
3331
3995
  line: "\u2571",
3332
3996
  scissors: "\u2702",
3333
3997
  knife: "\u27CB",
3334
- eraser: "\u232B"
3998
+ eraser: "\u232B",
3999
+ width: "\u21F9"
3335
4000
  };
3336
4001
  var SHAPE_TOOLS = /* @__PURE__ */ new Set(["rect", "ellipse", "polygon", "star", "line"]);
3337
4002
  var BOOLEAN_OPS = {
@@ -3385,6 +4050,7 @@ var VectorEditorController = class {
3385
4050
  doc;
3386
4051
  selection = [];
3387
4052
  anchors = [];
4053
+ widthPoint = null;
3388
4054
  editingId = null;
3389
4055
  pen = null;
3390
4056
  tool;
@@ -3463,6 +4129,7 @@ var VectorEditorController = class {
3463
4129
  tool: this.tool,
3464
4130
  selection: this.selection,
3465
4131
  selectedAnchors: this.anchors,
4132
+ selectedWidthPoint: this.widthPoint,
3466
4133
  editingId: this.editingId,
3467
4134
  selectionBounds: this.boundsOf(this.selection),
3468
4135
  selectionFrame: this.frameOf(this.selectedRoots()),
@@ -3531,6 +4198,10 @@ var VectorEditorController = class {
3531
4198
  return h?.nodeId ? "move" : "default";
3532
4199
  case "pen":
3533
4200
  return h?.anchor ? "pointer" : "crosshair";
4201
+ case "width":
4202
+ if (this.drag?.kind === "width") return this.drag.mode === "move" ? "grabbing" : "ew-resize";
4203
+ if (h?.widthPoint) return h.widthPoint.part === "center" ? "grab" : "ew-resize";
4204
+ return h?.strokeSpot ? "copy" : "default";
3534
4205
  default:
3535
4206
  return "crosshair";
3536
4207
  }
@@ -3656,6 +4327,7 @@ var VectorEditorController = class {
3656
4327
  this.tool = tool;
3657
4328
  this.hover = null;
3658
4329
  this.guides = null;
4330
+ this.widthPoint = null;
3659
4331
  if (tool !== "direct") {
3660
4332
  this.anchors = [];
3661
4333
  this.editingId = null;
@@ -3887,6 +4559,7 @@ var VectorEditorController = class {
3887
4559
  if (d.fillRule) out.fillRule = d.fillRule;
3888
4560
  if (d.strokeAlign && d.strokeAlign !== "center") out.strokeAlign = d.strokeAlign;
3889
4561
  for (const key of ["startArrow", "endArrow", "arrowAlign", "startArrowScale", "endArrowScale"]) if (d[key]) out[key] = d[key];
4562
+ if (d.strokeWidthProfile?.length) out.strokeWidthProfile = d.strokeWidthProfile.map((p) => ({ ...p }));
3890
4563
  return out;
3891
4564
  }
3892
4565
  /** Merges `patch` into a node (paths take style and geometry; groups name / visibility / opacity). */
@@ -3907,10 +4580,39 @@ var VectorEditorController = class {
3907
4580
  }
3908
4581
  if (!targets.size) return { ok: false, message: "Nothing is selected." };
3909
4582
  this.change(options.label ?? "Change Style", () => {
3910
- this.setDoc(mapTree(this.doc.nodes, (n) => targets.has(n.id) ? { ...n, ...style } : n));
4583
+ this.setDoc(mapTree(this.doc.nodes, (n) => targets.has(n.id) ? withStyle(n, style) : n));
3911
4584
  });
3912
4585
  return { ok: true };
3913
4586
  }
4587
+ /**
4588
+ * Sets the variable-width profile of the selected (or given) paths: a preset id
4589
+ * (`uniform` removes the profile), points, or a parametric taper / wave. Returns an error for
4590
+ * an unknown preset.
4591
+ */
4592
+ setWidthProfile(profile, options = {}) {
4593
+ let points;
4594
+ if (profile === null) points = [];
4595
+ else if (typeof profile === "string") {
4596
+ const preset = widthProfilePreset(profile);
4597
+ if (!preset) return { ok: false, message: `There is no width profile "${profile}".` };
4598
+ points = preset;
4599
+ } else if (Array.isArray(profile)) points = sanitizeWidthProfile(profile);
4600
+ else {
4601
+ const p = profile;
4602
+ points = p.taper || p.wave ? generateWidthProfile(p) : [];
4603
+ }
4604
+ this.widthPoint = null;
4605
+ return this.setStyle({ strokeWidthProfile: points.length ? points : null }, options.ids ?? this.selection, { label: points.length ? "Width Profile" : "Uniform Width" });
4606
+ }
4607
+ /** Removes one width point of a path's profile (the width tool's Delete). */
4608
+ removeWidthPoint(ref) {
4609
+ const node = this.getNode(ref.nodeId);
4610
+ if (node?.type !== "path" || !node.strokeWidthProfile?.[ref.index]) return { ok: false, message: "There is no such width point." };
4611
+ const rest = removeWidthPoints(sanitizeWidthProfile(node.strokeWidthProfile), [ref.index]);
4612
+ this.widthPoint = null;
4613
+ this.change("Delete Width Point", () => this.replaceNode(node.id, withStyle(node, { strokeWidthProfile: rest.length ? rest : null })));
4614
+ return { ok: true };
4615
+ }
3914
4616
  /**
3915
4617
  * Sets the live corner radius (and optionally the kind) of anchors: `refs`, else the selected
3916
4618
  * anchors (direct tool), else every anchor of the `ids` / selected paths. Radius 0 makes the
@@ -3981,6 +4683,7 @@ var VectorEditorController = class {
3981
4683
  arrowAlign: _al,
3982
4684
  startArrowScale: _ss,
3983
4685
  endArrowScale: _es,
4686
+ strokeWidthProfile: _wp,
3984
4687
  ...rest
3985
4688
  } = cloneVectorNode(p);
3986
4689
  const id = this.newId();
@@ -4282,7 +4985,7 @@ var VectorEditorController = class {
4282
4985
  if (!groups.length) return { ok: false, message: "Select a group to ungroup." };
4283
4986
  const set = new Set(groups.map((g) => g.id));
4284
4987
  this.change("Ungroup", () => {
4285
- const flatten = (list) => {
4988
+ const flatten2 = (list) => {
4286
4989
  const out = [];
4287
4990
  let changed = false;
4288
4991
  for (const n of list) {
@@ -4294,14 +4997,14 @@ var VectorEditorController = class {
4294
4997
  ...n.children.map((c) => n.transform ? transformVectorNode(c, m) : c).map((c) => opacity !== void 0 && opacity < 1 ? { ...c, opacity: (c.opacity ?? 1) * opacity } : c)
4295
4998
  );
4296
4999
  } else if (n.type === "group") {
4297
- const children = flatten(n.children);
5000
+ const children = flatten2(n.children);
4298
5001
  if (children !== n.children) changed = true;
4299
5002
  out.push(children !== n.children ? { ...n, children } : n);
4300
5003
  } else out.push(n);
4301
5004
  }
4302
5005
  return changed ? out : list;
4303
5006
  };
4304
- this.setDoc(flatten(this.doc.nodes));
5007
+ this.setDoc(flatten2(this.doc.nodes));
4305
5008
  this.selection = groups.flatMap((g) => g.children.map((c) => c.id));
4306
5009
  });
4307
5010
  return { ok: true };
@@ -4571,7 +5274,12 @@ var VectorEditorController = class {
4571
5274
  const set = new Set(ids.flatMap((id) => this.getNode(id) ? collectPathNodes(this.getNode(id)) : []).map((p) => p.id));
4572
5275
  if (!set.size) return { ok: false, message: "Select a path." };
4573
5276
  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));
5277
+ this.setDoc(
5278
+ mapTree(
5279
+ this.doc.nodes,
5280
+ (n) => n.type === "path" && set.has(n.id) ? { ...n, subpaths: n.subpaths.map(reverseSubpath), ...n.strokeWidthProfile?.length ? { strokeWidthProfile: reverseWidthProfile(n.strokeWidthProfile) } : {} } : n
5281
+ )
5282
+ );
4575
5283
  this.anchors = [];
4576
5284
  });
4577
5285
  return { ok: true };
@@ -4652,9 +5360,9 @@ var VectorEditorController = class {
4652
5360
  const cx = points.reduce((s, p) => s + p.at.x, 0) / points.length;
4653
5361
  const cy = points.reduce((s, p) => s + p.at.y, 0) / points.length;
4654
5362
  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;
5363
+ for (const { ref, at: at2 } of points) {
5364
+ const dx = axis === "horizontal" ? 0 : cx - at2.x;
5365
+ const dy = axis === "vertical" ? 0 : cy - at2.y;
4658
5366
  if (dx || dy) this.doc = this.movedAnchors(this.doc, [ref], dx, dy);
4659
5367
  }
4660
5368
  });
@@ -4738,8 +5446,8 @@ var VectorEditorController = class {
4738
5446
  const node = this.getNode(id);
4739
5447
  if (node.locked) continue;
4740
5448
  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;
5449
+ const at2 = list.filter((r) => r.subpath === i).map((r) => r.index);
5450
+ const pieces = at2.length ? cutSubpathAt(sp, at2) : null;
4743
5451
  return pieces && !(pieces.length === 1 && pieces[0].closed === sp.closed) ? pieces : null;
4744
5452
  });
4745
5453
  if (ids) created.push(...ids);
@@ -5053,10 +5761,131 @@ var VectorEditorController = class {
5053
5761
  const seg = this.nearestOutline(p);
5054
5762
  return seg ? { nodeId: seg.nodeId, segment: seg } : null;
5055
5763
  }
5764
+ case "width": {
5765
+ const hit = this.hitWidthHandle(screen);
5766
+ if (hit) return { nodeId: hit.ref.nodeId, widthPoint: hit.ref };
5767
+ const spot = this.strokeSpotAt(p);
5768
+ return spot ? { nodeId: spot.nodeId, strokeSpot: spot } : null;
5769
+ }
5056
5770
  default:
5057
5771
  return null;
5058
5772
  }
5059
5773
  }
5774
+ widthView() {
5775
+ return { document: this.doc, selection: this.selection, tool: this.tool, hover: this.hover, interaction: this.interaction, selectedWidthPoint: this.widthPoint };
5776
+ }
5777
+ /** The width point centre or side end under `screen` (width tool). */
5778
+ hitWidthHandle(screen) {
5779
+ let best = null;
5780
+ let bestD = Infinity;
5781
+ for (const h of getVectorWidthHandles(this.widthView())) {
5782
+ for (const [part, q] of [["center", h], ["left", h.left], ["right", h.right]]) {
5783
+ const s = this.documentToScreen(q);
5784
+ const d = Math.hypot(s.x - screen.x, s.y - screen.y);
5785
+ if (d <= this.hitRadius && d < bestD - 1e-9) {
5786
+ best = { ref: { ...h.ref, part } };
5787
+ bestD = d;
5788
+ }
5789
+ }
5790
+ }
5791
+ return best;
5792
+ }
5793
+ /** The nearest stroked, unlocked visible path whose stroke is under document point `p` (width tool). */
5794
+ strokeSpotAt(p) {
5795
+ let best = null;
5796
+ const tol = this.tolerance();
5797
+ walkVectorNodes(this.doc.nodes, (n) => {
5798
+ if (n.type !== "path" || n.visible === false || n.locked) return;
5799
+ if (hiddenOrLocked(this.doc.nodes, n.id)) return;
5800
+ const g = vectorStrokeWidthGeometry(n);
5801
+ if (!g) return;
5802
+ const m = this.worldOf(n.id);
5803
+ const local = applyMatrix(invertMatrix(m), p);
5804
+ const near2 = nearestStrokePosition(g.line, [local.x, local.y]);
5805
+ if (!near2) return;
5806
+ const profile = strokeWidthProfileOf(n);
5807
+ const w = widthProfileAt(profile, near2.t);
5808
+ const reach = Math.max(near2.side === "left" ? w.left : w.right, 0) * g.unit + tol;
5809
+ if (near2.distance > reach || near2.distance > widthProfileMaxScale(profile) * g.unit + tol) return;
5810
+ if (best && near2.distance >= best.distance) return;
5811
+ const nx = near2.tangent[1];
5812
+ const ny = -near2.tangent[0];
5813
+ const [cx, cy] = near2.point;
5814
+ best = {
5815
+ nodeId: n.id,
5816
+ subpath: near2.contour,
5817
+ t: near2.t,
5818
+ point: applyMatrix(m, { x: cx, y: cy }),
5819
+ left: applyMatrix(m, { x: cx + nx * w.left * g.unit, y: cy + ny * w.left * g.unit }),
5820
+ right: applyMatrix(m, { x: cx - nx * w.right * g.unit, y: cy - ny * w.right * g.unit }),
5821
+ distance: near2.distance
5822
+ };
5823
+ });
5824
+ if (!best) return null;
5825
+ const { distance: _d, ...spot } = best;
5826
+ return spot;
5827
+ }
5828
+ downWidth(screen, p, input) {
5829
+ const hit = this.hitWidthHandle(screen);
5830
+ const spot = hit ? null : this.strokeSpotAt(p);
5831
+ const nodeId = hit?.ref.nodeId ?? spot?.nodeId;
5832
+ const node = nodeId ? this.getNode(nodeId) : void 0;
5833
+ const g = node?.type === "path" ? vectorStrokeWidthGeometry(node) : null;
5834
+ if (!node || node.type !== "path" || !g) {
5835
+ if (this.widthPoint) {
5836
+ this.widthPoint = null;
5837
+ this.notify();
5838
+ }
5839
+ return;
5840
+ }
5841
+ const before = this.snapshot();
5842
+ const local = this.toLocal(node.id, p);
5843
+ let profile;
5844
+ let ref;
5845
+ let mode = "width";
5846
+ let side = "both";
5847
+ let t;
5848
+ let subpath;
5849
+ if (hit) {
5850
+ profile = sanitizeWidthProfile(node.strokeWidthProfile);
5851
+ ref = { nodeId: node.id, index: hit.ref.index, subpath: hit.ref.subpath };
5852
+ t = profile[ref.index]?.t ?? 0;
5853
+ subpath = hit.ref.subpath;
5854
+ if (hit.ref.part === "center") mode = "move";
5855
+ else if (input.alt) side = hit.ref.part;
5856
+ } else {
5857
+ const near2 = nearestStrokePosition(g.line, [local.x, local.y]);
5858
+ const inserted = insertWidthPoint(strokeWidthProfileOf(node), near2.t);
5859
+ profile = inserted.points;
5860
+ subpath = near2.contour;
5861
+ t = near2.t;
5862
+ ref = { nodeId: node.id, index: inserted.index, subpath };
5863
+ if (input.alt) side = near2.side;
5864
+ }
5865
+ const at2 = strokePositionPoint(g.line, subpath, t);
5866
+ if (!at2) return;
5867
+ const base = { ...this.doc, nodes: mapTree(this.doc.nodes, (n) => n.id === node.id ? { ...n, strokeWidthProfile: profile } : n) };
5868
+ this.doc = base;
5869
+ this.widthPoint = { nodeId: node.id, index: ref.index, subpath };
5870
+ this.drag = {
5871
+ kind: "width",
5872
+ ref,
5873
+ mode,
5874
+ side,
5875
+ origin: { x: at2.point[0], y: at2.point[1] },
5876
+ normal: { x: at2.tangent[1], y: -at2.tangent[0] },
5877
+ unit: g.unit,
5878
+ profile,
5879
+ before,
5880
+ base,
5881
+ start: p,
5882
+ moved: false,
5883
+ created: !hit
5884
+ };
5885
+ const w = profile[ref.index];
5886
+ this.interaction = { type: "width", ref, mode, width: (w.left + w.right) * g.unit };
5887
+ this.notify(!hit);
5888
+ }
5060
5889
  /** The nearest outline point of any visible, unlocked path within the pick distance of document point `p`. */
5061
5890
  nearestOutline(p) {
5062
5891
  let best = null;
@@ -5104,7 +5933,9 @@ var VectorEditorController = class {
5104
5933
  case "pen":
5105
5934
  return this.downPen(screen, p, input);
5106
5935
  case "pencil":
5107
- return this.downPencil(p);
5936
+ return this.downPencil(p, input);
5937
+ case "width":
5938
+ return this.downWidth(screen, p, input);
5108
5939
  case "scissors": {
5109
5940
  const seg = this.nearestOutline(p);
5110
5941
  if (seg) this.cutPathAt(seg.nodeId, seg.point);
@@ -5297,7 +6128,7 @@ var VectorEditorController = class {
5297
6128
  this.interaction = { type: "pen", nodeId: node.id };
5298
6129
  this.notify(true);
5299
6130
  }
5300
- downPencil(p) {
6131
+ downPencil(p, input = {}) {
5301
6132
  const before = this.snapshot();
5302
6133
  const node = {
5303
6134
  id: this.newId(),
@@ -5311,7 +6142,11 @@ var VectorEditorController = class {
5311
6142
  this.setDoc([...this.doc.nodes, node]);
5312
6143
  const lazy = new LazyMouse(this.editing.pencilSmoothing * VECTOR_PENCIL_STRING_PX / this.viewport.zoom);
5313
6144
  lazy.push([p.x, p.y]);
5314
- this.drag = { kind: "pencil", nodeId: node.id, points: [[p.x, p.y]], before, lazy };
6145
+ const mode = this.editing.pencilPressure;
6146
+ const hasPressure = typeof input.pressure === "number" && Number.isFinite(input.pressure);
6147
+ const record = hasPressure && (mode === "all" || mode === "pen" && input.pointerType === "pen");
6148
+ const pressure = hasPressure ? input.pressure : 0.5;
6149
+ this.drag = { kind: "pencil", nodeId: node.id, points: [[p.x, p.y]], before, lazy, pressures: record ? [pressure] : null, pressure };
5315
6150
  this.interaction = { type: "draw", tool: "pencil", nodeId: node.id, bounds: { x: p.x, y: p.y, width: 0, height: 0 } };
5316
6151
  this.notify(true);
5317
6152
  }
@@ -5520,12 +6355,36 @@ var VectorEditorController = class {
5520
6355
  this.notify(true);
5521
6356
  return;
5522
6357
  }
6358
+ case "width": {
6359
+ if (!drag.moved && Math.hypot(p.x - drag.start.x, p.y - drag.start.y) * this.viewport.zoom < 2) return;
6360
+ drag.moved = true;
6361
+ const node = findVectorNode(drag.base.nodes, drag.ref.nodeId);
6362
+ const local = this.toLocal(node.id, p, drag.base);
6363
+ let profile;
6364
+ if (drag.mode === "move") {
6365
+ const g = vectorStrokeWidthGeometry(node);
6366
+ const near2 = g ? nearestStrokePosition(g.line, [local.x, local.y], drag.ref.subpath) : null;
6367
+ if (!near2) return;
6368
+ profile = setWidthPoint(drag.profile, drag.ref.index, { t: near2.t });
6369
+ } else {
6370
+ const off = ((local.x - drag.origin.x) * drag.normal.x + (local.y - drag.origin.y) * drag.normal.y) / drag.unit;
6371
+ const patch = drag.side === "both" ? { left: Math.abs(off), right: Math.abs(off) } : drag.side === "left" ? { left: Math.max(0, off) } : { right: Math.max(0, -off) };
6372
+ profile = setWidthPoint(drag.profile, drag.ref.index, patch);
6373
+ }
6374
+ this.doc = { ...drag.base, nodes: mapTree(drag.base.nodes, (n) => n.id === node.id ? { ...n, strokeWidthProfile: profile } : n) };
6375
+ const w = profile[drag.ref.index];
6376
+ this.interaction = { type: "width", ref: drag.ref, mode: drag.mode, width: (w.left + w.right) * drag.unit };
6377
+ this.notify(true);
6378
+ return;
6379
+ }
5523
6380
  case "pencil": {
5524
6381
  const last = drag.points[drag.points.length - 1];
5525
6382
  let moved = false;
6383
+ if (typeof input.pressure === "number" && Number.isFinite(input.pressure)) drag.pressure = input.pressure;
5526
6384
  for (const q of drag.lazy.push([p.x, p.y])) {
5527
6385
  if (Math.hypot(q[0] - last[0], q[1] - last[1]) * this.viewport.zoom < 1.5) continue;
5528
6386
  drag.points.push(q);
6387
+ drag.pressures?.push(drag.pressure);
5529
6388
  moved = true;
5530
6389
  }
5531
6390
  const string = drag.lazy.string();
@@ -5606,7 +6465,7 @@ var VectorEditorController = class {
5606
6465
  }
5607
6466
  /** Primary button released. */
5608
6467
  pointerUp(screen, input = {}) {
5609
- if (screen && this.drag && this.drag.kind !== "pan") this.pointerMove(screen, input);
6468
+ if (screen && this.drag && this.drag.kind !== "pan") this.pointerMove(screen, { ...input, pressure: void 0 });
5610
6469
  const drag = this.drag;
5611
6470
  this.drag = null;
5612
6471
  this.interaction = null;
@@ -5658,6 +6517,15 @@ var VectorEditorController = class {
5658
6517
  this.notify();
5659
6518
  return;
5660
6519
  }
6520
+ case "width": {
6521
+ if (drag.moved) return this.commit(drag.mode === "move" ? "Move Width Point" : "Width Point", drag.before);
6522
+ if (drag.created) {
6523
+ this.doc = drag.before.doc;
6524
+ this.widthPoint = null;
6525
+ this.notify(true);
6526
+ } else this.notify();
6527
+ return;
6528
+ }
5661
6529
  case "stroke": {
5662
6530
  this.notify();
5663
6531
  if (drag.tool === "knife") this.knife(drag.points);
@@ -5688,7 +6556,10 @@ var VectorEditorController = class {
5688
6556
  this.notify(true);
5689
6557
  return;
5690
6558
  }
5691
- for (const q of drag.lazy.finish()) drag.points.push(q);
6559
+ for (const q of drag.lazy.finish()) {
6560
+ drag.points.push(q);
6561
+ drag.pressures?.push(drag.pressure);
6562
+ }
5692
6563
  const shape = this.editing.recognizeShapes ? recognizeInkShape(drag.points, { minSize: 6 / this.viewport.zoom }) : null;
5693
6564
  if (shape) {
5694
6565
  this.replaceNode(drag.nodeId, { ...node, name: inkShapeLabel(shape), subpaths: [inkShapeSubpath(shape)] });
@@ -5703,7 +6574,8 @@ var VectorEditorController = class {
5703
6574
  anchors.push({ x: c.p3[0], y: c.p3[1], in: { x: c.p2[0], y: c.p2[1] } });
5704
6575
  }
5705
6576
  const subpaths = anchors.length > 1 ? bezierToSubpaths(subpathsToBezier([{ anchors, closed: false }])) : node.subpaths;
5706
- this.replaceNode(drag.nodeId, { ...node, subpaths });
6577
+ const profile = drag.pressures ? pencilPressureProfile(drag.points, drag.pressures) : [];
6578
+ this.replaceNode(drag.nodeId, { ...node, subpaths, ...profile.length ? { strokeWidthProfile: profile } : {} });
5707
6579
  this.selection = [drag.nodeId];
5708
6580
  return this.commit("Draw", drag.before);
5709
6581
  }
@@ -5825,7 +6697,10 @@ var VectorEditorController = class {
5825
6697
  switch (key) {
5826
6698
  case "Escape":
5827
6699
  if (this.drag) this.cancelDrag();
5828
- else if (this.pen) this.finishPath();
6700
+ else if (this.tool === "width" && this.widthPoint) {
6701
+ this.widthPoint = null;
6702
+ this.notify();
6703
+ } else if (this.pen) this.finishPath();
5829
6704
  else if (this.tool === "direct" && this.anchors.length) {
5830
6705
  this.anchors = [];
5831
6706
  this.notify();
@@ -5846,6 +6721,7 @@ var VectorEditorController = class {
5846
6721
  return false;
5847
6722
  case "Delete":
5848
6723
  case "Backspace":
6724
+ if (this.tool === "width" && this.widthPoint) return this.removeWidthPoint(this.widthPoint).ok;
5849
6725
  if (this.pen) {
5850
6726
  const node = this.getNode(this.pen.nodeId);
5851
6727
  const sp = node.subpaths[this.pen.subpath];
@@ -5927,6 +6803,31 @@ function scaleFor(handle, b, p, mods = {}) {
5927
6803
  }
5928
6804
  return { sx, sy, origin };
5929
6805
  }
6806
+ function withStyle(node, style) {
6807
+ const next = { ...node, ...style };
6808
+ if ("strokeWidthProfile" in style) {
6809
+ const p = style.strokeWidthProfile ? sanitizeWidthProfile(style.strokeWidthProfile) : [];
6810
+ if (p.length) next.strokeWidthProfile = p;
6811
+ else delete next.strokeWidthProfile;
6812
+ }
6813
+ return next;
6814
+ }
6815
+ function hiddenOrLocked(nodes, id) {
6816
+ const loc = locateVectorNode(nodes, id);
6817
+ if (!loc) return false;
6818
+ const n = loc.list[loc.index];
6819
+ if (n.locked || n.visible === false) return true;
6820
+ return loc.parent ? hiddenOrLocked(nodes, loc.parent.id) : false;
6821
+ }
6822
+ function pencilPressureProfile(points, pressures) {
6823
+ const n = Math.min(points.length, pressures.length);
6824
+ if (n < 2) return [];
6825
+ const s = [0];
6826
+ for (let i = 1; i < n; i++) s.push(s[i - 1] + Math.hypot(points[i][0] - points[i - 1][0], points[i][1] - points[i - 1][1]));
6827
+ const total = s[n - 1];
6828
+ if (!(total > 0)) return [];
6829
+ return pressureWidthProfile(s.map((d, i) => ({ t: d / total, pressure: pressures[i] })));
6830
+ }
5930
6831
 
5931
6832
  // src/vector/render.ts
5932
6833
  var fmt = (n) => Math.round(n * 100) / 100;
@@ -6091,7 +6992,21 @@ function getVectorOverlayRenderData(state) {
6091
6992
  });
6092
6993
  if (it?.type === "corner") status = `R ${fmt(it.radius)}`;
6093
6994
  const cutPoint = state.tool === "scissors" && state.hover?.segment && !it ? (({ x, y }) => ({ x: fmt(x), y: fmt(y) }))(toScreen(state, state.hover.segment.point)) : null;
6094
- return { outlines, selectionBox, anchors, handles, marquee, penPreview, artboard, guides, status, toolStroke, pencilString, cutPoint, cornerWidgets };
6995
+ const sc = (p) => {
6996
+ const q = toScreen(state, p);
6997
+ return { x: fmt(q.x), y: fmt(q.y) };
6998
+ };
6999
+ const sameWidth = (a, b) => !!a && a.nodeId === b.nodeId && a.index === b.index;
7000
+ const widthPoints = getVectorWidthHandles(state).map((h) => {
7001
+ const c = sc(h);
7002
+ const hw = state.hover?.widthPoint;
7003
+ const hovered = hw && sameWidth(hw, h.ref) && hw.subpath === h.ref.subpath ? hw.part : null;
7004
+ return { ref: h.ref, x: c.x, y: c.y, left: sc(h.left), right: sc(h.right), selected: h.selected, hovered, active: it?.type === "width" && sameWidth(it.ref, h.ref) };
7005
+ });
7006
+ const spot = state.tool === "width" && !it ? state.hover?.strokeSpot : null;
7007
+ const widthSpot = spot ? { ...sc(spot.point), left: sc(spot.left), right: sc(spot.right) } : null;
7008
+ if (it?.type === "width") status = it.mode === "move" ? "Move width point" : `W ${fmt(it.width)}`;
7009
+ return { outlines, selectionBox, anchors, handles, marquee, penPreview, artboard, guides, status, toolStroke, pencilString, cutPoint, cornerWidgets, widthPoints, widthSpot };
6095
7010
  }
6096
7011
  var VECTOR_OVERLAY_THEME = {
6097
7012
  accent: "#0ea5e9",
@@ -6105,13 +7020,23 @@ var VECTOR_OVERLAY_THEME = {
6105
7020
  anchorSize: 7,
6106
7021
  handleRadius: 3.5,
6107
7022
  /** Radius of a live corner widget (a ring with a dot). */
6108
- cornerWidgetRadius: 4
7023
+ cornerWidgetRadius: 4,
7024
+ /** Half the diagonal of a width point's diamond. */
7025
+ widthPointSize: 5
6109
7026
  };
7027
+ function vectorDiamondPath(x, y, r = VECTOR_OVERLAY_THEME.widthPointSize) {
7028
+ return `M${fmt(x)} ${fmt(y - r)}L${fmt(x + r)} ${fmt(y)}L${fmt(x)} ${fmt(y + r)}L${fmt(x - r)} ${fmt(y)}Z`;
7029
+ }
6110
7030
  function toVectorModifiers(event) {
6111
7031
  return { shift: !!event.shiftKey, alt: !!event.altKey, mod: !!(event.metaKey || event.ctrlKey) };
6112
7032
  }
6113
7033
  function toVectorPointerInput(event) {
6114
- return { ...toVectorModifiers(event), button: event.button };
7034
+ return {
7035
+ ...toVectorModifiers(event),
7036
+ button: event.button,
7037
+ ...typeof event.pressure === "number" ? { pressure: event.pressure } : {},
7038
+ ...event.pointerType ? { pointerType: event.pointerType } : {}
7039
+ };
6115
7040
  }
6116
7041
  function toVectorKeyInput(event) {
6117
7042
  return { key: event.key, ...toVectorModifiers(event) };
@@ -6160,6 +7085,8 @@ function getVectorSelectionStyle(state) {
6160
7085
  arrowAlign: first.arrowAlign ?? "extend",
6161
7086
  startArrowScale: first.startArrowScale ?? 1,
6162
7087
  endArrowScale: first.endArrowScale ?? 1,
7088
+ strokeWidthProfile: profileOf(first),
7089
+ widthProfile: widthProfilePresetId(profileOf(first)),
6163
7090
  cornerRadius: c0.radius,
6164
7091
  cornerKind: c0.kind,
6165
7092
  corners: corners2.length,
@@ -6175,12 +7102,17 @@ function getVectorSelectionStyle(state) {
6175
7102
  arrowAlign: !same((p) => p.arrowAlign ?? "extend"),
6176
7103
  startArrowScale: !same((p) => p.startArrowScale ?? 1),
6177
7104
  endArrowScale: !same((p) => p.endArrowScale ?? 1),
7105
+ widthProfile: !same(profileOf),
6178
7106
  cornerRadius: corners2.some((c) => c.radius !== c0.radius),
6179
7107
  cornerKind: corners2.some((c) => c.kind !== c0.kind)
6180
7108
  },
6181
7109
  pathIds: paths.map((p) => p.id)
6182
7110
  };
6183
7111
  }
7112
+ function profileOf(p) {
7113
+ const points = sanitizeWidthProfile(p.strokeWidthProfile);
7114
+ return points.length && widthProfilePresetId(points) !== "uniform" ? points : null;
7115
+ }
6184
7116
  var VECTOR_STROKE_ALIGN_LABELS = { center: "Center", inside: "Inside", outside: "Outside" };
6185
7117
  var VECTOR_CORNER_KIND_LABELS = { round: "Round", inverted: "Inverted", chamfer: "Chamfer" };
6186
7118
  var VECTOR_ARROWHEAD_LABELS = {
@@ -6232,6 +7164,19 @@ var POINTS = {
6232
7164
  minItems: 1,
6233
7165
  description: "Polyline [[x, y], \u2026] in document units."
6234
7166
  };
7167
+ var WIDTH_POINTS = {
7168
+ type: ["array", "null"],
7169
+ items: {
7170
+ type: "object",
7171
+ properties: {
7172
+ t: { type: "number", minimum: 0, maximum: 1, description: "Position along each subpath by length (0 start, 1 end)." },
7173
+ left: { type: "number", minimum: 0, description: "Left side as a multiple of half the stroke width (1 = uniform)." },
7174
+ right: { type: "number", minimum: 0, description: "Right side (looking along the path, y down)." }
7175
+ },
7176
+ required: ["t", "left", "right"]
7177
+ },
7178
+ description: "Variable-width profile points, interpolated smoothly (monotone cubic); two points at one `t` make a step. `null` or `[]` strokes at a uniform width."
7179
+ };
6235
7180
  var PAINT = {
6236
7181
  description: 'A CSS colour, `null` for none, or a gradient `{ type: "linear", angle, stops: [{ offset, color }] }`.'
6237
7182
  };
@@ -6248,7 +7193,8 @@ var STYLE_PROPS = {
6248
7193
  endArrow: { type: ["string", "null"], enum: [...VECTOR_ARROWHEADS, null], description: "Head at the end of an open path, or null for none." },
6249
7194
  arrowAlign: { type: "string", enum: ["extend", "tip"], description: "`extend`: the tip sits past the end point; `tip`: on it, the stroke shortened under the head." },
6250
7195
  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)." }
7196
+ endArrowScale: { type: "number", exclusiveMinimum: 0, description: "End head size as a multiple of the stroke width (default 1)." },
7197
+ strokeWidthProfile: WIDTH_POINTS
6252
7198
  };
6253
7199
  var pickStyle = (params) => {
6254
7200
  const out = {};
@@ -6405,6 +7351,44 @@ function createVectorCommands(controller, options = {}) {
6405
7351
  return { ok: true };
6406
7352
  }
6407
7353
  },
7354
+ {
7355
+ ...base,
7356
+ id: `${prefix}setWidthProfile`,
7357
+ label: "Width Profile\u2026",
7358
+ keywords: ["variable width", "taper", "tapered", "pressure", "profile", "brush", "calligraphy", "bulge", "thick", "thin"],
7359
+ description: "Gives the stroke of paths a variable width: a `preset` (`uniform` removes the profile), explicit `points` ({ t, left, right }: side scales along each subpath), or a parametric `taper` (startLength / endLength as fractions of the path, startWidth / endWidth 0\u20131, startEase / endEase 0\u20131) and / or `wave` (amount 0\u20131, cycles, phase in degrees). Profiled strokes draw as filled outlines; Outline Stroke bakes them.",
7360
+ params: {
7361
+ type: "object",
7362
+ properties: {
7363
+ ids: IDS,
7364
+ preset: { type: "string", enum: VECTOR_WIDTH_PRESET_IDS },
7365
+ points: WIDTH_POINTS,
7366
+ taper: {
7367
+ type: "object",
7368
+ properties: {
7369
+ startLength: { type: "number", minimum: 0, maximum: 1 },
7370
+ endLength: { type: "number", minimum: 0, maximum: 1 },
7371
+ startWidth: { type: "number", minimum: 0, maximum: 1 },
7372
+ endWidth: { type: "number", minimum: 0, maximum: 1 },
7373
+ startEase: { type: "number", minimum: 0, maximum: 1 },
7374
+ endEase: { type: "number", minimum: 0, maximum: 1 }
7375
+ }
7376
+ },
7377
+ wave: { type: "object", properties: { amount: { type: "number", minimum: 0, maximum: 1 }, cycles: { type: "number", exclusiveMinimum: 0 }, phase: { type: "number" } } }
7378
+ }
7379
+ },
7380
+ enabled: enabledFor,
7381
+ undoable: true,
7382
+ run: (_ctx, params) => {
7383
+ const target = { ids: ids(params) };
7384
+ if (params?.preset) check(controller.setWidthProfile(params.preset, target));
7385
+ else if (params?.points !== void 0) check(controller.setWidthProfile(params.points ?? null, target));
7386
+ else if (params?.taper || params?.wave) check(controller.setWidthProfile({ taper: params.taper, wave: params.wave }, target));
7387
+ else fail("Pass `preset`, `points`, `taper` or `wave`.");
7388
+ const node = controller.getNode(target.ids[0]);
7389
+ return { ok: true, points: node?.type === "path" ? node.strokeWidthProfile ?? [] : [] };
7390
+ }
7391
+ },
6408
7392
  {
6409
7393
  ...base,
6410
7394
  id: `${prefix}outlineStroke`,
@@ -6711,7 +7695,7 @@ function createVectorCommands(controller, options = {}) {
6711
7695
  id: `${prefix}setEditing`,
6712
7696
  label: "Path Editing Options\u2026",
6713
7697
  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`.",
7698
+ description: "Sets how editing behaves: `keepShapeOnDelete`, `curvatureMoves` (anchor drags bend like the Curvature tool), `pencilSmoothing` (lazy mouse, 0\u20131), `recognizeShapes` (pencil scribbles become lines, rectangles, ellipses, polygons), `eraserSize` and `pencilPressure` (`pen`: record pen pressure on pencil strokes as a width profile, `all`: any pointer reporting pressure, `off`).",
6715
7699
  params: {
6716
7700
  type: "object",
6717
7701
  properties: {
@@ -6719,12 +7703,13 @@ function createVectorCommands(controller, options = {}) {
6719
7703
  curvatureMoves: { type: "boolean" },
6720
7704
  pencilSmoothing: { type: "number", minimum: 0, maximum: 1 },
6721
7705
  recognizeShapes: { type: "boolean" },
6722
- eraserSize: { type: "number", exclusiveMinimum: 0 }
7706
+ eraserSize: { type: "number", exclusiveMinimum: 0 },
7707
+ pencilPressure: { type: "string", enum: ["pen", "all", "off"] }
6723
7708
  }
6724
7709
  },
6725
7710
  run: (_ctx, params) => {
6726
7711
  const patch = {};
6727
- for (const key of ["keepShapeOnDelete", "curvatureMoves", "pencilSmoothing", "recognizeShapes", "eraserSize"]) if (params?.[key] !== void 0) patch[key] = params[key];
7712
+ for (const key of ["keepShapeOnDelete", "curvatureMoves", "pencilSmoothing", "recognizeShapes", "eraserSize", "pencilPressure"]) if (params?.[key] !== void 0) patch[key] = params[key];
6728
7713
  controller.setEditing(patch);
6729
7714
  return { editing: controller.getState().editing };
6730
7715
  }
@@ -6784,6 +7769,7 @@ export {
6784
7769
  VECTOR_OVERLAY_THEME,
6785
7770
  VECTOR_PATH_ACTIONS,
6786
7771
  VECTOR_PENCIL_STRING_PX,
7772
+ VECTOR_PRESSURE_NOMINAL,
6787
7773
  VECTOR_ROTATE_HANDLE_OFFSET,
6788
7774
  VECTOR_STROKE_ALIGN_LABELS,
6789
7775
  VECTOR_STYLE_KEYS,
@@ -6791,6 +7777,11 @@ export {
6791
7777
  VECTOR_TOOL_GLYPHS,
6792
7778
  VECTOR_TOOL_SHORTCUTS,
6793
7779
  VECTOR_TRANSFORM_HANDLES,
7780
+ VECTOR_WIDTH_PRESETS,
7781
+ VECTOR_WIDTH_PRESET_IDS,
7782
+ VECTOR_WIDTH_PROFILE_MAX_POINTS,
7783
+ VECTOR_WIDTH_PROFILE_MAX_SCALE,
7784
+ VECTOR_WIDTH_SAME_T,
6794
7785
  VectorEditorController,
6795
7786
  VectorSnapTargets,
6796
7787
  applyMatrix,
@@ -6816,6 +7807,7 @@ export {
6816
7807
  createVectorCommands,
6817
7808
  curvatureMoveAnchor,
6818
7809
  cutSubpathAt,
7810
+ dashIntervals,
6819
7811
  decomposeRigid,
6820
7812
  effectiveStrokeAlign,
6821
7813
  ellipseSubpath,
@@ -6826,6 +7818,7 @@ export {
6826
7818
  expandVectorCornersOf,
6827
7819
  findVectorNode,
6828
7820
  fitCornerRadius,
7821
+ generateWidthProfile,
6829
7822
  getAnchor,
6830
7823
  getAnchorWorld,
6831
7824
  getEditablePathIds,
@@ -6836,6 +7829,7 @@ export {
6836
7829
  getVectorSelectionBoxGeometry,
6837
7830
  getVectorSelectionFrame,
6838
7831
  getVectorSelectionStyle,
7832
+ getVectorWidthHandles,
6839
7833
  getVisibleAnchors,
6840
7834
  getVisibleHandles,
6841
7835
  hasPaint,
@@ -6846,46 +7840,58 @@ export {
6846
7840
  hitTestVectorStroke,
6847
7841
  inferAnchorType,
6848
7842
  insertAnchor,
7843
+ insertWidthPoint,
6849
7844
  invertMatrix,
6850
7845
  isClosedBezierShape,
6851
7846
  isIdentityMatrix,
7847
+ isUniformWidthProfile,
6852
7848
  isVectorTextTarget,
6853
7849
  joinSubpaths,
6854
7850
  knifePath,
6855
7851
  lineSubpath,
6856
7852
  locateVectorNode,
6857
7853
  matrixRotation,
7854
+ mirrorWidthProfile,
6858
7855
  moveAnchorHandle,
6859
7856
  multiplyMatrices,
6860
7857
  nearestSegment,
7858
+ nearestStrokePosition,
6861
7859
  nextVectorCornerKind,
6862
7860
  nodeMatrix,
6863
7861
  parseSvgTransform,
6864
7862
  parseVectorSvg,
6865
7863
  polygonSubpath,
6866
7864
  polylineBounds,
7865
+ pressureWidthProfile,
6867
7866
  recognizeInkShape,
6868
7867
  rectSubpath,
6869
7868
  removeAnchorKeepingShape,
6870
7869
  removeAnchors,
6871
7870
  removeAnchorsKeepingShape,
6872
7871
  removeRedundantPoints,
7872
+ removeWidthPoints,
6873
7873
  reverseSubpath,
7874
+ reverseWidthProfile,
6874
7875
  rotateMatrix,
6875
7876
  roundSubpaths,
7877
+ sanitizeWidthProfile,
6876
7878
  scaleFor,
6877
7879
  scaleMatrix,
6878
7880
  setAnchorCorner,
6879
7881
  setSubpathCorners,
7882
+ setWidthPoint,
6880
7883
  simplifyPolyline,
6881
7884
  smoothSubpath,
6882
7885
  splitIntoFaces,
6883
7886
  splitSubpathAt,
6884
7887
  starSubpath,
7888
+ strokePositionPoint,
7889
+ strokeWidthProfileOf,
6885
7890
  subpathPointAt,
6886
7891
  subpathSegment,
6887
7892
  subpathSegmentCount,
6888
7893
  subpathsToBezier,
7894
+ taperWaveFactor,
6889
7895
  toVectorKeyInput,
6890
7896
  toVectorModifiers,
6891
7897
  toVectorPointerInput,
@@ -6893,9 +7899,12 @@ export {
6893
7899
  translateAnchor,
6894
7900
  translateMatrix,
6895
7901
  unionBounds,
7902
+ variableStrokeOutline,
7903
+ variableStrokeRings,
6896
7904
  vectorCornerAt,
6897
7905
  vectorCorners,
6898
7906
  vectorCornersAttribute,
7907
+ vectorDiamondPath,
6899
7908
  vectorDocumentToSvg,
6900
7909
  vectorInkShapeName,
6901
7910
  vectorNodeBounds,
@@ -6907,13 +7916,21 @@ export {
6907
7916
  vectorPathData,
6908
7917
  vectorPathWorldBezier,
6909
7918
  vectorResizeCursor,
7919
+ vectorStrokeLineArea,
6910
7920
  vectorStrokeOutline,
6911
7921
  vectorStrokeOutlinePath,
6912
7922
  vectorStrokeParts,
6913
7923
  vectorStrokeReach,
6914
7924
  vectorStrokeSvgParts,
7925
+ vectorStrokeWidthGeometry,
6915
7926
  vectorToolLabel,
6916
7927
  vectorWorldMatrix,
6917
- walkVectorNodes
7928
+ walkVectorNodes,
7929
+ widthProfileAt,
7930
+ widthProfileFromAttribute,
7931
+ widthProfileMaxScale,
7932
+ widthProfilePreset,
7933
+ widthProfilePresetId,
7934
+ widthProfileToAttribute
6918
7935
  };
6919
7936
  //# sourceMappingURL=index.js.map