@pixerate/editor 0.33.0 → 0.35.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.
@@ -1,4 +1,4 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class;
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3;
2
2
 
3
3
  var _chunkQKLRAA4Tcjs = require('../chunk-QKLRAA4T.cjs');
4
4
 
@@ -15,10 +15,22 @@ var _chunkQKLRAA4Tcjs = require('../chunk-QKLRAA4T.cjs');
15
15
 
16
16
 
17
17
 
18
- var _chunkM4XE4UMTcjs = require('../chunk-M4XE4UMT.cjs');
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+
26
+
27
+
28
+
29
+
30
+ var _chunkBGNPT5PEcjs = require('../chunk-BGNPT5PE.cjs');
19
31
 
20
32
  // src/vector/types.ts
21
- var VECTOR_TOOLS = ["select", "direct", "pen", "pencil", "rect", "ellipse", "polygon", "star", "line"];
33
+ var VECTOR_TOOLS = ["select", "direct", "pen", "pencil", "rect", "ellipse", "polygon", "star", "line", "scissors", "knife", "eraser"];
22
34
  var VECTOR_TRANSFORM_HANDLES = ["nw", "n", "ne", "e", "se", "s", "sw", "w", "rotate"];
23
35
 
24
36
  // src/vector/geometry.ts
@@ -82,7 +94,7 @@ function vectorNodeToBezier(node) {
82
94
  }
83
95
  function vectorPathData(input, precision = 3) {
84
96
  const subpaths = Array.isArray(input) ? input : input.subpaths;
85
- return _chunkM4XE4UMTcjs.bezierPathToSvg.call(void 0, subpathsToBezier(subpaths), { precision });
97
+ return _chunkBGNPT5PEcjs.bezierPathToSvg.call(void 0, subpathsToBezier(subpaths), { precision });
86
98
  }
87
99
  function inferAnchorType(a) {
88
100
  if (isOnAnchor(a, a.in) || isOnAnchor(a, a.out)) return "corner";
@@ -98,7 +110,7 @@ function inferAnchorType(a) {
98
110
  return Math.abs(li - lo) <= 1e-3 * Math.max(li, lo) ? "symmetric" : "smooth";
99
111
  }
100
112
  function bezierToSubpaths(input) {
101
- const path = _chunkM4XE4UMTcjs.toBezierPath.call(void 0, input);
113
+ const path = _chunkBGNPT5PEcjs.toBezierPath.call(void 0, input);
102
114
  const out = [];
103
115
  let current = null;
104
116
  const last = () => current.anchors[current.anchors.length - 1];
@@ -195,14 +207,14 @@ function nodeMatrix(node) {
195
207
  function vectorPathWorldBezier(node, parent = IDENTITY_MATRIX) {
196
208
  const m = multiplyMatrices(nodeMatrix(node), parent);
197
209
  const path = subpathsToBezier(node.subpaths);
198
- return isIdentityMatrix(m) ? path : _chunkM4XE4UMTcjs.transformBezierPath.call(void 0, path, m);
210
+ return isIdentityMatrix(m) ? path : _chunkBGNPT5PEcjs.transformBezierPath.call(void 0, path, m);
199
211
  }
200
212
  function vectorNodeBounds(node, parent = IDENTITY_MATRIX) {
201
213
  if (node.type === "group") {
202
214
  const m = multiplyMatrices(nodeMatrix(node), parent);
203
215
  return unionBounds(node.children.map((c) => vectorNodeBounds(c, m)));
204
216
  }
205
- const b = _chunkM4XE4UMTcjs.bezierPathBounds.call(void 0, vectorPathWorldBezier(node, parent));
217
+ const b = _chunkBGNPT5PEcjs.bezierPathBounds.call(void 0, vectorPathWorldBezier(node, parent));
206
218
  if (!b) return null;
207
219
  return { x: b.minX, y: b.minY, width: b.maxX - b.minX, height: b.maxY - b.minY };
208
220
  }
@@ -236,9 +248,9 @@ function hitTestVectorNode(node, p, tolerance = 0, parent = IDENTITY_MATRIX) {
236
248
  const path = vectorPathWorldBezier(node, parent);
237
249
  if (path.length === 0) return false;
238
250
  const point = [p.x, p.y];
239
- if (hasPaint(node.fill) && _chunkM4XE4UMTcjs.pointInBezierPath.call(void 0, path, point, _nullishCoalesce(node.fillRule, () => ( "nonzero")))) return true;
251
+ if (hasPaint(node.fill) && _chunkBGNPT5PEcjs.pointInBezierPath.call(void 0, path, point, _nullishCoalesce(node.fillRule, () => ( "nonzero")))) return true;
240
252
  const reach = (hasPaint(node.stroke) ? (_nullishCoalesce(node.strokeWidth, () => ( 1))) / 2 : 0) + tolerance;
241
- return _chunkM4XE4UMTcjs.distanceToBezierPath.call(void 0, path, point) <= reach;
253
+ return _chunkBGNPT5PEcjs.distanceToBezierPath.call(void 0, path, point) <= reach;
242
254
  }
243
255
  function hitTestVectorNodes(nodes, p, tolerance = 0) {
244
256
  for (let i = nodes.length - 1; i >= 0; i--) {
@@ -254,9 +266,9 @@ function nearestSegment(node, p) {
254
266
  const count = subpathSegmentCount(sp);
255
267
  for (let i = 0; i < count; i++) {
256
268
  const seg = subpathSegment(sp, i);
257
- const hit = _chunkM4XE4UMTcjs.segmentNearest.call(void 0, seg, [p.x, p.y]);
269
+ const hit = _chunkBGNPT5PEcjs.segmentNearest.call(void 0, seg, [p.x, p.y]);
258
270
  if (!best || hit.distance < best.distance) {
259
- const pt = _chunkM4XE4UMTcjs.segmentPoint.call(void 0, seg, hit.t);
271
+ const pt = _chunkBGNPT5PEcjs.segmentPoint.call(void 0, seg, hit.t);
260
272
  best = { subpath: s, segment: i, t: hit.t, point: { x: pt[0], y: pt[1] }, distance: hit.distance };
261
273
  }
262
274
  }
@@ -427,7 +439,7 @@ function insertAnchor(sp, segment, t) {
427
439
  const ia = segment;
428
440
  const ib = (segment + 1) % n;
429
441
  const seg = subpathSegment(sp, segment);
430
- const [left, right] = _chunkM4XE4UMTcjs.splitSegment.call(void 0, seg, t);
442
+ const [left, right] = _chunkBGNPT5PEcjs.splitSegment.call(void 0, seg, t);
431
443
  const mid = left[left.length - 1];
432
444
  const anchor = { x: mid[0], y: mid[1] };
433
445
  if (seg.length === 4) {
@@ -535,7 +547,7 @@ var escapeAttr = (value) => value.replace(/&/g, "&amp;").replace(/"/g, "&quot;")
535
547
  function vectorPaintToSvg(paint, gradientId) {
536
548
  if (!hasPaint(paint)) return { value: "none", gradient: null };
537
549
  if (typeof paint === "string") return { value: paint, gradient: null };
538
- return { value: `url(#${gradientId})`, gradient: _chunkM4XE4UMTcjs.gradientToSvgDef.call(void 0, paint, gradientId) };
550
+ return { value: `url(#${gradientId})`, gradient: _chunkBGNPT5PEcjs.gradientToSvgDef.call(void 0, paint, gradientId) };
539
551
  }
540
552
  function gradientMarkup(def) {
541
553
  const tag = def.type === "radial" ? "radialGradient" : "linearGradient";
@@ -549,11 +561,11 @@ function vectorDocumentToSvg(doc, options = {}) {
549
561
  const pretty = options.pretty !== false;
550
562
  const defs = [];
551
563
  const lines = [];
552
- const num = (n) => _chunkM4XE4UMTcjs.formatNumber.call(void 0, n, precision);
564
+ const num = (n) => _chunkBGNPT5PEcjs.formatNumber.call(void 0, n, precision);
553
565
  const indent = (depth) => pretty ? " ".repeat(depth) : "";
554
566
  const common = (node) => {
555
567
  let out2 = ` id="${escapeAttr(node.id)}"`;
556
- if (node.transform) out2 += ` transform="matrix(${node.transform.map((n) => _chunkM4XE4UMTcjs.formatNumber.call(void 0, n, 6)).join(" ")})"`;
568
+ if (node.transform) out2 += ` transform="matrix(${node.transform.map((n) => _chunkBGNPT5PEcjs.formatNumber.call(void 0, n, 6)).join(" ")})"`;
557
569
  if (node.opacity !== void 0 && node.opacity < 1) out2 += ` opacity="${num(node.opacity)}"`;
558
570
  if (node.visible === false) out2 += ` display="none"`;
559
571
  return out2;
@@ -850,7 +862,1719 @@ function parseVectorSvg(svg, options = {}) {
850
862
  return { width, height, nodes };
851
863
  }
852
864
 
865
+ // src/vector/snap.ts
866
+ var VECTOR_CONSTRUCTION_ANGLES = [0, 45, 90, 135];
867
+ function constrainToLeave(leave, p) {
868
+ if (!_optionalChain([leave, 'optionalAccess', _8 => _8.constrain])) return p;
869
+ const dx = p.x - leave.at.x;
870
+ const dy = p.y - leave.at.y;
871
+ switch (leave.constrain) {
872
+ case "axis":
873
+ return Math.abs(dx) > Math.abs(dy) ? { x: p.x, y: leave.at.y } : { x: leave.at.x, y: p.y };
874
+ case "square": {
875
+ const s = Math.max(Math.abs(dx), Math.abs(dy));
876
+ return { x: leave.at.x + Math.sign(dx || 1) * s, y: leave.at.y + Math.sign(dy || 1) * s };
877
+ }
878
+ default: {
879
+ const len = Math.hypot(dx, dy);
880
+ const a = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);
881
+ return { x: leave.at.x + Math.cos(a) * len, y: leave.at.y + Math.sin(a) * len };
882
+ }
883
+ }
884
+ }
885
+ var BUDGET = 2e4;
886
+ var CROSSING_PAIRS = 32;
887
+ var CURVE_SAMPLES = 48;
888
+ var BISECT = 50;
889
+ var EQUAL = 1e-6;
890
+ function bound(sorted, key, v, strict) {
891
+ let lo = 0;
892
+ let hi = sorted.length;
893
+ while (lo < hi) {
894
+ const mid = lo + hi >>> 1;
895
+ const k = key(sorted[mid]);
896
+ if (strict ? k <= v : k < v) lo = mid + 1;
897
+ else hi = mid;
898
+ }
899
+ return lo;
900
+ }
901
+ function within(sorted, key, v, tol) {
902
+ return sorted.slice(bound(sorted, key, v - tol, false), bound(sorted, key, v + tol, true));
903
+ }
904
+ var linesNear = (lines, v, tol) => within(lines, (l) => l.v, v, tol);
905
+ var distance = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
906
+ var toPoint = (p) => [p.x, p.y];
907
+ var toVec = (p) => ({ x: p[0], y: p[1] });
908
+ var LABELS = {
909
+ anchor: "anchor",
910
+ centre: "centre",
911
+ intersect: "intersect",
912
+ perpendicular: "perpendicular",
913
+ tangent: "tangent",
914
+ path: "path",
915
+ construction: "angle",
916
+ align: "align",
917
+ spacing: "spacing",
918
+ size: "size"
919
+ };
920
+ var fmtLength = (n) => String(Math.round(n * 100) / 100);
921
+ function nodeFrame(node, parent, aabb) {
922
+ const angle = matrixRotation(multiplyMatrices(nodeMatrix(node), parent));
923
+ const b = angle ? vectorNodeBounds(node, multiplyMatrices(parent, rotateMatrix(-angle))) : aabb !== void 0 ? aabb : vectorNodeBounds(node, parent);
924
+ if (!b) return null;
925
+ const c = { x: b.x + b.width / 2, y: b.y + b.height / 2 };
926
+ return { width: b.width, height: b.height, centre: angle ? applyMatrix(rotateMatrix(angle), c) : c };
927
+ }
928
+ function contains(node, ids) {
929
+ if (ids.has(node.id)) return true;
930
+ return node.type === "group" && node.children.some((c) => contains(c, ids));
931
+ }
932
+ var VectorSnapTargets = (_class = class _VectorSnapTargets {
933
+ __init() {this.points = []}
934
+ __init2() {this.xs = []}
935
+ __init3() {this.ys = []}
936
+ __init4() {this.segments = []}
937
+ /** Bounds of the objects a moved box spaces itself evenly with. */
938
+ __init5() {this.boxes = []}
939
+ /** Side lengths of the other objects (in their own frames), which a resize may match. */
940
+ __init6() {this.sizes = []}
941
+ constructor() {;_class.prototype.__init.call(this);_class.prototype.__init2.call(this);_class.prototype.__init3.call(this);_class.prototype.__init4.call(this);_class.prototype.__init5.call(this);_class.prototype.__init6.call(this);
942
+ }
943
+ /** How many targets of each sort were gathered. */
944
+ get counts() {
945
+ return { points: this.points.length, lines: this.xs.length + this.ys.length, segments: this.segments.length, boxes: this.boxes.length };
946
+ }
947
+ /** Targets for moving nodes `ids`: the other art's anchors, bounds and centres, the artboard, and boxes to space evenly with. */
948
+ static forMove(doc, ids) {
949
+ return _VectorSnapTargets.collect(doc, new Set(ids), "bounds");
950
+ }
951
+ /** Targets for resizing nodes `ids`: as for a move, plus the other objects' sizes. */
952
+ static forResize(doc, ids) {
953
+ return _VectorSnapTargets.collect(doc, new Set(ids), "bounds");
954
+ }
955
+ /**
956
+ * Targets for a drawn point (pen, shape tools): the art but `exclude`, its anchors (as
957
+ * points and in line), centres, edges and segments. The anchors and segments of `exclude`
958
+ * (the path being drawn, whose bounds grow with it) stay targets.
959
+ */
960
+ static forDrawing(doc, exclude = []) {
961
+ const skip = new Set(exclude);
962
+ const t = _VectorSnapTargets.collect(doc, skip, "drawing", false);
963
+ t.pushPaths(doc, skip, () => false, true);
964
+ return t.index();
965
+ }
966
+ /**
967
+ * Targets for dragging anchors: the other art with its segments, and the anchors and
968
+ * segments that stay put on the paths being reshaped (their bounds move along, so they are
969
+ * no targets). The moving anchors are left out.
970
+ */
971
+ static forAnchorDrag(doc, anchors) {
972
+ const moving = /* @__PURE__ */ new Map();
973
+ for (const r of anchors) {
974
+ const set = _nullishCoalesce(moving.get(r.nodeId), () => ( /* @__PURE__ */ new Set()));
975
+ set.add(`${r.subpath}:${r.index}`);
976
+ moving.set(r.nodeId, set);
977
+ }
978
+ const t = _VectorSnapTargets.collect(doc, new Set(moving.keys()), "segments", false);
979
+ t.pushPaths(doc, new Set(moving.keys()), (id, s, i) => !!_optionalChain([moving, 'access', _9 => _9.get, 'call', _10 => _10(id), 'optionalAccess', _11 => _11.has, 'call', _12 => _12(`${s}:${i}`)]), true);
980
+ return t.index();
981
+ }
982
+ /**
983
+ * Targets for dragging a handle of path `nodeId`: the other art and that path's anchors
984
+ * (its bounds and the segments through the handle move with it, so they would chase it).
985
+ */
986
+ static forHandle(doc, nodeId) {
987
+ const skip = /* @__PURE__ */ new Set([nodeId]);
988
+ const t = _VectorSnapTargets.collect(doc, skip, "bounds", false);
989
+ t.pushPaths(doc, skip, () => false, false);
990
+ return t.index();
991
+ }
992
+ /** The anchors of the paths `ids` as targets (on them and in line), and their segments when `segments`. */
993
+ pushPaths(doc, ids, moving, segments) {
994
+ let anchors = BUDGET;
995
+ let segmentBudget = segments ? BUDGET : 0;
996
+ const visit = (list, parent) => {
997
+ for (const n of list) {
998
+ const m = multiplyMatrices(nodeMatrix(n), parent);
999
+ if (n.type === "group") visit(n.children, m);
1000
+ else if (ids.has(n.id) && n.visible !== false) {
1001
+ n.subpaths.forEach((sp, s) => {
1002
+ sp.anchors.forEach((a, i) => {
1003
+ if (anchors <= 0 || moving(n.id, s, i)) return;
1004
+ this.pushAnchor(applyMatrix(m, a));
1005
+ anchors--;
1006
+ });
1007
+ segmentBudget = this.pushSegments(sp, m, (i) => moving(n.id, s, i), segmentBudget);
1008
+ });
1009
+ }
1010
+ }
1011
+ };
1012
+ visit(doc.nodes, IDENTITY_MATRIX);
1013
+ }
1014
+ pushAnchor(p) {
1015
+ this.points.push({ p, kind: "anchor" });
1016
+ this.xs.push({ v: p.x, from: p, kind: "anchor" });
1017
+ this.ys.push({ v: p.y, from: p, kind: "anchor" });
1018
+ }
1019
+ /** The segments of `sp` (mapped by `m`) that stay put while the anchors `moving` accepts move. */
1020
+ pushSegments(sp, m, moving, budget) {
1021
+ const n = sp.anchors.length;
1022
+ const count = subpathSegmentCount(sp);
1023
+ for (let i = 0; i < count && budget > 0; i++) {
1024
+ if (moving(i) || moving((i + 1) % n)) continue;
1025
+ const seg = subpathSegment(sp, i).map((q) => toPoint(applyMatrix(m, toVec(q))));
1026
+ let minX = Infinity;
1027
+ let minY = Infinity;
1028
+ let maxX = -Infinity;
1029
+ let maxY = -Infinity;
1030
+ for (const [x, y] of seg) {
1031
+ minX = Math.min(minX, x);
1032
+ minY = Math.min(minY, y);
1033
+ maxX = Math.max(maxX, x);
1034
+ maxY = Math.max(maxY, y);
1035
+ }
1036
+ this.segments.push({ seg, minX, minY, maxX, maxY });
1037
+ budget--;
1038
+ }
1039
+ return budget;
1040
+ }
1041
+ pushRect(r, kind, centre) {
1042
+ const corners2 = [
1043
+ { x: r.x, y: r.y },
1044
+ { x: r.x + r.width, y: r.y },
1045
+ { x: r.x + r.width, y: r.y + r.height },
1046
+ { x: r.x, y: r.y + r.height }
1047
+ ];
1048
+ for (const p of corners2) {
1049
+ this.xs.push({ v: p.x, from: p, kind });
1050
+ this.ys.push({ v: p.y, from: p, kind });
1051
+ }
1052
+ if (centre) {
1053
+ this.points.push({ p: centre, kind: "centre" });
1054
+ this.xs.push({ v: centre.x, from: centre, kind: "centre" });
1055
+ this.ys.push({ v: centre.y, from: centre, kind: "centre" });
1056
+ }
1057
+ }
1058
+ /** Sorts the targets for `within` (stable: of targets alike, the first gathered wins). */
1059
+ index() {
1060
+ this.points.sort((a, b) => a.p.x - b.p.x);
1061
+ this.xs.sort((a, b) => a.v - b.v);
1062
+ this.ys.sort((a, b) => a.v - b.v);
1063
+ return this;
1064
+ }
1065
+ /**
1066
+ * Gathers targets from the artboard and every visible node but `exclude` (and what is inside
1067
+ * those): each leaf's anchors (also as lines for `drawing`), its segments (`segments`,
1068
+ * `drawing`), its bounds' corners as lines and its centre; and the top-level objects' boxes and
1069
+ * sizes (inside a group holding an excluded node, that group's other members).
1070
+ */
1071
+ static collect(doc, exclude, gather, sort = true) {
1072
+ const self = new _VectorSnapTargets();
1073
+ const artboard = { x: 0, y: 0, width: doc.width, height: doc.height };
1074
+ self.pushRect(artboard, "artboard", { x: doc.width / 2, y: doc.height / 2 });
1075
+ let anchors = BUDGET;
1076
+ let segments = BUDGET;
1077
+ const visit = (list, parent) => {
1078
+ for (const n of list) {
1079
+ if (exclude.has(n.id) || n.visible === false) continue;
1080
+ const m = multiplyMatrices(nodeMatrix(n), parent);
1081
+ if (n.type === "group") {
1082
+ visit(n.children, m);
1083
+ continue;
1084
+ }
1085
+ if (anchors <= 0) continue;
1086
+ for (const sp of n.subpaths) {
1087
+ for (const a of sp.anchors) {
1088
+ if (anchors-- <= 0) break;
1089
+ const p = applyMatrix(m, a);
1090
+ if (gather === "drawing") self.pushAnchor(p);
1091
+ else self.points.push({ p, kind: "anchor" });
1092
+ }
1093
+ if (gather !== "bounds") segments = self.pushSegments(sp, m, () => false, segments);
1094
+ }
1095
+ const b = vectorNodeBounds(n, parent);
1096
+ const f = nodeFrame(n, parent, b);
1097
+ if (b) self.pushRect(b, "edge", _nullishCoalesce(_optionalChain([f, 'optionalAccess', _13 => _13.centre]), () => ( null)));
1098
+ }
1099
+ };
1100
+ visit(doc.nodes, IDENTITY_MATRIX);
1101
+ const objects = (list, parent) => {
1102
+ for (const n of list) {
1103
+ if (exclude.has(n.id) || n.visible === false || self.boxes.length >= BUDGET) continue;
1104
+ if (n.type === "group" && contains(n, exclude)) {
1105
+ objects(n.children, multiplyMatrices(nodeMatrix(n), parent));
1106
+ continue;
1107
+ }
1108
+ const b = vectorNodeBounds(n, parent);
1109
+ if (b) self.boxes.push(b);
1110
+ const f = nodeFrame(n, parent, b);
1111
+ if (f) self.sizes.push(f.width, f.height);
1112
+ }
1113
+ };
1114
+ objects(doc.nodes, IDENTITY_MATRIX);
1115
+ return sort ? self.index() : self;
1116
+ }
1117
+ // -------------------------------------------------------------------------
1118
+ // Points
1119
+ // -------------------------------------------------------------------------
1120
+ /** Snaps a single point (see `snapFrom`). */
1121
+ snapPoint(p, tol) {
1122
+ return this.snapFrom(p, null, tol);
1123
+ }
1124
+ /**
1125
+ * Snaps a point `p`, drawn or dragged from `leave` when given. With `leave.constrain` it keeps
1126
+ * to its angle round `leave.at`, sliding along that way into line with the nearest target.
1127
+ * Otherwise it lands, in order, on an anchor or centre, an intersection of two paths, the
1128
+ * perpendicular foot or a tangent point from `leave.at`, a path, a construction guide
1129
+ * through `leave.at` (where a target's line crosses it if one does nearby), or else lines up
1130
+ * with targets on x and y.
1131
+ */
1132
+ snapFrom(p, leave, tol) {
1133
+ const none = (q) => ({ point: q, kind: null, x: false, y: false, lines: [], labels: [] });
1134
+ const at = (q, kind, lines2 = []) => ({
1135
+ point: q,
1136
+ kind,
1137
+ x: true,
1138
+ y: true,
1139
+ lines: lines2,
1140
+ labels: [this.label(q, kind)]
1141
+ });
1142
+ if (_optionalChain([leave, 'optionalAccess', _14 => _14.constrain])) {
1143
+ const q = constrainToLeave(leave, p);
1144
+ const hit = this.slide(leave.at, { x: q.x - leave.at.x, y: q.y - leave.at.y }, q, tol, true);
1145
+ return hit ? { point: hit.point, x: true, y: true, ...this.linedUp(hit.point, hit.from) } : none(q);
1146
+ }
1147
+ const point = this.nearestPoint(p, tol);
1148
+ if (point) return at(point.p, point.kind);
1149
+ const near2 = this.segmentsNear(p, tol);
1150
+ const crossing = this.crossing(near2, p, tol);
1151
+ if (crossing) return at(crossing, "intersect");
1152
+ if (leave && leave.curves !== false) {
1153
+ const foot = this.curvePoint(near2, leave.at, p, tol);
1154
+ if (foot) return at(foot.point, foot.kind, [line("construction", leave.at, foot.point)]);
1155
+ }
1156
+ const on = this.onSegment(near2);
1157
+ if (on) return at(on, "path");
1158
+ if (_optionalChain([leave, 'optionalAccess', _15 => _15.angles, 'optionalAccess', _16 => _16.length])) {
1159
+ const c = construction(leave, p, tol);
1160
+ if (c) {
1161
+ const hit = this.slide(leave.at, c.dir, c.point, tol, false);
1162
+ if (hit) {
1163
+ const lined = this.linedUp(hit.point, hit.from);
1164
+ return { point: hit.point, x: true, y: true, kind: lined.kind, lines: [line("construction", leave.at, hit.point), ...lined.lines], labels: lined.labels };
1165
+ }
1166
+ return { point: c.point, kind: "construction", x: true, y: true, lines: [line("construction", leave.at, c.point)], labels: [] };
1167
+ }
1168
+ }
1169
+ const nearest = (lines2, v) => {
1170
+ let best = null;
1171
+ for (const l of linesNear(lines2, v, tol)) if (!best || Math.abs(l.v - v) < Math.abs(best.v - v)) best = l;
1172
+ return best;
1173
+ };
1174
+ const bx = nearest(this.xs, p.x);
1175
+ const by = nearest(this.ys, p.y);
1176
+ if (!bx && !by) return none(p);
1177
+ const out = { x: bx ? bx.v : p.x, y: by ? by.v : p.y };
1178
+ const lines = [];
1179
+ if (bx) lines.push(line("align", bx.from, { x: out.x, y: out.y }));
1180
+ if (by) lines.push(line("align", by.from, { x: out.x, y: out.y }));
1181
+ return { point: out, kind: "align", x: !!bx, y: !!by, lines, labels: [this.label(out, "align")] };
1182
+ }
1183
+ label(p, kind, text = LABELS[kind]) {
1184
+ return { kind, x: p.x, y: p.y, text };
1185
+ }
1186
+ /** The anchor or centre nearest `p`, within `tol`. */
1187
+ nearestPoint(p, tol) {
1188
+ let best = null;
1189
+ let bestD = Infinity;
1190
+ for (const t of within(this.points, (e) => e.p.x, p.x, tol)) {
1191
+ const d = distance(t.p, p);
1192
+ if (d <= tol && d < bestD) {
1193
+ best = t;
1194
+ bestD = d;
1195
+ }
1196
+ }
1197
+ return best;
1198
+ }
1199
+ /**
1200
+ * Slides `q`, on the line through `at` along `dir` (beyond `at` only for a `ray`), to where
1201
+ * the nearest target line crosses it, at most twice `tol` away: where it lands and the
1202
+ * target point that line comes from.
1203
+ */
1204
+ slide(at, dir, q, tol, ray) {
1205
+ const len2 = dir.x * dir.x + dir.y * dir.y;
1206
+ if (!Number.isFinite(len2) || len2 <= 1e-18) return null;
1207
+ const reach = 2 * tol;
1208
+ let best = null;
1209
+ for (const vertical of [true, false]) {
1210
+ const d = vertical ? dir.x : dir.y;
1211
+ if (Math.abs(d) <= 1e-9) continue;
1212
+ const a = vertical ? at.x : at.y;
1213
+ for (const l of linesNear(vertical ? this.xs : this.ys, vertical ? q.x : q.y, reach)) {
1214
+ const s = (l.v - a) / d;
1215
+ const hit = vertical ? { x: l.v, y: at.y + dir.y * s } : { x: at.x + dir.x * s, y: l.v };
1216
+ const moved = distance(hit, q);
1217
+ if (moved <= reach && (!ray || s > 0) && (!best || moved < best.moved)) best = { moved, point: hit, from: l.from };
1218
+ }
1219
+ }
1220
+ return best && { point: best.point, from: best.from };
1221
+ }
1222
+ /** The guides of a point `q` slid into line with the target at `from`: the line and "align", or the target's own label when `q` sits on it. */
1223
+ linedUp(q, from) {
1224
+ const on = this.nearestPoint(q, 1e-9);
1225
+ if (on) return { kind: on.kind, lines: [], labels: [this.label(on.p, on.kind)] };
1226
+ return { kind: "align", lines: [line("align", from, q)], labels: [this.label(q, "align")] };
1227
+ }
1228
+ /** Target segments whose control box reaches `p` and which pass within `tol` of it, nearest first. */
1229
+ segmentsNear(p, tol) {
1230
+ const out = [];
1231
+ const pt = toPoint(p);
1232
+ for (const s of this.segments) {
1233
+ if (p.x < s.minX - tol || p.x > s.maxX + tol || p.y < s.minY - tol || p.y > s.maxY + tol) continue;
1234
+ const hit = _chunkBGNPT5PEcjs.segmentNearest.call(void 0, s.seg, pt);
1235
+ if (hit.distance <= tol) out.push({ seg: s.seg, t: hit.t, distance: hit.distance });
1236
+ }
1237
+ return out.sort((a, b) => a.distance - b.distance);
1238
+ }
1239
+ onSegment(near2) {
1240
+ return near2.length ? toVec(_chunkBGNPT5PEcjs.segmentPoint.call(void 0, near2[0].seg, near2[0].t)) : null;
1241
+ }
1242
+ /** Where two of the target segments near `p` cross, the crossing nearest `p` within `tol`. */
1243
+ crossing(near2, p, tol) {
1244
+ const list = near2.slice(0, CROSSING_PAIRS);
1245
+ let best = null;
1246
+ let bestD = Infinity;
1247
+ for (let i = 0; i < list.length; i++) {
1248
+ for (let j = i + 1; j < list.length; j++) {
1249
+ for (const hit of _chunkBGNPT5PEcjs.segmentIntersections.call(void 0, list[i].seg, list[j].seg)) {
1250
+ const q = toVec(hit.point);
1251
+ const d = distance(q, p);
1252
+ if (d <= tol && d < bestD) {
1253
+ best = q;
1254
+ bestD = d;
1255
+ }
1256
+ }
1257
+ }
1258
+ }
1259
+ return best;
1260
+ }
1261
+ /**
1262
+ * The foot of a perpendicular, or a tangent point, from `base` on a target segment near `p`
1263
+ * (within `tol` of it): roots of `(B(t) − base) · B′(t)` and `(B(t) − base) × B′(t)`
1264
+ * bracketed by sign changes of samples and narrowed by bisection.
1265
+ */
1266
+ curvePoint(near2, base, p, tol) {
1267
+ let best = null;
1268
+ for (const { seg } of near2) {
1269
+ for (const kind of ["perpendicular", "tangent"]) {
1270
+ if (kind === "tangent" && seg.length === 2) continue;
1271
+ for (const q of solveOnSegment(seg, base, kind === "perpendicular" ? dotCondition : crossCondition)) {
1272
+ const d = distance(q, p);
1273
+ if (d <= tol && (!best || d < best.d)) best = { point: q, kind, d };
1274
+ }
1275
+ }
1276
+ }
1277
+ return best && { point: best.point, kind: best.kind };
1278
+ }
1279
+ // -------------------------------------------------------------------------
1280
+ // Boxes
1281
+ // -------------------------------------------------------------------------
1282
+ /**
1283
+ * Snaps a moving box `r` (the moved selection's bounds): its edge or centre nearest a target
1284
+ * line on each axis (`axes` limits which may move). With `spacing`, an axis that lines up
1285
+ * with nothing spaces the box evenly in its row or column. Returns the correction and guides.
1286
+ */
1287
+ snapRect(r, tol, options = {}) {
1288
+ const axes = { x: _optionalChain([options, 'access', _17 => _17.axes, 'optionalAccess', _18 => _18.x]) !== false, y: _optionalChain([options, 'access', _19 => _19.axes, 'optionalAccess', _20 => _20.y]) !== false };
1289
+ const nearest = (lines, values) => {
1290
+ let best = null;
1291
+ for (const v of values) for (const l of linesNear(lines, v, tol)) if (!best || Math.abs(l.v - v) < Math.abs(best.d)) best = { d: l.v - v, from: l.from };
1292
+ return best;
1293
+ };
1294
+ const bx = axes.x ? nearest(this.xs, [r.x, r.x + r.width / 2, r.x + r.width]) : null;
1295
+ const by = axes.y ? nearest(this.ys, [r.y, r.y + r.height / 2, r.y + r.height]) : null;
1296
+ const out = { dx: _nullishCoalesce(_optionalChain([bx, 'optionalAccess', _21 => _21.d]), () => ( 0)), dy: _nullishCoalesce(_optionalChain([by, 'optionalAccess', _22 => _22.d]), () => ( 0)), x: !!bx, y: !!by, lines: [], labels: [] };
1297
+ const done = { ...r, x: r.x + out.dx, y: r.y + out.dy };
1298
+ if (bx) {
1299
+ const x = bx.from.x;
1300
+ out.lines.push({ kind: "align", x1: x, y1: Math.min(bx.from.y, done.y), x2: x, y2: Math.max(bx.from.y, done.y + done.height) });
1301
+ }
1302
+ if (by) {
1303
+ const y = by.from.y;
1304
+ out.lines.push({ kind: "align", x1: Math.min(by.from.x, done.x), y1: y, x2: Math.max(by.from.x, done.x + done.width), y2: y });
1305
+ }
1306
+ if (options.spacing && this.boxes.length) {
1307
+ for (const horizontal of [true, false]) {
1308
+ if (horizontal ? out.x || !axes.x : out.y || !axes.y) continue;
1309
+ const moved = { ...r, x: r.x + out.dx, y: r.y + out.dy };
1310
+ const even = this.evenSpacing(moved, horizontal, tol);
1311
+ if (!even) continue;
1312
+ if (horizontal) {
1313
+ out.dx += even.shift;
1314
+ out.x = true;
1315
+ } else {
1316
+ out.dy += even.shift;
1317
+ out.y = true;
1318
+ }
1319
+ for (const gap of even.gaps) this.gapMarks(gap, horizontal, tol, out);
1320
+ }
1321
+ }
1322
+ if (bx || by) out.labels.unshift(this.label({ x: r.x + out.dx + r.width / 2, y: r.y + out.dy + r.height / 2 }, "align"));
1323
+ return out;
1324
+ }
1325
+ /**
1326
+ * Equal spacing along x (`horizontal`) or y for the moved box `r`, among the objects of its
1327
+ * row (column): the shift within `tol` that centres it between its neighbours, or leaves it
1328
+ * as far from the neighbour before or after it as two other objects of the row are apart;
1329
+ * and the gaps that are equal then, as (start, end, where across).
1330
+ */
1331
+ evenSpacing(r, horizontal, tol) {
1332
+ const span = (b, x) => x ? [b.x, b.x + b.width] : [b.y, b.y + b.height];
1333
+ const [r0, r1] = span(r, horizontal);
1334
+ const len = r1 - r0;
1335
+ const [c0, c1] = span(r, !horizontal);
1336
+ const row = this.boxes.map((b) => ({ b, a0: span(b, horizontal)[0], a1: span(b, horizontal)[1] })).filter((e) => span(e.b, !horizontal)[0] < c1 && span(e.b, !horizontal)[1] > c0 && (e.a1 <= r0 + tol || e.a0 >= r1 - tol)).sort((a, b) => a.a0 - b.a0);
1337
+ if (!row.length) return null;
1338
+ const mid = (a, b) => (Math.max(span(a, !horizontal)[0], span(b, !horizontal)[0]) + Math.min(span(a, !horizontal)[1], span(b, !horizontal)[1])) / 2;
1339
+ const pairs = [];
1340
+ for (const p of row) {
1341
+ const next = bound(row, (e) => e.a0, p.a1, false);
1342
+ const q = row[next];
1343
+ if (!q) continue;
1344
+ const straddles = p.a1 <= r0 + tol && q.a0 >= r1 - tol;
1345
+ if (q.a0 > p.a1 && !straddles) pairs.push({ g: q.a0 - p.a1, gap: [p.a1, q.a0, mid(p.b, q.b)] });
1346
+ }
1347
+ let before = null;
1348
+ let after = null;
1349
+ for (const e of row) {
1350
+ if (e.a1 <= r0 + tol && (!before || e.a1 > before.a1)) before = e;
1351
+ if (e.a0 >= r1 - tol && (!after || e.a0 < after.a0)) after = e;
1352
+ }
1353
+ const starts = [];
1354
+ if (before && after && after.a0 - before.a1 > len) starts.push({ start: (before.a1 + after.a0 - len) / 2, repeats: null });
1355
+ for (const { g } of pairs) {
1356
+ if (before) starts.push({ start: before.a1 + g, repeats: g });
1357
+ if (after) starts.push({ start: after.a0 - g - len, repeats: g });
1358
+ }
1359
+ let pick = null;
1360
+ for (const s of starts) if (Math.abs(s.start - r0) <= tol && (!pick || Math.abs(s.start - r0) < Math.abs(pick.start - r0))) pick = s;
1361
+ if (!pick) return null;
1362
+ const { start, repeats } = pick;
1363
+ const at = horizontal ? { ...r, x: start } : { ...r, y: start };
1364
+ const end = start + len;
1365
+ const gaps = [];
1366
+ if (before && (repeats === null || Math.abs(start - before.a1 - repeats) <= EQUAL)) gaps.push([before.a1, start, mid(before.b, at)]);
1367
+ if (after && (repeats === null || Math.abs(after.a0 - end - repeats) <= EQUAL)) gaps.push([end, after.a0, mid(after.b, at)]);
1368
+ if (repeats !== null) {
1369
+ for (const p of pairs) if (Math.abs(p.g - repeats) <= EQUAL) gaps.push(p.gap);
1370
+ }
1371
+ return { shift: start - r0, gaps };
1372
+ }
1373
+ /** A gap mark: a line across the gap with a tick at each end, and its length as a label. */
1374
+ gapMarks([u0, u1, c], horizontal, tick, out) {
1375
+ const pt = (u, v) => horizontal ? { x: u, y: v } : { x: v, y: u };
1376
+ out.lines.push(line("spacing", pt(u0, c), pt(u1, c)), line("spacing", pt(u0, c - tick), pt(u0, c + tick)), line("spacing", pt(u1, c - tick), pt(u1, c + tick)));
1377
+ out.labels.push(this.label(pt((u0 + u1) / 2, c), "spacing", fmtLength(u1 - u0)));
1378
+ }
1379
+ /** The other objects' side length nearest `length`, within `tol`, or null. */
1380
+ matchSize(length2, tol) {
1381
+ let best = null;
1382
+ for (const s of this.sizes) if (Math.abs(s - length2) <= tol && (best === null || Math.abs(s - length2) < Math.abs(best - length2))) best = s;
1383
+ return best;
1384
+ }
1385
+ /**
1386
+ * Snaps a selection-box resize. `sx / sy` are the scale `scaleFor` gave for dragging
1387
+ * `handle` of `frame` (in the box's own, possibly rotated frame), `mods.shift` keeps the
1388
+ * proportions and `mods.alt` scales about the centre. The handle lands on an anchor or
1389
+ * centre within `tol`, else the corner or the side it moves lines up with the nearest
1390
+ * target, whatever the box's angle; a handle with one way to go (a side, or a proportional
1391
+ * corner) slides along it until one of them is on a target. An axis that snaps to nothing
1392
+ * may match another object's width or height instead.
1393
+ */
1394
+ snapScale(frame, handle, sx, sy, mods, tol) {
1395
+ const toDoc = frame.angle ? rotateMatrix(frame.angle) : IDENTITY_MATRIX;
1396
+ const toLocal = frame.angle ? rotateMatrix(-frame.angle) : IDENTITY_MATRIX;
1397
+ const corner = handle.length === 2;
1398
+ const proportional = !!mods.shift;
1399
+ const centre = { x: frame.x + frame.width / 2, y: frame.y + frame.height / 2 };
1400
+ const origin = mods.alt ? centre : handlePos(opposite(handle), frame);
1401
+ const hp = handlePos(handle, frame);
1402
+ const d0 = { x: hp.x - origin.x, y: hp.y - origin.y };
1403
+ const valid = (n, s) => Number.isFinite(n) && n * s > 0;
1404
+ const nearest = (v, lines) => {
1405
+ let best = null;
1406
+ for (const l of linesNear(lines, v, tol)) if (!best || Math.abs(l.v - v) < Math.abs(best.v - v)) best = l;
1407
+ return best;
1408
+ };
1409
+ let nx = sx;
1410
+ let ny = sy;
1411
+ let snap = { hx: null, hy: null, on: null };
1412
+ if (corner && !proportional) {
1413
+ const p = applyMatrix(toDoc, { x: origin.x + d0.x * sx, y: origin.y + d0.y * sy });
1414
+ const hx = nearest(p.x, this.xs);
1415
+ const hy = nearest(p.y, this.ys);
1416
+ const on = this.nearestPoint(p, tol);
1417
+ const tries = [];
1418
+ if (on) tries.push({ tx: on.p.x, ty: on.p.y, s: { hx: null, hy: null, on } });
1419
+ tries.push({ tx: _nullishCoalesce(_optionalChain([hx, 'optionalAccess', _23 => _23.v]), () => ( null)), ty: _nullishCoalesce(_optionalChain([hy, 'optionalAccess', _24 => _24.v]), () => ( null)), s: { hx, hy, on: null } });
1420
+ tries.push({ tx: _nullishCoalesce(_optionalChain([hx, 'optionalAccess', _25 => _25.v]), () => ( null)), ty: null, s: { hx, hy: null, on: null } });
1421
+ tries.push({ tx: null, ty: _nullishCoalesce(_optionalChain([hy, 'optionalAccess', _26 => _26.v]), () => ( null)), s: { hx: null, hy, on: null } });
1422
+ for (const { tx, ty, s } of tries) {
1423
+ if (tx === null && ty === null) continue;
1424
+ const q = applyMatrix(toLocal, { x: _nullishCoalesce(tx, () => ( p.x)), y: _nullishCoalesce(ty, () => ( p.y)) });
1425
+ let cx = (q.x - origin.x) / d0.x;
1426
+ let cy = (q.y - origin.y) / d0.y;
1427
+ if (!frame.angle) {
1428
+ if (tx === null) cx = sx;
1429
+ if (ty === null) cy = sy;
1430
+ }
1431
+ if (valid(cx, sx) && valid(cy, sy)) {
1432
+ nx = cx;
1433
+ ny = cy;
1434
+ snap = s;
1435
+ break;
1436
+ }
1437
+ }
1438
+ } else {
1439
+ const movesX2 = handle === "e" || handle === "w";
1440
+ let e;
1441
+ let c;
1442
+ let s0;
1443
+ if (corner) [e, c, s0] = [{ x: Math.sign(sx), y: Math.sign(sy) }, { x: 0, y: 0 }, Math.abs(sx)];
1444
+ else if (movesX2) [e, c, s0] = proportional ? [{ x: 1, y: Math.sign(sx) }, { x: 0, y: 0 }, sx] : [{ x: 1, y: 0 }, { x: 0, y: 1 }, sx];
1445
+ else [e, c, s0] = proportional ? [{ x: Math.sign(sy), y: 1 }, { x: 0, y: 0 }, sy] : [{ x: 0, y: 1 }, { x: 1, y: 0 }, sy];
1446
+ const i = RING.indexOf(handle);
1447
+ const ends = corner ? [] : [RING[(i + 1) % 8], RING[(i + 7) % 8]];
1448
+ const travel = Math.hypot(e.x * d0.x, e.y * d0.y);
1449
+ let best = null;
1450
+ const consider = (s, inLine, found2) => {
1451
+ const moved = Math.abs(s - s0) * travel;
1452
+ if (!valid(s, s0) || moved > 2 * tol) return;
1453
+ if (!best || (inLine === best.inLine ? moved < best.moved : !inLine)) best = { s, inLine, moved, snap: found2 };
1454
+ };
1455
+ for (const h of [handle, ...ends]) {
1456
+ const hv = handlePos(h, frame);
1457
+ const v = { x: hv.x - origin.x, y: hv.y - origin.y };
1458
+ const base = applyMatrix(toDoc, { x: origin.x + c.x * v.x, y: origin.y + c.y * v.y });
1459
+ const dir = applyMatrix(toDoc, { x: e.x * v.x, y: e.y * v.y });
1460
+ const p = { x: base.x + dir.x * s0, y: base.y + dir.y * s0 };
1461
+ const l2 = dir.x * dir.x + dir.y * dir.y;
1462
+ const on = l2 > 1e-18 ? this.nearestPoint(p, tol) : null;
1463
+ if (on) consider(((on.p.x - base.x) * dir.x + (on.p.y - base.y) * dir.y) / l2, false, { hx: null, hy: null, on });
1464
+ if (Math.abs(dir.x) > 1e-9) {
1465
+ const hit = nearest(p.x, this.xs);
1466
+ if (hit) consider((hit.v - base.x) / dir.x, true, { hx: hit, hy: null, on: null });
1467
+ }
1468
+ if (Math.abs(dir.y) > 1e-9) {
1469
+ const hit = nearest(p.y, this.ys);
1470
+ if (hit) consider((hit.v - base.y) / dir.y, true, { hx: null, hy: hit, on: null });
1471
+ }
1472
+ }
1473
+ const found = best;
1474
+ if (found) {
1475
+ nx = c.x + found.s * e.x;
1476
+ ny = c.y + found.s * e.y;
1477
+ snap = found.snap;
1478
+ }
1479
+ }
1480
+ const out = { sx: nx, sy: ny, snapped: false, lines: [], labels: [] };
1481
+ const handleAt = () => applyMatrix(toDoc, { x: origin.x + d0.x * out.sx, y: origin.y + d0.y * out.sy });
1482
+ if (snap.on) {
1483
+ out.snapped = true;
1484
+ out.labels.push(this.label(snap.on.p, snap.on.kind));
1485
+ return out;
1486
+ }
1487
+ const movesX = handle !== "n" && handle !== "s";
1488
+ const movesY = handle !== "e" && handle !== "w";
1489
+ const sized = [];
1490
+ if (!proportional) {
1491
+ if (movesX && !snap.hx && !(frame.angle && snap.hy) && frame.width > 1e-9) {
1492
+ const len = this.matchSize(Math.abs(out.sx) * frame.width, tol);
1493
+ if (len !== null && len > 0) {
1494
+ out.sx = Math.sign(out.sx || 1) * (len / frame.width);
1495
+ sized.push("x");
1496
+ }
1497
+ }
1498
+ if (movesY && !snap.hy && !(frame.angle && snap.hx) && frame.height > 1e-9) {
1499
+ const len = this.matchSize(Math.abs(out.sy) * frame.height, tol);
1500
+ if (len !== null && len > 0) {
1501
+ out.sy = Math.sign(out.sy || 1) * (len / frame.height);
1502
+ sized.push("y");
1503
+ }
1504
+ }
1505
+ }
1506
+ const x0 = origin.x + (frame.x - origin.x) * out.sx;
1507
+ const x1 = origin.x + (frame.x + frame.width - origin.x) * out.sx;
1508
+ const y0 = origin.y + (frame.y - origin.y) * out.sy;
1509
+ const y1 = origin.y + (frame.y + frame.height - origin.y) * out.sy;
1510
+ const box = { x0: Math.min(x0, x1), x1: Math.max(x0, x1), y0: Math.min(y0, y1), y1: Math.max(y0, y1) };
1511
+ const page = [
1512
+ { x: box.x0, y: box.y0 },
1513
+ { x: box.x1, y: box.y0 },
1514
+ { x: box.x1, y: box.y1 },
1515
+ { x: box.x0, y: box.y1 }
1516
+ ].map((q) => applyMatrix(toDoc, q));
1517
+ const pminY = Math.min(...page.map((q) => q.y));
1518
+ const pmaxY = Math.max(...page.map((q) => q.y));
1519
+ const pminX = Math.min(...page.map((q) => q.x));
1520
+ const pmaxX = Math.max(...page.map((q) => q.x));
1521
+ if (snap.hx) out.lines.push({ kind: "align", x1: snap.hx.v, y1: Math.min(snap.hx.from.y, pminY), x2: snap.hx.v, y2: Math.max(snap.hx.from.y, pmaxY) });
1522
+ if (snap.hy) out.lines.push({ kind: "align", x1: Math.min(snap.hy.from.x, pminX), y1: snap.hy.v, x2: Math.max(snap.hy.from.x, pmaxX), y2: snap.hy.v });
1523
+ if (snap.hx || snap.hy) {
1524
+ out.snapped = true;
1525
+ out.labels.push(this.label(handleAt(), "align"));
1526
+ }
1527
+ for (const axis of sized) {
1528
+ out.snapped = true;
1529
+ const off = tol * 2;
1530
+ const [a, b] = axis === "x" ? [{ x: box.x0, y: box.y1 + off }, { x: box.x1, y: box.y1 + off }] : [{ x: box.x1 + off, y: box.y0 }, { x: box.x1 + off, y: box.y1 }];
1531
+ const pa = applyMatrix(toDoc, a);
1532
+ const pb = applyMatrix(toDoc, b);
1533
+ out.lines.push(line("size", pa, pb));
1534
+ out.labels.push(this.label({ x: (pa.x + pb.x) / 2, y: (pa.y + pb.y) / 2 }, "size", `${axis === "x" ? "W" : "H"} ${fmtLength(axis === "x" ? box.x1 - box.x0 : box.y1 - box.y0)}`));
1535
+ }
1536
+ return out;
1537
+ }
1538
+ }, _class);
1539
+ var RING = ["nw", "n", "ne", "e", "se", "s", "sw", "w"];
1540
+ function opposite(h) {
1541
+ return RING[(RING.indexOf(h) + 4) % 8];
1542
+ }
1543
+ function handlePos(h, r) {
1544
+ const x = h.includes("w") ? r.x : h.includes("e") ? r.x + r.width : r.x + r.width / 2;
1545
+ const y = h.startsWith("n") ? r.y : h.startsWith("s") ? r.y + r.height : r.y + r.height / 2;
1546
+ return { x, y };
1547
+ }
1548
+ function line(kind, a, b) {
1549
+ return { kind, x1: a.x, y1: a.y, x2: b.x, y2: b.y };
1550
+ }
1551
+ function construction(leave, p, tol) {
1552
+ const v = { x: p.x - leave.at.x, y: p.y - leave.at.y };
1553
+ if (Math.hypot(v.x, v.y) <= tol) return null;
1554
+ let best = null;
1555
+ for (const a of _nullishCoalesce(leave.angles, () => ( []))) {
1556
+ const r = a * Math.PI / 180;
1557
+ const exact = (n) => Math.abs(n) < 1e-12 ? 0 : n;
1558
+ const dir = { x: exact(Math.cos(r)), y: exact(-Math.sin(r)) };
1559
+ const k = v.x * dir.x + v.y * dir.y;
1560
+ const point = { x: leave.at.x + dir.x * k, y: leave.at.y + dir.y * k };
1561
+ const d = distance(point, p);
1562
+ if (d <= tol && (!best || d < best.d)) best = { dir, point, d };
1563
+ }
1564
+ return best && { dir: best.dir, point: best.point };
1565
+ }
1566
+ var dotCondition = (r, d) => r[0] * d[0] + r[1] * d[1];
1567
+ var crossCondition = (r, d) => r[0] * d[1] - r[1] * d[0];
1568
+ function solveOnSegment(seg, base, cond) {
1569
+ const b = [base.x, base.y];
1570
+ const g = (t) => {
1571
+ const p = _chunkBGNPT5PEcjs.segmentPoint.call(void 0, seg, t);
1572
+ return cond([p[0] - b[0], p[1] - b[1]], _chunkBGNPT5PEcjs.segmentDerivative.call(void 0, seg, t));
1573
+ };
1574
+ const vals = [];
1575
+ let scale = 0;
1576
+ for (let i = 0; i <= CURVE_SAMPLES; i++) {
1577
+ const t = i / CURVE_SAMPLES;
1578
+ const p = _chunkBGNPT5PEcjs.segmentPoint.call(void 0, seg, t);
1579
+ const d = _chunkBGNPT5PEcjs.segmentDerivative.call(void 0, seg, t);
1580
+ const r = [p[0] - b[0], p[1] - b[1]];
1581
+ vals.push(cond(r, d));
1582
+ scale = Math.max(scale, Math.hypot(r[0], r[1]) * Math.hypot(d[0], d[1]));
1583
+ }
1584
+ if (!(scale > 0) || vals.every((v) => Math.abs(v) <= scale * 1e-12)) return [];
1585
+ const out = [];
1586
+ for (let i = 0; i < CURVE_SAMPLES; i++) {
1587
+ const ga = vals[i];
1588
+ const gb = vals[i + 1];
1589
+ if (!(Number.isFinite(ga) && Number.isFinite(gb)) || ga > 0 === gb > 0 && ga !== 0) continue;
1590
+ let lo = i / CURVE_SAMPLES;
1591
+ let hi = (i + 1) / CURVE_SAMPLES;
1592
+ let flo = ga;
1593
+ for (let k = 0; k < BISECT; k++) {
1594
+ const m = (lo + hi) / 2;
1595
+ const fm = g(m);
1596
+ if (fm === 0) {
1597
+ lo = hi = m;
1598
+ break;
1599
+ }
1600
+ if (fm > 0 === flo > 0) {
1601
+ lo = m;
1602
+ flo = fm;
1603
+ } else hi = m;
1604
+ }
1605
+ const t = (lo + hi) / 2;
1606
+ const p = _chunkBGNPT5PEcjs.segmentPoint.call(void 0, seg, t);
1607
+ const dv = _chunkBGNPT5PEcjs.segmentDerivative.call(void 0, seg, t);
1608
+ const r = Math.hypot(p[0] - b[0], p[1] - b[1]);
1609
+ const dl = Math.hypot(dv[0], dv[1]);
1610
+ if (!(r > 1e-9 && dl > 0) || Math.abs(cond([p[0] - b[0], p[1] - b[1]], dv)) > 1e-6 * r * dl) continue;
1611
+ if (!out.some((q) => Math.hypot(q.x - p[0], q.y - p[1]) <= 1e-9 * (1 + r))) out.push({ x: p[0], y: p[1] });
1612
+ }
1613
+ return out;
1614
+ }
1615
+
1616
+ // src/vector/pathEdit.ts
1617
+ var EPS2 = 1e-9;
1618
+ var unit = (x, y) => {
1619
+ const l = Math.hypot(x, y);
1620
+ return l > 1e-12 && Number.isFinite(l) ? [x / l, y / l] : null;
1621
+ };
1622
+ function cubicOf(sp, i) {
1623
+ const s = subpathSegment(sp, i);
1624
+ if (s.length === 4) return s;
1625
+ const [a, b] = s;
1626
+ return [a, [a[0] + (b[0] - a[0]) / 3, a[1] + (b[1] - a[1]) / 3], [a[0] + 2 * (b[0] - a[0]) / 3, a[1] + 2 * (b[1] - a[1]) / 3], b];
1627
+ }
1628
+ var isLine = (sp, i) => subpathSegment(sp, i).length === 2;
1629
+ function startTangent(c) {
1630
+ for (let k = 1; k < 4; k++) {
1631
+ const u = unit(c[k][0] - c[0][0], c[k][1] - c[0][1]);
1632
+ if (u) return u;
1633
+ }
1634
+ return [1, 0];
1635
+ }
1636
+ function endTangent(c) {
1637
+ for (let k = 2; k >= 0; k--) {
1638
+ const u = unit(c[3][0] - c[k][0], c[3][1] - c[k][1]);
1639
+ if (u) return u;
1640
+ }
1641
+ return [1, 0];
1642
+ }
1643
+ function sample(c, n, out, withStart) {
1644
+ for (let i = withStart ? 0 : 1; i <= n; i++) out.push(_chunkBGNPT5PEcjs.segmentPoint.call(void 0, c, i / n));
1645
+ }
1646
+ var onAnchor = (a, h) => !h || Math.abs(h.x - a.x) < EPS2 && Math.abs(h.y - a.y) < EPS2;
1647
+ function tidy(a, keepType = true) {
1648
+ const next = cloneAnchor(a);
1649
+ if (onAnchor(next, next.in)) delete next.in;
1650
+ if (onAnchor(next, next.out)) delete next.out;
1651
+ const shown = inferAnchorType(next);
1652
+ const type = keepType && a.type ? a.type === "symmetric" && shown !== "symmetric" ? shown === "corner" ? "corner" : "smooth" : a.type === "smooth" && shown === "corner" ? "corner" : a.type : shown;
1653
+ if (type === "corner") delete next.type;
1654
+ else next.type = type;
1655
+ return next;
1656
+ }
1657
+ function neighbours(sp, i) {
1658
+ const n = sp.anchors.length;
1659
+ const wrap = sp.closed && n > 1;
1660
+ return [i > 0 ? i - 1 : wrap ? n - 1 : -1, i < n - 1 ? i + 1 : wrap ? 0 : -1];
1661
+ }
1662
+ function goldenMin(f, a, b) {
1663
+ const R = 0.618033988749895;
1664
+ let x1 = b - R * (b - a);
1665
+ let x2 = a + R * (b - a);
1666
+ let f1 = f(x1);
1667
+ let f2 = f(x2);
1668
+ for (let i = 0; i < 40; i++) {
1669
+ if (f1.error < f2.error || Number.isNaN(f2.error)) {
1670
+ b = x2;
1671
+ x2 = x1;
1672
+ f2 = f1;
1673
+ x1 = b - R * (b - a);
1674
+ f1 = f(x1);
1675
+ } else {
1676
+ a = x1;
1677
+ x1 = x2;
1678
+ f1 = f2;
1679
+ x2 = a + R * (b - a);
1680
+ f2 = f(x2);
1681
+ }
1682
+ }
1683
+ return f1.error <= f2.error ? f1 : f2;
1684
+ }
1685
+ function fitAcross(pts, N, t0, t1) {
1686
+ const fit = (k) => {
1687
+ const u = [];
1688
+ for (let i = 0; i <= N; i++) u.push(k * i / N);
1689
+ for (let i = 1; i <= N; i++) u.push(k + (1 - k) * i / N);
1690
+ return _chunkBGNPT5PEcjs.fitCubicWithTangents.call(void 0, pts, u, t0, t1);
1691
+ };
1692
+ return goldenMin(fit, 0, 1);
1693
+ }
1694
+ function removeAnchorKeepingShape(sp, index) {
1695
+ const anchors = sp.anchors.map(cloneAnchor);
1696
+ const n = anchors.length;
1697
+ if (index < 0 || index >= n) return { anchors, closed: sp.closed };
1698
+ if ((sp.closed || index > 0 && index < n - 1) && n >= 3) {
1699
+ const pi = (index + n - 1) % n;
1700
+ const ni = (index + 1) % n;
1701
+ if (!(isLine(sp, pi) && isLine(sp, index))) {
1702
+ const left = cubicOf(sp, pi);
1703
+ const right = cubicOf(sp, index);
1704
+ const N = 16;
1705
+ const pts = [];
1706
+ sample(left, N, pts, true);
1707
+ sample(right, N, pts, false);
1708
+ const t0 = startTangent(left);
1709
+ const t1 = endTangent(right);
1710
+ const { curve } = fitAcross(pts, N, t0, t1);
1711
+ if ([curve.p1, curve.p2].every((p) => Number.isFinite(p[0]) && Number.isFinite(p[1]))) {
1712
+ anchors[pi] = tidy({ ...anchors[pi], out: { x: curve.p1[0], y: curve.p1[1] } });
1713
+ anchors[ni] = tidy({ ...anchors[ni], in: { x: curve.p2[0], y: curve.p2[1] } });
1714
+ }
1715
+ }
1716
+ }
1717
+ anchors.splice(index, 1);
1718
+ let closed = sp.closed;
1719
+ if (!closed && anchors.length) {
1720
+ anchors[0] = tidy({ ...anchors[0], in: null });
1721
+ const last = anchors.length - 1;
1722
+ anchors[last] = tidy({ ...anchors[last], out: null });
1723
+ }
1724
+ if (anchors.length < 3) closed = closed && anchors.length === 2 && anchors.some((a) => a.in || a.out);
1725
+ return { anchors, closed };
1726
+ }
1727
+ function removeAnchorsKeepingShape(sp, indices) {
1728
+ const order = [...new Set(indices)].filter((i) => i >= 0 && i < sp.anchors.length).sort((a, b) => b - a);
1729
+ return order.reduce((acc, i) => removeAnchorKeepingShape(acc, i), { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed });
1730
+ }
1731
+ function bendSegment(sp, segment, t, p) {
1732
+ const anchors = sp.anchors.map(cloneAnchor);
1733
+ const n = anchors.length;
1734
+ if (segment < 0 || segment >= subpathSegmentCount(sp) || !Number.isFinite(t) || !Number.isFinite(p.x) || !Number.isFinite(p.y)) return { anchors, closed: sp.closed };
1735
+ const tc = Math.min(0.95, Math.max(0.05, t));
1736
+ const ia = segment;
1737
+ const ib = (segment + 1) % n;
1738
+ const a = anchors[ia];
1739
+ const b = anchors[ib];
1740
+ const k = 3 * tc * (1 - tc);
1741
+ const ox = (p.x - (a.x + (b.x - a.x) * tc)) / k;
1742
+ const oy = (p.y - (a.y + (b.y - a.y) * tc)) / k;
1743
+ const c1 = { x: a.x + (b.x - a.x) / 3 + ox, y: a.y + (b.y - a.y) / 3 + oy };
1744
+ const c2 = { x: b.x + (a.x - b.x) / 3 + ox, y: b.y + (a.y - b.y) / 3 + oy };
1745
+ anchors[ia] = moveAnchorHandle(a, "out", c1);
1746
+ anchors[ib] = moveAnchorHandle(b, "in", c2);
1747
+ return { anchors, closed: sp.closed };
1748
+ }
1749
+ function subpathPointAt(sp, segment, t) {
1750
+ const [x, y] = _chunkBGNPT5PEcjs.segmentPoint.call(void 0, subpathSegment(sp, segment), t);
1751
+ return { x, y };
1752
+ }
1753
+ function curvatureMoveAnchor(sp, index, q) {
1754
+ const anchors = sp.anchors.map(cloneAnchor);
1755
+ const a = sp.anchors[index];
1756
+ if (!a) return { anchors, closed: sp.closed };
1757
+ const own = [
1758
+ a.in && !onAnchor(a, a.in) ? { x: a.in.x - a.x, y: a.in.y - a.y } : null,
1759
+ a.out && !onAnchor(a, a.out) ? { x: a.out.x - a.x, y: a.out.y - a.y } : null
1760
+ ];
1761
+ const ends = [null, null];
1762
+ neighbours(sp, index).forEach((j, side) => {
1763
+ if (j < 0 || j === index) return;
1764
+ const b = anchors[j];
1765
+ const old = Math.hypot(a.x - b.x, a.y - b.y);
1766
+ const s = old > 1e-9 ? Math.hypot(q.x - b.x, q.y - b.y) / old : 1;
1767
+ const key = side === 0 ? "out" : "in";
1768
+ const h = b[key];
1769
+ if (h) b[key] = { x: b.x + (h.x - b.x) * s, y: b.y + (h.y - b.y) * s };
1770
+ const o = own[side];
1771
+ if (o) own[side] = { x: o.x * s, y: o.y * s };
1772
+ ends[side] = { x: b.x, y: b.y };
1773
+ });
1774
+ const moved = { x: q.x, y: q.y };
1775
+ if (own[0]) moved.in = { x: q.x + own[0].x, y: q.y + own[0].y };
1776
+ if (own[1]) moved.out = { x: q.x + own[1].x, y: q.y + own[1].y };
1777
+ if (a.type) moved.type = a.type;
1778
+ const [prev, next] = ends;
1779
+ if (a.type && a.type !== "corner" && prev && next) {
1780
+ const dx = next.x - prev.x;
1781
+ const dy = next.y - prev.y;
1782
+ const l = Math.hypot(dx, dy);
1783
+ if (l > 1e-9) {
1784
+ const len = (h, to) => h ? Math.hypot(h.x, h.y) : Math.hypot(q.x - to.x, q.y - to.y) / 3;
1785
+ let li = len(own[0], prev);
1786
+ let lo = len(own[1], next);
1787
+ if (a.type === "symmetric") li = lo = (li + lo) / 2;
1788
+ moved.in = { x: q.x - dx * li / l, y: q.y - dy * li / l };
1789
+ moved.out = { x: q.x + dx * lo / l, y: q.y + dy * lo / l };
1790
+ }
1791
+ }
1792
+ anchors[index] = moved;
1793
+ return { anchors, closed: sp.closed };
1794
+ }
1795
+ function cutSubpathAt(sp, at) {
1796
+ const n = sp.anchors.length;
1797
+ const cuts = [...new Set(at)].filter((i) => Number.isInteger(i) && i >= 0 && i < n && (sp.closed || i > 0 && i < n - 1)).sort((a, b) => a - b);
1798
+ if (!cuts.length || n < 2) return [{ anchors: sp.anchors.map(cloneAnchor), closed: sp.closed }];
1799
+ const first = cuts[0];
1800
+ const run = sp.closed ? Array.from({ length: n + 1 }, (_, i) => sp.anchors[(first + i) % n]) : sp.anchors;
1801
+ const cutSet = new Set(sp.closed ? [...cuts.map((c) => (c - first + n) % n), n] : cuts);
1802
+ const pieces = [];
1803
+ let cur = [];
1804
+ run.forEach((a, i) => {
1805
+ if (!cutSet.has(i)) {
1806
+ cur.push(cloneAnchor(a));
1807
+ return;
1808
+ }
1809
+ if (cur.length) {
1810
+ cur.push(tidy({ ...a, out: null }, false));
1811
+ pieces.push({ anchors: cur, closed: false });
1812
+ cur = [];
1813
+ }
1814
+ if (i < n) cur.push(tidy({ ...a, in: null }, false));
1815
+ });
1816
+ if (cur.length > 1) pieces.push({ anchors: cur, closed: false });
1817
+ return pieces;
1818
+ }
1819
+ function splitSubpathAt(sp, segment, t, endTolerance = 1e-6) {
1820
+ const n = sp.anchors.length;
1821
+ if (segment < 0 || segment >= subpathSegmentCount(sp)) return [{ anchors: sp.anchors.map(cloneAnchor), closed: sp.closed }];
1822
+ if (t <= endTolerance) return cutSubpathAt(sp, [segment]);
1823
+ if (t >= 1 - endTolerance) return cutSubpathAt(sp, [(segment + 1) % n]);
1824
+ const { subpath, index } = insertAnchor(sp, segment, t);
1825
+ return cutSubpathAt(subpath, [index]);
1826
+ }
1827
+ var endOf = (sp, last) => sp.anchors[last ? sp.anchors.length - 1 : 0];
1828
+ var dist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
1829
+ function reverse(sp) {
1830
+ return {
1831
+ closed: sp.closed,
1832
+ anchors: sp.anchors.slice().reverse().map((a) => {
1833
+ const next = { x: a.x, y: a.y };
1834
+ if (a.out) next.in = { ...a.out };
1835
+ if (a.in) next.out = { ...a.in };
1836
+ if (a.type) next.type = a.type;
1837
+ return next;
1838
+ })
1839
+ };
1840
+ }
1841
+ function joinSubpaths(subpaths, tolerance) {
1842
+ const closed = [];
1843
+ let open = [];
1844
+ for (const sp of subpaths) {
1845
+ const copy = { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed };
1846
+ if (sp.closed || sp.anchors.length < 2) {
1847
+ if (sp.anchors.length) closed.push(copy);
1848
+ } else open.push(copy);
1849
+ }
1850
+ if (open.length === 1) {
1851
+ const sp = open[0];
1852
+ const first = sp.anchors[0];
1853
+ const last = sp.anchors[sp.anchors.length - 1];
1854
+ if (sp.anchors.length > 2 && dist(first, last) <= tolerance) {
1855
+ sp.anchors.pop();
1856
+ sp.anchors[0] = tidy({ ...first, in: _nullishCoalesce(last.in, () => ( null)) }, false);
1857
+ } else {
1858
+ sp.anchors[0] = tidy({ ...first, in: null }, false);
1859
+ sp.anchors[sp.anchors.length - 1] = tidy({ ...sp.anchors[sp.anchors.length - 1], out: null }, false);
1860
+ }
1861
+ sp.closed = true;
1862
+ return [...closed, sp];
1863
+ }
1864
+ while (open.length > 1) {
1865
+ let best = { d: Infinity, i: 0, j: 1, ei: true, ej: false };
1866
+ for (let i = 0; i < open.length; i++) {
1867
+ for (let j = i + 1; j < open.length; j++) {
1868
+ for (const ei of [false, true]) {
1869
+ for (const ej of [false, true]) {
1870
+ const d = dist(endOf(open[i], ei), endOf(open[j], ej));
1871
+ if (d < best.d) best = { d, i, j, ei, ej };
1872
+ }
1873
+ }
1874
+ }
1875
+ }
1876
+ let a = open[best.i];
1877
+ let b = open[best.j];
1878
+ open = open.filter((_, k) => k !== best.j);
1879
+ if (!best.ei) a = reverse(a);
1880
+ if (best.ej) b = reverse(b);
1881
+ const anchors = a.anchors;
1882
+ if (best.d <= tolerance) {
1883
+ const [head, ...rest] = b.anchors;
1884
+ const l = anchors[anchors.length - 1];
1885
+ anchors[anchors.length - 1] = tidy({ ...l, out: _nullishCoalesce(head.out, () => ( null)) }, false);
1886
+ anchors.push(...rest);
1887
+ } else {
1888
+ const l = anchors[anchors.length - 1];
1889
+ anchors[anchors.length - 1] = tidy({ ...l, out: null }, false);
1890
+ anchors.push(tidy({ ...b.anchors[0], in: null }, false), ...b.anchors.slice(1));
1891
+ }
1892
+ open[best.i > best.j ? best.i - 1 : best.i] = { anchors, closed: false };
1893
+ }
1894
+ return [...closed, ...open];
1895
+ }
1896
+ function smoothSubpath(sp, amount = 1, only) {
1897
+ const t = Math.min(1, Math.max(0, amount));
1898
+ const orig = sp.anchors;
1899
+ const n = orig.length;
1900
+ const anchors = orig.map(cloneAnchor);
1901
+ if (n < 3 || t === 0) return { anchors, closed: sp.closed };
1902
+ for (let i = 0; i < n; i++) {
1903
+ if (!sp.closed && (i === 0 || i === n - 1)) continue;
1904
+ if (only && !only.has(i)) continue;
1905
+ const prev = orig[(i + n - 1) % n];
1906
+ const next = orig[(i + 1) % n];
1907
+ const p = orig[i];
1908
+ const dir = unit(next.x - prev.x, next.y - prev.y);
1909
+ if (!dir) continue;
1910
+ const li = dist(p, prev) / 3;
1911
+ const lo = dist(p, next) / 3;
1912
+ const hin = _nullishCoalesce(p.in, () => ( p));
1913
+ const hout = _nullishCoalesce(p.out, () => ( p));
1914
+ const target = (h, x, y) => ({ x: h.x + (x - h.x) * t, y: h.y + (y - h.y) * t });
1915
+ anchors[i] = tidy(
1916
+ {
1917
+ x: p.x,
1918
+ y: p.y,
1919
+ in: target(hin, p.x - dir[0] * li, p.y - dir[1] * li),
1920
+ out: target(hout, p.x + dir[0] * lo, p.y + dir[1] * lo)
1921
+ },
1922
+ false
1923
+ );
1924
+ }
1925
+ return { anchors, closed: sp.closed };
1926
+ }
1927
+ function segsOf(sp) {
1928
+ const out = [];
1929
+ for (let i = 0; i < subpathSegmentCount(sp); i++) {
1930
+ const s = subpathSegment(sp, i);
1931
+ out.push(s.length === 2 ? { c: s, line: true } : { c: s, line: false });
1932
+ }
1933
+ return out;
1934
+ }
1935
+ function segsToSubpath(segs, closed, types) {
1936
+ if (!segs.length) return null;
1937
+ const anchors = [];
1938
+ const at = (p) => ({ x: p[0], y: p[1] });
1939
+ anchors.push(at(segs[0].c[0]));
1940
+ for (const s of segs) {
1941
+ const end = s.c[s.c.length - 1];
1942
+ if (!s.line) anchors[anchors.length - 1].out = { x: s.c[1][0], y: s.c[1][1] };
1943
+ const next = at(end);
1944
+ if (!s.line) next.in = { x: s.c[2][0], y: s.c[2][1] };
1945
+ anchors.push(next);
1946
+ }
1947
+ if (closed) {
1948
+ const last = anchors.pop();
1949
+ if (last.in) anchors[0].in = last.in;
1950
+ }
1951
+ const typed = anchors.map((a) => {
1952
+ const type = types.get(`${a.x},${a.y}`);
1953
+ return tidy(type ? { ...a, type } : a, !!type);
1954
+ });
1955
+ return { anchors: typed, closed };
1956
+ }
1957
+ var isZeroSeg = (s) => s.c.every((p) => Math.abs(p[0] - s.c[0][0]) < 1e-9 && Math.abs(p[1] - s.c[0][1]) < 1e-9);
1958
+ function mergePair(a, b, tol) {
1959
+ const a0 = a.c[0];
1960
+ const a3 = a.c[a.c.length - 1];
1961
+ const b3 = b.c[b.c.length - 1];
1962
+ if (a.line && b.line) {
1963
+ const cx = b3[0] - a0[0];
1964
+ const cy = b3[1] - a0[1];
1965
+ const len = Math.hypot(cx, cy);
1966
+ if (len < 1e-12) return null;
1967
+ const mx = a3[0] - a0[0];
1968
+ const my = a3[1] - a0[1];
1969
+ const along = (mx * cx + my * cy) / len;
1970
+ return Math.abs((mx * cy - my * cx) / len) <= tol && along >= 0 && along <= len ? { c: [a0, b3], line: true } : null;
1971
+ }
1972
+ const ca = a.line ? [a0, a0, a3, a3] : a.c;
1973
+ const cb = b.line ? [a3, a3, b3, b3] : b.c;
1974
+ const ta = endTangent(ca);
1975
+ const tb = startTangent(cb);
1976
+ if (ta[0] * tb[0] + ta[1] * tb[1] < 0.99) return null;
1977
+ const pts = [];
1978
+ sample(ca, 16, pts, true);
1979
+ sample(cb, 16, pts, false);
1980
+ const { curve, error } = fitAcross(pts, 16, startTangent(ca), endTangent(cb));
1981
+ return error <= tol ? { c: [curve.p0, curve.p1, curve.p2, curve.p3], line: false } : null;
1982
+ }
1983
+ function removeRedundantPoints(sp, tolerance) {
1984
+ const tol = Math.max(tolerance, 1e-9);
1985
+ const types = new Map(sp.anchors.filter((a) => a.type).map((a) => [`${a.x},${a.y}`, a.type]));
1986
+ const segs = segsOf(sp).filter((s) => !isZeroSeg(s));
1987
+ if (!segs.length) return { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed };
1988
+ const minSegs = sp.closed ? 2 : 1;
1989
+ let changed = true;
1990
+ while (changed && segs.length > minSegs) {
1991
+ changed = false;
1992
+ const n = segs.length;
1993
+ const joints = sp.closed ? n : n - 1;
1994
+ for (let j = 0; j < joints; j++) {
1995
+ const [i0, i1] = sp.closed ? [(j + n - 1) % n, j] : [j, j + 1];
1996
+ const m = mergePair(segs[i0], segs[i1], tol);
1997
+ if (!m) continue;
1998
+ segs[i0] = m;
1999
+ segs.splice(i1, 1);
2000
+ if (sp.closed && i1 === 0) segs.unshift(segs.pop());
2001
+ changed = true;
2002
+ break;
2003
+ }
2004
+ }
2005
+ return _nullishCoalesce(segsToSubpath(segs, sp.closed, types), () => ( { anchors: sp.anchors.map(cloneAnchor), closed: sp.closed }));
2006
+ }
2007
+
2008
+ // src/vector/pathCut.ts
2009
+ var loopArea = (l) => _chunkBGNPT5PEcjs.contourSignedArea.call(void 0, l.segs);
2010
+ function loopSample(l) {
2011
+ const k = l.chord.findIndex((c) => !c);
2012
+ return _chunkBGNPT5PEcjs.segmentPoint.call(void 0, l.segs[k >= 0 ? k : 0], 0.5);
2013
+ }
2014
+ var contains2 = (l, p) => _chunkBGNPT5PEcjs.windingNumber.call(void 0, [l.segs], p) !== 0;
2015
+ function groupFaces(loops) {
2016
+ const usable = loops.filter((l) => l.segs.length && Math.abs(loopArea(l)) > 1e-12);
2017
+ if (!usable.length) return [];
2018
+ const largest = usable.reduce((a, b) => Math.abs(loopArea(b)) > Math.abs(loopArea(a)) ? b : a);
2019
+ const sign = Math.sign(loopArea(largest));
2020
+ const outers = usable.filter((l) => Math.sign(loopArea(l)) === sign).sort((a, b) => Math.abs(loopArea(a)) - Math.abs(loopArea(b)));
2021
+ const faces = new Map(outers.map((o) => [o, { loops: [o] }]));
2022
+ for (const hole of usable.filter((l) => Math.sign(loopArea(l)) !== sign)) {
2023
+ const p = loopSample(hole);
2024
+ const parent = outers.find((o) => Math.abs(loopArea(o)) > Math.abs(loopArea(hole)) && contains2(o, p));
2025
+ if (parent) faces.get(parent).loops.push(hole);
2026
+ }
2027
+ return outers.reverse().map((o) => faces.get(o));
2028
+ }
2029
+ function faceToPath(face) {
2030
+ return _chunkBGNPT5PEcjs.contoursToPath.call(void 0, face.loops.map((l) => ({ segments: l.segs, closed: true })));
2031
+ }
2032
+ function facesOf(path, fillRule) {
2033
+ const normalized = _chunkBGNPT5PEcjs.normalizeBezierPath.call(void 0, path, fillRule);
2034
+ const loops = _chunkBGNPT5PEcjs.pathToContours.call(void 0, normalized).map((c) => {
2035
+ const segs = _chunkBGNPT5PEcjs.closeContour.call(void 0, c, true).map((s) => s.slice());
2036
+ return { segs, chord: segs.map(() => false) };
2037
+ });
2038
+ return groupFaces(loops);
2039
+ }
2040
+ function splitIntoFaces(path, fillRule = "nonzero") {
2041
+ return facesOf(path, fillRule).map(faceToPath);
2042
+ }
2043
+ function rootsInUnit(a, b, c, d, scale) {
2044
+ const f = (t) => ((a * t + b) * t + c) * t + d;
2045
+ const tol = 1e-12 * Math.max(1, scale);
2046
+ const cuts = [0];
2047
+ const qa = 3 * a;
2048
+ const qb = 2 * b;
2049
+ if (Math.abs(qa) > 1e-18) {
2050
+ const disc = qb * qb - 4 * qa * c;
2051
+ if (disc >= 0) {
2052
+ const r = Math.sqrt(disc);
2053
+ for (const t of [(-qb - r) / (2 * qa), (-qb + r) / (2 * qa)].sort((x, y) => x - y)) if (t > 0 && t < 1) cuts.push(t);
2054
+ }
2055
+ } else if (Math.abs(qb) > 1e-18) {
2056
+ const t = -c / qb;
2057
+ if (t > 0 && t < 1) cuts.push(t);
2058
+ }
2059
+ cuts.push(1);
2060
+ const roots = [];
2061
+ const push = (t) => {
2062
+ if (!roots.some((r) => Math.abs(r - t) < 1e-9)) roots.push(t);
2063
+ };
2064
+ for (let i = 0; i < cuts.length - 1; i++) {
2065
+ let lo = cuts[i];
2066
+ let hi = cuts[i + 1];
2067
+ let flo = f(lo);
2068
+ const fhi = f(hi);
2069
+ if (Math.abs(flo) <= tol) push(lo);
2070
+ if (Math.abs(fhi) <= tol) push(hi);
2071
+ if (flo * fhi >= 0) continue;
2072
+ for (let k = 0; k < 80; k++) {
2073
+ const m = (lo + hi) / 2;
2074
+ const fm = f(m);
2075
+ if (fm === 0) {
2076
+ lo = hi = m;
2077
+ break;
2078
+ }
2079
+ if (fm * flo < 0) hi = m;
2080
+ else {
2081
+ lo = m;
2082
+ flo = fm;
2083
+ }
2084
+ }
2085
+ push((lo + hi) / 2);
2086
+ }
2087
+ return roots.sort((x, y) => x - y);
2088
+ }
2089
+ function lineHits(p, q, seg) {
2090
+ const dx = q[0] - p[0];
2091
+ const dy = q[1] - p[1];
2092
+ const len2 = dx * dx + dy * dy;
2093
+ if (len2 < 1e-24) return [];
2094
+ const h = (pt) => (pt[0] - p[0]) * -dy + (pt[1] - p[1]) * dx;
2095
+ let coeffs;
2096
+ if (seg.length === 2) coeffs = [0, 0, h(seg[1]) - h(seg[0]), h(seg[0])];
2097
+ else {
2098
+ const [h0, h1, h2, h3] = seg.map(h);
2099
+ coeffs = [-h0 + 3 * h1 - 3 * h2 + h3, 3 * h0 - 6 * h1 + 3 * h2, -3 * h0 + 3 * h1, h0];
2100
+ }
2101
+ if (coeffs.every((c) => Math.abs(c) < 1e-12 * Math.max(1, len2))) return [];
2102
+ const scale = Math.sqrt(len2) * Math.max(1, ...seg.map((pt) => Math.abs(pt[0]) + Math.abs(pt[1])));
2103
+ const out = [];
2104
+ for (const t of rootsInUnit(...coeffs, scale)) {
2105
+ const point = _chunkBGNPT5PEcjs.segmentPoint.call(void 0, seg, t);
2106
+ const s = ((point[0] - p[0]) * dx + (point[1] - p[1]) * dy) / len2;
2107
+ if (s >= -1e-9 && s <= 1 + 1e-9) out.push({ s: Math.min(1, Math.max(0, s)), t, point });
2108
+ }
2109
+ return out;
2110
+ }
2111
+ var near = (a, b, eps) => Math.abs(a[0] - b[0]) <= eps && Math.abs(a[1] - b[1]) <= eps;
2112
+ var degenerate = (seg, eps) => seg.every((p) => near(p, seg[0], eps));
2113
+ function arc(l, a, b, eps) {
2114
+ const n = l.segs.length;
2115
+ const segs = [];
2116
+ const chord = [];
2117
+ const push = (k, t0, t1) => {
2118
+ const piece = t0 <= 0 && t1 >= 1 ? l.segs[k].slice() : _chunkBGNPT5PEcjs.subSegment.call(void 0, l.segs[k], t0, t1);
2119
+ if (!degenerate(piece, eps)) {
2120
+ segs.push(piece);
2121
+ chord.push(l.chord[k]);
2122
+ }
2123
+ };
2124
+ if (a.seg === b.seg && b.t >= a.t + 1e-12) {
2125
+ push(a.seg, a.t, b.t);
2126
+ return { segs, chord };
2127
+ }
2128
+ push(a.seg, a.t, 1);
2129
+ for (let k = (a.seg + 1) % n; k !== b.seg; k = (k + 1) % n) push(k, 0, 1);
2130
+ push(b.seg, 0, b.t);
2131
+ return { segs, chord };
2132
+ }
2133
+ function polySegs(points, eps) {
2134
+ const out = [];
2135
+ for (let i = 1; i < points.length; i++) if (!near(points[i - 1], points[i], eps)) out.push([points[i - 1], points[i]]);
2136
+ return out;
2137
+ }
2138
+ function knifePath(path, fillRule, knife) {
2139
+ const pts = knife.filter((p) => Number.isFinite(p[0]) && Number.isFinite(p[1]));
2140
+ if (pts.length < 2) return null;
2141
+ let faces = facesOf(path, fillRule);
2142
+ if (!faces.length) return null;
2143
+ const b = _chunkBGNPT5PEcjs.bezierPathBounds.call(void 0, path);
2144
+ const size = b ? Math.max(b.maxX - b.minX, b.maxY - b.minY, 1) : 1;
2145
+ const eps = size * 1e-9;
2146
+ let cursor = -Infinity;
2147
+ let cuts = 0;
2148
+ for (let guard = 0; guard < 512; guard++) {
2149
+ let best = null;
2150
+ faces.forEach((face2, fi2) => {
2151
+ const hits = [];
2152
+ for (let m = 0; m < pts.length - 1; m++) {
2153
+ if (m + 1 < cursor - 1e-9) continue;
2154
+ const kb = { minX: Math.min(pts[m][0], pts[m + 1][0]), maxX: Math.max(pts[m][0], pts[m + 1][0]), minY: Math.min(pts[m][1], pts[m + 1][1]), maxY: Math.max(pts[m][1], pts[m + 1][1]) };
2155
+ face2.loops.forEach(
2156
+ (l, li) => l.segs.forEach((seg, si) => {
2157
+ const sb = _chunkBGNPT5PEcjs.segmentBounds.call(void 0, seg);
2158
+ if (sb.maxX < kb.minX - eps || sb.minX > kb.maxX + eps || sb.maxY < kb.minY - eps || sb.minY > kb.maxY + eps) return;
2159
+ for (const h of lineHits(pts[m], pts[m + 1], seg)) {
2160
+ const s = m + h.s;
2161
+ if (s >= cursor - 1e-9) hits.push({ s, loop: li, seg: si, t: h.t, point: h.point });
2162
+ }
2163
+ })
2164
+ );
2165
+ }
2166
+ hits.sort((x, y) => x.s - y.s);
2167
+ const unique = hits.filter((h, i) => i === 0 || h.s - hits[i - 1].s > 1e-9 || !near(h.point, hits[i - 1].point, eps * 10));
2168
+ const inside = (s) => {
2169
+ const m = Math.min(pts.length - 2, Math.floor(s));
2170
+ const u = s - m;
2171
+ const q = [pts[m][0] + (pts[m + 1][0] - pts[m][0]) * u, pts[m][1] + (pts[m + 1][1] - pts[m][1]) * u];
2172
+ return _chunkBGNPT5PEcjs.windingNumber.call(void 0, face2.loops.map((l) => l.segs), q) !== 0;
2173
+ };
2174
+ for (let i = 0; i < unique.length - 1; i++) {
2175
+ const from2 = unique[i];
2176
+ const to2 = unique[i + 1];
2177
+ if (near(from2.point, to2.point, eps * 10) || !inside((from2.s + to2.s) / 2)) continue;
2178
+ if (!best || from2.s < best.from.s) best = { face: fi2, from: from2, to: to2 };
2179
+ break;
2180
+ }
2181
+ });
2182
+ if (!best) break;
2183
+ const { face: fi, from, to } = best;
2184
+ const face = faces[fi];
2185
+ const corners2 = [from.point];
2186
+ for (let m = Math.floor(from.s) + 1; m <= to.s - 1e-12 && m < pts.length; m++) if (m > from.s) corners2.push(pts[m]);
2187
+ corners2.push(to.point);
2188
+ const forward = polySegs(corners2, eps);
2189
+ const back = polySegs(corners2.slice().reverse(), eps);
2190
+ const la = face.loops[from.loop];
2191
+ const loops = face.loops.filter((_, i) => i !== from.loop && i !== to.loop);
2192
+ if (from.loop === to.loop) {
2193
+ const ab = arc(la, from, to, eps);
2194
+ const ba = arc(la, to, from, eps);
2195
+ loops.push(
2196
+ { segs: [...ab.segs, ...back], chord: [...ab.chord, ...back.map(() => true)] },
2197
+ { segs: [...ba.segs, ...forward], chord: [...ba.chord, ...forward.map(() => true)] }
2198
+ );
2199
+ } else {
2200
+ const lb = face.loops[to.loop];
2201
+ const roundA = arc(la, from, from, eps);
2202
+ const roundB = arc(lb, to, to, eps);
2203
+ loops.push({
2204
+ segs: [...roundA.segs, ...forward, ...roundB.segs, ...back],
2205
+ chord: [...roundA.chord, ...forward.map(() => true), ...roundB.chord, ...back.map(() => true)]
2206
+ });
2207
+ }
2208
+ faces = [...faces.slice(0, fi), ...groupFaces(loops), ...faces.slice(fi + 1)];
2209
+ cursor = to.s;
2210
+ cuts++;
2211
+ }
2212
+ return cuts ? faces.map(faceToPath) : null;
2213
+ }
2214
+ function eraserRegion(points, width) {
2215
+ const pts = points.filter((p) => Number.isFinite(p[0]) && Number.isFinite(p[1]));
2216
+ const r = width / 2;
2217
+ if (!pts.length || !(r > 0)) return [];
2218
+ const moved = pts.some((p) => !near(p, pts[0], 1e-9));
2219
+ if (!moved) {
2220
+ const [cx, cy] = pts[0];
2221
+ const k = r * 0.5522847498307936;
2222
+ return [
2223
+ { type: "M", x: cx, y: cy - r },
2224
+ { type: "C", x1: cx + k, y1: cy - r, x2: cx + r, y2: cy - k, x: cx + r, y: cy },
2225
+ { type: "C", x1: cx + r, y1: cy + k, x2: cx + k, y2: cy + r, x: cx, y: cy + r },
2226
+ { type: "C", x1: cx - k, y1: cy + r, x2: cx - r, y2: cy + k, x: cx - r, y: cy },
2227
+ { type: "C", x1: cx - r, y1: cy - k, x2: cx - k, y2: cy - r, x: cx, y: cy - r },
2228
+ { type: "Z" }
2229
+ ];
2230
+ }
2231
+ const line2 = [{ type: "M", x: pts[0][0], y: pts[0][1] }, ...pts.slice(1).map((p) => ({ type: "L", x: p[0], y: p[1] }))];
2232
+ return _chunkBGNPT5PEcjs.outlineStroke.call(void 0, line2, { width, cap: "round", join: "round" });
2233
+ }
2234
+ function eraseFromFill(path, fillRule, region) {
2235
+ const overlap = _chunkBGNPT5PEcjs.pathBoolean.call(void 0, "intersection", [{ path, fillRule }, { path: region }]).path;
2236
+ if (!_chunkBGNPT5PEcjs.pathToContours.call(void 0, overlap).some((c) => Math.abs(_chunkBGNPT5PEcjs.contourSignedArea.call(void 0, _chunkBGNPT5PEcjs.closeContour.call(void 0, c, true))) > 1e-9)) return null;
2237
+ const rest = _chunkBGNPT5PEcjs.pathBoolean.call(void 0, "difference", [{ path, fillRule }, { path: region }]).path;
2238
+ return splitIntoFaces(rest);
2239
+ }
2240
+ function eraseFromOutline(sp, region) {
2241
+ const regionSegs = _chunkBGNPT5PEcjs.pathToContours.call(void 0, region).flatMap((c) => _chunkBGNPT5PEcjs.closeContour.call(void 0, c, true));
2242
+ if (!regionSegs.length) return null;
2243
+ const rb = _chunkBGNPT5PEcjs.bezierPathBounds.call(void 0, region);
2244
+ const count = subpathSegmentCount(sp);
2245
+ if (!rb || count === 0) return null;
2246
+ const removed = (p) => _chunkBGNPT5PEcjs.pointInBezierPath.call(void 0, region, p, "nonzero");
2247
+ const runs = [];
2248
+ let cur = [];
2249
+ let any = false;
2250
+ for (let i = 0; i < count; i++) {
2251
+ const seg = subpathSegment(sp, i);
2252
+ const sb = _chunkBGNPT5PEcjs.segmentBounds.call(void 0, seg);
2253
+ const ts = [0, 1];
2254
+ if (!(sb.maxX < rb.minX || sb.minX > rb.maxX || sb.maxY < rb.minY || sb.minY > rb.maxY)) {
2255
+ for (const r of regionSegs) {
2256
+ const b = _chunkBGNPT5PEcjs.segmentBounds.call(void 0, r);
2257
+ if (b.maxX < sb.minX || b.minX > sb.maxX || b.maxY < sb.minY || b.minY > sb.maxY) continue;
2258
+ for (const hit of _chunkBGNPT5PEcjs.segmentIntersections.call(void 0, seg, r)) if (hit.ta > 1e-9 && hit.ta < 1 - 1e-9) ts.push(hit.ta);
2259
+ }
2260
+ }
2261
+ ts.sort((a, b) => a - b);
2262
+ for (let k = 0; k < ts.length - 1; k++) {
2263
+ const t0 = ts[k];
2264
+ const t1 = ts[k + 1];
2265
+ if (t1 - t0 < 1e-9) continue;
2266
+ if (removed(_chunkBGNPT5PEcjs.segmentPoint.call(void 0, seg, (t0 + t1) / 2))) {
2267
+ any = true;
2268
+ if (cur.length) runs.push(cur);
2269
+ cur = [];
2270
+ } else cur.push({ seg: i, t0, t1 });
2271
+ }
2272
+ }
2273
+ if (cur.length) runs.push(cur);
2274
+ if (!any) return null;
2275
+ if (sp.closed && runs.length >= 2) {
2276
+ const first = runs[0][0];
2277
+ const lastRun = runs[runs.length - 1];
2278
+ const last = lastRun[lastRun.length - 1];
2279
+ if (first.seg === 0 && first.t0 === 0 && last.seg === count - 1 && last.t1 === 1) lastRun.push(...runs.shift());
2280
+ }
2281
+ const out = [];
2282
+ for (const run of runs) {
2283
+ const anchors = [];
2284
+ for (const { seg, t0, t1 } of run) {
2285
+ const c = _chunkBGNPT5PEcjs.subSegment.call(void 0, subpathSegment(sp, seg), t0, t1);
2286
+ const line2 = c.length === 2;
2287
+ const start = c[0];
2288
+ const end = c[c.length - 1];
2289
+ if (!anchors.length) anchors.push({ x: start[0], y: start[1] });
2290
+ if (!line2) anchors[anchors.length - 1].out = { x: c[1][0], y: c[1][1] };
2291
+ const next = { x: end[0], y: end[1] };
2292
+ if (!line2) next.in = { x: c[2][0], y: c[2][1] };
2293
+ anchors.push(next);
2294
+ }
2295
+ if (anchors.length < 2) continue;
2296
+ for (const a of anchors) {
2297
+ const type = inferAnchorType(a);
2298
+ if (type !== "corner") a.type = type;
2299
+ }
2300
+ out.push({ anchors, closed: false });
2301
+ }
2302
+ return out;
2303
+ }
2304
+ function polylineBounds(points, pad = 0) {
2305
+ if (!points.length) return null;
2306
+ let minX = Infinity;
2307
+ let minY = Infinity;
2308
+ let maxX = -Infinity;
2309
+ let maxY = -Infinity;
2310
+ for (const [x, y] of points) {
2311
+ minX = Math.min(minX, x);
2312
+ minY = Math.min(minY, y);
2313
+ maxX = Math.max(maxX, x);
2314
+ maxY = Math.max(maxY, y);
2315
+ }
2316
+ return { x: minX - pad, y: minY - pad, width: maxX - minX + 2 * pad, height: maxY - minY + 2 * pad };
2317
+ }
2318
+ function simplifyPolyline(points, tolerance) {
2319
+ const n = points.length;
2320
+ if (n < 3) return points.slice();
2321
+ const keep = new Array(n).fill(false);
2322
+ keep[0] = keep[n - 1] = true;
2323
+ const stack = [[0, n - 1]];
2324
+ while (stack.length) {
2325
+ const [a, b] = stack.pop();
2326
+ const [ax, ay] = points[a];
2327
+ const dx = points[b][0] - ax;
2328
+ const dy = points[b][1] - ay;
2329
+ const len = Math.hypot(dx, dy);
2330
+ let far = -1;
2331
+ let farD = 0;
2332
+ for (let i = a + 1; i < b; i++) {
2333
+ const [px, py] = points[i];
2334
+ const d = len < 1e-12 ? Math.hypot(px - ax, py - ay) : Math.abs((px - ax) * dy - (py - ay) * dx) / len;
2335
+ if (d > farD) {
2336
+ farD = d;
2337
+ far = i;
2338
+ }
2339
+ }
2340
+ if (far >= 0 && farD > tolerance) {
2341
+ keep[far] = true;
2342
+ stack.push([a, far], [far, b]);
2343
+ }
2344
+ }
2345
+ return points.filter((_, i) => keep[i]);
2346
+ }
2347
+
2348
+ // src/vector/ink.ts
2349
+ var LazyMouse = (_class2 = class {
2350
+ /** `length` is the string length in the units of the points (0 turns smoothing off). */
2351
+ constructor(length2) {;_class2.prototype.__init7.call(this);_class2.prototype.__init8.call(this);
2352
+ this.length = length2;
2353
+ }
2354
+
2355
+ __init7() {this.pos = null}
2356
+ __init8() {this.last = null}
2357
+ push(p) {
2358
+ this.last = p;
2359
+ if (!(this.length > 0)) return [p];
2360
+ const pos = this.pos;
2361
+ if (!pos) {
2362
+ this.pos = p;
2363
+ return [p];
2364
+ }
2365
+ const dx = p[0] - pos[0];
2366
+ const dy = p[1] - pos[1];
2367
+ const d = Math.hypot(dx, dy);
2368
+ if (d <= this.length) return [];
2369
+ const k = (d - this.length) / d;
2370
+ const next = [pos[0] + dx * k, pos[1] + dy * k];
2371
+ this.pos = next;
2372
+ return [next];
2373
+ }
2374
+ /** The string from the pen to the pointer while they are apart, else null. */
2375
+ string() {
2376
+ const { pos, last } = this;
2377
+ if (!(this.length > 0) || !pos || !last || pos[0] === last[0] && pos[1] === last[1]) return null;
2378
+ return [pos, last];
2379
+ }
2380
+ /** End of stroke: the pen catches up with the pointer (the point to add, if it lagged). */
2381
+ finish() {
2382
+ const { pos, last } = this;
2383
+ if (!(this.length > 0) || !pos || !last || pos[0] === last[0] && pos[1] === last[1]) return [];
2384
+ this.pos = last;
2385
+ return [last];
2386
+ }
2387
+ }, _class2);
2388
+ var VECTOR_INK_MAX_CORNERS = 8;
2389
+ var MAX_POINTS = 1e5;
2390
+ function vectorInkShapeName(shape) {
2391
+ switch (shape.kind) {
2392
+ case "line":
2393
+ return "line";
2394
+ case "rect":
2395
+ return shape.width === shape.height ? "square" : "rectangle";
2396
+ case "ellipse":
2397
+ return shape.width === shape.height ? "circle" : "ellipse";
2398
+ case "polygon":
2399
+ return shape.points.length === 3 ? "triangle" : "polygon";
2400
+ }
2401
+ }
2402
+ var dist2 = (a, b) => Math.hypot(b[0] - a[0], b[1] - a[1]);
2403
+ function bbox(pts) {
2404
+ if (!pts.length) return [0, 0, 0, 0];
2405
+ let [x0, y0] = pts[0];
2406
+ let [x1, y1] = pts[0];
2407
+ for (const [x, y] of pts) {
2408
+ x0 = Math.min(x0, x);
2409
+ y0 = Math.min(y0, y);
2410
+ x1 = Math.max(x1, x);
2411
+ y1 = Math.max(y1, y);
2412
+ }
2413
+ return [x0, y0, x1 - x0, y1 - y0];
2414
+ }
2415
+ function segDistance(a, b, p) {
2416
+ const dx = b[0] - a[0];
2417
+ const dy = b[1] - a[1];
2418
+ const len2 = dx * dx + dy * dy;
2419
+ const t = len2 > 0 ? Math.min(1, Math.max(0, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / len2)) : 0;
2420
+ return dist2([a[0] + t * dx, a[1] + t * dy], p);
2421
+ }
2422
+ function polygonDistance(c, p) {
2423
+ let best = Infinity;
2424
+ for (let i = 0; i < c.length; i++) best = Math.min(best, segDistance(c[i], c[(i + 1) % c.length], p));
2425
+ return best;
2426
+ }
2427
+ function ellipseRms(p, x0, y0, w, h) {
2428
+ const rx = w / 2;
2429
+ const ry = h / 2;
2430
+ if (rx <= 0 || ry <= 0 || !p.length) return Infinity;
2431
+ const cx = x0 + rx;
2432
+ const cy = y0 + ry;
2433
+ let sum = 0;
2434
+ for (const [x, y] of p) sum += (Math.hypot((x - cx) / rx, (y - cy) / ry) - 1) ** 2;
2435
+ return Math.sqrt(sum / p.length);
2436
+ }
2437
+ function snapSquare(x, y, w, h) {
2438
+ if (Math.abs(w - h) <= 0.12 * Math.max(w, h)) {
2439
+ const s = (w + h) / 2;
2440
+ return [x + (w - s) / 2, y + (h - s) / 2, s, s];
2441
+ }
2442
+ return [x, y, w, h];
2443
+ }
2444
+ function turning(a, b, c) {
2445
+ const u = [b[0] - a[0], b[1] - a[1]];
2446
+ const v = [c[0] - b[0], c[1] - b[1]];
2447
+ const lu = Math.hypot(u[0], u[1]);
2448
+ const lv = Math.hypot(v[0], v[1]);
2449
+ if (lu <= 0 || lv <= 0) return 0;
2450
+ return Math.acos(Math.min(1, Math.max(-1, (u[0] * v[0] + u[1] * v[1]) / (lu * lv)))) * 180 / Math.PI;
2451
+ }
2452
+ function rdp(p, eps) {
2453
+ const n = p.length;
2454
+ if (n < 3) return p.slice();
2455
+ const keep = new Array(n).fill(false);
2456
+ keep[0] = keep[n - 1] = true;
2457
+ const stack = [[0, n - 1]];
2458
+ while (stack.length) {
2459
+ const [a, b] = stack.pop();
2460
+ let far = a;
2461
+ let farD = 0;
2462
+ for (let i = a + 1; i < b; i++) {
2463
+ const d = segDistance(p[a], p[b], p[i]);
2464
+ if (d > farD) {
2465
+ farD = d;
2466
+ far = i;
2467
+ }
2468
+ }
2469
+ if (farD > eps) {
2470
+ keep[far] = true;
2471
+ stack.push([a, far], [far, b]);
2472
+ }
2473
+ }
2474
+ return p.filter((_, i) => keep[i]);
2475
+ }
2476
+ function corners(p, diag) {
2477
+ const eps = 0.05 * diag;
2478
+ const start = p[0];
2479
+ let far = 0;
2480
+ let farD = 0;
2481
+ p.forEach((q, i) => {
2482
+ const d = dist2(start, q);
2483
+ if (d > farD) {
2484
+ farD = d;
2485
+ far = i;
2486
+ }
2487
+ });
2488
+ const a = p.slice(0, far);
2489
+ const b = p.slice(far);
2490
+ const ring = rdp([...a, ...b.slice(0, 1)], eps);
2491
+ const back = rdp([...b, start], eps);
2492
+ ring.pop();
2493
+ back.pop();
2494
+ ring.push(...back);
2495
+ while (ring.length > 3) {
2496
+ const n = ring.length;
2497
+ const turn = (i) => turning(ring[(i + n - 1) % n], ring[i], ring[(i + 1) % n]);
2498
+ let flat = 0;
2499
+ let flatT = Infinity;
2500
+ for (let i = 0; i < n; i++) {
2501
+ const t = turn(i);
2502
+ if (t < flatT) {
2503
+ flatT = t;
2504
+ flat = i;
2505
+ }
2506
+ }
2507
+ let short = -1;
2508
+ for (let j = 0; j < n; j++) {
2509
+ if (dist2(ring[j], ring[(j + 1) % n]) < 0.1 * diag) {
2510
+ short = j;
2511
+ break;
2512
+ }
2513
+ }
2514
+ if (flatT < 30) ring.splice(flat, 1);
2515
+ else if (short >= 0) {
2516
+ const k = turn(short) < turn((short + 1) % n) ? short : (short + 1) % n;
2517
+ ring.splice(k, 1);
2518
+ } else break;
2519
+ }
2520
+ return ring;
2521
+ }
2522
+ function upright(c) {
2523
+ const n = c.length;
2524
+ const dirs = c.map((a, i) => {
2525
+ const b = c[(i + 1) % n];
2526
+ const deg = (Math.atan2(b[1] - a[1], b[0] - a[0]) * 180 / Math.PI % 180 + 180) % 180;
2527
+ if (deg <= 15 || deg >= 165) return "across";
2528
+ if (deg >= 75 && deg <= 105) return "down";
2529
+ return null;
2530
+ });
2531
+ return dirs.every((d) => d !== null) && dirs.every((d, i) => d !== dirs[(i + 1) % n]);
2532
+ }
2533
+ function recognizeInkShape(points, options = {}) {
2534
+ const minSize = _nullishCoalesce(options.minSize, () => ( 6));
2535
+ const p = [];
2536
+ for (const q of points.slice(0, MAX_POINTS)) {
2537
+ if (!Number.isFinite(q[0]) || !Number.isFinite(q[1]) || Math.abs(q[0]) >= 1e9 || Math.abs(q[1]) >= 1e9) continue;
2538
+ const last2 = p[p.length - 1];
2539
+ if (!last2 || last2[0] !== q[0] || last2[1] !== q[1]) p.push([q[0], q[1]]);
2540
+ }
2541
+ if (p.length < 2) return null;
2542
+ const first = p[0];
2543
+ const last = p[p.length - 1];
2544
+ const [x0, y0, w, h] = bbox(p);
2545
+ const size = Math.max(w, h);
2546
+ const diag = Math.hypot(w, h);
2547
+ if (size < minSize) return null;
2548
+ let length2 = 0;
2549
+ for (let i = 1; i < p.length; i++) length2 += dist2(p[i - 1], p[i]);
2550
+ const gap = dist2(first, last);
2551
+ if (gap > 0.85 * length2 && gap >= minSize) {
2552
+ const off = Math.max(...p.map((q) => segDistance(first, last, q)));
2553
+ if (off <= Math.max(0.06 * gap, 1.5 * minSize / 6)) return { kind: "line", a: first, b: last };
2554
+ }
2555
+ if (gap > 0.2 * size || length2 < 2 * size) return null;
2556
+ const rms = ellipseRms(p, x0, y0, w, h);
2557
+ const roundish = w > 0.2 * size && h > 0.2 * size;
2558
+ const ellipse = () => {
2559
+ const [x, y, sw, sh] = snapSquare(x0, y0, w, h);
2560
+ return { kind: "ellipse", x, y, width: sw, height: sh };
2561
+ };
2562
+ if (roundish && rms < 0.06) return ellipse();
2563
+ const c = corners(p, diag);
2564
+ const fits = c.length >= 3 && p.every((q) => polygonDistance(c, q) <= 0.08 * diag);
2565
+ if (fits && c.length === 4 && upright(c)) {
2566
+ const [bx, by, bw, bh] = bbox(c);
2567
+ const [x, y, sw, sh] = snapSquare(bx, by, bw, bh);
2568
+ return { kind: "rect", x, y, width: sw, height: sh };
2569
+ }
2570
+ if (fits && c.length <= VECTOR_INK_MAX_CORNERS) return { kind: "polygon", points: c };
2571
+ return roundish && rms < 0.1 ? ellipse() : null;
2572
+ }
2573
+
853
2574
  // src/vector/controller.ts
2575
+ var DEFAULT_VECTOR_SNAP = { grid: 0, objects: true, guides: true, threshold: 5, angles: VECTOR_CONSTRUCTION_ANGLES };
2576
+ var DEFAULT_VECTOR_PATH_EDIT = { keepShapeOnDelete: true, curvatureMoves: false, pencilSmoothing: 0, recognizeShapes: false, eraserSize: 10 };
2577
+ var VECTOR_PENCIL_STRING_PX = 100;
854
2578
  var VECTOR_CLIPBOARD_TYPE = "pixerate/vector";
855
2579
  var DEFAULT_DEFAULTS = {
856
2580
  fill: "#d4d4d8",
@@ -878,7 +2602,7 @@ function getEditablePathIds(state) {
878
2602
  }
879
2603
  }
880
2604
  }
881
- return [...ids].filter((id) => _optionalChain([findVectorNode, 'call', _8 => _8(state.document.nodes, id), 'optionalAccess', _9 => _9.type]) === "path");
2605
+ return [...ids].filter((id) => _optionalChain([findVectorNode, 'call', _27 => _27(state.document.nodes, id), 'optionalAccess', _28 => _28.type]) === "path");
882
2606
  }
883
2607
  function getVisibleAnchors(state) {
884
2608
  const out = [];
@@ -893,7 +2617,7 @@ function getVisibleHandles(state) {
893
2617
  const seen = /* @__PURE__ */ new Set();
894
2618
  const push = (node, s, i, handle) => {
895
2619
  const sp = node.subpaths[s];
896
- const a = _optionalChain([sp, 'optionalAccess', _10 => _10.anchors, 'access', _11 => _11[i]]);
2620
+ const a = _optionalChain([sp, 'optionalAccess', _29 => _29.anchors, 'access', _30 => _30[i]]);
897
2621
  if (!a || !a[handle]) return;
898
2622
  const key = `${node.id}:${s}:${i}:${handle}`;
899
2623
  if (seen.has(key)) return;
@@ -902,8 +2626,8 @@ function getVisibleHandles(state) {
902
2626
  };
903
2627
  if (state.tool === "pen" && state.pen) {
904
2628
  const node = findVectorNode(state.document.nodes, state.pen.nodeId);
905
- const sp = _optionalChain([node, 'optionalAccess', _12 => _12.type]) === "path" ? node.subpaths[state.pen.subpath] : void 0;
906
- if (_optionalChain([node, 'optionalAccess', _13 => _13.type]) === "path" && sp) {
2629
+ const sp = _optionalChain([node, 'optionalAccess', _31 => _31.type]) === "path" ? node.subpaths[state.pen.subpath] : void 0;
2630
+ if (_optionalChain([node, 'optionalAccess', _32 => _32.type]) === "path" && sp) {
907
2631
  const last = sp.anchors.length - 1;
908
2632
  push(node, state.pen.subpath, last, "in");
909
2633
  push(node, state.pen.subpath, last, "out");
@@ -929,7 +2653,7 @@ function getVisibleHandles(state) {
929
2653
  }
930
2654
  function getAnchor(doc, ref) {
931
2655
  const node = findVectorNode(doc.nodes, ref.nodeId);
932
- return _optionalChain([node, 'optionalAccess', _14 => _14.type]) === "path" ? _optionalChain([node, 'access', _15 => _15.subpaths, 'access', _16 => _16[ref.subpath], 'optionalAccess', _17 => _17.anchors, 'access', _18 => _18[ref.index]]) : void 0;
2656
+ return _optionalChain([node, 'optionalAccess', _33 => _33.type]) === "path" ? _optionalChain([node, 'access', _34 => _34.subpaths, 'access', _35 => _35[ref.subpath], 'optionalAccess', _36 => _36.anchors, 'access', _37 => _37[ref.index]]) : void 0;
933
2657
  }
934
2658
  function getAnchorWorld(doc, ref) {
935
2659
  const a = getAnchor(doc, ref);
@@ -969,14 +2693,14 @@ function getVectorSelectionBoxGeometry(frame, viewport) {
969
2693
  const rot = rotateMatrix(frame.angle, { x: viewport.x, y: viewport.y });
970
2694
  const toScreen2 = (p) => frame.angle ? applyMatrix(rot, p) : { x: p.x, y: p.y };
971
2695
  const handles = getTransformHandlePositions(box).map((h) => ({ ...h, screen: toScreen2(h) }));
972
- const corners = [
2696
+ const corners2 = [
973
2697
  { x: box.x, y: box.y },
974
2698
  { x: box.x + box.width, y: box.y },
975
2699
  { x: box.x + box.width, y: box.y + box.height },
976
2700
  { x: box.x, y: box.y + box.height }
977
2701
  ].map(toScreen2);
978
2702
  const r = (n) => Math.round(n * 1e3) / 1e3;
979
- return { box, angle: frame.angle, transform: frame.angle ? `rotate(${r(frame.angle)} ${r(viewport.x)} ${r(viewport.y)})` : "", handles, corners };
2703
+ return { box, angle: frame.angle, transform: frame.angle ? `rotate(${r(frame.angle)} ${r(viewport.x)} ${r(viewport.y)})` : "", handles, corners: corners2 };
980
2704
  }
981
2705
  var RESIZE_CURSOR_ORDER = ["ew-resize", "nwse-resize", "ns-resize", "nesw-resize"];
982
2706
  var HANDLE_ANGLES = { e: 0, se: 45, s: 90, sw: 135, w: 180, nw: 225, n: 270, ne: 315 };
@@ -994,9 +2718,12 @@ var TOOL_LABELS = {
994
2718
  ellipse: "Ellipse",
995
2719
  polygon: "Polygon",
996
2720
  star: "Star",
997
- line: "Line"
2721
+ line: "Line",
2722
+ scissors: "Scissors",
2723
+ knife: "Knife",
2724
+ eraser: "Eraser"
998
2725
  };
999
- var TOOL_KEYS = { v: "select", a: "direct", p: "pen", n: "pencil", r: "rect", o: "ellipse", e: "ellipse", y: "polygon", s: "star", l: "line" };
2726
+ var TOOL_KEYS = { v: "select", a: "direct", p: "pen", n: "pencil", r: "rect", o: "ellipse", y: "polygon", s: "star", l: "line", c: "scissors", k: "knife", e: "eraser" };
1000
2727
  function vectorToolLabel(tool) {
1001
2728
  return TOOL_LABELS[tool];
1002
2729
  }
@@ -1009,7 +2736,10 @@ var VECTOR_TOOL_SHORTCUTS = {
1009
2736
  ellipse: "O",
1010
2737
  polygon: "Y",
1011
2738
  star: "S",
1012
- line: "L"
2739
+ line: "L",
2740
+ scissors: "C",
2741
+ knife: "K",
2742
+ eraser: "E"
1013
2743
  };
1014
2744
  var VECTOR_TOOL_GLYPHS = {
1015
2745
  select: "\u2196",
@@ -1020,8 +2750,12 @@ var VECTOR_TOOL_GLYPHS = {
1020
2750
  ellipse: "\u25EF",
1021
2751
  polygon: "\u2B21",
1022
2752
  star: "\u2606",
1023
- line: "\u2571"
2753
+ line: "\u2571",
2754
+ scissors: "\u2702",
2755
+ knife: "\u27CB",
2756
+ eraser: "\u232B"
1024
2757
  };
2758
+ var SHAPE_TOOLS = /* @__PURE__ */ new Set(["rect", "ellipse", "polygon", "star", "line"]);
1025
2759
  var BOOLEAN_OPS = {
1026
2760
  union: "union",
1027
2761
  subtract: "difference",
@@ -1043,43 +2777,72 @@ function mapTree(nodes, fn) {
1043
2777
  }
1044
2778
  return changed ? out : nodes;
1045
2779
  }
2780
+ function spliceTree(nodes, id, replacements) {
2781
+ let changed = false;
2782
+ const out = [];
2783
+ for (const node of nodes) {
2784
+ if (node.id === id) {
2785
+ changed = true;
2786
+ out.push(...replacements);
2787
+ continue;
2788
+ }
2789
+ if (node.type === "group") {
2790
+ const children = spliceTree(node.children, id, replacements);
2791
+ if (children !== node.children) {
2792
+ changed = true;
2793
+ out.push({ ...node, children });
2794
+ continue;
2795
+ }
2796
+ }
2797
+ out.push(node);
2798
+ }
2799
+ return changed ? out : nodes;
2800
+ }
2801
+ var toPoint2 = (p) => [p.x, p.y];
1046
2802
  var isRoot = (m) => m === IDENTITY_MATRIX || m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0;
1047
2803
  var idCounter = 0;
1048
2804
  var defaultCreateId = () => `vn-${Date.now().toString(36)}${(++idCounter).toString(36)}`;
1049
- var VectorEditorController = (_class = class {
2805
+ var VectorEditorController = (_class3 = class {
2806
+
1050
2807
 
2808
+ __init9() {this.selection = []}
2809
+ __init10() {this.anchors = []}
2810
+ __init11() {this.editingId = null}
2811
+ __init12() {this.pen = null}
1051
2812
 
1052
- __init() {this.selection = []}
1053
- __init2() {this.anchors = []}
1054
- __init3() {this.editingId = null}
1055
- __init4() {this.pen = null}
1056
2813
 
1057
2814
 
1058
2815
 
1059
2816
 
1060
2817
 
1061
- __init5() {this.hover = null}
1062
- __init6() {this.pointer = null}
1063
- __init7() {this.drag = null}
1064
- __init8() {this.interaction = null}
1065
- __init9() {this.spaceHeld = false}
1066
- __init10() {this.clipboard = null}
2818
+ __init13() {this.hover = null}
2819
+ __init14() {this.pointer = null}
2820
+ __init15() {this.drag = null}
2821
+ __init16() {this.interaction = null}
2822
+ __init17() {this.guides = null}
2823
+ /** Whether the last `snapDrawn` landed on an object (guides may be hidden). */
2824
+ __init18() {this.snappedToObject = false}
2825
+ /** Drawing targets for hovering the pen and shape tools, kept while the document and the excluded path stay the same. */
2826
+ __init19() {this.hoverTargets = null}
2827
+ __init20() {this.spaceHeld = false}
2828
+ __init21() {this.clipboard = null}
1067
2829
 
1068
2830
 
1069
- __init11() {this.listeners = /* @__PURE__ */ new Set()}
1070
- __init12() {this.cleanups = []}
1071
- __init13() {this.applying = false}
2831
+ __init22() {this.listeners = /* @__PURE__ */ new Set()}
2832
+ __init23() {this.cleanups = []}
2833
+ __init24() {this.applying = false}
1072
2834
  /** Set while a commit records a change that was already applied, so `restore` still reports it. */
1073
- __init14() {this.committing = false}
1074
- __init15() {this.revision = 0}
2835
+ __init25() {this.committing = false}
2836
+ __init26() {this.revision = 0}
1075
2837
 
1076
- constructor(options = {}) {;_class.prototype.__init.call(this);_class.prototype.__init2.call(this);_class.prototype.__init3.call(this);_class.prototype.__init4.call(this);_class.prototype.__init5.call(this);_class.prototype.__init6.call(this);_class.prototype.__init7.call(this);_class.prototype.__init8.call(this);_class.prototype.__init9.call(this);_class.prototype.__init10.call(this);_class.prototype.__init11.call(this);_class.prototype.__init12.call(this);_class.prototype.__init13.call(this);_class.prototype.__init14.call(this);_class.prototype.__init15.call(this);_class.prototype.__init16.call(this);_class.prototype.__init17.call(this);
2838
+ constructor(options = {}) {;_class3.prototype.__init9.call(this);_class3.prototype.__init10.call(this);_class3.prototype.__init11.call(this);_class3.prototype.__init12.call(this);_class3.prototype.__init13.call(this);_class3.prototype.__init14.call(this);_class3.prototype.__init15.call(this);_class3.prototype.__init16.call(this);_class3.prototype.__init17.call(this);_class3.prototype.__init18.call(this);_class3.prototype.__init19.call(this);_class3.prototype.__init20.call(this);_class3.prototype.__init21.call(this);_class3.prototype.__init22.call(this);_class3.prototype.__init23.call(this);_class3.prototype.__init24.call(this);_class3.prototype.__init25.call(this);_class3.prototype.__init26.call(this);_class3.prototype.__init27.call(this);_class3.prototype.__init28.call(this);
1077
2839
  this.history = _nullishCoalesce(options.history, () => ( new (0, _chunkQKLRAA4Tcjs.HistoryManager)()));
1078
2840
  this.doc = options.document ? cloneVectorDocument(options.document) : { width: 800, height: 600, nodes: [] };
1079
2841
  this.tool = _nullishCoalesce(options.tool, () => ( "select"));
1080
2842
  this.defaults = { ...DEFAULT_DEFAULTS, ..._nullishCoalesce(options.defaults, () => ( {})) };
1081
2843
  this.viewport = { x: 0, y: 0, zoom: 1, ..._nullishCoalesce(options.viewport, () => ( {})) };
1082
- this.snap = { grid: 0, ..._nullishCoalesce(options.snap, () => ( {})) };
2844
+ this.snap = { ...DEFAULT_VECTOR_SNAP, ..._nullishCoalesce(options.snap, () => ( {})) };
2845
+ this.editing = { ...DEFAULT_VECTOR_PATH_EDIT, ..._nullishCoalesce(options.editing, () => ( {})) };
1083
2846
  this.hitRadius = _nullishCoalesce(options.hitRadius, () => ( 6));
1084
2847
  this.createId = _nullishCoalesce(options.createId, () => ( defaultCreateId));
1085
2848
  this.onChange = options.onChange;
@@ -1101,9 +2864,9 @@ var VectorEditorController = (_class = class {
1101
2864
  // State
1102
2865
  // -------------------------------------------------------------------------
1103
2866
  /** The current snapshot (stable until the next change). */
1104
- __init16() {this.getState = () => this.state}
2867
+ __init27() {this.getState = () => this.state}
1105
2868
  /** Calls `listener` after every change. Returns an unsubscribe function. */
1106
- __init17() {this.subscribe = (listener) => {
2869
+ __init28() {this.subscribe = (listener) => {
1107
2870
  this.listeners.add(listener);
1108
2871
  return () => {
1109
2872
  this.listeners.delete(listener);
@@ -1128,6 +2891,8 @@ var VectorEditorController = (_class = class {
1128
2891
  viewport: this.viewport,
1129
2892
  defaults: this.defaults,
1130
2893
  snap: this.snap,
2894
+ editing: this.editing,
2895
+ guides: this.guides,
1131
2896
  hitRadius: this.hitRadius,
1132
2897
  hover: this.hover,
1133
2898
  cursor: this.cursorFor(),
@@ -1144,7 +2909,7 @@ var VectorEditorController = (_class = class {
1144
2909
  notify(docChanged = false) {
1145
2910
  this.revision++;
1146
2911
  this.state = this.computeState();
1147
- if (docChanged) _optionalChain([this, 'access', _19 => _19.onChange, 'optionalCall', _20 => _20(this.doc)]);
2912
+ if (docChanged) _optionalChain([this, 'access', _38 => _38.onChange, 'optionalCall', _39 => _39(this.doc)]);
1148
2913
  const state = this.state;
1149
2914
  this.listeners.forEach((listener) => {
1150
2915
  try {
@@ -1174,20 +2939,20 @@ var VectorEditorController = (_class = class {
1174
2939
  return applyMatrix(invertMatrix(this.worldOf(id, doc)), p);
1175
2940
  }
1176
2941
  cursorFor() {
1177
- if (_optionalChain([this, 'access', _21 => _21.drag, 'optionalAccess', _22 => _22.kind]) === "pan" || this.spaceHeld) return this.drag ? "grabbing" : "grab";
1178
- if (_optionalChain([this, 'access', _23 => _23.drag, 'optionalAccess', _24 => _24.kind]) === "rotate") return "grabbing";
1179
- if (_optionalChain([this, 'access', _25 => _25.drag, 'optionalAccess', _26 => _26.kind]) === "move") return "move";
2942
+ if (_optionalChain([this, 'access', _40 => _40.drag, 'optionalAccess', _41 => _41.kind]) === "pan" || this.spaceHeld) return this.drag ? "grabbing" : "grab";
2943
+ if (_optionalChain([this, 'access', _42 => _42.drag, 'optionalAccess', _43 => _43.kind]) === "rotate") return "grabbing";
2944
+ if (_optionalChain([this, 'access', _44 => _44.drag, 'optionalAccess', _45 => _45.kind]) === "move") return "move";
1180
2945
  const h = this.hover;
1181
2946
  switch (this.tool) {
1182
2947
  case "select":
1183
- if (_optionalChain([h, 'optionalAccess', _27 => _27.transformHandle])) return vectorResizeCursor(h.transformHandle, _nullishCoalesce(_optionalChain([this, 'access', _28 => _28.frameOf, 'call', _29 => _29(this.selectedRoots()), 'optionalAccess', _30 => _30.angle]), () => ( 0)));
1184
- return _optionalChain([h, 'optionalAccess', _31 => _31.nodeId]) ? "move" : "default";
2948
+ if (_optionalChain([h, 'optionalAccess', _46 => _46.transformHandle])) return vectorResizeCursor(h.transformHandle, _nullishCoalesce(_optionalChain([this, 'access', _47 => _47.frameOf, 'call', _48 => _48(this.selectedRoots()), 'optionalAccess', _49 => _49.angle]), () => ( 0)));
2949
+ return _optionalChain([h, 'optionalAccess', _50 => _50.nodeId]) ? "move" : "default";
1185
2950
  case "direct":
1186
- if (_optionalChain([h, 'optionalAccess', _32 => _32.anchor]) || _optionalChain([h, 'optionalAccess', _33 => _33.handle])) return "pointer";
1187
- if (_optionalChain([h, 'optionalAccess', _34 => _34.segment])) return "copy";
1188
- return _optionalChain([h, 'optionalAccess', _35 => _35.nodeId]) ? "move" : "default";
2951
+ if (_optionalChain([h, 'optionalAccess', _51 => _51.anchor]) || _optionalChain([h, 'optionalAccess', _52 => _52.handle])) return "pointer";
2952
+ if (_optionalChain([h, 'optionalAccess', _53 => _53.segment])) return "copy";
2953
+ return _optionalChain([h, 'optionalAccess', _54 => _54.nodeId]) ? "move" : "default";
1189
2954
  case "pen":
1190
- return _optionalChain([h, 'optionalAccess', _36 => _36.anchor]) ? "pointer" : "crosshair";
2955
+ return _optionalChain([h, 'optionalAccess', _55 => _55.anchor]) ? "pointer" : "crosshair";
1191
2956
  default:
1192
2957
  return "crosshair";
1193
2958
  }
@@ -1207,7 +2972,7 @@ var VectorEditorController = (_class = class {
1207
2972
  this.selection = s.selection.filter((id) => this.getNode(id));
1208
2973
  this.anchors = s.anchors.filter((r) => getAnchor(this.doc, r));
1209
2974
  this.editingId = s.editingId && this.getNode(s.editingId) ? s.editingId : null;
1210
- this.pen = s.pen && _optionalChain([this, 'access', _37 => _37.getNode, 'call', _38 => _38(s.pen.nodeId), 'optionalAccess', _39 => _39.subpaths, 'optionalAccess', _40 => _40[s.pen.subpath]]) ? s.pen : null;
2975
+ this.pen = s.pen && _optionalChain([this, 'access', _56 => _56.getNode, 'call', _57 => _57(s.pen.nodeId), 'optionalAccess', _58 => _58.subpaths, 'optionalAccess', _59 => _59[s.pen.subpath]]) ? s.pen : null;
1211
2976
  } finally {
1212
2977
  this.applying = false;
1213
2978
  }
@@ -1312,6 +3077,7 @@ var VectorEditorController = (_class = class {
1312
3077
  this.cancelDrag();
1313
3078
  this.tool = tool;
1314
3079
  this.hover = null;
3080
+ this.guides = null;
1315
3081
  if (tool !== "direct") {
1316
3082
  this.anchors = [];
1317
3083
  this.editingId = null;
@@ -1324,18 +3090,82 @@ var VectorEditorController = (_class = class {
1324
3090
  }
1325
3091
  setSnap(snap) {
1326
3092
  this.snap = { ...this.snap, ...snap };
3093
+ if (!this.snap.guides || !this.snap.objects) this.guides = null;
1327
3094
  this.notify();
1328
3095
  }
1329
- snapPoint(p) {
1330
- const g = this.snap.grid;
1331
- if (!g || g <= 0) return p;
1332
- return { x: Math.round(p.x / g) * g, y: Math.round(p.y / g) * g };
3096
+ /** Changes how deleting, dragging anchors, the pencil and the eraser behave. */
3097
+ setEditing(editing) {
3098
+ const next = { ...this.editing, ...editing };
3099
+ next.pencilSmoothing = Math.min(1, Math.max(0, Number.isFinite(next.pencilSmoothing) ? next.pencilSmoothing : 0));
3100
+ if (!(next.eraserSize > 0)) next.eraserSize = this.editing.eraserSize;
3101
+ this.editing = next;
3102
+ this.notify();
3103
+ }
3104
+ /** Turns object snapping on or off (the toolbar's snapping toggle). */
3105
+ toggleSnapping(on = !this.snap.objects) {
3106
+ this.setSnap({ objects: on });
1333
3107
  }
1334
- snapDelta(dx, dy, origin) {
3108
+ gridValue(v) {
1335
3109
  const g = this.snap.grid;
1336
- if (!g || g <= 0 || !origin) return { dx, dy };
1337
- const target = this.snapPoint({ x: origin.x + dx, y: origin.y + dy });
1338
- return { dx: target.x - origin.x, dy: target.y - origin.y };
3110
+ return g > 0 ? Math.round(v / g) * g : v;
3111
+ }
3112
+ snapPoint(p) {
3113
+ return { x: this.gridValue(p.x), y: this.gridValue(p.y) };
3114
+ }
3115
+ /** The pull distance in document units. */
3116
+ snapTolerance() {
3117
+ return this.snap.threshold / this.viewport.zoom;
3118
+ }
3119
+ setGuides(guides) {
3120
+ this.guides = guides && this.snap.guides && (guides.lines.length || guides.labels.length) ? guides : null;
3121
+ }
3122
+ /**
3123
+ * Where a drawn or dragged point `p` goes: onto the smart guide `targets` (gathered by
3124
+ * `gather` when missing), else onto the grid, kept to `leave`'s constraint. Cmd / Ctrl
3125
+ * bypasses snapping.
3126
+ */
3127
+ snapDrawn(p, leave, input, targets) {
3128
+ this.snappedToObject = false;
3129
+ if (input.mod) {
3130
+ this.setGuides(null);
3131
+ return constrainToLeave(leave, p);
3132
+ }
3133
+ if (this.snap.objects) {
3134
+ const withAngles = leave ? { ...leave, angles: this.snap.angles } : null;
3135
+ const res = targets().snapFrom(p, withAngles, this.snapTolerance());
3136
+ if (res.kind) {
3137
+ this.snappedToObject = true;
3138
+ this.setGuides(res);
3139
+ return _optionalChain([leave, 'optionalAccess', _60 => _60.constrain]) ? res.point : { x: res.x ? res.point.x : this.gridValue(res.point.x), y: res.y ? res.point.y : this.gridValue(res.point.y) };
3140
+ }
3141
+ }
3142
+ this.setGuides(null);
3143
+ return constrainToLeave(leave, this.snapPoint(p));
3144
+ }
3145
+ /** Drawing targets for the pen and shape tools, reused while the document stays the same. */
3146
+ drawingTargets(exclude) {
3147
+ const key = _nullishCoalesce(exclude, () => ( ""));
3148
+ const cached = this.hoverTargets;
3149
+ if (cached && cached.doc === this.doc && cached.exclude === key) return cached.targets;
3150
+ const targets = VectorSnapTargets.forDrawing(this.doc, exclude ? [exclude] : []);
3151
+ this.hoverTargets = { doc: this.doc, exclude: key, targets };
3152
+ return targets;
3153
+ }
3154
+ /** Where the pointer at `p` would place a point with the pen or a shape tool (and the guides showing why). */
3155
+ hoverSnap(p, input) {
3156
+ if (this.tool === "pen") {
3157
+ const sp = this.pen ? _optionalChain([this, 'access', _61 => _61.getNode, 'call', _62 => _62(this.pen.nodeId), 'optionalAccess', _63 => _63.subpaths, 'access', _64 => _64[this.pen.subpath]]) : void 0;
3158
+ if (this.pen && sp && _optionalChain([this, 'access', _65 => _65.hover, 'optionalAccess', _66 => _66.anchor])) {
3159
+ this.setGuides(null);
3160
+ return p;
3161
+ }
3162
+ const last = this.pen && sp ? getAnchorWorld(this.doc, { nodeId: this.pen.nodeId, subpath: this.pen.subpath, index: sp.anchors.length - 1 }) : void 0;
3163
+ const leave = last ? { at: { x: last.x, y: last.y }, constrain: input.shift ? "angle" : null } : null;
3164
+ return this.snapDrawn(p, leave, input, () => this.drawingTargets(_nullishCoalesce(_optionalChain([this, 'access', _67 => _67.pen, 'optionalAccess', _68 => _68.nodeId]), () => ( null))));
3165
+ }
3166
+ if (SHAPE_TOOLS.has(this.tool)) return this.snapDrawn(p, null, input, () => this.drawingTargets(null));
3167
+ this.setGuides(null);
3168
+ return p;
1339
3169
  }
1340
3170
  // -------------------------------------------------------------------------
1341
3171
  // Document
@@ -1566,10 +3396,10 @@ var VectorEditorController = (_class = class {
1566
3396
  return this.selection.filter((id) => {
1567
3397
  let covered = false;
1568
3398
  const loc = locateVectorNode(this.doc.nodes, id);
1569
- let parent = _nullishCoalesce(_optionalChain([loc, 'optionalAccess', _41 => _41.parent]), () => ( null));
3399
+ let parent = _nullishCoalesce(_optionalChain([loc, 'optionalAccess', _69 => _69.parent]), () => ( null));
1570
3400
  while (parent) {
1571
3401
  if (this.selection.includes(parent.id)) covered = true;
1572
- parent = _nullishCoalesce(_optionalChain([locateVectorNode, 'call', _42 => _42(this.doc.nodes, parent.id), 'optionalAccess', _43 => _43.parent]), () => ( null));
3402
+ parent = _nullishCoalesce(_optionalChain([locateVectorNode, 'call', _70 => _70(this.doc.nodes, parent.id), 'optionalAccess', _71 => _71.parent]), () => ( null));
1573
3403
  }
1574
3404
  return !covered;
1575
3405
  });
@@ -1752,8 +3582,8 @@ var VectorEditorController = (_class = class {
1752
3582
  if (!ids.length) return null;
1753
3583
  const locs = ids.map((id2) => locateVectorNode(this.doc.nodes, id2));
1754
3584
  if (locs.some((l) => !l)) return null;
1755
- const parent = _nullishCoalesce(_optionalChain([locs, 'access', _44 => _44[0], 'access', _45 => _45.parent, 'optionalAccess', _46 => _46.id]), () => ( null));
1756
- if (locs.some((l) => (_nullishCoalesce(_optionalChain([l, 'access', _47 => _47.parent, 'optionalAccess', _48 => _48.id]), () => ( null))) !== parent)) return null;
3585
+ const parent = _nullishCoalesce(_optionalChain([locs, 'access', _72 => _72[0], 'access', _73 => _73.parent, 'optionalAccess', _74 => _74.id]), () => ( null));
3586
+ if (locs.some((l) => (_nullishCoalesce(_optionalChain([l, 'access', _75 => _75.parent, 'optionalAccess', _76 => _76.id]), () => ( null))) !== parent)) return null;
1757
3587
  const id = this.newId();
1758
3588
  this.change("Group", () => {
1759
3589
  const set = new Set(ids);
@@ -1776,7 +3606,7 @@ var VectorEditorController = (_class = class {
1776
3606
  }
1777
3607
  /** Replaces selected groups with their children. */
1778
3608
  ungroup(options = {}) {
1779
- const groups = (_nullishCoalesce(options.ids, () => ( this.selection))).map((id) => this.getNode(id)).filter((n) => _optionalChain([n, 'optionalAccess', _49 => _49.type]) === "group");
3609
+ const groups = (_nullishCoalesce(options.ids, () => ( this.selection))).map((id) => this.getNode(id)).filter((n) => _optionalChain([n, 'optionalAccess', _77 => _77.type]) === "group");
1780
3610
  if (!groups.length) return { ok: false, message: "Select a group to ungroup." };
1781
3611
  const set = new Set(groups.map((g) => g.id));
1782
3612
  this.change("Ungroup", () => {
@@ -1836,7 +3666,7 @@ var VectorEditorController = (_class = class {
1836
3666
  });
1837
3667
  const paths = [...new Set(order)].map((id2) => this.getNode(id2));
1838
3668
  if (paths.length < 2) return null;
1839
- const result = _chunkM4XE4UMTcjs.pathBoolean.call(void 0,
3669
+ const result = _chunkBGNPT5PEcjs.pathBoolean.call(void 0,
1840
3670
  BOOLEAN_OPS[op],
1841
3671
  paths.map((p) => ({
1842
3672
  path: vectorPathWorldBezier(
@@ -1886,11 +3716,11 @@ var VectorEditorController = (_class = class {
1886
3716
  /** A document-space path mapped into the parent space of node `id`. */
1887
3717
  intoParentSpace(id, path) {
1888
3718
  const parent = _nullishCoalesce(vectorParentMatrix(this.doc.nodes, id), () => ( IDENTITY_MATRIX));
1889
- return isRoot(parent) ? path : _chunkM4XE4UMTcjs.transformBezierPath.call(void 0, path, invertMatrix(parent));
3719
+ return isRoot(parent) ? path : _chunkBGNPT5PEcjs.transformBezierPath.call(void 0, path, invertMatrix(parent));
1890
3720
  }
1891
3721
  /** Splits selected compound paths into one path per subpath. */
1892
3722
  breakApart(options = {}) {
1893
- const paths = (_nullishCoalesce(options.ids, () => ( this.selection))).map((id) => this.getNode(id)).filter((n) => _optionalChain([n, 'optionalAccess', _50 => _50.type]) === "path" && n.subpaths.length > 1);
3723
+ const paths = (_nullishCoalesce(options.ids, () => ( this.selection))).map((id) => this.getNode(id)).filter((n) => _optionalChain([n, 'optionalAccess', _78 => _78.type]) === "path" && n.subpaths.length > 1);
1894
3724
  if (!paths.length) return { ok: false, message: "Select a compound path to break apart." };
1895
3725
  const created = [];
1896
3726
  const set = new Set(paths.map((p) => p.id));
@@ -1936,12 +3766,47 @@ var VectorEditorController = (_class = class {
1936
3766
  return translateAnchor(a, d.x, d.y);
1937
3767
  });
1938
3768
  }
1939
- /** Moves one anchor to a document position (handles follow). */
1940
- moveAnchor(ref, to) {
3769
+ /**
3770
+ * Moves one anchor to a document position. Its handles follow; with `curvature` (default:
3771
+ * the `curvatureMoves` setting) its two segments bend instead, as the Curvature tool does.
3772
+ */
3773
+ moveAnchor(ref, to, options = {}) {
1941
3774
  const a = getAnchorWorld(this.doc, ref);
1942
3775
  if (!a) return { ok: false, message: "There is no such anchor." };
3776
+ const curvature = _nullishCoalesce(options.curvature, () => ( this.editing.curvatureMoves));
1943
3777
  this.change("Move Point", () => {
1944
- this.doc = this.movedAnchors(this.doc, [ref], to.x - a.x, to.y - a.y);
3778
+ this.doc = curvature ? this.curvatureMoved(this.doc, ref, to) : this.movedAnchors(this.doc, [ref], to.x - a.x, to.y - a.y);
3779
+ });
3780
+ return { ok: true };
3781
+ }
3782
+ /** `doc` with anchor `ref` moved to document point `to` by a Curvature-tool move. */
3783
+ curvatureMoved(doc, ref, to) {
3784
+ const node = findVectorNode(doc.nodes, ref.nodeId);
3785
+ if (_optionalChain([node, 'optionalAccess', _79 => _79.type]) !== "path" || node.locked || !node.subpaths[ref.subpath]) return doc;
3786
+ const q = this.toLocal(ref.nodeId, to, doc);
3787
+ const subpaths = node.subpaths.map((sp, i) => i === ref.subpath ? curvatureMoveAnchor(sp, ref.index, q) : sp);
3788
+ const nodes = mapTree(doc.nodes, (n) => n.id === ref.nodeId ? { ...node, subpaths } : n);
3789
+ return { ...doc, nodes };
3790
+ }
3791
+ /** `doc` with segment `ref` bent through document point `p` at parameter `t`. */
3792
+ bent(doc, ref, t, p) {
3793
+ const node = findVectorNode(doc.nodes, ref.nodeId);
3794
+ if (_optionalChain([node, 'optionalAccess', _80 => _80.type]) !== "path" || node.locked || !node.subpaths[ref.subpath]) return doc;
3795
+ const q = this.toLocal(ref.nodeId, p, doc);
3796
+ const subpaths = node.subpaths.map((sp, i) => i === ref.subpath ? bendSegment(sp, ref.segment, t, q) : sp);
3797
+ return { ...doc, nodes: mapTree(doc.nodes, (n) => n.id === ref.nodeId ? { ...node, subpaths } : n) };
3798
+ }
3799
+ /**
3800
+ * Bends a segment so the curve passes through document point `to` at parameter `t`
3801
+ * (default 0.5, clamped to 0.05–0.95); see `bendSegment` in `pathEdit`.
3802
+ */
3803
+ bendSegment(ref, to, t = 0.5) {
3804
+ const node = this.getNode(ref.nodeId);
3805
+ const sp = _optionalChain([node, 'optionalAccess', _81 => _81.type]) === "path" ? node.subpaths[ref.subpath] : void 0;
3806
+ if (!sp || ref.segment < 0 || ref.segment >= (sp.closed ? sp.anchors.length : sp.anchors.length - 1) || sp.anchors.length < 2) return { ok: false, message: "There is no such segment." };
3807
+ if (node.locked) return { ok: false, message: "The path is locked." };
3808
+ this.change("Bend Segment", () => {
3809
+ this.doc = this.bent(this.doc, ref, t, to);
1945
3810
  });
1946
3811
  return { ok: true };
1947
3812
  }
@@ -1969,72 +3834,344 @@ var VectorEditorController = (_class = class {
1969
3834
  });
1970
3835
  return { ok: true };
1971
3836
  }
1972
- /** Adds an anchor on the nearest segment of `nodeId` to document point `point` (shape unchanged). */
1973
- insertAnchorAt(nodeId, point) {
3837
+ /** Adds an anchor on the nearest segment of `nodeId` to document point `point` (shape unchanged). */
3838
+ insertAnchorAt(nodeId, point) {
3839
+ const node = this.getNode(nodeId);
3840
+ if (_optionalChain([node, 'optionalAccess', _82 => _82.type]) !== "path") return null;
3841
+ const hit = nearestSegment(node, this.toLocal(nodeId, point));
3842
+ if (!hit) return null;
3843
+ const { subpath, index } = insertAnchor(node.subpaths[hit.subpath], hit.segment, hit.t);
3844
+ const ref = { nodeId, subpath: hit.subpath, index };
3845
+ this.change("Add Point", () => {
3846
+ this.replaceNode(nodeId, { ...node, subpaths: node.subpaths.map((sp, i) => i === hit.subpath ? subpath : sp) });
3847
+ this.editingId = nodeId;
3848
+ this.anchors = [ref];
3849
+ });
3850
+ return ref;
3851
+ }
3852
+ /**
3853
+ * Deletes the selected (or given) anchors; subpaths left with one anchor and empty paths go
3854
+ * too. With `keepShape` (default: the `keepShapeOnDelete` setting, on) the neighbours'
3855
+ * handles are refitted so the path keeps its shape; off, the neighbours are joined as they are.
3856
+ */
3857
+ deleteAnchors(refs = this.anchors, options = {}) {
3858
+ if (!refs.length) return { ok: false, message: "Select points to delete." };
3859
+ const keepShape = _nullishCoalesce(options.keepShape, () => ( this.editing.keepShapeOnDelete));
3860
+ this.change("Delete Points", () => {
3861
+ const nodes = mapTree(this.doc.nodes, (n) => {
3862
+ if (n.type !== "path" || n.locked) return n;
3863
+ const mine = refs.filter((r) => r.nodeId === n.id);
3864
+ if (!mine.length) return n;
3865
+ const subpaths = n.subpaths.map((sp, s) => {
3866
+ const indices = mine.filter((r) => r.subpath === s).map((r) => r.index);
3867
+ if (!indices.length) return sp;
3868
+ return keepShape ? removeAnchorsKeepingShape(sp, indices) : removeAnchors(sp, indices);
3869
+ }).filter((sp) => sp.anchors.length > 1);
3870
+ return subpaths.length ? { ...n, subpaths } : null;
3871
+ });
3872
+ this.setDoc(nodes);
3873
+ this.anchors = [];
3874
+ this.selection = this.selection.filter((id) => this.getNode(id));
3875
+ if (this.editingId && !this.getNode(this.editingId)) this.editingId = null;
3876
+ if (this.pen && !this.getNode(this.pen.nodeId)) this.pen = null;
3877
+ });
3878
+ return { ok: true };
3879
+ }
3880
+ /** Opens or closes the subpaths that contain selected anchors (or all subpaths of `nodeId`). */
3881
+ setClosed(closed, nodeId) {
3882
+ const targets = nodeId ? (() => {
3883
+ const n = this.getNode(nodeId);
3884
+ return _optionalChain([n, 'optionalAccess', _83 => _83.type]) === "path" ? n.subpaths.map((_, s) => ({ nodeId, subpath: s })) : [];
3885
+ })() : this.anchors.map((r) => ({ nodeId: r.nodeId, subpath: r.subpath }));
3886
+ if (!targets.length) return { ok: false, message: "Select a path or points." };
3887
+ this.change(closed ? "Close Path" : "Open Path", () => {
3888
+ this.setDoc(
3889
+ mapTree(
3890
+ this.doc.nodes,
3891
+ (n) => n.type === "path" && targets.some((t) => t.nodeId === n.id) ? { ...n, subpaths: n.subpaths.map((sp, s) => targets.some((t) => t.nodeId === n.id && t.subpath === s) ? { ...sp, closed } : sp) } : n
3892
+ )
3893
+ );
3894
+ });
3895
+ return { ok: true };
3896
+ }
3897
+ /** Reverses the direction of every subpath of the selected paths. */
3898
+ reversePaths(ids = this.selection) {
3899
+ const set = new Set(ids.flatMap((id) => this.getNode(id) ? collectPathNodes(this.getNode(id)) : []).map((p) => p.id));
3900
+ if (!set.size) return { ok: false, message: "Select a path." };
3901
+ this.change("Reverse Path", () => {
3902
+ this.setDoc(mapTree(this.doc.nodes, (n) => n.type === "path" && set.has(n.id) ? { ...n, subpaths: n.subpaths.map(reverseSubpath) } : n));
3903
+ this.anchors = [];
3904
+ });
3905
+ return { ok: true };
3906
+ }
3907
+ // -------------------------------------------------------------------------
3908
+ // Path clean-up, Scissors, Knife and Eraser
3909
+ // -------------------------------------------------------------------------
3910
+ /** Unlocked path nodes in the given roots (default: the paths being edited, else the selection). */
3911
+ pathTargets(ids) {
3912
+ const roots = _nullishCoalesce(ids, () => ( (this.tool === "direct" && this.anchors.length ? [...new Set(this.anchors.map((r) => r.nodeId))] : this.selection)));
3913
+ const out = /* @__PURE__ */ new Map();
3914
+ for (const id of roots) {
3915
+ const node = this.getNode(id);
3916
+ if (node && !node.locked) {
3917
+ for (const p of collectPathNodes(node)) if (!p.locked) out.set(p.id, p);
3918
+ }
3919
+ }
3920
+ const order = [];
3921
+ walkVectorNodes(this.doc.nodes, (n) => {
3922
+ if (out.has(n.id)) order.push(n.id);
3923
+ });
3924
+ return order.map((id) => out.get(id));
3925
+ }
3926
+ /** Subpaths of node `from` mapped into node `to`'s own space. */
3927
+ subpathsIn(from, to) {
3928
+ const m = multiplyMatrices(this.worldOf(from.id), invertMatrix(this.worldOf(to)));
3929
+ if (isRoot(m)) return from.subpaths;
3930
+ const map = (p) => applyMatrix(m, p);
3931
+ return from.subpaths.map((sp) => ({
3932
+ closed: sp.closed,
3933
+ anchors: sp.anchors.map((a) => ({ ...map(a), ...a.in ? { in: map(a.in) } : {}, ...a.out ? { out: map(a.out) } : {}, ...a.type ? { type: a.type } : {} }))
3934
+ }));
3935
+ }
3936
+ /**
3937
+ * Replaces a path with pieces: each entry becomes one path (the first keeps the node's id and
3938
+ * place, the others go just above it with the same style). No pieces removes the path.
3939
+ * Returns the ids of the paths now standing in for it.
3940
+ */
3941
+ replaceWithPieces(node, pieces) {
3942
+ const nodes = pieces.filter((p) => p.length).map((subpaths, i) => ({ ...node, id: i === 0 ? node.id : this.newId(), subpaths }));
3943
+ this.setDoc(spliceTree(this.doc.nodes, node.id, nodes));
3944
+ return nodes.map((n) => n.id);
3945
+ }
3946
+ /** Clears anchor selections, editing and pen state that no longer point at anything. */
3947
+ dropStaleRefs() {
3948
+ this.selection = this.selection.filter((id) => this.getNode(id));
3949
+ this.anchors = this.anchors.filter((r) => getAnchor(this.doc, r));
3950
+ if (this.editingId && !this.getNode(this.editingId)) this.editingId = null;
3951
+ if (this.pen && !this.getNode(this.pen.nodeId)) this.pen = null;
3952
+ }
3953
+ /**
3954
+ * Join: the open subpaths of the selected (or given) paths become one open path, joined
3955
+ * nearest endpoints first (ends within `tolerance` document units merge into one anchor,
3956
+ * others get a straight segment); a single open subpath is closed. The result replaces the
3957
+ * lowest path, in its space and style. Default tolerance: the pick distance.
3958
+ */
3959
+ joinPaths(options = {}) {
3960
+ const paths = this.pathTargets(options.ids);
3961
+ if (!paths.length) return { ok: false, message: "Select paths to join." };
3962
+ const base = paths[0];
3963
+ const all = paths.flatMap((p) => this.subpathsIn(p, base.id));
3964
+ if (!all.some((sp) => !sp.closed && sp.anchors.length > 1)) return { ok: false, message: "There are no open paths to join." };
3965
+ const subpaths = joinSubpaths(all, _nullishCoalesce(options.tolerance, () => ( this.tolerance())));
3966
+ const drop = new Set(paths.slice(1).map((p) => p.id));
3967
+ this.change("Join", () => {
3968
+ this.setDoc(mapTree(this.doc.nodes, (n) => n.id === base.id ? { ...base, subpaths } : drop.has(n.id) ? null : n));
3969
+ this.selection = [base.id];
3970
+ this.anchors = [];
3971
+ if (this.editingId && drop.has(this.editingId)) this.editingId = base.id;
3972
+ this.dropStaleRefs();
3973
+ });
3974
+ return { ok: true };
3975
+ }
3976
+ /** Average: moves the selected (or given) anchors onto their average position along `axis`, in document space. */
3977
+ averageAnchors(axis = "both", refs = this.anchors) {
3978
+ const points = refs.map((r) => ({ ref: r, at: getAnchorWorld(this.doc, r) })).filter((p) => !!p.at);
3979
+ if (points.length < 2) return { ok: false, message: "Select at least two points to average." };
3980
+ const cx = points.reduce((s, p) => s + p.at.x, 0) / points.length;
3981
+ const cy = points.reduce((s, p) => s + p.at.y, 0) / points.length;
3982
+ this.change("Average", () => {
3983
+ for (const { ref, at } of points) {
3984
+ const dx = axis === "horizontal" ? 0 : cx - at.x;
3985
+ const dy = axis === "vertical" ? 0 : cy - at.y;
3986
+ if (dx || dy) this.doc = this.movedAnchors(this.doc, [ref], dx, dy);
3987
+ }
3988
+ });
3989
+ return { ok: true };
3990
+ }
3991
+ /** Applies `fn` to every subpath of the target paths as one undo step. */
3992
+ editSubpaths(label, ids, fn) {
3993
+ const paths = this.pathTargets(ids);
3994
+ if (!paths.length) return { ok: false, message: "Select a path." };
3995
+ const set = new Set(paths.map((p) => p.id));
3996
+ this.change(label, () => {
3997
+ this.setDoc(mapTree(this.doc.nodes, (n) => n.type === "path" && set.has(n.id) ? { ...n, subpaths: n.subpaths.map((sp, i) => fn(sp, n, i)) } : n));
3998
+ this.dropStaleRefs();
3999
+ });
4000
+ return { ok: true };
4001
+ }
4002
+ /**
4003
+ * Remove Redundant Points: drops anchors of the selected (or given) paths that can go
4004
+ * without moving the outline more than `tolerance` document units (default 0.5).
4005
+ */
4006
+ removeRedundantPoints(options = {}) {
4007
+ const tolerance = _nullishCoalesce(options.tolerance, () => ( 0.5));
4008
+ return this.editSubpaths("Remove Redundant Points", options.ids, (sp) => removeRedundantPoints(sp, tolerance));
4009
+ }
4010
+ /**
4011
+ * Smooth: pulls handles towards smooth tangents by `amount` (0–1, default 1). With the direct
4012
+ * tool and points selected, only those points change.
4013
+ */
4014
+ smoothPaths(options = {}) {
4015
+ const refs = _nullishCoalesce(options.refs, () => ( (!options.ids && this.tool === "direct" && this.anchors.length ? this.anchors : null)));
4016
+ const ids = _nullishCoalesce(options.ids, () => ( (refs ? [...new Set(refs.map((r) => r.nodeId))] : void 0)));
4017
+ return this.editSubpaths("Smooth", ids, (sp, node, i) => {
4018
+ const only = refs ? new Set(refs.filter((r) => r.nodeId === node.id && r.subpath === i).map((r) => r.index)) : void 0;
4019
+ return only && !only.size ? sp : smoothSubpath(sp, _nullishCoalesce(options.amount, () => ( 1)), only);
4020
+ });
4021
+ }
4022
+ /** Splits a cut subpath into pieces: separate paths for a one-subpath node, subpaths in place for a compound one. */
4023
+ applyCuts(node, cut) {
4024
+ const results = node.subpaths.map((sp, i) => cut(sp, i));
4025
+ if (results.every((r) => !r)) return null;
4026
+ const subpaths = node.subpaths.flatMap((sp, i) => _nullishCoalesce(results[i], () => ( [sp])));
4027
+ if (node.subpaths.length === 1) return this.replaceWithPieces(node, subpaths.map((sp) => [sp]));
4028
+ this.replaceNode(node.id, { ...node, subpaths });
4029
+ return [node.id];
4030
+ }
4031
+ /**
4032
+ * Scissors: cuts path `nodeId` at the outline point nearest document point `point` (at an
4033
+ * anchor when within the pick distance of one). A closed subpath opens there; an open one
4034
+ * splits in two (a one-subpath path becomes two paths). Returns the ids of the paths.
4035
+ */
4036
+ cutPathAt(nodeId, point) {
1974
4037
  const node = this.getNode(nodeId);
1975
- if (_optionalChain([node, 'optionalAccess', _51 => _51.type]) !== "path") return null;
1976
- const hit = nearestSegment(node, this.toLocal(nodeId, point));
4038
+ if (_optionalChain([node, 'optionalAccess', _84 => _84.type]) !== "path" || node.locked) return null;
4039
+ const local = this.toLocal(nodeId, point);
4040
+ const hit = nearestSegment(node, local);
1977
4041
  if (!hit) return null;
1978
- const { subpath, index } = insertAnchor(node.subpaths[hit.subpath], hit.segment, hit.t);
1979
- const ref = { nodeId, subpath: hit.subpath, index };
1980
- this.change("Add Point", () => {
1981
- this.replaceNode(nodeId, { ...node, subpaths: node.subpaths.map((sp, i) => i === hit.subpath ? subpath : sp) });
1982
- this.editingId = nodeId;
1983
- this.anchors = [ref];
4042
+ const sp = node.subpaths[hit.subpath];
4043
+ const n = sp.anchors.length;
4044
+ const near2 = [hit.segment, (hit.segment + 1) % n].find((i) => Math.hypot(sp.anchors[i].x - hit.point.x, sp.anchors[i].y - hit.point.y) <= this.tolerance());
4045
+ if (!sp.closed && n > 1 && (near2 === 0 || near2 === n - 1) && !(hit.t > 1e-6 && hit.t < 1 - 1e-6)) return null;
4046
+ let ids = null;
4047
+ this.change("Cut Path", () => {
4048
+ ids = this.applyCuts(node, (s, i) => i !== hit.subpath ? null : near2 !== void 0 ? cutSubpathAt(s, [near2]) : splitSubpathAt(s, hit.segment, hit.t));
4049
+ if (ids) {
4050
+ this.selection = ids;
4051
+ this.anchors = [];
4052
+ if (this.editingId === nodeId) this.editingId = null;
4053
+ }
4054
+ this.dropStaleRefs();
1984
4055
  });
1985
- return ref;
4056
+ return ids;
1986
4057
  }
1987
- /** Deletes the selected anchors; empty subpaths and paths go too. */
1988
- deleteAnchors(refs = this.anchors) {
1989
- if (!refs.length) return { ok: false, message: "Select points to delete." };
1990
- this.change("Delete Points", () => {
1991
- const nodes = mapTree(this.doc.nodes, (n) => {
1992
- if (n.type !== "path") return n;
1993
- const mine = refs.filter((r) => r.nodeId === n.id);
1994
- if (!mine.length) return n;
1995
- const subpaths = n.subpaths.map((sp, s) => removeAnchors(sp, mine.filter((r) => r.subpath === s).map((r) => r.index))).filter((sp) => sp.anchors.length > 1);
1996
- return subpaths.length ? { ...n, subpaths } : null;
1997
- });
1998
- this.setDoc(nodes);
4058
+ /** Scissors on the selected (or given) anchors: each cuts its subpath there (see `cutPathAt`). */
4059
+ cutAtAnchors(refs = this.anchors) {
4060
+ const byNode = /* @__PURE__ */ new Map();
4061
+ for (const r of refs) if (getAnchor(this.doc, r)) byNode.set(r.nodeId, [..._nullishCoalesce(byNode.get(r.nodeId), () => ( [])), r]);
4062
+ if (!byNode.size) return { ok: false, message: "Select points to cut at." };
4063
+ const created = [];
4064
+ this.change("Cut Path", () => {
4065
+ for (const [id, list] of byNode) {
4066
+ const node = this.getNode(id);
4067
+ if (node.locked) continue;
4068
+ const ids = this.applyCuts(node, (sp, i) => {
4069
+ const at = list.filter((r) => r.subpath === i).map((r) => r.index);
4070
+ const pieces = at.length ? cutSubpathAt(sp, at) : null;
4071
+ return pieces && !(pieces.length === 1 && pieces[0].closed === sp.closed) ? pieces : null;
4072
+ });
4073
+ if (ids) created.push(...ids);
4074
+ }
4075
+ if (created.length) this.selection = created;
1999
4076
  this.anchors = [];
2000
- this.selection = this.selection.filter((id) => this.getNode(id));
2001
- if (this.editingId && !this.getNode(this.editingId)) this.editingId = null;
2002
- if (this.pen && !this.getNode(this.pen.nodeId)) this.pen = null;
4077
+ this.dropStaleRefs();
2003
4078
  });
2004
- return { ok: true };
4079
+ return created.length ? { ok: true } : { ok: false, message: "Points at the ends of open paths do not cut." };
2005
4080
  }
2006
- /** Opens or closes the subpaths that contain selected anchors (or all subpaths of `nodeId`). */
2007
- setClosed(closed, nodeId) {
2008
- const targets = nodeId ? (() => {
2009
- const n = this.getNode(nodeId);
2010
- return _optionalChain([n, 'optionalAccess', _52 => _52.type]) === "path" ? n.subpaths.map((_, s) => ({ nodeId, subpath: s })) : [];
2011
- })() : this.anchors.map((r) => ({ nodeId: r.nodeId, subpath: r.subpath }));
2012
- if (!targets.length) return { ok: false, message: "Select a path or points." };
2013
- this.change(closed ? "Close Path" : "Open Path", () => {
2014
- this.setDoc(
2015
- mapTree(
2016
- this.doc.nodes,
2017
- (n) => n.type === "path" && targets.some((t) => t.nodeId === n.id) ? { ...n, subpaths: n.subpaths.map((sp, s) => targets.some((t) => t.nodeId === n.id && t.subpath === s) ? { ...sp, closed } : sp) } : n
2018
- )
2019
- );
4081
+ /** Visible, unlocked paths a stroke over `points` can reach: in `ids`, the selection, or (with neither) any whose bounds it crosses. */
4082
+ strokeTargets(points, ids, pad) {
4083
+ const blocked = /* @__PURE__ */ new Set();
4084
+ walkVectorNodes(this.doc.nodes, (n, parent) => {
4085
+ if (n.visible === false || n.locked || parent && blocked.has(parent.id)) blocked.add(n.id);
2020
4086
  });
2021
- return { ok: true };
4087
+ const visible = (p) => !blocked.has(p.id);
4088
+ if (_optionalChain([ids, 'optionalAccess', _85 => _85.length]) || this.selection.length) return this.pathTargets(_nullishCoalesce(ids, () => ( this.selection))).filter(visible);
4089
+ const box = polylineBounds(points.map(toPoint2), pad);
4090
+ if (!box) return [];
4091
+ const out = [];
4092
+ walkVectorNodes(this.doc.nodes, (n) => {
4093
+ if (n.type !== "path" || !visible(n)) return;
4094
+ const b = vectorNodeBounds(n, _nullishCoalesce(vectorParentMatrix(this.doc.nodes, n.id), () => ( IDENTITY_MATRIX)));
4095
+ if (b && boundsIntersect(b, box)) out.push(n);
4096
+ });
4097
+ return out;
2022
4098
  }
2023
- /** Reverses the direction of every subpath of the selected paths. */
2024
- reversePaths(ids = this.selection) {
2025
- const set = new Set(ids.flatMap((id) => this.getNode(id) ? collectPathNodes(this.getNode(id)) : []).map((p) => p.id));
2026
- if (!set.size) return { ok: false, message: "Select a path." };
2027
- this.change("Reverse Path", () => {
2028
- this.setDoc(mapTree(this.doc.nodes, (n) => n.type === "path" && set.has(n.id) ? { ...n, subpaths: n.subpaths.map(reverseSubpath) } : n));
4099
+ /** A stroke's points in node `id`'s own space. */
4100
+ localPoints(id, points) {
4101
+ const inv = invertMatrix(this.worldOf(id));
4102
+ return points.map((p) => toPoint2(applyMatrix(inv, p)));
4103
+ }
4104
+ /**
4105
+ * Knife: cuts closed paths along the polyline `points` (document space) wherever it crosses
4106
+ * them from edge to edge, with no gap between the pieces. It cuts the given paths, the
4107
+ * selection, or (with neither) every path it crosses. Returns the ids of the pieces (empty
4108
+ * when nothing was cut).
4109
+ */
4110
+ knife(points, options = {}) {
4111
+ const pts = simplifyPolyline(points.map(toPoint2), 0.25 / this.viewport.zoom).map(([x, y]) => ({ x, y }));
4112
+ if (pts.length < 2) return [];
4113
+ const plan = [];
4114
+ for (const node of this.strokeTargets(pts, options.ids, 0)) {
4115
+ const closed = node.subpaths.filter((sp) => sp.anchors.length > 1);
4116
+ if (!closed.length || closed.some((sp) => !sp.closed)) continue;
4117
+ const result = knifePath(subpathsToBezier(closed), _nullishCoalesce(node.fillRule, () => ( "nonzero")), this.localPoints(node.id, pts));
4118
+ if (result) plan.push({ node, pieces: result.map((path) => bezierToSubpaths(path)) });
4119
+ }
4120
+ if (!plan.length) return [];
4121
+ const created = [];
4122
+ this.change("Knife", () => {
4123
+ for (const { node, pieces } of plan) created.push(...this.replaceWithPieces(node, pieces));
4124
+ this.selection = created;
2029
4125
  this.anchors = [];
4126
+ this.dropStaleRefs();
2030
4127
  });
2031
- return { ok: true };
4128
+ return created;
4129
+ }
4130
+ /**
4131
+ * Eraser: rubs out a round stroke of diameter `size` (default: the `eraserSize` setting)
4132
+ * along `points` (document space). Filled closed paths lose that area (pieces that come
4133
+ * apart become separate paths); outlines are cut where they pass under it. It erases from
4134
+ * the given paths, the selection, or (with neither) every path it reaches. Returns the ids
4135
+ * of the paths it changed or created.
4136
+ */
4137
+ erase(points, options = {}) {
4138
+ const width = _nullishCoalesce(options.size, () => ( this.editing.eraserSize));
4139
+ if (!(width > 0) || !points.length) return [];
4140
+ const pts = simplifyPolyline(points.map(toPoint2), Math.min(width * 0.05, 0.25 / this.viewport.zoom));
4141
+ const region = eraserRegion(pts, width);
4142
+ if (!region.length) return [];
4143
+ const plan = [];
4144
+ const rb = _chunkBGNPT5PEcjs.bezierPathBounds.call(void 0, region);
4145
+ const reach = rb ? [{ x: rb.minX, y: rb.minY }, { x: rb.maxX, y: rb.maxY }] : points;
4146
+ for (const node of this.strokeTargets(reach, options.ids, 0)) {
4147
+ const local = _chunkBGNPT5PEcjs.transformBezierPath.call(void 0, region, invertMatrix(this.worldOf(node.id)));
4148
+ const live = node.subpaths.filter((sp) => sp.anchors.length > 1);
4149
+ if (!live.length) continue;
4150
+ if (hasPaint(node.fill) && live.every((sp) => sp.closed)) {
4151
+ const faces = eraseFromFill(subpathsToBezier(live), _nullishCoalesce(node.fillRule, () => ( "nonzero")), local);
4152
+ if (faces) plan.push({ node, pieces: faces.map((f) => bezierToSubpaths(f)) });
4153
+ continue;
4154
+ }
4155
+ const results = live.map((sp) => eraseFromOutline(sp, local));
4156
+ if (results.every((r) => !r)) continue;
4157
+ const subpaths = live.flatMap((sp, i) => _nullishCoalesce(results[i], () => ( [sp])));
4158
+ plan.push({ node, pieces: live.length === 1 ? subpaths.map((sp) => [sp]) : [subpaths] });
4159
+ }
4160
+ if (!plan.length) return [];
4161
+ const changed = [];
4162
+ this.change("Erase", () => {
4163
+ for (const { node, pieces } of plan) changed.push(...this.replaceWithPieces(node, pieces));
4164
+ this.anchors = [];
4165
+ this.dropStaleRefs();
4166
+ if (this.selection.length || options.ids) this.selection = [.../* @__PURE__ */ new Set([...this.selection, ...changed.filter((id) => this.getNode(id))])];
4167
+ });
4168
+ return changed;
2032
4169
  }
2033
4170
  /** Ends the pen path being drawn (a lone anchor is removed). */
2034
4171
  finishPath() {
2035
4172
  if (!this.pen) return;
2036
4173
  const node = this.getNode(this.pen.nodeId);
2037
- const sp = _optionalChain([node, 'optionalAccess', _53 => _53.subpaths, 'access', _54 => _54[this.pen.subpath]]);
4174
+ const sp = _optionalChain([node, 'optionalAccess', _86 => _86.subpaths, 'access', _87 => _87[this.pen.subpath]]);
2038
4175
  const pen = this.pen;
2039
4176
  this.pen = null;
2040
4177
  this.pointer = null;
@@ -2078,7 +4215,7 @@ var VectorEditorController = (_class = class {
2078
4215
  if (trimmed.startsWith("{")) {
2079
4216
  try {
2080
4217
  const data = JSON.parse(trimmed);
2081
- if (_optionalChain([data, 'optionalAccess', _55 => _55.type]) === VECTOR_CLIPBOARD_TYPE && Array.isArray(data.nodes)) nodes = data.nodes;
4218
+ if (_optionalChain([data, 'optionalAccess', _88 => _88.type]) === VECTOR_CLIPBOARD_TYPE && Array.isArray(data.nodes)) nodes = data.nodes;
2082
4219
  } catch (e2) {
2083
4220
  nodes = null;
2084
4221
  }
@@ -2090,7 +4227,7 @@ var VectorEditorController = (_class = class {
2090
4227
  }
2091
4228
  }
2092
4229
  } else nodes = this.clipboard;
2093
- if (!_optionalChain([nodes, 'optionalAccess', _56 => _56.length])) return [];
4230
+ if (!_optionalChain([nodes, 'optionalAccess', _89 => _89.length])) return [];
2094
4231
  const offset = _nullishCoalesce(options.offset, () => ( (text && !text.trim().startsWith("{") ? { x: 0, y: 0 } : { x: 10, y: 10 })));
2095
4232
  const copies = nodes.map((n) => transformVectorNode(this.freshIds(n), translateMatrix(offset.x, offset.y)));
2096
4233
  this.change("Paste", () => {
@@ -2141,7 +4278,7 @@ var VectorEditorController = (_class = class {
2141
4278
  hitHandle(screen) {
2142
4279
  for (const ref of getVisibleHandles(this.view()).reverse()) {
2143
4280
  const a = getAnchorWorld(this.doc, ref);
2144
- const h = _optionalChain([a, 'optionalAccess', _57 => _57[ref.handle]]);
4281
+ const h = _optionalChain([a, 'optionalAccess', _90 => _90[ref.handle]]);
2145
4282
  if (!h) continue;
2146
4283
  const s = this.documentToScreen(h);
2147
4284
  if (Math.hypot(s.x - screen.x, s.y - screen.y) <= this.hitRadius) return ref;
@@ -2222,10 +4359,38 @@ var VectorEditorController = (_class = class {
2222
4359
  const anchor = this.hitAnchor(screen, refs);
2223
4360
  return anchor ? { nodeId: anchor.nodeId, anchor } : null;
2224
4361
  }
4362
+ case "scissors": {
4363
+ const seg = this.nearestOutline(p);
4364
+ return seg ? { nodeId: seg.nodeId, segment: seg } : null;
4365
+ }
2225
4366
  default:
2226
4367
  return null;
2227
4368
  }
2228
4369
  }
4370
+ /** The nearest outline point of any visible, unlocked path within the pick distance of document point `p`. */
4371
+ nearestOutline(p) {
4372
+ let best = null;
4373
+ const tol = this.tolerance();
4374
+ const visit = (list, parent) => {
4375
+ for (let i = list.length - 1; i >= 0; i--) {
4376
+ const n = list[i];
4377
+ if (n.visible === false || n.locked) continue;
4378
+ const m = multiplyMatrices(nodeMatrix(n), parent);
4379
+ if (n.type === "group") {
4380
+ visit(n.children, m);
4381
+ continue;
4382
+ }
4383
+ const hit = nearestSegment(n, applyMatrix(invertMatrix(m), p));
4384
+ if (hit && hit.distance <= tol && (!best || hit.distance < best.distance - 1e-9)) {
4385
+ best = { nodeId: n.id, subpath: hit.subpath, segment: hit.segment, t: hit.t, point: applyMatrix(m, hit.point), distance: hit.distance };
4386
+ }
4387
+ }
4388
+ };
4389
+ visit(this.doc.nodes, IDENTITY_MATRIX);
4390
+ if (!best) return null;
4391
+ const { distance: _d, ...seg } = best;
4392
+ return seg;
4393
+ }
2229
4394
  sameHover(a, b) {
2230
4395
  return JSON.stringify(a) === JSON.stringify(b);
2231
4396
  }
@@ -2250,8 +4415,19 @@ var VectorEditorController = (_class = class {
2250
4415
  return this.downPen(screen, p, input);
2251
4416
  case "pencil":
2252
4417
  return this.downPencil(p);
4418
+ case "scissors": {
4419
+ const seg = this.nearestOutline(p);
4420
+ if (seg) this.cutPathAt(seg.nodeId, seg.point);
4421
+ return;
4422
+ }
4423
+ case "knife":
4424
+ case "eraser":
4425
+ this.drag = { kind: "stroke", tool: this.tool, points: [p], before: this.snapshot() };
4426
+ this.interaction = { type: "stroke", tool: this.tool, points: [p], width: this.tool === "eraser" ? this.editing.eraserSize : 0 };
4427
+ this.notify();
4428
+ return;
2253
4429
  default:
2254
- return this.downShape(p);
4430
+ return this.downShape(p, input);
2255
4431
  }
2256
4432
  }
2257
4433
  downSelect(screen, p, input) {
@@ -2320,7 +4496,7 @@ var VectorEditorController = (_class = class {
2320
4496
  return;
2321
4497
  }
2322
4498
  } else if (!selected) this.anchors = [anchor];
2323
- this.drag = { kind: "anchors", start: p, before: this.snapshot(), base: this.doc, moved: false, clickRef: selected && !input.shift ? anchor : null };
4499
+ this.drag = { kind: "anchors", start: p, before: this.snapshot(), base: this.doc, moved: false, clickRef: selected && !input.shift ? anchor : null, lead: anchor };
2324
4500
  this.notify();
2325
4501
  return;
2326
4502
  }
@@ -2336,7 +4512,7 @@ var VectorEditorController = (_class = class {
2336
4512
  const ref = { nodeId: id, subpath: seg.subpath, index };
2337
4513
  this.anchors = [ref];
2338
4514
  this.editingId = id;
2339
- this.drag = { kind: "anchors", start: p, before, base: this.doc, moved: true, clickRef: null };
4515
+ this.drag = { kind: "anchors", start: p, before, base: this.doc, moved: true, clickRef: null, lead: ref };
2340
4516
  this.notify(true);
2341
4517
  return;
2342
4518
  }
@@ -2344,7 +4520,7 @@ var VectorEditorController = (_class = class {
2344
4520
  const b = { nodeId: id, subpath: seg.subpath, index: (seg.segment + 1) % sp.anchors.length };
2345
4521
  this.anchors = input.shift ? [...this.anchors.filter((r) => !sameRef(r, a) && !sameRef(r, b)), a, b] : [a, b];
2346
4522
  this.editingId = id;
2347
- this.drag = { kind: "anchors", start: p, before: this.snapshot(), base: this.doc, moved: false, clickRef: null };
4523
+ this.drag = { kind: "bend", ref: { nodeId: id, subpath: seg.subpath, segment: seg.segment }, t: seg.t, start: p, before: this.snapshot(), base: this.doc, moved: false };
2348
4524
  this.notify();
2349
4525
  return;
2350
4526
  }
@@ -2354,7 +4530,7 @@ var VectorEditorController = (_class = class {
2354
4530
  if (!input.shift) this.selection = [path.id];
2355
4531
  else if (!this.selection.includes(path.id)) this.selection = [...this.selection, path.id];
2356
4532
  this.anchors = path.subpaths.flatMap((sp, s) => sp.anchors.map((_, i) => ({ nodeId: path.id, subpath: s, index: i })));
2357
- this.drag = { kind: "anchors", start: p, before: this.snapshot(), base: this.doc, moved: false, clickRef: null };
4533
+ this.drag = { kind: "anchors", start: p, before: this.snapshot(), base: this.doc, moved: false, clickRef: null, lead: null };
2358
4534
  this.notify();
2359
4535
  return;
2360
4536
  }
@@ -2367,7 +4543,7 @@ var VectorEditorController = (_class = class {
2367
4543
  }
2368
4544
  downPen(screen, p, input) {
2369
4545
  const before = this.snapshot();
2370
- const sp = this.pen ? _optionalChain([this, 'access', _58 => _58.getNode, 'call', _59 => _59(this.pen.nodeId), 'optionalAccess', _60 => _60.subpaths, 'access', _61 => _61[this.pen.subpath]]) : void 0;
4546
+ const sp = this.pen ? _optionalChain([this, 'access', _91 => _91.getNode, 'call', _92 => _92(this.pen.nodeId), 'optionalAccess', _93 => _93.subpaths, 'access', _94 => _94[this.pen.subpath]]) : void 0;
2371
4547
  if (this.pen && sp) {
2372
4548
  const pen = this.pen;
2373
4549
  const first = { nodeId: pen.nodeId, subpath: pen.subpath, index: 0 };
@@ -2379,9 +4555,9 @@ var VectorEditorController = (_class = class {
2379
4555
  this.notify(true);
2380
4556
  return;
2381
4557
  }
2382
- let pt2 = this.snapPoint(p);
2383
4558
  const last = getAnchorWorld(this.doc, { nodeId: pen.nodeId, subpath: pen.subpath, index: sp.anchors.length - 1 });
2384
- if (input.shift) pt2 = constrain45(last, pt2);
4559
+ const leave = { at: { x: last.x, y: last.y }, constrain: input.shift ? "angle" : null };
4560
+ let pt2 = this.snapDrawn(p, leave, input, () => this.drawingTargets(pen.nodeId));
2385
4561
  pt2 = this.toLocal(pen.nodeId, pt2);
2386
4562
  const node2 = this.getNode(pen.nodeId);
2387
4563
  const anchors = [...sp.anchors, { x: pt2.x, y: pt2.y }];
@@ -2412,7 +4588,7 @@ var VectorEditorController = (_class = class {
2412
4588
  this.notify(this.doc !== before.doc);
2413
4589
  return;
2414
4590
  }
2415
- const pt = this.snapPoint(p);
4591
+ const pt = this.snapDrawn(p, null, input, () => this.drawingTargets(null));
2416
4592
  const node = { id: this.newId(), type: "path", name: "Path", ...this.styleFor(true), subpaths: [{ anchors: [{ x: pt.x, y: pt.y }], closed: false }] };
2417
4593
  this.setDoc([...this.doc.nodes, node]);
2418
4594
  this.pen = { nodeId: node.id, subpath: 0 };
@@ -2436,17 +4612,20 @@ var VectorEditorController = (_class = class {
2436
4612
  subpaths: [{ anchors: [{ x: p.x, y: p.y }], closed: false }]
2437
4613
  };
2438
4614
  this.setDoc([...this.doc.nodes, node]);
2439
- this.drag = { kind: "pencil", nodeId: node.id, points: [[p.x, p.y]], before };
4615
+ const lazy = new LazyMouse(this.editing.pencilSmoothing * VECTOR_PENCIL_STRING_PX / this.viewport.zoom);
4616
+ lazy.push([p.x, p.y]);
4617
+ this.drag = { kind: "pencil", nodeId: node.id, points: [[p.x, p.y]], before, lazy };
2440
4618
  this.interaction = { type: "draw", tool: "pencil", nodeId: node.id, bounds: { x: p.x, y: p.y, width: 0, height: 0 } };
2441
4619
  this.notify(true);
2442
4620
  }
2443
- downShape(p) {
4621
+ downShape(p, input) {
2444
4622
  const before = this.snapshot();
2445
- const start = this.snapPoint(p);
4623
+ const targets = this.drawingTargets(null);
4624
+ const start = this.snapDrawn(p, null, input, () => targets);
2446
4625
  const kind = this.tool;
2447
4626
  const node = this.createShapeNode({ kind, x: start.x, y: start.y, width: 0, height: 0 });
2448
4627
  this.setDoc([...this.doc.nodes, node]);
2449
- this.drag = { kind: "shape", tool: this.tool, start, nodeId: node.id, before };
4628
+ this.drag = { kind: "shape", tool: this.tool, start, nodeId: node.id, before, targets };
2450
4629
  this.interaction = { type: "draw", tool: this.tool, nodeId: node.id, bounds: { x: start.x, y: start.y, width: 0, height: 0 } };
2451
4630
  this.notify(true);
2452
4631
  }
@@ -2457,9 +4636,11 @@ var VectorEditorController = (_class = class {
2457
4636
  if (!drag) {
2458
4637
  const hover = this.computeHover(screen);
2459
4638
  const pointerMatters = this.tool === "pen" && !!this.pen;
2460
- if (this.sameHover(hover, this.hover) && !pointerMatters) return;
4639
+ const guides = this.guides;
2461
4640
  this.hover = hover;
2462
- this.pointer = p;
4641
+ const pointer = this.hoverSnap(p, input);
4642
+ if (this.sameHover(hover, this.state.hover) && !pointerMatters && JSON.stringify(guides) === JSON.stringify(this.guides)) return;
4643
+ this.pointer = pointer;
2463
4644
  this.notify();
2464
4645
  return;
2465
4646
  }
@@ -2480,8 +4661,9 @@ var VectorEditorController = (_class = class {
2480
4661
  const created = [];
2481
4662
  drag.base = this.withDuplicates(drag.base, this.selectedRoots(), { x: 0, y: 0 }, created);
2482
4663
  this.selection = created;
4664
+ drag.targets = void 0;
2483
4665
  }
2484
- ({ dx, dy } = this.snapDelta(dx, dy, drag.origin));
4666
+ ({ dx, dy } = this.snapMove(drag, dx, dy, input));
2485
4667
  this.doc = this.transformed(drag.base, new Set(this.selectedRoots()), translateMatrix(dx, dy));
2486
4668
  this.interaction = { type: "move", dx, dy };
2487
4669
  this.notify(true);
@@ -2490,7 +4672,13 @@ var VectorEditorController = (_class = class {
2490
4672
  case "scale": {
2491
4673
  const angle = drag.frame.angle;
2492
4674
  const q = angle ? applyMatrix(rotateMatrix(-angle), p) : p;
2493
- const { sx, sy, origin } = scaleFor(drag.handle, drag.frame, q, input);
4675
+ let { sx, sy, origin } = scaleFor(drag.handle, drag.frame, q, input);
4676
+ if (this.snap.objects && !input.mod && drag.handle !== "rotate") {
4677
+ drag.targets ??= VectorSnapTargets.forResize(drag.base, this.selectedRoots());
4678
+ const res = drag.targets.snapScale(drag.frame, drag.handle, sx, sy, input, this.snapTolerance());
4679
+ ({ sx, sy } = res);
4680
+ this.setGuides(res);
4681
+ } else this.setGuides(null);
2494
4682
  const ids = new Set(this.selectedRoots());
2495
4683
  this.doc = this.transformed(drag.base, ids, this.inFrame(angle, scaleMatrix(sx, sy, origin)));
2496
4684
  this.interaction = { type: "scale", handle: drag.handle, sx, sy };
@@ -2531,24 +4719,47 @@ var VectorEditorController = (_class = class {
2531
4719
  let dy = p.y - drag.start.y;
2532
4720
  if (!drag.moved && Math.hypot(dx, dy) * this.viewport.zoom < 2) return;
2533
4721
  drag.moved = true;
2534
- if (input.shift) Math.abs(dx) > Math.abs(dy) ? dy = 0 : dx = 0;
2535
- const lead = this.anchors.length ? getAnchorWorld(drag.base, this.anchors[0]) : void 0;
2536
- ({ dx, dy } = this.snapDelta(dx, dy, _nullishCoalesce(lead, () => ( null))));
2537
- this.doc = this.movedAnchors(drag.base, this.anchors, dx, dy);
4722
+ const lead = this.leadAnchor(drag);
4723
+ if (lead) {
4724
+ const leave = input.shift ? { at: lead, constrain: "axis", curves: false } : null;
4725
+ const q = this.snapDrawn({ x: lead.x + dx, y: lead.y + dy }, leave, input, () => drag.targets ??= VectorSnapTargets.forAnchorDrag(drag.base, this.anchors));
4726
+ dx = q.x - lead.x;
4727
+ dy = q.y - lead.y;
4728
+ } else if (input.shift) Math.abs(dx) > Math.abs(dy) ? dy = 0 : dx = 0;
4729
+ const single = this.anchors.length === 1 ? this.anchors[0] : null;
4730
+ const from = single ? getAnchorWorld(drag.base, single) : void 0;
4731
+ if (single && from && this.editing.curvatureMoves !== !!input.alt) this.doc = this.curvatureMoved(drag.base, single, { x: from.x + dx, y: from.y + dy });
4732
+ else this.doc = this.movedAnchors(drag.base, this.anchors, dx, dy);
2538
4733
  this.interaction = { type: "anchors", dx, dy };
2539
4734
  this.notify(true);
2540
4735
  return;
2541
4736
  }
2542
4737
  case "handle": {
2543
4738
  const anchor = getAnchorWorld(drag.base, drag.ref);
2544
- let pt = this.snapPoint(p);
2545
- if (input.shift && anchor) pt = constrain45(anchor, pt);
4739
+ const leave = anchor ? { at: { x: anchor.x, y: anchor.y }, constrain: input.shift ? "angle" : null } : null;
4740
+ const pt = this.snapDrawn(p, leave, input, () => drag.targets ??= VectorSnapTargets.forHandle(drag.base, drag.ref.nodeId));
2546
4741
  const local = this.toLocal(drag.ref.nodeId, pt, drag.base);
2547
4742
  this.doc = this.updateAnchors(drag.base, [drag.ref], (a) => moveAnchorHandle(a, drag.ref.handle, local, input.alt));
2548
4743
  this.interaction = { type: "handle", ref: drag.ref };
2549
4744
  this.notify(true);
2550
4745
  return;
2551
4746
  }
4747
+ case "bend": {
4748
+ if (!drag.moved && Math.hypot(p.x - drag.start.x, p.y - drag.start.y) * this.viewport.zoom < 2) return;
4749
+ drag.moved = true;
4750
+ this.doc = this.bent(drag.base, drag.ref, drag.t, p);
4751
+ this.interaction = { type: "bend", ref: drag.ref };
4752
+ this.notify(true);
4753
+ return;
4754
+ }
4755
+ case "stroke": {
4756
+ const last = drag.points[drag.points.length - 1];
4757
+ if (Math.hypot(p.x - last.x, p.y - last.y) * this.viewport.zoom < 1.5) return;
4758
+ drag.points.push(p);
4759
+ this.interaction = { type: "stroke", tool: drag.tool, points: drag.points.slice(), width: drag.tool === "eraser" ? this.editing.eraserSize : 0 };
4760
+ this.notify();
4761
+ return;
4762
+ }
2552
4763
  case "pull": {
2553
4764
  const anchor = getAnchorWorld(drag.base, drag.ref);
2554
4765
  if (!anchor) return;
@@ -2557,6 +4768,7 @@ var VectorEditorController = (_class = class {
2557
4768
  this.notify(true);
2558
4769
  return;
2559
4770
  }
4771
+ this.guides = null;
2560
4772
  let pt = p;
2561
4773
  if (input.shift) pt = constrain45(anchor, pt);
2562
4774
  const side = drag.closing ? "in" : "out";
@@ -2570,8 +4782,10 @@ var VectorEditorController = (_class = class {
2570
4782
  }
2571
4783
  case "shape": {
2572
4784
  const node = this.getNode(drag.nodeId);
2573
- if (_optionalChain([node, 'optionalAccess', _62 => _62.type]) !== "path") return;
2574
- let end = this.snapPoint(p);
4785
+ if (_optionalChain([node, 'optionalAccess', _95 => _95.type]) !== "path") return;
4786
+ const leave = { at: drag.start, constrain: input.shift ? drag.tool === "line" ? "angle" : "square" : null, curves: false };
4787
+ let end = this.snapDrawn(p, leave, input, () => drag.targets ??= this.drawingTargets(null));
4788
+ if (!this.snappedToObject && drag.tool !== "line" && !input.shift && !input.mod && this.snap.objects && drag.targets) end = this.matchShapeSize(drag.targets, drag.start, end, input.alt);
2575
4789
  let start = drag.start;
2576
4790
  let bounds;
2577
4791
  if (drag.tool === "line") {
@@ -2599,10 +4813,22 @@ var VectorEditorController = (_class = class {
2599
4813
  }
2600
4814
  case "pencil": {
2601
4815
  const last = drag.points[drag.points.length - 1];
2602
- if (Math.hypot(p.x - last[0], p.y - last[1]) * this.viewport.zoom < 1.5) return;
2603
- drag.points.push([p.x, p.y]);
4816
+ let moved = false;
4817
+ for (const q of drag.lazy.push([p.x, p.y])) {
4818
+ if (Math.hypot(q[0] - last[0], q[1] - last[1]) * this.viewport.zoom < 1.5) continue;
4819
+ drag.points.push(q);
4820
+ moved = true;
4821
+ }
4822
+ const string = drag.lazy.string();
2604
4823
  const node = this.getNode(drag.nodeId);
2605
- if (_optionalChain([node, 'optionalAccess', _63 => _63.type]) !== "path") return;
4824
+ if (_optionalChain([node, 'optionalAccess', _96 => _96.type]) !== "path") return;
4825
+ if (!moved) {
4826
+ if (string && _optionalChain([this, 'access', _97 => _97.interaction, 'optionalAccess', _98 => _98.type]) === "draw") {
4827
+ this.interaction = { ...this.interaction, string: [{ x: string[0][0], y: string[0][1] }, { x: string[1][0], y: string[1][1] }] };
4828
+ this.notify();
4829
+ }
4830
+ return;
4831
+ }
2606
4832
  this.replaceNode(drag.nodeId, { ...node, subpaths: [{ anchors: drag.points.map(([x, y]) => ({ x, y })), closed: false }] });
2607
4833
  const xs = drag.points.map((q) => q[0]);
2608
4834
  const ys = drag.points.map((q) => q[1]);
@@ -2610,19 +4836,72 @@ var VectorEditorController = (_class = class {
2610
4836
  type: "draw",
2611
4837
  tool: "pencil",
2612
4838
  nodeId: drag.nodeId,
2613
- bounds: { x: Math.min(...xs), y: Math.min(...ys), width: Math.max(...xs) - Math.min(...xs), height: Math.max(...ys) - Math.min(...ys) }
4839
+ bounds: { x: Math.min(...xs), y: Math.min(...ys), width: Math.max(...xs) - Math.min(...xs), height: Math.max(...ys) - Math.min(...ys) },
4840
+ ...string ? { string: [{ x: string[0][0], y: string[0][1] }, { x: string[1][0], y: string[1][1] }] } : {}
2614
4841
  };
2615
4842
  this.notify(true);
2616
4843
  return;
2617
4844
  }
2618
4845
  }
2619
4846
  }
4847
+ /** Snaps a move of the selection by (dx, dy): its bounds line up with targets or space evenly, else go to the grid. */
4848
+ snapMove(drag, dx, dy, input) {
4849
+ const origin = drag.origin;
4850
+ if (input.mod || !origin) {
4851
+ this.setGuides(null);
4852
+ return { dx, dy };
4853
+ }
4854
+ const axes = { x: !input.shift || dx !== 0, y: !input.shift || dy !== 0 };
4855
+ let res = null;
4856
+ if (this.snap.objects) {
4857
+ drag.targets ??= VectorSnapTargets.forMove(drag.base, this.selectedRoots());
4858
+ res = drag.targets.snapRect({ ...origin, x: origin.x + dx, y: origin.y + dy }, this.snapTolerance(), { spacing: true, axes });
4859
+ }
4860
+ this.setGuides(res);
4861
+ return {
4862
+ dx: _optionalChain([res, 'optionalAccess', _99 => _99.x]) ? dx + res.dx : axes.x ? this.gridValue(origin.x + dx) - origin.x : dx,
4863
+ dy: _optionalChain([res, 'optionalAccess', _100 => _100.y]) ? dy + res.dy : axes.y ? this.gridValue(origin.y + dy) - origin.y : dy
4864
+ };
4865
+ }
4866
+ /** The anchor a point drag snaps by, where it started: the grabbed one, else the moving one nearest the press. */
4867
+ leadAnchor(drag) {
4868
+ const ref = drag.lead && this.anchors.some((a) => sameRef(a, drag.lead)) ? drag.lead : null;
4869
+ if (ref) {
4870
+ const a = getAnchorWorld(drag.base, ref);
4871
+ return a ? { x: a.x, y: a.y } : null;
4872
+ }
4873
+ let best = null;
4874
+ for (const r of this.anchors) {
4875
+ const a = getAnchorWorld(drag.base, r);
4876
+ if (a && (!best || Math.hypot(a.x - drag.start.x, a.y - drag.start.y) < Math.hypot(best.x - drag.start.x, best.y - drag.start.y))) best = { x: a.x, y: a.y };
4877
+ }
4878
+ return best;
4879
+ }
4880
+ /** A shape's dragged corner moved so its width or height matches another object's, when one is within reach. */
4881
+ matchShapeSize(targets, start, end, fromCentre = false) {
4882
+ const k = fromCentre ? 2 : 1;
4883
+ const tol = this.snapTolerance() * k;
4884
+ const out = { ...end };
4885
+ const guides = { lines: [], labels: [] };
4886
+ for (const axis of ["x", "y"]) {
4887
+ const d = end[axis] - start[axis];
4888
+ const len = Math.abs(d) > 1e-9 ? targets.matchSize(Math.abs(d) * k, tol) : null;
4889
+ if (len === null || len <= 0) continue;
4890
+ out[axis] = start[axis] + Math.sign(d) * len / k;
4891
+ const from = { ...out, [axis]: fromCentre ? 2 * start[axis] - out[axis] : start[axis] };
4892
+ guides.lines.push({ kind: "size", x1: from.x, y1: from.y, x2: out.x, y2: out.y });
4893
+ guides.labels.push({ kind: "size", x: (from.x + out.x) / 2, y: (from.y + out.y) / 2, text: `${axis === "x" ? "W" : "H"} ${Math.round(len * 100) / 100}` });
4894
+ }
4895
+ this.setGuides(guides);
4896
+ return out;
4897
+ }
2620
4898
  /** Primary button released. */
2621
4899
  pointerUp(screen, input = {}) {
2622
4900
  if (screen && this.drag && this.drag.kind !== "pan") this.pointerMove(screen, input);
2623
4901
  const drag = this.drag;
2624
4902
  this.drag = null;
2625
4903
  this.interaction = null;
4904
+ this.guides = null;
2626
4905
  if (!drag) return;
2627
4906
  switch (drag.kind) {
2628
4907
  case "pan":
@@ -2650,6 +4929,18 @@ var VectorEditorController = (_class = class {
2650
4929
  return this.commit(drag.before.doc !== drag.base ? "Add Point" : "Move Points", drag.before);
2651
4930
  case "handle":
2652
4931
  return this.commit("Move Handle", drag.before);
4932
+ case "bend":
4933
+ if (!drag.moved) {
4934
+ this.notify();
4935
+ return;
4936
+ }
4937
+ return this.commit("Bend Segment", drag.before);
4938
+ case "stroke": {
4939
+ this.notify();
4940
+ if (drag.tool === "knife") this.knife(drag.points);
4941
+ else this.erase(drag.points);
4942
+ return;
4943
+ }
2653
4944
  case "pull":
2654
4945
  if (drag.closing) this.pen = null;
2655
4946
  return this.commit(drag.label, drag.before);
@@ -2669,12 +4960,19 @@ var VectorEditorController = (_class = class {
2669
4960
  }
2670
4961
  case "pencil": {
2671
4962
  const node = this.getNode(drag.nodeId);
2672
- if (_optionalChain([node, 'optionalAccess', _64 => _64.type]) !== "path" || drag.points.length < 2) {
4963
+ if (_optionalChain([node, 'optionalAccess', _101 => _101.type]) !== "path" || drag.points.length < 2) {
2673
4964
  this.doc = drag.before.doc;
2674
4965
  this.notify(true);
2675
4966
  return;
2676
4967
  }
2677
- const fitted = _chunkM4XE4UMTcjs.fitCubics.call(void 0, drag.points, 1.5 / this.viewport.zoom, { cornerAngleDeg: 60 });
4968
+ for (const q of drag.lazy.finish()) drag.points.push(q);
4969
+ const shape = this.editing.recognizeShapes ? recognizeInkShape(drag.points, { minSize: 6 / this.viewport.zoom }) : null;
4970
+ if (shape) {
4971
+ this.replaceNode(drag.nodeId, { ...node, name: inkShapeLabel(shape), subpaths: [inkShapeSubpath(shape)] });
4972
+ this.selection = [drag.nodeId];
4973
+ return this.commit(`Draw ${inkShapeLabel(shape)}`, drag.before);
4974
+ }
4975
+ const fitted = _chunkBGNPT5PEcjs.fitCubics.call(void 0, drag.points, 1.5 / this.viewport.zoom, { cornerAngleDeg: 60 });
2678
4976
  const anchors = [];
2679
4977
  for (const c of fitted) {
2680
4978
  if (!anchors.length) anchors.push({ x: c.p0[0], y: c.p0[1] });
@@ -2691,9 +4989,10 @@ var VectorEditorController = (_class = class {
2691
4989
  /** The pointer left the canvas. */
2692
4990
  pointerLeave() {
2693
4991
  if (this.drag) return;
2694
- if (!this.hover && !this.pointer) return;
4992
+ if (!this.hover && !this.pointer && !this.guides) return;
2695
4993
  this.hover = null;
2696
4994
  this.pointer = null;
4995
+ this.guides = null;
2697
4996
  this.notify();
2698
4997
  }
2699
4998
  /** Abandons the drag in progress (Escape, lost capture). */
@@ -2702,6 +5001,7 @@ var VectorEditorController = (_class = class {
2702
5001
  if (!drag) return;
2703
5002
  this.drag = null;
2704
5003
  this.interaction = null;
5004
+ this.guides = null;
2705
5005
  if (drag.kind === "pan") {
2706
5006
  this.notify();
2707
5007
  return;
@@ -2734,7 +5034,7 @@ var VectorEditorController = (_class = class {
2734
5034
  this.convertAnchors(a.in || a.out ? "corner" : "smooth", [anchor]);
2735
5035
  return;
2736
5036
  }
2737
- const id = _nullishCoalesce(_optionalChain([this, 'access', _65 => _65.hover, 'optionalAccess', _66 => _66.segment, 'optionalAccess', _67 => _67.nodeId]), () => ( this.editingId));
5037
+ const id = _nullishCoalesce(_optionalChain([this, 'access', _102 => _102.hover, 'optionalAccess', _103 => _103.segment, 'optionalAccess', _104 => _104.nodeId]), () => ( this.editingId));
2738
5038
  if (id) {
2739
5039
  const node = this.getNode(id);
2740
5040
  const seg = node ? nearestSegment(node, this.toLocal(id, p)) : null;
@@ -2778,6 +5078,8 @@ var VectorEditorController = (_class = class {
2778
5078
  case "g":
2779
5079
  input.shift ? this.ungroup() : this.group();
2780
5080
  return true;
5081
+ case "j":
5082
+ return input.alt ? this.averageAnchors("both").ok : this.joinPaths().ok;
2781
5083
  case "]":
2782
5084
  this.arrange(input.alt ? "front" : "forward");
2783
5085
  return true;
@@ -2812,7 +5114,7 @@ var VectorEditorController = (_class = class {
2812
5114
  this.finishPath();
2813
5115
  return true;
2814
5116
  }
2815
- if (this.tool === "select" && this.selection.length === 1 && _optionalChain([this, 'access', _68 => _68.getNode, 'call', _69 => _69(this.selection[0]), 'optionalAccess', _70 => _70.type]) === "path") {
5117
+ if (this.tool === "select" && this.selection.length === 1 && _optionalChain([this, 'access', _105 => _105.getNode, 'call', _106 => _106(this.selection[0]), 'optionalAccess', _107 => _107.type]) === "path") {
2816
5118
  this.tool = "direct";
2817
5119
  this.editingId = this.selection[0];
2818
5120
  this.notify();
@@ -2857,7 +5159,23 @@ var VectorEditorController = (_class = class {
2857
5159
  this.notify();
2858
5160
  }
2859
5161
  }
2860
- }, _class);
5162
+ }, _class3);
5163
+ function inkShapeLabel(shape) {
5164
+ const name = vectorInkShapeName(shape);
5165
+ return name[0].toUpperCase() + name.slice(1);
5166
+ }
5167
+ function inkShapeSubpath(shape) {
5168
+ switch (shape.kind) {
5169
+ case "line":
5170
+ return lineSubpath({ x: shape.a[0], y: shape.a[1] }, { x: shape.b[0], y: shape.b[1] });
5171
+ case "rect":
5172
+ return rectSubpath(shape);
5173
+ case "ellipse":
5174
+ return ellipseSubpath(shape);
5175
+ case "polygon":
5176
+ return { anchors: shape.points.map(([x, y]) => ({ x, y })), closed: true };
5177
+ }
5178
+ }
2861
5179
  function constrain45(origin, p) {
2862
5180
  const dx = p.x - origin.x;
2863
5181
  const dy = p.y - origin.y;
@@ -2910,7 +5228,7 @@ function renderNode(node, defs, idPrefix) {
2910
5228
  strokeWidth: _nullishCoalesce(node.strokeWidth, () => ( 1)),
2911
5229
  ...node.strokeLinecap ? { strokeLinecap: node.strokeLinecap } : {},
2912
5230
  ...node.strokeLinejoin ? { strokeLinejoin: node.strokeLinejoin } : {},
2913
- ..._optionalChain([node, 'access', _71 => _71.strokeDasharray, 'optionalAccess', _72 => _72.length]) ? { strokeDasharray: node.strokeDasharray.join(" ") } : {},
5231
+ ..._optionalChain([node, 'access', _108 => _108.strokeDasharray, 'optionalAccess', _109 => _109.length]) ? { strokeDasharray: node.strokeDasharray.join(" ") } : {},
2914
5232
  fillRule: _nullishCoalesce(node.fillRule, () => ( "nonzero")),
2915
5233
  opacity: _nullishCoalesce(node.opacity, () => ( 1))
2916
5234
  };
@@ -2934,7 +5252,7 @@ function screenOutline(state, node) {
2934
5252
  const v = state.viewport;
2935
5253
  const view = [v.zoom, 0, 0, v.zoom, v.x, v.y];
2936
5254
  const parent = _nullishCoalesce(vectorParentMatrix(state.document.nodes, node.id), () => ( IDENTITY_MATRIX));
2937
- return collectWithMatrix(node, parent).map(({ path, parent: m }) => _chunkM4XE4UMTcjs.bezierPathToSvg.call(void 0, _chunkM4XE4UMTcjs.transformBezierPath.call(void 0, vectorPathWorldBezier(path, m), view), { precision: 2 })).join(" ");
5255
+ return collectWithMatrix(node, parent).map(({ path, parent: m }) => _chunkBGNPT5PEcjs.bezierPathToSvg.call(void 0, _chunkBGNPT5PEcjs.transformBezierPath.call(void 0, vectorPathWorldBezier(path, m), view), { precision: 2 })).join(" ");
2938
5256
  }
2939
5257
  function collectWithMatrix(node, parent) {
2940
5258
  if (node.type === "path") return [{ path: node, parent }];
@@ -2959,9 +5277,9 @@ function getVectorOverlayRenderData(state) {
2959
5277
  };
2960
5278
  for (const id of state.selection) outline(id, true);
2961
5279
  if (state.editingId) outline(state.editingId, true);
2962
- if (_optionalChain([state, 'access', _73 => _73.hover, 'optionalAccess', _74 => _74.nodeId]) && !state.interaction) outline(state.hover.nodeId, false);
5280
+ if (_optionalChain([state, 'access', _110 => _110.hover, 'optionalAccess', _111 => _111.nodeId]) && !state.interaction) outline(state.hover.nodeId, false);
2963
5281
  let selectionBox = null;
2964
- if (state.tool === "select" && state.selectionFrame && _optionalChain([state, 'access', _75 => _75.interaction, 'optionalAccess', _76 => _76.type]) !== "marquee") {
5282
+ if (state.tool === "select" && state.selectionFrame && _optionalChain([state, 'access', _112 => _112.interaction, 'optionalAccess', _113 => _113.type]) !== "marquee") {
2965
5283
  const g = getVectorSelectionBoxGeometry(state.selectionFrame, state.viewport);
2966
5284
  selectionBox = { ...g.box, angle: g.angle, transform: g.transform, corners: g.corners, handles: g.handles };
2967
5285
  }
@@ -2971,9 +5289,9 @@ function getVectorOverlayRenderData(state) {
2971
5289
  const a = getAnchorWorld(doc, ref);
2972
5290
  const s = toScreen(state, a);
2973
5291
  const closes = !!state.pen && state.pen.nodeId === ref.nodeId && state.pen.subpath === ref.subpath && ref.index === 0;
2974
- return { ref, x: s.x, y: s.y, type: _nullishCoalesce(a.type, () => ( inferAnchorType(a))), selected: selected(ref), hovered: sameAnchor(_optionalChain([state, 'access', _77 => _77.hover, 'optionalAccess', _78 => _78.anchor]), ref), closes };
5292
+ return { ref, x: s.x, y: s.y, type: _nullishCoalesce(a.type, () => ( inferAnchorType(a))), selected: selected(ref), hovered: sameAnchor(_optionalChain([state, 'access', _114 => _114.hover, 'optionalAccess', _115 => _115.anchor]), ref), closes };
2975
5293
  });
2976
- if (state.tool === "pen" && !state.pen && _optionalChain([state, 'access', _79 => _79.hover, 'optionalAccess', _80 => _80.anchor])) {
5294
+ if (state.tool === "pen" && !state.pen && _optionalChain([state, 'access', _116 => _116.hover, 'optionalAccess', _117 => _117.anchor])) {
2977
5295
  const a = getAnchorWorld(doc, state.hover.anchor);
2978
5296
  if (a) {
2979
5297
  const s = toScreen(state, a);
@@ -2982,15 +5300,15 @@ function getVectorOverlayRenderData(state) {
2982
5300
  }
2983
5301
  const handles = getVisibleHandles(state).flatMap((ref) => {
2984
5302
  const a = getAnchorWorld(doc, ref);
2985
- const h = _optionalChain([a, 'optionalAccess', _81 => _81[ref.handle]]);
5303
+ const h = _optionalChain([a, 'optionalAccess', _118 => _118[ref.handle]]);
2986
5304
  if (!a || !h) return [];
2987
5305
  const s = toScreen(state, h);
2988
5306
  const sa = toScreen(state, a);
2989
- const hovered = !!_optionalChain([state, 'access', _82 => _82.hover, 'optionalAccess', _83 => _83.handle]) && sameAnchor(state.hover.handle, ref) && state.hover.handle.handle === ref.handle;
5307
+ const hovered = !!_optionalChain([state, 'access', _119 => _119.hover, 'optionalAccess', _120 => _120.handle]) && sameAnchor(state.hover.handle, ref) && state.hover.handle.handle === ref.handle;
2990
5308
  return [{ ref, x: s.x, y: s.y, anchorX: sa.x, anchorY: sa.y, hovered }];
2991
5309
  });
2992
5310
  let marquee = null;
2993
- if (_optionalChain([state, 'access', _84 => _84.interaction, 'optionalAccess', _85 => _85.type]) === "marquee") {
5311
+ if (_optionalChain([state, 'access', _121 => _121.interaction, 'optionalAccess', _122 => _122.type]) === "marquee") {
2994
5312
  const b = state.interaction.bounds;
2995
5313
  const tl = toScreen(state, b);
2996
5314
  marquee = { x: tl.x, y: tl.y, width: b.width * z, height: b.height * z };
@@ -2998,7 +5316,7 @@ function getVectorOverlayRenderData(state) {
2998
5316
  let penPreview = null;
2999
5317
  if (state.tool === "pen" && state.pen && state.pointer && !state.interaction) {
3000
5318
  const node = findVectorNode(doc.nodes, state.pen.nodeId);
3001
- const sp = _optionalChain([node, 'optionalAccess', _86 => _86.type]) === "path" ? node.subpaths[state.pen.subpath] : void 0;
5319
+ const sp = _optionalChain([node, 'optionalAccess', _123 => _123.type]) === "path" ? node.subpaths[state.pen.subpath] : void 0;
3002
5320
  const last = sp ? getAnchorWorld(doc, { nodeId: state.pen.nodeId, subpath: state.pen.subpath, index: sp.anchors.length - 1 }) : void 0;
3003
5321
  if (last) {
3004
5322
  const a = toScreen(state, last);
@@ -3011,18 +5329,47 @@ function getVectorOverlayRenderData(state) {
3011
5329
  const artboard = { x: origin.x, y: origin.y, width: doc.width * z, height: doc.height * z };
3012
5330
  let status = null;
3013
5331
  const it = state.interaction;
3014
- if (_optionalChain([it, 'optionalAccess', _87 => _87.type]) === "rotate") status = `${Math.round(it.angle)}\xB0`;
3015
- else if (_optionalChain([it, 'optionalAccess', _88 => _88.type]) === "move" || _optionalChain([it, 'optionalAccess', _89 => _89.type]) === "anchors") status = `\u0394 ${fmt(it.dx)}, ${fmt(it.dy)}`;
3016
- else if (_optionalChain([it, 'optionalAccess', _90 => _90.type]) === "scale" || _optionalChain([it, 'optionalAccess', _91 => _91.type]) === "draw") {
5332
+ if (_optionalChain([it, 'optionalAccess', _124 => _124.type]) === "rotate") status = `${Math.round(it.angle)}\xB0`;
5333
+ else if (_optionalChain([it, 'optionalAccess', _125 => _125.type]) === "move" || _optionalChain([it, 'optionalAccess', _126 => _126.type]) === "anchors") status = `\u0394 ${fmt(it.dx)}, ${fmt(it.dy)}`;
5334
+ else if (_optionalChain([it, 'optionalAccess', _127 => _127.type]) === "scale" || _optionalChain([it, 'optionalAccess', _128 => _128.type]) === "draw") {
3017
5335
  const b = it.type === "draw" ? findVectorNode(doc.nodes, it.nodeId) ? vectorNodeBounds(findVectorNode(doc.nodes, it.nodeId)) : null : state.selectionFrame;
3018
5336
  if (b) status = `W ${fmt(b.width)} \xD7 H ${fmt(b.height)}`;
3019
5337
  }
3020
- return { outlines, selectionBox, anchors, handles, marquee, penPreview, artboard, status };
5338
+ const guides = { lines: [], labels: [] };
5339
+ if (state.guides) {
5340
+ for (const l of state.guides.lines) {
5341
+ const a = toScreen(state, { x: l.x1, y: l.y1 });
5342
+ const b = toScreen(state, { x: l.x2, y: l.y2 });
5343
+ guides.lines.push({ kind: l.kind, x1: fmt(a.x), y1: fmt(a.y), x2: fmt(b.x), y2: fmt(b.y) });
5344
+ }
5345
+ for (const l of state.guides.labels) {
5346
+ const p = toScreen(state, l);
5347
+ guides.labels.push({ kind: l.kind, x: fmt(p.x), y: fmt(p.y), text: l.text });
5348
+ }
5349
+ }
5350
+ let toolStroke = null;
5351
+ if (_optionalChain([it, 'optionalAccess', _129 => _129.type]) === "stroke") {
5352
+ const pts = it.points.map((q) => toScreen(state, q));
5353
+ const d = pts.map((q, i) => `${i ? "L" : "M"}${fmt(q.x)} ${fmt(q.y)}`).join("") + (pts.length === 1 ? `L${fmt(pts[0].x)} ${fmt(pts[0].y)}` : "");
5354
+ toolStroke = { tool: it.tool, d, width: fmt(it.width * z) };
5355
+ }
5356
+ let pencilString = null;
5357
+ if (_optionalChain([it, 'optionalAccess', _130 => _130.type]) === "draw" && it.string) {
5358
+ const a = toScreen(state, it.string[0]);
5359
+ const b = toScreen(state, it.string[1]);
5360
+ pencilString = { x1: fmt(a.x), y1: fmt(a.y), x2: fmt(b.x), y2: fmt(b.y) };
5361
+ }
5362
+ const cutPoint = state.tool === "scissors" && _optionalChain([state, 'access', _131 => _131.hover, 'optionalAccess', _132 => _132.segment]) && !it ? (({ x, y }) => ({ x: fmt(x), y: fmt(y) }))(toScreen(state, state.hover.segment.point)) : null;
5363
+ return { outlines, selectionBox, anchors, handles, marquee, penPreview, artboard, guides, status, toolStroke, pencilString, cutPoint };
3021
5364
  }
3022
5365
  var VECTOR_OVERLAY_THEME = {
3023
5366
  accent: "#0ea5e9",
5367
+ /** Smart guide lines and labels. */
5368
+ guide: "#ff3dfc",
3024
5369
  handleFill: "#ffffff",
3025
5370
  marqueeFill: "rgba(14, 165, 233, 0.08)",
5371
+ /** The eraser's swath while it is dragged. */
5372
+ eraserFill: "rgba(239, 68, 68, 0.25)",
3026
5373
  transformHandleSize: 8,
3027
5374
  anchorSize: 7,
3028
5375
  handleRadius: 3.5
@@ -3041,7 +5388,7 @@ function isVectorTextTarget(target) {
3041
5388
  if (!el || typeof el !== "object") return false;
3042
5389
  const tag = (_nullishCoalesce(el.tagName, () => ( ""))).toUpperCase();
3043
5390
  if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
3044
- return !!el.isContentEditable || _optionalChain([el, 'access', _92 => _92.getAttribute, 'optionalCall', _93 => _93("contenteditable")]) === "true";
5391
+ return !!el.isContentEditable || _optionalChain([el, 'access', _133 => _133.getAttribute, 'optionalCall', _134 => _134("contenteditable")]) === "true";
3045
5392
  }
3046
5393
  function getVectorSelectionStyle(state) {
3047
5394
  const paths = [];
@@ -3068,12 +5415,39 @@ function vectorPaintToHex(paint) {
3068
5415
  const hex = m[1].length === 3 ? m[1].replace(/./g, (c) => c + c) : m[1];
3069
5416
  return `#${hex.toLowerCase()}`;
3070
5417
  }
5418
+ var VECTOR_PATH_ACTIONS = [
5419
+ { id: "join", label: "\u22C8", title: "Join", shortcut: "J", enabled: (s, p) => s + p > 0, run: (c) => c.joinPaths() },
5420
+ { id: "average", label: "\u22B9", title: "Average Points", shortcut: "Alt+J", enabled: (_s, p) => p > 1, run: (c) => c.averageAnchors("both") },
5421
+ { id: "cut", label: "\u2702\xB7", title: "Cut at Points", enabled: (_s, p) => p > 0, run: (c) => c.cutAtAnchors() },
5422
+ { id: "clean", label: "\u2301", title: "Remove Redundant Points", enabled: (s, p) => s + p > 0, run: (c) => c.removeRedundantPoints() },
5423
+ { id: "smooth", label: "\u223F", title: "Smooth", enabled: (s, p) => s + p > 0, run: (c) => c.smoothPaths() }
5424
+ ];
5425
+ var VECTOR_EDIT_TOGGLES = [
5426
+ { key: "keepShapeOnDelete", label: "Keep", title: "Keep shape when deleting points" },
5427
+ { key: "curvatureMoves", label: "Curv", title: "Curvature point moves (Alt flips while dragging)" },
5428
+ { key: "recognizeShapes", label: "Shapes", title: "Turn pencil scribbles into shapes" }
5429
+ ];
3071
5430
 
3072
5431
  // src/vector/commands.ts
3073
5432
  var fail = (message) => {
3074
5433
  throw new Error(message);
3075
5434
  };
3076
5435
  var IDS = { type: "array", items: { type: "string" }, description: "Node ids (default: the selection)." };
5436
+ var ANCHORS = {
5437
+ type: "array",
5438
+ items: {
5439
+ type: "object",
5440
+ properties: { nodeId: { type: "string" }, subpath: { type: "integer", minimum: 0 }, index: { type: "integer", minimum: 0 } },
5441
+ required: ["nodeId", "subpath", "index"]
5442
+ },
5443
+ description: "Anchors as { nodeId, subpath, index } (default: the selected points)."
5444
+ };
5445
+ var POINTS = {
5446
+ type: "array",
5447
+ items: { type: "array", items: { type: "number" }, minItems: 2, maxItems: 2 },
5448
+ minItems: 1,
5449
+ description: "Polyline [[x, y], \u2026] in document units."
5450
+ };
3077
5451
  var PAINT = {
3078
5452
  description: 'A CSS colour, `null` for none, or a gradient `{ type: "linear", angle, stops: [{ offset, color }] }`.'
3079
5453
  };
@@ -3100,12 +5474,20 @@ function createVectorCommands(controller, options = {}) {
3100
5474
  const base = { group, ...options.scope ? { scope: options.scope } : {} };
3101
5475
  const hasSelection = () => controller.getState().selection.length ? true : "Nothing is selected.";
3102
5476
  const ids = (params) => {
3103
- const list2 = _nullishCoalesce(_optionalChain([params, 'optionalAccess', _94 => _94.ids]), () => ( controller.getState().selection));
5477
+ const list2 = _nullishCoalesce(_optionalChain([params, 'optionalAccess', _135 => _135.ids]), () => ( controller.getState().selection));
3104
5478
  for (const id of list2) if (!controller.getNode(id)) fail(`There is no node "${id}".`);
3105
5479
  if (!list2.length) fail("Nothing is selected; pass ids.");
3106
5480
  return list2;
3107
5481
  };
3108
- const enabledFor = (_, params) => _optionalChain([params, 'optionalAccess', _95 => _95.ids, 'optionalAccess', _96 => _96.length]) ? true : hasSelection();
5482
+ const enabledFor = (_, params) => _optionalChain([params, 'optionalAccess', _136 => _136.ids, 'optionalAccess', _137 => _137.length]) ? true : hasSelection();
5483
+ const anchorsOf = (refs) => {
5484
+ if (!refs) return [...controller.getState().selectedAnchors];
5485
+ for (const r of refs) {
5486
+ const node = controller.getNode(r.nodeId);
5487
+ if (_optionalChain([node, 'optionalAccess', _138 => _138.type]) !== "path" || !_optionalChain([node, 'access', _139 => _139.subpaths, 'access', _140 => _140[r.subpath], 'optionalAccess', _141 => _141.anchors, 'access', _142 => _142[r.index]])) fail(`There is no anchor ${r.index} on subpath ${r.subpath} of "${r.nodeId}".`);
5488
+ }
5489
+ return refs;
5490
+ };
3109
5491
  const list = () => {
3110
5492
  const state = controller.getState();
3111
5493
  const out = [];
@@ -3192,7 +5574,7 @@ function createVectorCommands(controller, options = {}) {
3192
5574
  run: (_ctx, params) => {
3193
5575
  const id = controller.addPath(params.d, { ...pickStyle(params), ...params.name ? { name: params.name } : {} });
3194
5576
  const node = controller.getNode(id);
3195
- if (_optionalChain([node, 'optionalAccess', _97 => _97.type]) === "path" && !node.subpaths.length) {
5577
+ if (_optionalChain([node, 'optionalAccess', _143 => _143.type]) === "path" && !node.subpaths.length) {
3196
5578
  controller.undo();
3197
5579
  fail("The path data has no points.");
3198
5580
  }
@@ -3325,7 +5707,7 @@ function createVectorCommands(controller, options = {}) {
3325
5707
  enabled: enabledFor,
3326
5708
  undoable: true,
3327
5709
  run: (_ctx, params) => {
3328
- const id = controller.group({ ids: ids(params), name: _optionalChain([params, 'optionalAccess', _98 => _98.name]) });
5710
+ const id = controller.group({ ids: ids(params), name: _optionalChain([params, 'optionalAccess', _144 => _144.name]) });
3329
5711
  if (!id) fail("Only nodes that share a parent can be grouped.");
3330
5712
  return { id };
3331
5713
  }
@@ -3344,6 +5726,186 @@ function createVectorCommands(controller, options = {}) {
3344
5726
  return { selection: controller.getState().selection };
3345
5727
  }
3346
5728
  },
5729
+ {
5730
+ ...base,
5731
+ id: `${prefix}deleteAnchors`,
5732
+ label: "Delete Points",
5733
+ keywords: ["remove", "anchor", "point", "node"],
5734
+ description: "Deletes anchors. With `keepShape` (default: the editor setting, on) the neighbouring handles are refitted so the outline keeps its shape; off, the neighbours are joined as they are.",
5735
+ params: { type: "object", properties: { anchors: ANCHORS, keepShape: { type: "boolean" } } },
5736
+ undoable: true,
5737
+ run: (_ctx, params) => {
5738
+ check(controller.deleteAnchors(anchorsOf(_optionalChain([params, 'optionalAccess', _145 => _145.anchors])), { keepShape: _optionalChain([params, 'optionalAccess', _146 => _146.keepShape]) }));
5739
+ return { ok: true };
5740
+ }
5741
+ },
5742
+ {
5743
+ ...base,
5744
+ id: `${prefix}moveAnchor`,
5745
+ label: "Move Point\u2026",
5746
+ keywords: ["anchor", "drag", "curvature"],
5747
+ description: "Moves one anchor to (x, y) in document units. `curvature: true` bends its two segments like the Curvature tool instead of carrying its handles along.",
5748
+ params: {
5749
+ type: "object",
5750
+ properties: { nodeId: { type: "string" }, subpath: { type: "integer", minimum: 0 }, index: { type: "integer", minimum: 0 }, x: { type: "number" }, y: { type: "number" }, curvature: { type: "boolean" } },
5751
+ required: ["nodeId", "subpath", "index", "x", "y"]
5752
+ },
5753
+ undoable: true,
5754
+ run: (_ctx, params) => {
5755
+ check(controller.moveAnchor({ nodeId: params.nodeId, subpath: params.subpath, index: params.index }, { x: params.x, y: params.y }, { curvature: params.curvature }));
5756
+ return { ok: true };
5757
+ }
5758
+ },
5759
+ {
5760
+ ...base,
5761
+ id: `${prefix}bendSegment`,
5762
+ label: "Bend Segment\u2026",
5763
+ keywords: ["curve", "segment", "drag"],
5764
+ description: "Bends segment `segment` (from anchor `segment` to the next) so the curve passes through (x, y) at parameter `t` (default 0.5).",
5765
+ params: {
5766
+ type: "object",
5767
+ properties: {
5768
+ nodeId: { type: "string" },
5769
+ subpath: { type: "integer", minimum: 0 },
5770
+ segment: { type: "integer", minimum: 0 },
5771
+ x: { type: "number" },
5772
+ y: { type: "number" },
5773
+ t: { type: "number", minimum: 0, maximum: 1 }
5774
+ },
5775
+ required: ["nodeId", "subpath", "segment", "x", "y"]
5776
+ },
5777
+ undoable: true,
5778
+ run: (_ctx, params) => {
5779
+ check(controller.bendSegment({ nodeId: params.nodeId, subpath: params.subpath, segment: params.segment }, { x: params.x, y: params.y }, params.t));
5780
+ return { ok: true };
5781
+ }
5782
+ },
5783
+ {
5784
+ ...base,
5785
+ id: `${prefix}join`,
5786
+ label: "Join",
5787
+ shortcut: "Mod+J",
5788
+ keywords: ["connect", "close", "merge", "endpoints"],
5789
+ description: "Joins the open subpaths of paths nearest endpoints first (ends within `tolerance` merge); a single open path is closed.",
5790
+ params: { type: "object", properties: { ids: IDS, tolerance: { type: "number", minimum: 0 } } },
5791
+ undoable: true,
5792
+ run: (_ctx, params) => {
5793
+ check(controller.joinPaths({ ids: _optionalChain([params, 'optionalAccess', _147 => _147.ids]) ? ids(params) : void 0, tolerance: _optionalChain([params, 'optionalAccess', _148 => _148.tolerance]) }));
5794
+ return { selection: controller.getState().selection };
5795
+ }
5796
+ },
5797
+ {
5798
+ ...base,
5799
+ id: `${prefix}average`,
5800
+ label: "Average Points\u2026",
5801
+ shortcut: "Mod+Alt+J",
5802
+ keywords: ["align", "points", "anchors", "collapse"],
5803
+ description: "Moves anchors onto their average position: the same y (`horizontal`), the same x (`vertical`) or the same point (`both`, default).",
5804
+ params: { type: "object", properties: { axis: { type: "string", enum: ["horizontal", "vertical", "both"] }, anchors: ANCHORS } },
5805
+ undoable: true,
5806
+ run: (_ctx, params) => {
5807
+ check(controller.averageAnchors(_nullishCoalesce(_optionalChain([params, 'optionalAccess', _149 => _149.axis]), () => ( "both")), anchorsOf(_optionalChain([params, 'optionalAccess', _150 => _150.anchors]))));
5808
+ return { ok: true };
5809
+ }
5810
+ },
5811
+ {
5812
+ ...base,
5813
+ id: `${prefix}removeRedundantPoints`,
5814
+ label: "Remove Redundant Points",
5815
+ keywords: ["simplify", "clean up", "anchors", "collinear"],
5816
+ description: "Drops anchors that can go without moving the outline more than `tolerance` document units (default 0.5).",
5817
+ params: { type: "object", properties: { ids: IDS, tolerance: { type: "number", minimum: 0 } } },
5818
+ enabled: enabledFor,
5819
+ undoable: true,
5820
+ run: (_ctx, params) => {
5821
+ check(controller.removeRedundantPoints({ ids: _optionalChain([params, 'optionalAccess', _151 => _151.ids]) ? ids(params) : void 0, tolerance: _optionalChain([params, 'optionalAccess', _152 => _152.tolerance]) }));
5822
+ return { ok: true };
5823
+ }
5824
+ },
5825
+ {
5826
+ ...base,
5827
+ id: `${prefix}smooth`,
5828
+ label: "Smooth Path",
5829
+ keywords: ["round", "soften", "handles", "curves"],
5830
+ description: "Pulls the handles of paths (or of the selected points) towards smooth tangents by `amount` (0\u20131, default 1).",
5831
+ params: { type: "object", properties: { ids: IDS, amount: { type: "number", minimum: 0, maximum: 1 }, anchors: ANCHORS } },
5832
+ undoable: true,
5833
+ run: (_ctx, params) => {
5834
+ check(controller.smoothPaths({ ids: _optionalChain([params, 'optionalAccess', _153 => _153.ids]) ? ids(params) : void 0, amount: _optionalChain([params, 'optionalAccess', _154 => _154.amount]), refs: _optionalChain([params, 'optionalAccess', _155 => _155.anchors]) }));
5835
+ return { ok: true };
5836
+ }
5837
+ },
5838
+ {
5839
+ ...base,
5840
+ id: `${prefix}cut`,
5841
+ label: "Cut Path (Scissors)\u2026",
5842
+ keywords: ["scissors", "split", "open", "break"],
5843
+ description: "Scissors: cuts path `nodeId` at the outline point nearest (x, y), or cuts at `anchors`. A closed path opens; an open one splits in two.",
5844
+ params: { type: "object", properties: { nodeId: { type: "string" }, x: { type: "number" }, y: { type: "number" }, anchors: ANCHORS } },
5845
+ undoable: true,
5846
+ run: (_ctx, params) => {
5847
+ if (_optionalChain([params, 'optionalAccess', _156 => _156.nodeId]) !== void 0) {
5848
+ if (!controller.getNode(params.nodeId)) fail(`There is no node "${params.nodeId}".`);
5849
+ if (params.x === void 0 || params.y === void 0) fail("Pass x and y with nodeId.");
5850
+ const created = controller.cutPathAt(params.nodeId, { x: params.x, y: params.y });
5851
+ if (!created) fail("Nothing to cut there (the end of an open path, or a locked path).");
5852
+ return { ids: created };
5853
+ }
5854
+ check(controller.cutAtAnchors(anchorsOf(_optionalChain([params, 'optionalAccess', _157 => _157.anchors]))));
5855
+ return { ids: controller.getState().selection };
5856
+ }
5857
+ },
5858
+ {
5859
+ ...base,
5860
+ id: `${prefix}knife`,
5861
+ label: "Knife\u2026",
5862
+ keywords: ["slice", "cut", "split", "divide"],
5863
+ description: "Cuts closed paths along a polyline wherever it crosses them edge to edge (no gap between pieces). Cuts `ids`, else the selection, else every path the line crosses.",
5864
+ params: { type: "object", properties: { points: { ...POINTS, minItems: 2 }, ids: IDS }, required: ["points"] },
5865
+ undoable: true,
5866
+ run: (_ctx, params) => {
5867
+ const created = controller.knife(params.points.map(([x, y]) => ({ x, y })), { ids: params.ids ? ids(params) : void 0 });
5868
+ if (!created.length) fail("The knife did not cross any closed path from edge to edge.");
5869
+ return { ids: created };
5870
+ }
5871
+ },
5872
+ {
5873
+ ...base,
5874
+ id: `${prefix}erase`,
5875
+ label: "Erase\u2026",
5876
+ keywords: ["eraser", "rub out", "remove"],
5877
+ description: "Erases a round stroke of diameter `size` (default: the eraser size setting) along a polyline: filled shapes lose the area, outlines are cut under it. Erases from `ids`, else the selection, else every path it reaches.",
5878
+ params: { type: "object", properties: { points: POINTS, size: { type: "number", exclusiveMinimum: 0 }, ids: IDS }, required: ["points"] },
5879
+ undoable: true,
5880
+ run: (_ctx, params) => {
5881
+ const changed = controller.erase(params.points.map(([x, y]) => ({ x, y })), { size: params.size, ids: params.ids ? ids(params) : void 0 });
5882
+ if (!changed.length) fail("The eraser did not touch any path.");
5883
+ return { ids: changed.filter((id) => controller.getNode(id)) };
5884
+ }
5885
+ },
5886
+ {
5887
+ ...base,
5888
+ id: `${prefix}setEditing`,
5889
+ label: "Path Editing Options\u2026",
5890
+ keywords: ["settings", "curvature", "pencil", "smoothing", "lazy mouse", "shapes", "eraser"],
5891
+ description: "Sets how editing behaves: `keepShapeOnDelete`, `curvatureMoves` (anchor drags bend like the Curvature tool), `pencilSmoothing` (lazy mouse, 0\u20131), `recognizeShapes` (pencil scribbles become lines, rectangles, ellipses, polygons) and `eraserSize`.",
5892
+ params: {
5893
+ type: "object",
5894
+ properties: {
5895
+ keepShapeOnDelete: { type: "boolean" },
5896
+ curvatureMoves: { type: "boolean" },
5897
+ pencilSmoothing: { type: "number", minimum: 0, maximum: 1 },
5898
+ recognizeShapes: { type: "boolean" },
5899
+ eraserSize: { type: "number", exclusiveMinimum: 0 }
5900
+ }
5901
+ },
5902
+ run: (_ctx, params) => {
5903
+ const patch = {};
5904
+ for (const key of ["keepShapeOnDelete", "curvatureMoves", "pencilSmoothing", "recognizeShapes", "eraserSize"]) if (_optionalChain([params, 'optionalAccess', _158 => _158[key]]) !== void 0) patch[key] = params[key];
5905
+ controller.setEditing(patch);
5906
+ return { editing: controller.getState().editing };
5907
+ }
5908
+ },
3347
5909
  {
3348
5910
  ...base,
3349
5911
  id: `${prefix}setTool`,
@@ -3364,7 +5926,7 @@ function createVectorCommands(controller, options = {}) {
3364
5926
  keywords: ["download", "save", "markup"],
3365
5927
  description: "Returns the document as SVG markup.",
3366
5928
  params: { type: "object", properties: { precision: { type: "integer", minimum: 0, maximum: 8 } } },
3367
- run: (_ctx, params) => ({ svg: controller.toSvg({ precision: _optionalChain([params, 'optionalAccess', _99 => _99.precision]) }) })
5929
+ run: (_ctx, params) => ({ svg: controller.toSvg({ precision: _optionalChain([params, 'optionalAccess', _159 => _159.precision]) }) })
3368
5930
  },
3369
5931
  {
3370
5932
  ...base,
@@ -3467,5 +6029,34 @@ function createVectorCommands(controller, options = {}) {
3467
6029
 
3468
6030
 
3469
6031
 
3470
- exports.IDENTITY_MATRIX = IDENTITY_MATRIX; exports.KAPPA = KAPPA; exports.VECTOR_CLIPBOARD_TYPE = VECTOR_CLIPBOARD_TYPE; exports.VECTOR_OVERLAY_THEME = VECTOR_OVERLAY_THEME; exports.VECTOR_ROTATE_HANDLE_OFFSET = VECTOR_ROTATE_HANDLE_OFFSET; exports.VECTOR_TOOLS = VECTOR_TOOLS; exports.VECTOR_TOOL_GLYPHS = VECTOR_TOOL_GLYPHS; exports.VECTOR_TOOL_SHORTCUTS = VECTOR_TOOL_SHORTCUTS; exports.VECTOR_TRANSFORM_HANDLES = VECTOR_TRANSFORM_HANDLES; exports.VectorEditorController = VectorEditorController; exports.applyMatrix = applyMatrix; exports.applyMatrixLinear = applyMatrixLinear; exports.bezierToSubpaths = bezierToSubpaths; exports.boundsFromPoints = boundsFromPoints; exports.boundsIntersect = boundsIntersect; exports.cloneAnchor = cloneAnchor; exports.cloneSubpath = cloneSubpath; exports.cloneVectorDocument = cloneVectorDocument; exports.cloneVectorNode = cloneVectorNode; exports.collectPathNodes = collectPathNodes; exports.convertAnchor = convertAnchor; exports.createVectorCommands = createVectorCommands; exports.decomposeRigid = decomposeRigid; exports.ellipseSubpath = ellipseSubpath; exports.findVectorNode = findVectorNode; exports.getAnchor = getAnchor; exports.getAnchorWorld = getAnchorWorld; exports.getEditablePathIds = getEditablePathIds; exports.getTransformHandlePositions = getTransformHandlePositions; exports.getVectorOverlayRenderData = getVectorOverlayRenderData; exports.getVectorSceneRenderData = getVectorSceneRenderData; exports.getVectorSelectionBoxGeometry = getVectorSelectionBoxGeometry; exports.getVectorSelectionFrame = getVectorSelectionFrame; exports.getVectorSelectionStyle = getVectorSelectionStyle; exports.getVisibleAnchors = getVisibleAnchors; exports.getVisibleHandles = getVisibleHandles; exports.hasPaint = hasPaint; exports.hitTestVectorNode = hitTestVectorNode; exports.hitTestVectorNodes = hitTestVectorNodes; exports.inferAnchorType = inferAnchorType; exports.insertAnchor = insertAnchor; exports.invertMatrix = invertMatrix; exports.isIdentityMatrix = isIdentityMatrix; exports.isVectorTextTarget = isVectorTextTarget; exports.lineSubpath = lineSubpath; exports.locateVectorNode = locateVectorNode; exports.matrixRotation = matrixRotation; exports.moveAnchorHandle = moveAnchorHandle; exports.multiplyMatrices = multiplyMatrices; exports.nearestSegment = nearestSegment; exports.nodeMatrix = nodeMatrix; exports.parseSvgTransform = parseSvgTransform; exports.parseVectorSvg = parseVectorSvg; exports.polygonSubpath = polygonSubpath; exports.rectSubpath = rectSubpath; exports.removeAnchors = removeAnchors; exports.reverseSubpath = reverseSubpath; exports.rotateMatrix = rotateMatrix; exports.roundSubpaths = roundSubpaths; exports.scaleFor = scaleFor; exports.scaleMatrix = scaleMatrix; exports.starSubpath = starSubpath; exports.subpathSegment = subpathSegment; exports.subpathSegmentCount = subpathSegmentCount; exports.subpathsToBezier = subpathsToBezier; exports.toVectorKeyInput = toVectorKeyInput; exports.toVectorModifiers = toVectorModifiers; exports.toVectorPointerInput = toVectorPointerInput; exports.transformVectorNode = transformVectorNode; exports.translateAnchor = translateAnchor; exports.translateMatrix = translateMatrix; exports.unionBounds = unionBounds; exports.vectorDocumentToSvg = vectorDocumentToSvg; exports.vectorNodeBounds = vectorNodeBounds; exports.vectorNodeRotation = vectorNodeRotation; exports.vectorNodeToBezier = vectorNodeToBezier; exports.vectorPaintToHex = vectorPaintToHex; exports.vectorPaintToSvg = vectorPaintToSvg; exports.vectorParentMatrix = vectorParentMatrix; exports.vectorPathData = vectorPathData; exports.vectorPathWorldBezier = vectorPathWorldBezier; exports.vectorResizeCursor = vectorResizeCursor; exports.vectorToolLabel = vectorToolLabel; exports.vectorWorldMatrix = vectorWorldMatrix; exports.walkVectorNodes = walkVectorNodes;
6032
+
6033
+
6034
+
6035
+
6036
+
6037
+
6038
+
6039
+
6040
+
6041
+
6042
+
6043
+
6044
+
6045
+
6046
+
6047
+
6048
+
6049
+
6050
+
6051
+
6052
+
6053
+
6054
+
6055
+
6056
+
6057
+
6058
+
6059
+
6060
+
6061
+ exports.DEFAULT_VECTOR_PATH_EDIT = DEFAULT_VECTOR_PATH_EDIT; exports.DEFAULT_VECTOR_SNAP = DEFAULT_VECTOR_SNAP; exports.IDENTITY_MATRIX = IDENTITY_MATRIX; exports.KAPPA = KAPPA; exports.LazyMouse = LazyMouse; exports.VECTOR_CLIPBOARD_TYPE = VECTOR_CLIPBOARD_TYPE; exports.VECTOR_CONSTRUCTION_ANGLES = VECTOR_CONSTRUCTION_ANGLES; exports.VECTOR_EDIT_TOGGLES = VECTOR_EDIT_TOGGLES; exports.VECTOR_INK_MAX_CORNERS = VECTOR_INK_MAX_CORNERS; exports.VECTOR_OVERLAY_THEME = VECTOR_OVERLAY_THEME; exports.VECTOR_PATH_ACTIONS = VECTOR_PATH_ACTIONS; exports.VECTOR_PENCIL_STRING_PX = VECTOR_PENCIL_STRING_PX; exports.VECTOR_ROTATE_HANDLE_OFFSET = VECTOR_ROTATE_HANDLE_OFFSET; exports.VECTOR_TOOLS = VECTOR_TOOLS; exports.VECTOR_TOOL_GLYPHS = VECTOR_TOOL_GLYPHS; exports.VECTOR_TOOL_SHORTCUTS = VECTOR_TOOL_SHORTCUTS; exports.VECTOR_TRANSFORM_HANDLES = VECTOR_TRANSFORM_HANDLES; exports.VectorEditorController = VectorEditorController; exports.VectorSnapTargets = VectorSnapTargets; exports.applyMatrix = applyMatrix; exports.applyMatrixLinear = applyMatrixLinear; exports.bendSegment = bendSegment; exports.bezierToSubpaths = bezierToSubpaths; exports.boundsFromPoints = boundsFromPoints; exports.boundsIntersect = boundsIntersect; exports.cloneAnchor = cloneAnchor; exports.cloneSubpath = cloneSubpath; exports.cloneVectorDocument = cloneVectorDocument; exports.cloneVectorNode = cloneVectorNode; exports.collectPathNodes = collectPathNodes; exports.constrainToLeave = constrainToLeave; exports.convertAnchor = convertAnchor; exports.createVectorCommands = createVectorCommands; exports.curvatureMoveAnchor = curvatureMoveAnchor; exports.cutSubpathAt = cutSubpathAt; exports.decomposeRigid = decomposeRigid; exports.ellipseSubpath = ellipseSubpath; exports.eraseFromFill = eraseFromFill; exports.eraseFromOutline = eraseFromOutline; exports.eraserRegion = eraserRegion; exports.findVectorNode = findVectorNode; exports.getAnchor = getAnchor; exports.getAnchorWorld = getAnchorWorld; exports.getEditablePathIds = getEditablePathIds; exports.getTransformHandlePositions = getTransformHandlePositions; exports.getVectorOverlayRenderData = getVectorOverlayRenderData; exports.getVectorSceneRenderData = getVectorSceneRenderData; exports.getVectorSelectionBoxGeometry = getVectorSelectionBoxGeometry; exports.getVectorSelectionFrame = getVectorSelectionFrame; exports.getVectorSelectionStyle = getVectorSelectionStyle; exports.getVisibleAnchors = getVisibleAnchors; exports.getVisibleHandles = getVisibleHandles; exports.hasPaint = hasPaint; exports.hitTestVectorNode = hitTestVectorNode; exports.hitTestVectorNodes = hitTestVectorNodes; exports.inferAnchorType = inferAnchorType; exports.insertAnchor = insertAnchor; exports.invertMatrix = invertMatrix; exports.isIdentityMatrix = isIdentityMatrix; exports.isVectorTextTarget = isVectorTextTarget; exports.joinSubpaths = joinSubpaths; exports.knifePath = knifePath; exports.lineSubpath = lineSubpath; exports.locateVectorNode = locateVectorNode; exports.matrixRotation = matrixRotation; exports.moveAnchorHandle = moveAnchorHandle; exports.multiplyMatrices = multiplyMatrices; exports.nearestSegment = nearestSegment; exports.nodeMatrix = nodeMatrix; exports.parseSvgTransform = parseSvgTransform; exports.parseVectorSvg = parseVectorSvg; exports.polygonSubpath = polygonSubpath; exports.polylineBounds = polylineBounds; exports.recognizeInkShape = recognizeInkShape; exports.rectSubpath = rectSubpath; exports.removeAnchorKeepingShape = removeAnchorKeepingShape; exports.removeAnchors = removeAnchors; exports.removeAnchorsKeepingShape = removeAnchorsKeepingShape; exports.removeRedundantPoints = removeRedundantPoints; exports.reverseSubpath = reverseSubpath; exports.rotateMatrix = rotateMatrix; exports.roundSubpaths = roundSubpaths; exports.scaleFor = scaleFor; exports.scaleMatrix = scaleMatrix; exports.simplifyPolyline = simplifyPolyline; exports.smoothSubpath = smoothSubpath; exports.splitIntoFaces = splitIntoFaces; exports.splitSubpathAt = splitSubpathAt; exports.starSubpath = starSubpath; exports.subpathPointAt = subpathPointAt; exports.subpathSegment = subpathSegment; exports.subpathSegmentCount = subpathSegmentCount; exports.subpathsToBezier = subpathsToBezier; exports.toVectorKeyInput = toVectorKeyInput; exports.toVectorModifiers = toVectorModifiers; exports.toVectorPointerInput = toVectorPointerInput; exports.transformVectorNode = transformVectorNode; exports.translateAnchor = translateAnchor; exports.translateMatrix = translateMatrix; exports.unionBounds = unionBounds; exports.vectorDocumentToSvg = vectorDocumentToSvg; exports.vectorInkShapeName = vectorInkShapeName; exports.vectorNodeBounds = vectorNodeBounds; exports.vectorNodeRotation = vectorNodeRotation; exports.vectorNodeToBezier = vectorNodeToBezier; exports.vectorPaintToHex = vectorPaintToHex; exports.vectorPaintToSvg = vectorPaintToSvg; exports.vectorParentMatrix = vectorParentMatrix; exports.vectorPathData = vectorPathData; exports.vectorPathWorldBezier = vectorPathWorldBezier; exports.vectorResizeCursor = vectorResizeCursor; exports.vectorToolLabel = vectorToolLabel; exports.vectorWorldMatrix = vectorWorldMatrix; exports.walkVectorNodes = walkVectorNodes;
3471
6062
  //# sourceMappingURL=index.cjs.map