@pixerate/editor 0.37.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,
@@ -18,6 +19,7 @@ import {
18
19
  outlineStroke,
19
20
  pathBoolean,
20
21
  pathToContours,
22
+ pathfinder,
21
23
  pointInBezierPath,
22
24
  polygonBoolean,
23
25
  segmentBounds,
@@ -34,13 +36,30 @@ import {
34
36
  transformBezierPath,
35
37
  trimSegments,
36
38
  windingNumber
37
- } from "../chunk-HPEKB352.js";
39
+ } from "../chunk-ZCCU3VNI.js";
38
40
 
39
41
  // src/vector/types.ts
40
42
  var VECTOR_CORNER_KINDS = ["round", "inverted", "chamfer"];
41
43
  var VECTOR_ARROWHEADS = ["triangle", "arrow", "open", "circle", "square", "bar"];
42
- var VECTOR_TOOLS = ["select", "direct", "pen", "pencil", "rect", "ellipse", "polygon", "star", "line", "scissors", "knife", "eraser", "width"];
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
+ ];
43
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"];
44
63
 
45
64
  // src/vector/corners.ts
46
65
  var EPS = 1e-9;
@@ -344,14 +363,14 @@ function mirrorWidthProfile(points) {
344
363
  function editable(points) {
345
364
  return points?.length ? points.map((p) => ({ ...p })) : [sym(0, 1), sym(1, 1)];
346
365
  }
347
- function insertWidthPoint(points, t, sides = {}, options = {}) {
366
+ function insertWidthPoint(points, t, sides2 = {}, options = {}) {
348
367
  const pts = editable(points);
349
368
  const tt = Math.min(Math.max(Number.isFinite(t) ? t : 0, 0), 1);
350
369
  const here = widthProfileAt(points?.length ? points : null, tt);
351
370
  const p = {
352
371
  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
372
+ left: clean(sides2.left, VECTOR_WIDTH_PROFILE_MAX_SCALE) ?? here.left,
373
+ right: clean(sides2.right, VECTOR_WIDTH_PROFILE_MAX_SCALE) ?? here.right
355
374
  };
356
375
  const same = pts.findIndex((q) => Math.abs(q.t - tt) <= VECTOR_WIDTH_SAME_T);
357
376
  if (same >= 0 && !options.step) {
@@ -581,7 +600,7 @@ function variableStrokeRings(path, options) {
581
600
  const poly = flatten(contour.segments, contour.closed, Math.min(tol, half) / 4);
582
601
  if (!poly) continue;
583
602
  const len2 = poly.s[poly.s.length - 1];
584
- const sides = (s) => {
603
+ const sides2 = (s) => {
585
604
  const w = widthProfileAt(options.profile, s / len2);
586
605
  return [w.left * half, w.right * half];
587
606
  };
@@ -595,7 +614,7 @@ function variableStrokeRings(path, options) {
595
614
  pts.push({ p: pb, s: b, corner: false });
596
615
  const run = pts.filter((q, i) => i === 0 || dist(q.p, pts[i - 1].p) > 1e-9);
597
616
  if (run.length < 2) {
598
- const [l, r] = sides(a);
617
+ const [l, r] = sides2(a);
599
618
  const w2 = (l + r) / 2;
600
619
  if (cap === "round" && w2 > 0) rings.push([[pa[0] + w2, pa[1]], ...arcPoints(pa, 0, 2 * Math.PI, w2, w2, tol)]);
601
620
  continue;
@@ -605,7 +624,7 @@ function variableStrokeRings(path, options) {
605
624
  const dirs = [];
606
625
  for (let i = 0; i < m; i++) dirs.push(unitOf(sub2(run[i + 1].p, run[i].p)));
607
626
  const normals = dirs.map(leftNormal);
608
- const w = run.map((q) => sides(q.s));
627
+ const w = run.map((q) => sides2(q.s));
609
628
  const side = (sigma) => {
610
629
  const k = sigma > 0 ? 0 : 1;
611
630
  const S = (i) => along(run[i].p, normals[i], sigma * w[i][k]);
@@ -1474,8 +1493,8 @@ function ellipseSubpath(b) {
1474
1493
  ]
1475
1494
  };
1476
1495
  }
1477
- function polygonSubpath(b, sides = 6) {
1478
- const n = Math.max(3, Math.round(sides));
1496
+ function polygonSubpath(b, sides2 = 6) {
1497
+ const n = Math.max(3, Math.round(sides2));
1479
1498
  const cx = b.x + b.width / 2;
1480
1499
  const cy = b.y + b.height / 2;
1481
1500
  const anchors = [];
@@ -3157,13 +3176,13 @@ function loopSample(l) {
3157
3176
  }
3158
3177
  var contains2 = (l, p) => windingNumber([l.segs], p) !== 0;
3159
3178
  function groupFaces(loops) {
3160
- const usable = loops.filter((l) => l.segs.length && Math.abs(loopArea(l)) > 1e-12);
3161
- if (!usable.length) return [];
3162
- 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);
3163
3182
  const sign = Math.sign(loopArea(largest));
3164
- 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)));
3165
3184
  const faces = new Map(outers.map((o) => [o, { loops: [o] }]));
3166
- 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)) {
3167
3186
  const p = loopSample(hole);
3168
3187
  const parent = outers.find((o) => Math.abs(loopArea(o)) > Math.abs(loopArea(hole)) && contains2(o, p));
3169
3188
  if (parent) faces.get(parent).loops.push(hole);
@@ -3715,15 +3734,187 @@ function recognizeInkShape(points, options = {}) {
3715
3734
  return roundish && rms < 0.1 ? ellipse() : null;
3716
3735
  }
3717
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
+
3718
3907
  // src/vector/controller.ts
3719
3908
  var DEFAULT_VECTOR_SNAP = { grid: 0, objects: true, guides: true, threshold: 5, angles: VECTOR_CONSTRUCTION_ANGLES };
3909
+ var VECTOR_FILL_GAP = 6;
3720
3910
  var DEFAULT_VECTOR_PATH_EDIT = {
3721
3911
  keepShapeOnDelete: true,
3722
3912
  curvatureMoves: false,
3723
3913
  pencilSmoothing: 0,
3724
3914
  recognizeShapes: false,
3725
3915
  eraserSize: 10,
3726
- pencilPressure: "pen"
3916
+ pencilPressure: "pen",
3917
+ fillGap: 0
3727
3918
  };
3728
3919
  var VECTOR_PENCIL_STRING_PX = 100;
3729
3920
  var VECTOR_STYLE_KEYS = [
@@ -3962,9 +4153,27 @@ var TOOL_LABELS = {
3962
4153
  scissors: "Scissors",
3963
4154
  knife: "Knife",
3964
4155
  eraser: "Eraser",
3965
- width: "Width"
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"
3966
4176
  };
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" };
3968
4177
  function vectorToolLabel(tool) {
3969
4178
  return TOOL_LABELS[tool];
3970
4179
  }
@@ -3981,7 +4190,9 @@ var VECTOR_TOOL_SHORTCUTS = {
3981
4190
  scissors: "C",
3982
4191
  knife: "K",
3983
4192
  eraser: "E",
3984
- width: "W"
4193
+ width: "W",
4194
+ shapeBuilder: "M",
4195
+ paintBucket: "B"
3985
4196
  };
3986
4197
  var VECTOR_TOOL_GLYPHS = {
3987
4198
  select: "\u2196",
@@ -3996,7 +4207,9 @@ var VECTOR_TOOL_GLYPHS = {
3996
4207
  scissors: "\u2702",
3997
4208
  knife: "\u27CB",
3998
4209
  eraser: "\u232B",
3999
- width: "\u21F9"
4210
+ width: "\u21F9",
4211
+ shapeBuilder: "\u2295",
4212
+ paintBucket: "\u25CD"
4000
4213
  };
4001
4214
  var SHAPE_TOOLS = /* @__PURE__ */ new Set(["rect", "ellipse", "polygon", "star", "line"]);
4002
4215
  var BOOLEAN_OPS = {
@@ -4006,6 +4219,14 @@ var BOOLEAN_OPS = {
4006
4219
  exclude: "xor"
4007
4220
  };
4008
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
+ };
4009
4230
  function mapTree(nodes, fn) {
4010
4231
  let changed = false;
4011
4232
  const out = [];
@@ -4070,6 +4291,13 @@ var VectorEditorController = class {
4070
4291
  hoverTargets = null;
4071
4292
  spaceHeld = false;
4072
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;
4073
4301
  createId;
4074
4302
  onChange;
4075
4303
  listeners = /* @__PURE__ */ new Set();
@@ -4144,6 +4372,7 @@ var VectorEditorController = class {
4144
4372
  interaction: this.interaction,
4145
4373
  pen: this.pen,
4146
4374
  pointer: this.pointer,
4375
+ builder: this.builderPreview,
4147
4376
  canUndo: h.canUndo,
4148
4377
  canRedo: h.canRedo,
4149
4378
  undoLabel: h.undoName,
@@ -4202,6 +4431,11 @@ var VectorEditorController = class {
4202
4431
  if (this.drag?.kind === "width") return this.drag.mode === "move" ? "grabbing" : "ew-resize";
4203
4432
  if (h?.widthPoint) return h.widthPoint.part === "center" ? "grab" : "ew-resize";
4204
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";
4205
4439
  default:
4206
4440
  return "crosshair";
4207
4441
  }
@@ -4328,6 +4562,8 @@ var VectorEditorController = class {
4328
4562
  this.hover = null;
4329
4563
  this.guides = null;
4330
4564
  this.widthPoint = null;
4565
+ this.builderPreview = null;
4566
+ this.builderHoverKey = null;
4331
4567
  if (tool !== "direct") {
4332
4568
  this.anchors = [];
4333
4569
  this.editingId = null;
@@ -5115,6 +5351,248 @@ var VectorEditorController = class {
5115
5351
  return { ok: true };
5116
5352
  }
5117
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
+ // -------------------------------------------------------------------------
5118
5596
  // Anchor editing
5119
5597
  // -------------------------------------------------------------------------
5120
5598
  updateAnchors(doc, refs, fn) {
@@ -5651,8 +6129,8 @@ var VectorEditorController = class {
5651
6129
  if (!frame) return null;
5652
6130
  const { box, handles } = getVectorSelectionBoxGeometry(frame, this.viewport);
5653
6131
  const r = this.hitRadius + 2;
5654
- const usable = box.width < 2 * r || box.height < 2 * r ? handles.filter((h) => h.handle.length === 2 || h.handle === "rotate") : handles;
5655
- 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;
5656
6134
  return null;
5657
6135
  }
5658
6136
  hitHandle(screen) {
@@ -5936,6 +6414,9 @@ var VectorEditorController = class {
5936
6414
  return this.downPencil(p, input);
5937
6415
  case "width":
5938
6416
  return this.downWidth(screen, p, input);
6417
+ case "shapeBuilder":
6418
+ case "paintBucket":
6419
+ return this.downBuilder(p, input);
5939
6420
  case "scissors": {
5940
6421
  const seg = this.nearestOutline(p);
5941
6422
  if (seg) this.cutPathAt(seg.nodeId, seg.point);
@@ -6165,6 +6646,11 @@ var VectorEditorController = class {
6165
6646
  pointerMove(screen, input = {}) {
6166
6647
  const p = this.screenToDocument(screen);
6167
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
+ }
6168
6654
  if (!drag) {
6169
6655
  const hover = this.computeHover(screen);
6170
6656
  const pointerMatters = this.tool === "pen" && !!this.pen;
@@ -6296,6 +6782,8 @@ var VectorEditorController = class {
6296
6782
  this.notify(true);
6297
6783
  return;
6298
6784
  }
6785
+ case "builder":
6786
+ return this.moveBuilder(drag, p);
6299
6787
  case "stroke": {
6300
6788
  const last = drag.points[drag.points.length - 1];
6301
6789
  if (Math.hypot(p.x - last.x, p.y - last.y) * this.viewport.zoom < 1.5) return;
@@ -6526,6 +7014,8 @@ var VectorEditorController = class {
6526
7014
  } else this.notify();
6527
7015
  return;
6528
7016
  }
7017
+ case "builder":
7018
+ return this.upBuilder(drag);
6529
7019
  case "stroke": {
6530
7020
  this.notify();
6531
7021
  if (drag.tool === "knife") this.knife(drag.points);
@@ -6597,7 +7087,8 @@ var VectorEditorController = class {
6597
7087
  this.drag = null;
6598
7088
  this.interaction = null;
6599
7089
  this.guides = null;
6600
- if (drag.kind === "pan") {
7090
+ if (drag.kind === "pan" || drag.kind === "builder") {
7091
+ if (drag.kind === "builder") this.builderPreview = null;
6601
7092
  this.notify();
6602
7093
  return;
6603
7094
  }
@@ -7006,7 +7497,19 @@ function getVectorOverlayRenderData(state) {
7006
7497
  const spot = state.tool === "width" && !it ? state.hover?.strokeSpot : null;
7007
7498
  const widthSpot = spot ? { ...sc(spot.point), left: sc(spot.left), right: sc(spot.right) } : null;
7008
7499
  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 };
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 };
7010
7513
  }
7011
7514
  var VECTOR_OVERLAY_THEME = {
7012
7515
  accent: "#0ea5e9",
@@ -7022,7 +7525,12 @@ var VECTOR_OVERLAY_THEME = {
7022
7525
  /** Radius of a live corner widget (a ring with a dot). */
7023
7526
  cornerWidgetRadius: 4,
7024
7527
  /** Half the diagonal of a width point's diamond. */
7025
- widthPointSize: 5
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)"
7026
7534
  };
7027
7535
  function vectorDiamondPath(x, y, r = VECTOR_OVERLAY_THEME.widthPointSize) {
7028
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`;
@@ -7138,6 +7646,14 @@ var VECTOR_PATH_ACTIONS = [
7138
7646
  { id: "smooth", label: "\u223F", title: "Smooth", enabled: (s, p) => s + p > 0, run: (c) => c.smoothPaths() },
7139
7647
  { id: "outline", label: "\u22A1", title: "Outline Stroke", enabled: (s) => s > 0, run: (c) => c.outlineStroke() }
7140
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
+ ];
7141
7657
  var VECTOR_EDIT_TOGGLES = [
7142
7658
  { key: "keepShapeOnDelete", label: "Keep", title: "Keep shape when deleting points" },
7143
7659
  { key: "curvatureMoves", label: "Curv", title: "Curvature point moves (Alt flips while dragging)" },
@@ -7461,6 +7977,62 @@ function createVectorCommands(controller, options = {}) {
7461
7977
  return { id };
7462
7978
  }
7463
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
+ },
7464
8036
  {
7465
8037
  ...base,
7466
8038
  id: `${prefix}align`,
@@ -7765,8 +8337,12 @@ export {
7765
8337
  VECTOR_CORNER_KIND_LABELS,
7766
8338
  VECTOR_CORNER_WIDGET_INSET,
7767
8339
  VECTOR_EDIT_TOGGLES,
8340
+ VECTOR_FILL_GAP,
7768
8341
  VECTOR_INK_MAX_CORNERS,
7769
8342
  VECTOR_OVERLAY_THEME,
8343
+ VECTOR_PATHFINDER_ACTIONS,
8344
+ VECTOR_PATHFINDER_LABELS,
8345
+ VECTOR_PATHFINDER_OPERATIONS,
7770
8346
  VECTOR_PATH_ACTIONS,
7771
8347
  VECTOR_PENCIL_STRING_PX,
7772
8348
  VECTOR_PRESSURE_NOMINAL,
@@ -7784,6 +8360,7 @@ export {
7784
8360
  VECTOR_WIDTH_SAME_T,
7785
8361
  VectorEditorController,
7786
8362
  VectorSnapTargets,
8363
+ addBuilderHit,
7787
8364
  applyMatrix,
7788
8365
  applyMatrixLinear,
7789
8366
  applyVectorCornersAttribute,
@@ -7860,6 +8437,7 @@ export {
7860
8437
  nodeMatrix,
7861
8438
  parseSvgTransform,
7862
8439
  parseVectorSvg,
8440
+ planShapeBuilder,
7863
8441
  polygonSubpath,
7864
8442
  polylineBounds,
7865
8443
  pressureWidthProfile,
@@ -7874,6 +8452,7 @@ export {
7874
8452
  reverseWidthProfile,
7875
8453
  rotateMatrix,
7876
8454
  roundSubpaths,
8455
+ samplePolyline,
7877
8456
  sanitizeWidthProfile,
7878
8457
  scaleFor,
7879
8458
  scaleMatrix,
@@ -7887,6 +8466,7 @@ export {
7887
8466
  starSubpath,
7888
8467
  strokePositionPoint,
7889
8468
  strokeWidthProfileOf,
8469
+ subpathEnclosesArea,
7890
8470
  subpathPointAt,
7891
8471
  subpathSegment,
7892
8472
  subpathSegmentCount,
@@ -7906,6 +8486,7 @@ export {
7906
8486
  vectorCornersAttribute,
7907
8487
  vectorDiamondPath,
7908
8488
  vectorDocumentToSvg,
8489
+ vectorFillMap,
7909
8490
  vectorInkShapeName,
7910
8491
  vectorNodeBounds,
7911
8492
  vectorNodeRotation,
@@ -7914,8 +8495,13 @@ export {
7914
8495
  vectorPaintToSvg,
7915
8496
  vectorParentMatrix,
7916
8497
  vectorPathData,
8498
+ vectorPathEntries,
7917
8499
  vectorPathWorldBezier,
8500
+ vectorPathfinderShape,
7918
8501
  vectorResizeCursor,
8502
+ vectorResultStyle,
8503
+ vectorShapeBuilderHit,
8504
+ vectorShapeBuilderMap,
7919
8505
  vectorStrokeLineArea,
7920
8506
  vectorStrokeOutline,
7921
8507
  vectorStrokeOutlinePath,