@pixerate/editor 0.32.0 → 0.34.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;
2
2
 
3
3
  var _chunkQKLRAA4Tcjs = require('../chunk-QKLRAA4T.cjs');
4
4
 
@@ -15,7 +15,9 @@ var _chunkQKLRAA4Tcjs = require('../chunk-QKLRAA4T.cjs');
15
15
 
16
16
 
17
17
 
18
- var _chunkM4XE4UMTcjs = require('../chunk-M4XE4UMT.cjs');
18
+
19
+
20
+ var _chunkUG5DVXJKcjs = require('../chunk-UG5DVXJK.cjs');
19
21
 
20
22
  // src/vector/types.ts
21
23
  var VECTOR_TOOLS = ["select", "direct", "pen", "pencil", "rect", "ellipse", "polygon", "star", "line"];
@@ -82,7 +84,7 @@ function vectorNodeToBezier(node) {
82
84
  }
83
85
  function vectorPathData(input, precision = 3) {
84
86
  const subpaths = Array.isArray(input) ? input : input.subpaths;
85
- return _chunkM4XE4UMTcjs.bezierPathToSvg.call(void 0, subpathsToBezier(subpaths), { precision });
87
+ return _chunkUG5DVXJKcjs.bezierPathToSvg.call(void 0, subpathsToBezier(subpaths), { precision });
86
88
  }
87
89
  function inferAnchorType(a) {
88
90
  if (isOnAnchor(a, a.in) || isOnAnchor(a, a.out)) return "corner";
@@ -98,7 +100,7 @@ function inferAnchorType(a) {
98
100
  return Math.abs(li - lo) <= 1e-3 * Math.max(li, lo) ? "symmetric" : "smooth";
99
101
  }
100
102
  function bezierToSubpaths(input) {
101
- const path = _chunkM4XE4UMTcjs.toBezierPath.call(void 0, input);
103
+ const path = _chunkUG5DVXJKcjs.toBezierPath.call(void 0, input);
102
104
  const out = [];
103
105
  let current = null;
104
106
  const last = () => current.anchors[current.anchors.length - 1];
@@ -189,9 +191,20 @@ function collectPathNodes(node) {
189
191
  });
190
192
  return out;
191
193
  }
192
- function vectorNodeBounds(node) {
193
- if (node.type === "group") return unionBounds(node.children.map(vectorNodeBounds));
194
- const b = _chunkM4XE4UMTcjs.bezierPathBounds.call(void 0, subpathsToBezier(node.subpaths));
194
+ function nodeMatrix(node) {
195
+ return _nullishCoalesce(node.transform, () => ( IDENTITY_MATRIX));
196
+ }
197
+ function vectorPathWorldBezier(node, parent = IDENTITY_MATRIX) {
198
+ const m = multiplyMatrices(nodeMatrix(node), parent);
199
+ const path = subpathsToBezier(node.subpaths);
200
+ return isIdentityMatrix(m) ? path : _chunkUG5DVXJKcjs.transformBezierPath.call(void 0, path, m);
201
+ }
202
+ function vectorNodeBounds(node, parent = IDENTITY_MATRIX) {
203
+ if (node.type === "group") {
204
+ const m = multiplyMatrices(nodeMatrix(node), parent);
205
+ return unionBounds(node.children.map((c) => vectorNodeBounds(c, m)));
206
+ }
207
+ const b = _chunkUG5DVXJKcjs.bezierPathBounds.call(void 0, vectorPathWorldBezier(node, parent));
195
208
  if (!b) return null;
196
209
  return { x: b.minX, y: b.minY, width: b.maxX - b.minX, height: b.maxY - b.minY };
197
210
  }
@@ -216,15 +229,18 @@ function boundsIntersect(a, b) {
216
229
  function boundsFromPoints(a, b) {
217
230
  return { x: Math.min(a.x, b.x), y: Math.min(a.y, b.y), width: Math.abs(b.x - a.x), height: Math.abs(b.y - a.y) };
218
231
  }
219
- function hitTestVectorNode(node, p, tolerance = 0) {
232
+ function hitTestVectorNode(node, p, tolerance = 0, parent = IDENTITY_MATRIX) {
220
233
  if (node.visible === false) return false;
221
- if (node.type === "group") return node.children.some((c) => hitTestVectorNode(c, p, tolerance));
222
- const path = subpathsToBezier(node.subpaths);
234
+ if (node.type === "group") {
235
+ const m = multiplyMatrices(nodeMatrix(node), parent);
236
+ return node.children.some((c) => hitTestVectorNode(c, p, tolerance, m));
237
+ }
238
+ const path = vectorPathWorldBezier(node, parent);
223
239
  if (path.length === 0) return false;
224
240
  const point = [p.x, p.y];
225
- if (hasPaint(node.fill) && _chunkM4XE4UMTcjs.pointInBezierPath.call(void 0, path, point, _nullishCoalesce(node.fillRule, () => ( "nonzero")))) return true;
241
+ if (hasPaint(node.fill) && _chunkUG5DVXJKcjs.pointInBezierPath.call(void 0, path, point, _nullishCoalesce(node.fillRule, () => ( "nonzero")))) return true;
226
242
  const reach = (hasPaint(node.stroke) ? (_nullishCoalesce(node.strokeWidth, () => ( 1))) / 2 : 0) + tolerance;
227
- return _chunkM4XE4UMTcjs.distanceToBezierPath.call(void 0, path, point) <= reach;
243
+ return _chunkUG5DVXJKcjs.distanceToBezierPath.call(void 0, path, point) <= reach;
228
244
  }
229
245
  function hitTestVectorNodes(nodes, p, tolerance = 0) {
230
246
  for (let i = nodes.length - 1; i >= 0; i--) {
@@ -240,9 +256,9 @@ function nearestSegment(node, p) {
240
256
  const count = subpathSegmentCount(sp);
241
257
  for (let i = 0; i < count; i++) {
242
258
  const seg = subpathSegment(sp, i);
243
- const hit = _chunkM4XE4UMTcjs.segmentNearest.call(void 0, seg, [p.x, p.y]);
259
+ const hit = _chunkUG5DVXJKcjs.segmentNearest.call(void 0, seg, [p.x, p.y]);
244
260
  if (!best || hit.distance < best.distance) {
245
- const pt = _chunkM4XE4UMTcjs.segmentPoint.call(void 0, seg, hit.t);
261
+ const pt = _chunkUG5DVXJKcjs.segmentPoint.call(void 0, seg, hit.t);
246
262
  best = { subpath: s, segment: i, t: hit.t, point: { x: pt[0], y: pt[1] }, distance: hit.distance };
247
263
  }
248
264
  }
@@ -268,7 +284,38 @@ function rotateMatrix(degrees, origin = { x: 0, y: 0 }) {
268
284
  function applyMatrix(m, p) {
269
285
  return { x: m[0] * p.x + m[2] * p.y + m[4], y: m[1] * p.x + m[3] * p.y + m[5] };
270
286
  }
271
- function transformVectorNode(node, m) {
287
+ function isIdentityMatrix(m, eps = 1e-12) {
288
+ return Math.abs(m[0] - 1) < eps && Math.abs(m[1]) < eps && Math.abs(m[2]) < eps && Math.abs(m[3] - 1) < eps && Math.abs(m[4]) < eps && Math.abs(m[5]) < eps;
289
+ }
290
+ function invertMatrix(m) {
291
+ const [a, b, c, d, e, f] = m;
292
+ const det = a * d - b * c;
293
+ if (!det || !Number.isFinite(det)) return IDENTITY_MATRIX;
294
+ return [d / det, -b / det, -c / det, a / det, (c * f - d * e) / det, (b * e - a * f) / det];
295
+ }
296
+ function applyMatrixLinear(m, v) {
297
+ return { x: m[0] * v.x + m[2] * v.y, y: m[1] * v.x + m[3] * v.y };
298
+ }
299
+ function matrixRotation(m) {
300
+ const [a, b, c, d] = m;
301
+ const mirrored = a * d - b * c < 0;
302
+ const deg = (mirrored ? Math.atan2(-b - c, d - a) : Math.atan2(b - c, a + d)) * 180 / Math.PI;
303
+ return Math.abs(deg) < 1e-9 ? 0 : deg;
304
+ }
305
+ function decomposeRigid(m) {
306
+ const [a, b, c, d, e, f] = m;
307
+ const deg = matrixRotation(m);
308
+ if (deg === 0) return { rigid: IDENTITY_MATRIX, residual: m };
309
+ const r = deg * Math.PI / 180;
310
+ const cos = Math.cos(r);
311
+ const sin = Math.sin(r);
312
+ return {
313
+ rigid: [cos, sin, -sin, cos, e, f],
314
+ residual: [cos * a + sin * b, -sin * a + cos * b, cos * c + sin * d, -sin * c + cos * d, 0, 0]
315
+ };
316
+ }
317
+ function bakeIntoGeometry(node, m) {
318
+ if (isIdentityMatrix(m)) return node;
272
319
  if (node.type === "group") return { ...node, children: node.children.map((c) => transformVectorNode(c, m)) };
273
320
  const path = node;
274
321
  return {
@@ -286,6 +333,36 @@ function transformVectorNode(node, m) {
286
333
  }))
287
334
  };
288
335
  }
336
+ function transformVectorNode(node, m) {
337
+ const total = multiplyMatrices(nodeMatrix(node), m);
338
+ const { rigid, residual } = decomposeRigid(total);
339
+ const { transform: _old, ...rest } = node;
340
+ const baked = bakeIntoGeometry(rest, residual);
341
+ return isIdentityMatrix(rigid) ? baked : { ...baked, transform: rigid };
342
+ }
343
+ function vectorNodeRotation(node, parent = IDENTITY_MATRIX) {
344
+ return matrixRotation(multiplyMatrices(nodeMatrix(node), parent));
345
+ }
346
+ function vectorWorldMatrix(nodes, id) {
347
+ const search = (list, parent) => {
348
+ for (const n of list) {
349
+ const m = multiplyMatrices(nodeMatrix(n), parent);
350
+ if (n.id === id) return m;
351
+ if (n.type === "group") {
352
+ const found = search(n.children, m);
353
+ if (found) return found;
354
+ }
355
+ }
356
+ return null;
357
+ };
358
+ return search(nodes, IDENTITY_MATRIX);
359
+ }
360
+ function vectorParentMatrix(nodes, id) {
361
+ const node = findVectorNode(nodes, id);
362
+ const world = vectorWorldMatrix(nodes, id);
363
+ if (!node || !world) return null;
364
+ return multiplyMatrices(invertMatrix(nodeMatrix(node)), world);
365
+ }
289
366
  function translateAnchor(a, dx, dy) {
290
367
  const next = cloneAnchor(a);
291
368
  next.x += dx;
@@ -352,7 +429,7 @@ function insertAnchor(sp, segment, t) {
352
429
  const ia = segment;
353
430
  const ib = (segment + 1) % n;
354
431
  const seg = subpathSegment(sp, segment);
355
- const [left, right] = _chunkM4XE4UMTcjs.splitSegment.call(void 0, seg, t);
432
+ const [left, right] = _chunkUG5DVXJKcjs.splitSegment.call(void 0, seg, t);
356
433
  const mid = left[left.length - 1];
357
434
  const anchor = { x: mid[0], y: mid[1] };
358
435
  if (seg.length === 4) {
@@ -460,7 +537,7 @@ var escapeAttr = (value) => value.replace(/&/g, "&amp;").replace(/"/g, "&quot;")
460
537
  function vectorPaintToSvg(paint, gradientId) {
461
538
  if (!hasPaint(paint)) return { value: "none", gradient: null };
462
539
  if (typeof paint === "string") return { value: paint, gradient: null };
463
- return { value: `url(#${gradientId})`, gradient: _chunkM4XE4UMTcjs.gradientToSvgDef.call(void 0, paint, gradientId) };
540
+ return { value: `url(#${gradientId})`, gradient: _chunkUG5DVXJKcjs.gradientToSvgDef.call(void 0, paint, gradientId) };
464
541
  }
465
542
  function gradientMarkup(def) {
466
543
  const tag = def.type === "radial" ? "radialGradient" : "linearGradient";
@@ -474,10 +551,11 @@ function vectorDocumentToSvg(doc, options = {}) {
474
551
  const pretty = options.pretty !== false;
475
552
  const defs = [];
476
553
  const lines = [];
477
- const num = (n) => _chunkM4XE4UMTcjs.formatNumber.call(void 0, n, precision);
554
+ const num = (n) => _chunkUG5DVXJKcjs.formatNumber.call(void 0, n, precision);
478
555
  const indent = (depth) => pretty ? " ".repeat(depth) : "";
479
556
  const common = (node) => {
480
557
  let out2 = ` id="${escapeAttr(node.id)}"`;
558
+ if (node.transform) out2 += ` transform="matrix(${node.transform.map((n) => _chunkUG5DVXJKcjs.formatNumber.call(void 0, n, 6)).join(" ")})"`;
481
559
  if (node.opacity !== void 0 && node.opacity < 1) out2 += ` opacity="${num(node.opacity)}"`;
482
560
  if (node.visible === false) out2 += ` display="none"`;
483
561
  return out2;
@@ -774,7 +852,759 @@ function parseVectorSvg(svg, options = {}) {
774
852
  return { width, height, nodes };
775
853
  }
776
854
 
855
+ // src/vector/snap.ts
856
+ var VECTOR_CONSTRUCTION_ANGLES = [0, 45, 90, 135];
857
+ function constrainToLeave(leave, p) {
858
+ if (!_optionalChain([leave, 'optionalAccess', _8 => _8.constrain])) return p;
859
+ const dx = p.x - leave.at.x;
860
+ const dy = p.y - leave.at.y;
861
+ switch (leave.constrain) {
862
+ case "axis":
863
+ return Math.abs(dx) > Math.abs(dy) ? { x: p.x, y: leave.at.y } : { x: leave.at.x, y: p.y };
864
+ case "square": {
865
+ const s = Math.max(Math.abs(dx), Math.abs(dy));
866
+ return { x: leave.at.x + Math.sign(dx || 1) * s, y: leave.at.y + Math.sign(dy || 1) * s };
867
+ }
868
+ default: {
869
+ const len = Math.hypot(dx, dy);
870
+ const a = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);
871
+ return { x: leave.at.x + Math.cos(a) * len, y: leave.at.y + Math.sin(a) * len };
872
+ }
873
+ }
874
+ }
875
+ var BUDGET = 2e4;
876
+ var CROSSING_PAIRS = 32;
877
+ var CURVE_SAMPLES = 48;
878
+ var BISECT = 50;
879
+ var EQUAL = 1e-6;
880
+ function bound(sorted, key, v, strict) {
881
+ let lo = 0;
882
+ let hi = sorted.length;
883
+ while (lo < hi) {
884
+ const mid = lo + hi >>> 1;
885
+ const k = key(sorted[mid]);
886
+ if (strict ? k <= v : k < v) lo = mid + 1;
887
+ else hi = mid;
888
+ }
889
+ return lo;
890
+ }
891
+ function within(sorted, key, v, tol) {
892
+ return sorted.slice(bound(sorted, key, v - tol, false), bound(sorted, key, v + tol, true));
893
+ }
894
+ var linesNear = (lines, v, tol) => within(lines, (l) => l.v, v, tol);
895
+ var distance = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
896
+ var toPoint = (p) => [p.x, p.y];
897
+ var toVec = (p) => ({ x: p[0], y: p[1] });
898
+ var LABELS = {
899
+ anchor: "anchor",
900
+ centre: "centre",
901
+ intersect: "intersect",
902
+ perpendicular: "perpendicular",
903
+ tangent: "tangent",
904
+ path: "path",
905
+ construction: "angle",
906
+ align: "align",
907
+ spacing: "spacing",
908
+ size: "size"
909
+ };
910
+ var fmtLength = (n) => String(Math.round(n * 100) / 100);
911
+ function nodeFrame(node, parent, aabb) {
912
+ const angle = matrixRotation(multiplyMatrices(nodeMatrix(node), parent));
913
+ const b = angle ? vectorNodeBounds(node, multiplyMatrices(parent, rotateMatrix(-angle))) : aabb !== void 0 ? aabb : vectorNodeBounds(node, parent);
914
+ if (!b) return null;
915
+ const c = { x: b.x + b.width / 2, y: b.y + b.height / 2 };
916
+ return { width: b.width, height: b.height, centre: angle ? applyMatrix(rotateMatrix(angle), c) : c };
917
+ }
918
+ function contains(node, ids) {
919
+ if (ids.has(node.id)) return true;
920
+ return node.type === "group" && node.children.some((c) => contains(c, ids));
921
+ }
922
+ var VectorSnapTargets = (_class = class _VectorSnapTargets {
923
+ __init() {this.points = []}
924
+ __init2() {this.xs = []}
925
+ __init3() {this.ys = []}
926
+ __init4() {this.segments = []}
927
+ /** Bounds of the objects a moved box spaces itself evenly with. */
928
+ __init5() {this.boxes = []}
929
+ /** Side lengths of the other objects (in their own frames), which a resize may match. */
930
+ __init6() {this.sizes = []}
931
+ 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);
932
+ }
933
+ /** How many targets of each sort were gathered. */
934
+ get counts() {
935
+ return { points: this.points.length, lines: this.xs.length + this.ys.length, segments: this.segments.length, boxes: this.boxes.length };
936
+ }
937
+ /** Targets for moving nodes `ids`: the other art's anchors, bounds and centres, the artboard, and boxes to space evenly with. */
938
+ static forMove(doc, ids) {
939
+ return _VectorSnapTargets.collect(doc, new Set(ids), "bounds");
940
+ }
941
+ /** Targets for resizing nodes `ids`: as for a move, plus the other objects' sizes. */
942
+ static forResize(doc, ids) {
943
+ return _VectorSnapTargets.collect(doc, new Set(ids), "bounds");
944
+ }
945
+ /**
946
+ * Targets for a drawn point (pen, shape tools): the art but `exclude`, its anchors (as
947
+ * points and in line), centres, edges and segments. The anchors and segments of `exclude`
948
+ * (the path being drawn, whose bounds grow with it) stay targets.
949
+ */
950
+ static forDrawing(doc, exclude = []) {
951
+ const skip = new Set(exclude);
952
+ const t = _VectorSnapTargets.collect(doc, skip, "drawing", false);
953
+ t.pushPaths(doc, skip, () => false, true);
954
+ return t.index();
955
+ }
956
+ /**
957
+ * Targets for dragging anchors: the other art with its segments, and the anchors and
958
+ * segments that stay put on the paths being reshaped (their bounds move along, so they are
959
+ * no targets). The moving anchors are left out.
960
+ */
961
+ static forAnchorDrag(doc, anchors) {
962
+ const moving = /* @__PURE__ */ new Map();
963
+ for (const r of anchors) {
964
+ const set = _nullishCoalesce(moving.get(r.nodeId), () => ( /* @__PURE__ */ new Set()));
965
+ set.add(`${r.subpath}:${r.index}`);
966
+ moving.set(r.nodeId, set);
967
+ }
968
+ const t = _VectorSnapTargets.collect(doc, new Set(moving.keys()), "segments", false);
969
+ 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);
970
+ return t.index();
971
+ }
972
+ /**
973
+ * Targets for dragging a handle of path `nodeId`: the other art and that path's anchors
974
+ * (its bounds and the segments through the handle move with it, so they would chase it).
975
+ */
976
+ static forHandle(doc, nodeId) {
977
+ const skip = /* @__PURE__ */ new Set([nodeId]);
978
+ const t = _VectorSnapTargets.collect(doc, skip, "bounds", false);
979
+ t.pushPaths(doc, skip, () => false, false);
980
+ return t.index();
981
+ }
982
+ /** The anchors of the paths `ids` as targets (on them and in line), and their segments when `segments`. */
983
+ pushPaths(doc, ids, moving, segments) {
984
+ let anchors = BUDGET;
985
+ let segmentBudget = segments ? BUDGET : 0;
986
+ const visit = (list, parent) => {
987
+ for (const n of list) {
988
+ const m = multiplyMatrices(nodeMatrix(n), parent);
989
+ if (n.type === "group") visit(n.children, m);
990
+ else if (ids.has(n.id) && n.visible !== false) {
991
+ n.subpaths.forEach((sp, s) => {
992
+ sp.anchors.forEach((a, i) => {
993
+ if (anchors <= 0 || moving(n.id, s, i)) return;
994
+ this.pushAnchor(applyMatrix(m, a));
995
+ anchors--;
996
+ });
997
+ segmentBudget = this.pushSegments(sp, m, (i) => moving(n.id, s, i), segmentBudget);
998
+ });
999
+ }
1000
+ }
1001
+ };
1002
+ visit(doc.nodes, IDENTITY_MATRIX);
1003
+ }
1004
+ pushAnchor(p) {
1005
+ this.points.push({ p, kind: "anchor" });
1006
+ this.xs.push({ v: p.x, from: p, kind: "anchor" });
1007
+ this.ys.push({ v: p.y, from: p, kind: "anchor" });
1008
+ }
1009
+ /** The segments of `sp` (mapped by `m`) that stay put while the anchors `moving` accepts move. */
1010
+ pushSegments(sp, m, moving, budget) {
1011
+ const n = sp.anchors.length;
1012
+ const count = subpathSegmentCount(sp);
1013
+ for (let i = 0; i < count && budget > 0; i++) {
1014
+ if (moving(i) || moving((i + 1) % n)) continue;
1015
+ const seg = subpathSegment(sp, i).map((q) => toPoint(applyMatrix(m, toVec(q))));
1016
+ let minX = Infinity;
1017
+ let minY = Infinity;
1018
+ let maxX = -Infinity;
1019
+ let maxY = -Infinity;
1020
+ for (const [x, y] of seg) {
1021
+ minX = Math.min(minX, x);
1022
+ minY = Math.min(minY, y);
1023
+ maxX = Math.max(maxX, x);
1024
+ maxY = Math.max(maxY, y);
1025
+ }
1026
+ this.segments.push({ seg, minX, minY, maxX, maxY });
1027
+ budget--;
1028
+ }
1029
+ return budget;
1030
+ }
1031
+ pushRect(r, kind, centre) {
1032
+ const corners = [
1033
+ { x: r.x, y: r.y },
1034
+ { x: r.x + r.width, y: r.y },
1035
+ { x: r.x + r.width, y: r.y + r.height },
1036
+ { x: r.x, y: r.y + r.height }
1037
+ ];
1038
+ for (const p of corners) {
1039
+ this.xs.push({ v: p.x, from: p, kind });
1040
+ this.ys.push({ v: p.y, from: p, kind });
1041
+ }
1042
+ if (centre) {
1043
+ this.points.push({ p: centre, kind: "centre" });
1044
+ this.xs.push({ v: centre.x, from: centre, kind: "centre" });
1045
+ this.ys.push({ v: centre.y, from: centre, kind: "centre" });
1046
+ }
1047
+ }
1048
+ /** Sorts the targets for `within` (stable: of targets alike, the first gathered wins). */
1049
+ index() {
1050
+ this.points.sort((a, b) => a.p.x - b.p.x);
1051
+ this.xs.sort((a, b) => a.v - b.v);
1052
+ this.ys.sort((a, b) => a.v - b.v);
1053
+ return this;
1054
+ }
1055
+ /**
1056
+ * Gathers targets from the artboard and every visible node but `exclude` (and what is inside
1057
+ * those): each leaf's anchors (also as lines for `drawing`), its segments (`segments`,
1058
+ * `drawing`), its bounds' corners as lines and its centre; and the top-level objects' boxes and
1059
+ * sizes (inside a group holding an excluded node, that group's other members).
1060
+ */
1061
+ static collect(doc, exclude, gather, sort = true) {
1062
+ const self = new _VectorSnapTargets();
1063
+ const artboard = { x: 0, y: 0, width: doc.width, height: doc.height };
1064
+ self.pushRect(artboard, "artboard", { x: doc.width / 2, y: doc.height / 2 });
1065
+ let anchors = BUDGET;
1066
+ let segments = BUDGET;
1067
+ const visit = (list, parent) => {
1068
+ for (const n of list) {
1069
+ if (exclude.has(n.id) || n.visible === false) continue;
1070
+ const m = multiplyMatrices(nodeMatrix(n), parent);
1071
+ if (n.type === "group") {
1072
+ visit(n.children, m);
1073
+ continue;
1074
+ }
1075
+ if (anchors <= 0) continue;
1076
+ for (const sp of n.subpaths) {
1077
+ for (const a of sp.anchors) {
1078
+ if (anchors-- <= 0) break;
1079
+ const p = applyMatrix(m, a);
1080
+ if (gather === "drawing") self.pushAnchor(p);
1081
+ else self.points.push({ p, kind: "anchor" });
1082
+ }
1083
+ if (gather !== "bounds") segments = self.pushSegments(sp, m, () => false, segments);
1084
+ }
1085
+ const b = vectorNodeBounds(n, parent);
1086
+ const f = nodeFrame(n, parent, b);
1087
+ if (b) self.pushRect(b, "edge", _nullishCoalesce(_optionalChain([f, 'optionalAccess', _13 => _13.centre]), () => ( null)));
1088
+ }
1089
+ };
1090
+ visit(doc.nodes, IDENTITY_MATRIX);
1091
+ const objects = (list, parent) => {
1092
+ for (const n of list) {
1093
+ if (exclude.has(n.id) || n.visible === false || self.boxes.length >= BUDGET) continue;
1094
+ if (n.type === "group" && contains(n, exclude)) {
1095
+ objects(n.children, multiplyMatrices(nodeMatrix(n), parent));
1096
+ continue;
1097
+ }
1098
+ const b = vectorNodeBounds(n, parent);
1099
+ if (b) self.boxes.push(b);
1100
+ const f = nodeFrame(n, parent, b);
1101
+ if (f) self.sizes.push(f.width, f.height);
1102
+ }
1103
+ };
1104
+ objects(doc.nodes, IDENTITY_MATRIX);
1105
+ return sort ? self.index() : self;
1106
+ }
1107
+ // -------------------------------------------------------------------------
1108
+ // Points
1109
+ // -------------------------------------------------------------------------
1110
+ /** Snaps a single point (see `snapFrom`). */
1111
+ snapPoint(p, tol) {
1112
+ return this.snapFrom(p, null, tol);
1113
+ }
1114
+ /**
1115
+ * Snaps a point `p`, drawn or dragged from `leave` when given. With `leave.constrain` it keeps
1116
+ * to its angle round `leave.at`, sliding along that way into line with the nearest target.
1117
+ * Otherwise it lands, in order, on an anchor or centre, an intersection of two paths, the
1118
+ * perpendicular foot or a tangent point from `leave.at`, a path, a construction guide
1119
+ * through `leave.at` (where a target's line crosses it if one does nearby), or else lines up
1120
+ * with targets on x and y.
1121
+ */
1122
+ snapFrom(p, leave, tol) {
1123
+ const none = (q) => ({ point: q, kind: null, x: false, y: false, lines: [], labels: [] });
1124
+ const at = (q, kind, lines2 = []) => ({
1125
+ point: q,
1126
+ kind,
1127
+ x: true,
1128
+ y: true,
1129
+ lines: lines2,
1130
+ labels: [this.label(q, kind)]
1131
+ });
1132
+ if (_optionalChain([leave, 'optionalAccess', _14 => _14.constrain])) {
1133
+ const q = constrainToLeave(leave, p);
1134
+ const hit = this.slide(leave.at, { x: q.x - leave.at.x, y: q.y - leave.at.y }, q, tol, true);
1135
+ return hit ? { point: hit.point, x: true, y: true, ...this.linedUp(hit.point, hit.from) } : none(q);
1136
+ }
1137
+ const point = this.nearestPoint(p, tol);
1138
+ if (point) return at(point.p, point.kind);
1139
+ const near = this.segmentsNear(p, tol);
1140
+ const crossing = this.crossing(near, p, tol);
1141
+ if (crossing) return at(crossing, "intersect");
1142
+ if (leave && leave.curves !== false) {
1143
+ const foot = this.curvePoint(near, leave.at, p, tol);
1144
+ if (foot) return at(foot.point, foot.kind, [line("construction", leave.at, foot.point)]);
1145
+ }
1146
+ const on = this.onSegment(near);
1147
+ if (on) return at(on, "path");
1148
+ if (_optionalChain([leave, 'optionalAccess', _15 => _15.angles, 'optionalAccess', _16 => _16.length])) {
1149
+ const c = construction(leave, p, tol);
1150
+ if (c) {
1151
+ const hit = this.slide(leave.at, c.dir, c.point, tol, false);
1152
+ if (hit) {
1153
+ const lined = this.linedUp(hit.point, hit.from);
1154
+ return { point: hit.point, x: true, y: true, kind: lined.kind, lines: [line("construction", leave.at, hit.point), ...lined.lines], labels: lined.labels };
1155
+ }
1156
+ return { point: c.point, kind: "construction", x: true, y: true, lines: [line("construction", leave.at, c.point)], labels: [] };
1157
+ }
1158
+ }
1159
+ const nearest = (lines2, v) => {
1160
+ let best = null;
1161
+ for (const l of linesNear(lines2, v, tol)) if (!best || Math.abs(l.v - v) < Math.abs(best.v - v)) best = l;
1162
+ return best;
1163
+ };
1164
+ const bx = nearest(this.xs, p.x);
1165
+ const by = nearest(this.ys, p.y);
1166
+ if (!bx && !by) return none(p);
1167
+ const out = { x: bx ? bx.v : p.x, y: by ? by.v : p.y };
1168
+ const lines = [];
1169
+ if (bx) lines.push(line("align", bx.from, { x: out.x, y: out.y }));
1170
+ if (by) lines.push(line("align", by.from, { x: out.x, y: out.y }));
1171
+ return { point: out, kind: "align", x: !!bx, y: !!by, lines, labels: [this.label(out, "align")] };
1172
+ }
1173
+ label(p, kind, text = LABELS[kind]) {
1174
+ return { kind, x: p.x, y: p.y, text };
1175
+ }
1176
+ /** The anchor or centre nearest `p`, within `tol`. */
1177
+ nearestPoint(p, tol) {
1178
+ let best = null;
1179
+ let bestD = Infinity;
1180
+ for (const t of within(this.points, (e) => e.p.x, p.x, tol)) {
1181
+ const d = distance(t.p, p);
1182
+ if (d <= tol && d < bestD) {
1183
+ best = t;
1184
+ bestD = d;
1185
+ }
1186
+ }
1187
+ return best;
1188
+ }
1189
+ /**
1190
+ * Slides `q`, on the line through `at` along `dir` (beyond `at` only for a `ray`), to where
1191
+ * the nearest target line crosses it, at most twice `tol` away: where it lands and the
1192
+ * target point that line comes from.
1193
+ */
1194
+ slide(at, dir, q, tol, ray) {
1195
+ const len2 = dir.x * dir.x + dir.y * dir.y;
1196
+ if (!Number.isFinite(len2) || len2 <= 1e-18) return null;
1197
+ const reach = 2 * tol;
1198
+ let best = null;
1199
+ for (const vertical of [true, false]) {
1200
+ const d = vertical ? dir.x : dir.y;
1201
+ if (Math.abs(d) <= 1e-9) continue;
1202
+ const a = vertical ? at.x : at.y;
1203
+ for (const l of linesNear(vertical ? this.xs : this.ys, vertical ? q.x : q.y, reach)) {
1204
+ const s = (l.v - a) / d;
1205
+ const hit = vertical ? { x: l.v, y: at.y + dir.y * s } : { x: at.x + dir.x * s, y: l.v };
1206
+ const moved = distance(hit, q);
1207
+ if (moved <= reach && (!ray || s > 0) && (!best || moved < best.moved)) best = { moved, point: hit, from: l.from };
1208
+ }
1209
+ }
1210
+ return best && { point: best.point, from: best.from };
1211
+ }
1212
+ /** 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. */
1213
+ linedUp(q, from) {
1214
+ const on = this.nearestPoint(q, 1e-9);
1215
+ if (on) return { kind: on.kind, lines: [], labels: [this.label(on.p, on.kind)] };
1216
+ return { kind: "align", lines: [line("align", from, q)], labels: [this.label(q, "align")] };
1217
+ }
1218
+ /** Target segments whose control box reaches `p` and which pass within `tol` of it, nearest first. */
1219
+ segmentsNear(p, tol) {
1220
+ const out = [];
1221
+ const pt = toPoint(p);
1222
+ for (const s of this.segments) {
1223
+ if (p.x < s.minX - tol || p.x > s.maxX + tol || p.y < s.minY - tol || p.y > s.maxY + tol) continue;
1224
+ const hit = _chunkUG5DVXJKcjs.segmentNearest.call(void 0, s.seg, pt);
1225
+ if (hit.distance <= tol) out.push({ seg: s.seg, t: hit.t, distance: hit.distance });
1226
+ }
1227
+ return out.sort((a, b) => a.distance - b.distance);
1228
+ }
1229
+ onSegment(near) {
1230
+ return near.length ? toVec(_chunkUG5DVXJKcjs.segmentPoint.call(void 0, near[0].seg, near[0].t)) : null;
1231
+ }
1232
+ /** Where two of the target segments near `p` cross, the crossing nearest `p` within `tol`. */
1233
+ crossing(near, p, tol) {
1234
+ const list = near.slice(0, CROSSING_PAIRS);
1235
+ let best = null;
1236
+ let bestD = Infinity;
1237
+ for (let i = 0; i < list.length; i++) {
1238
+ for (let j = i + 1; j < list.length; j++) {
1239
+ for (const hit of _chunkUG5DVXJKcjs.segmentIntersections.call(void 0, list[i].seg, list[j].seg)) {
1240
+ const q = toVec(hit.point);
1241
+ const d = distance(q, p);
1242
+ if (d <= tol && d < bestD) {
1243
+ best = q;
1244
+ bestD = d;
1245
+ }
1246
+ }
1247
+ }
1248
+ }
1249
+ return best;
1250
+ }
1251
+ /**
1252
+ * The foot of a perpendicular, or a tangent point, from `base` on a target segment near `p`
1253
+ * (within `tol` of it): roots of `(B(t) − base) · B′(t)` and `(B(t) − base) × B′(t)`
1254
+ * bracketed by sign changes of samples and narrowed by bisection.
1255
+ */
1256
+ curvePoint(near, base, p, tol) {
1257
+ let best = null;
1258
+ for (const { seg } of near) {
1259
+ for (const kind of ["perpendicular", "tangent"]) {
1260
+ if (kind === "tangent" && seg.length === 2) continue;
1261
+ for (const q of solveOnSegment(seg, base, kind === "perpendicular" ? dotCondition : crossCondition)) {
1262
+ const d = distance(q, p);
1263
+ if (d <= tol && (!best || d < best.d)) best = { point: q, kind, d };
1264
+ }
1265
+ }
1266
+ }
1267
+ return best && { point: best.point, kind: best.kind };
1268
+ }
1269
+ // -------------------------------------------------------------------------
1270
+ // Boxes
1271
+ // -------------------------------------------------------------------------
1272
+ /**
1273
+ * Snaps a moving box `r` (the moved selection's bounds): its edge or centre nearest a target
1274
+ * line on each axis (`axes` limits which may move). With `spacing`, an axis that lines up
1275
+ * with nothing spaces the box evenly in its row or column. Returns the correction and guides.
1276
+ */
1277
+ snapRect(r, tol, options = {}) {
1278
+ 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 };
1279
+ const nearest = (lines, values) => {
1280
+ let best = null;
1281
+ 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 };
1282
+ return best;
1283
+ };
1284
+ const bx = axes.x ? nearest(this.xs, [r.x, r.x + r.width / 2, r.x + r.width]) : null;
1285
+ const by = axes.y ? nearest(this.ys, [r.y, r.y + r.height / 2, r.y + r.height]) : null;
1286
+ 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: [] };
1287
+ const done = { ...r, x: r.x + out.dx, y: r.y + out.dy };
1288
+ if (bx) {
1289
+ const x = bx.from.x;
1290
+ 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) });
1291
+ }
1292
+ if (by) {
1293
+ const y = by.from.y;
1294
+ 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 });
1295
+ }
1296
+ if (options.spacing && this.boxes.length) {
1297
+ for (const horizontal of [true, false]) {
1298
+ if (horizontal ? out.x || !axes.x : out.y || !axes.y) continue;
1299
+ const moved = { ...r, x: r.x + out.dx, y: r.y + out.dy };
1300
+ const even = this.evenSpacing(moved, horizontal, tol);
1301
+ if (!even) continue;
1302
+ if (horizontal) {
1303
+ out.dx += even.shift;
1304
+ out.x = true;
1305
+ } else {
1306
+ out.dy += even.shift;
1307
+ out.y = true;
1308
+ }
1309
+ for (const gap of even.gaps) this.gapMarks(gap, horizontal, tol, out);
1310
+ }
1311
+ }
1312
+ 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"));
1313
+ return out;
1314
+ }
1315
+ /**
1316
+ * Equal spacing along x (`horizontal`) or y for the moved box `r`, among the objects of its
1317
+ * row (column): the shift within `tol` that centres it between its neighbours, or leaves it
1318
+ * as far from the neighbour before or after it as two other objects of the row are apart;
1319
+ * and the gaps that are equal then, as (start, end, where across).
1320
+ */
1321
+ evenSpacing(r, horizontal, tol) {
1322
+ const span = (b, x) => x ? [b.x, b.x + b.width] : [b.y, b.y + b.height];
1323
+ const [r0, r1] = span(r, horizontal);
1324
+ const len = r1 - r0;
1325
+ const [c0, c1] = span(r, !horizontal);
1326
+ 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);
1327
+ if (!row.length) return null;
1328
+ 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;
1329
+ const pairs = [];
1330
+ for (const p of row) {
1331
+ const next = bound(row, (e) => e.a0, p.a1, false);
1332
+ const q = row[next];
1333
+ if (!q) continue;
1334
+ const straddles = p.a1 <= r0 + tol && q.a0 >= r1 - tol;
1335
+ if (q.a0 > p.a1 && !straddles) pairs.push({ g: q.a0 - p.a1, gap: [p.a1, q.a0, mid(p.b, q.b)] });
1336
+ }
1337
+ let before = null;
1338
+ let after = null;
1339
+ for (const e of row) {
1340
+ if (e.a1 <= r0 + tol && (!before || e.a1 > before.a1)) before = e;
1341
+ if (e.a0 >= r1 - tol && (!after || e.a0 < after.a0)) after = e;
1342
+ }
1343
+ const starts = [];
1344
+ if (before && after && after.a0 - before.a1 > len) starts.push({ start: (before.a1 + after.a0 - len) / 2, repeats: null });
1345
+ for (const { g } of pairs) {
1346
+ if (before) starts.push({ start: before.a1 + g, repeats: g });
1347
+ if (after) starts.push({ start: after.a0 - g - len, repeats: g });
1348
+ }
1349
+ let pick = null;
1350
+ 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;
1351
+ if (!pick) return null;
1352
+ const { start, repeats } = pick;
1353
+ const at = horizontal ? { ...r, x: start } : { ...r, y: start };
1354
+ const end = start + len;
1355
+ const gaps = [];
1356
+ if (before && (repeats === null || Math.abs(start - before.a1 - repeats) <= EQUAL)) gaps.push([before.a1, start, mid(before.b, at)]);
1357
+ if (after && (repeats === null || Math.abs(after.a0 - end - repeats) <= EQUAL)) gaps.push([end, after.a0, mid(after.b, at)]);
1358
+ if (repeats !== null) {
1359
+ for (const p of pairs) if (Math.abs(p.g - repeats) <= EQUAL) gaps.push(p.gap);
1360
+ }
1361
+ return { shift: start - r0, gaps };
1362
+ }
1363
+ /** A gap mark: a line across the gap with a tick at each end, and its length as a label. */
1364
+ gapMarks([u0, u1, c], horizontal, tick, out) {
1365
+ const pt = (u, v) => horizontal ? { x: u, y: v } : { x: v, y: u };
1366
+ 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)));
1367
+ out.labels.push(this.label(pt((u0 + u1) / 2, c), "spacing", fmtLength(u1 - u0)));
1368
+ }
1369
+ /** The other objects' side length nearest `length`, within `tol`, or null. */
1370
+ matchSize(length2, tol) {
1371
+ let best = null;
1372
+ for (const s of this.sizes) if (Math.abs(s - length2) <= tol && (best === null || Math.abs(s - length2) < Math.abs(best - length2))) best = s;
1373
+ return best;
1374
+ }
1375
+ /**
1376
+ * Snaps a selection-box resize. `sx / sy` are the scale `scaleFor` gave for dragging
1377
+ * `handle` of `frame` (in the box's own, possibly rotated frame), `mods.shift` keeps the
1378
+ * proportions and `mods.alt` scales about the centre. The handle lands on an anchor or
1379
+ * centre within `tol`, else the corner or the side it moves lines up with the nearest
1380
+ * target, whatever the box's angle; a handle with one way to go (a side, or a proportional
1381
+ * corner) slides along it until one of them is on a target. An axis that snaps to nothing
1382
+ * may match another object's width or height instead.
1383
+ */
1384
+ snapScale(frame, handle, sx, sy, mods, tol) {
1385
+ const toDoc = frame.angle ? rotateMatrix(frame.angle) : IDENTITY_MATRIX;
1386
+ const toLocal = frame.angle ? rotateMatrix(-frame.angle) : IDENTITY_MATRIX;
1387
+ const corner = handle.length === 2;
1388
+ const proportional = !!mods.shift;
1389
+ const centre = { x: frame.x + frame.width / 2, y: frame.y + frame.height / 2 };
1390
+ const origin = mods.alt ? centre : handlePos(opposite(handle), frame);
1391
+ const hp = handlePos(handle, frame);
1392
+ const d0 = { x: hp.x - origin.x, y: hp.y - origin.y };
1393
+ const valid = (n, s) => Number.isFinite(n) && n * s > 0;
1394
+ const nearest = (v, lines) => {
1395
+ let best = null;
1396
+ for (const l of linesNear(lines, v, tol)) if (!best || Math.abs(l.v - v) < Math.abs(best.v - v)) best = l;
1397
+ return best;
1398
+ };
1399
+ let nx = sx;
1400
+ let ny = sy;
1401
+ let snap = { hx: null, hy: null, on: null };
1402
+ if (corner && !proportional) {
1403
+ const p = applyMatrix(toDoc, { x: origin.x + d0.x * sx, y: origin.y + d0.y * sy });
1404
+ const hx = nearest(p.x, this.xs);
1405
+ const hy = nearest(p.y, this.ys);
1406
+ const on = this.nearestPoint(p, tol);
1407
+ const tries = [];
1408
+ if (on) tries.push({ tx: on.p.x, ty: on.p.y, s: { hx: null, hy: null, on } });
1409
+ 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 } });
1410
+ tries.push({ tx: _nullishCoalesce(_optionalChain([hx, 'optionalAccess', _25 => _25.v]), () => ( null)), ty: null, s: { hx, hy: null, on: null } });
1411
+ tries.push({ tx: null, ty: _nullishCoalesce(_optionalChain([hy, 'optionalAccess', _26 => _26.v]), () => ( null)), s: { hx: null, hy, on: null } });
1412
+ for (const { tx, ty, s } of tries) {
1413
+ if (tx === null && ty === null) continue;
1414
+ const q = applyMatrix(toLocal, { x: _nullishCoalesce(tx, () => ( p.x)), y: _nullishCoalesce(ty, () => ( p.y)) });
1415
+ let cx = (q.x - origin.x) / d0.x;
1416
+ let cy = (q.y - origin.y) / d0.y;
1417
+ if (!frame.angle) {
1418
+ if (tx === null) cx = sx;
1419
+ if (ty === null) cy = sy;
1420
+ }
1421
+ if (valid(cx, sx) && valid(cy, sy)) {
1422
+ nx = cx;
1423
+ ny = cy;
1424
+ snap = s;
1425
+ break;
1426
+ }
1427
+ }
1428
+ } else {
1429
+ const movesX2 = handle === "e" || handle === "w";
1430
+ let e;
1431
+ let c;
1432
+ let s0;
1433
+ if (corner) [e, c, s0] = [{ x: Math.sign(sx), y: Math.sign(sy) }, { x: 0, y: 0 }, Math.abs(sx)];
1434
+ 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];
1435
+ 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];
1436
+ const i = RING.indexOf(handle);
1437
+ const ends = corner ? [] : [RING[(i + 1) % 8], RING[(i + 7) % 8]];
1438
+ const travel = Math.hypot(e.x * d0.x, e.y * d0.y);
1439
+ let best = null;
1440
+ const consider = (s, inLine, found2) => {
1441
+ const moved = Math.abs(s - s0) * travel;
1442
+ if (!valid(s, s0) || moved > 2 * tol) return;
1443
+ if (!best || (inLine === best.inLine ? moved < best.moved : !inLine)) best = { s, inLine, moved, snap: found2 };
1444
+ };
1445
+ for (const h of [handle, ...ends]) {
1446
+ const hv = handlePos(h, frame);
1447
+ const v = { x: hv.x - origin.x, y: hv.y - origin.y };
1448
+ const base = applyMatrix(toDoc, { x: origin.x + c.x * v.x, y: origin.y + c.y * v.y });
1449
+ const dir = applyMatrix(toDoc, { x: e.x * v.x, y: e.y * v.y });
1450
+ const p = { x: base.x + dir.x * s0, y: base.y + dir.y * s0 };
1451
+ const l2 = dir.x * dir.x + dir.y * dir.y;
1452
+ const on = l2 > 1e-18 ? this.nearestPoint(p, tol) : null;
1453
+ if (on) consider(((on.p.x - base.x) * dir.x + (on.p.y - base.y) * dir.y) / l2, false, { hx: null, hy: null, on });
1454
+ if (Math.abs(dir.x) > 1e-9) {
1455
+ const hit = nearest(p.x, this.xs);
1456
+ if (hit) consider((hit.v - base.x) / dir.x, true, { hx: hit, hy: null, on: null });
1457
+ }
1458
+ if (Math.abs(dir.y) > 1e-9) {
1459
+ const hit = nearest(p.y, this.ys);
1460
+ if (hit) consider((hit.v - base.y) / dir.y, true, { hx: null, hy: hit, on: null });
1461
+ }
1462
+ }
1463
+ const found = best;
1464
+ if (found) {
1465
+ nx = c.x + found.s * e.x;
1466
+ ny = c.y + found.s * e.y;
1467
+ snap = found.snap;
1468
+ }
1469
+ }
1470
+ const out = { sx: nx, sy: ny, snapped: false, lines: [], labels: [] };
1471
+ const handleAt = () => applyMatrix(toDoc, { x: origin.x + d0.x * out.sx, y: origin.y + d0.y * out.sy });
1472
+ if (snap.on) {
1473
+ out.snapped = true;
1474
+ out.labels.push(this.label(snap.on.p, snap.on.kind));
1475
+ return out;
1476
+ }
1477
+ const movesX = handle !== "n" && handle !== "s";
1478
+ const movesY = handle !== "e" && handle !== "w";
1479
+ const sized = [];
1480
+ if (!proportional) {
1481
+ if (movesX && !snap.hx && !(frame.angle && snap.hy) && frame.width > 1e-9) {
1482
+ const len = this.matchSize(Math.abs(out.sx) * frame.width, tol);
1483
+ if (len !== null && len > 0) {
1484
+ out.sx = Math.sign(out.sx || 1) * (len / frame.width);
1485
+ sized.push("x");
1486
+ }
1487
+ }
1488
+ if (movesY && !snap.hy && !(frame.angle && snap.hx) && frame.height > 1e-9) {
1489
+ const len = this.matchSize(Math.abs(out.sy) * frame.height, tol);
1490
+ if (len !== null && len > 0) {
1491
+ out.sy = Math.sign(out.sy || 1) * (len / frame.height);
1492
+ sized.push("y");
1493
+ }
1494
+ }
1495
+ }
1496
+ const x0 = origin.x + (frame.x - origin.x) * out.sx;
1497
+ const x1 = origin.x + (frame.x + frame.width - origin.x) * out.sx;
1498
+ const y0 = origin.y + (frame.y - origin.y) * out.sy;
1499
+ const y1 = origin.y + (frame.y + frame.height - origin.y) * out.sy;
1500
+ const box = { x0: Math.min(x0, x1), x1: Math.max(x0, x1), y0: Math.min(y0, y1), y1: Math.max(y0, y1) };
1501
+ const page = [
1502
+ { x: box.x0, y: box.y0 },
1503
+ { x: box.x1, y: box.y0 },
1504
+ { x: box.x1, y: box.y1 },
1505
+ { x: box.x0, y: box.y1 }
1506
+ ].map((q) => applyMatrix(toDoc, q));
1507
+ const pminY = Math.min(...page.map((q) => q.y));
1508
+ const pmaxY = Math.max(...page.map((q) => q.y));
1509
+ const pminX = Math.min(...page.map((q) => q.x));
1510
+ const pmaxX = Math.max(...page.map((q) => q.x));
1511
+ 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) });
1512
+ 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 });
1513
+ if (snap.hx || snap.hy) {
1514
+ out.snapped = true;
1515
+ out.labels.push(this.label(handleAt(), "align"));
1516
+ }
1517
+ for (const axis of sized) {
1518
+ out.snapped = true;
1519
+ const off = tol * 2;
1520
+ 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 }];
1521
+ const pa = applyMatrix(toDoc, a);
1522
+ const pb = applyMatrix(toDoc, b);
1523
+ out.lines.push(line("size", pa, pb));
1524
+ 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)}`));
1525
+ }
1526
+ return out;
1527
+ }
1528
+ }, _class);
1529
+ var RING = ["nw", "n", "ne", "e", "se", "s", "sw", "w"];
1530
+ function opposite(h) {
1531
+ return RING[(RING.indexOf(h) + 4) % 8];
1532
+ }
1533
+ function handlePos(h, r) {
1534
+ const x = h.includes("w") ? r.x : h.includes("e") ? r.x + r.width : r.x + r.width / 2;
1535
+ const y = h.startsWith("n") ? r.y : h.startsWith("s") ? r.y + r.height : r.y + r.height / 2;
1536
+ return { x, y };
1537
+ }
1538
+ function line(kind, a, b) {
1539
+ return { kind, x1: a.x, y1: a.y, x2: b.x, y2: b.y };
1540
+ }
1541
+ function construction(leave, p, tol) {
1542
+ const v = { x: p.x - leave.at.x, y: p.y - leave.at.y };
1543
+ if (Math.hypot(v.x, v.y) <= tol) return null;
1544
+ let best = null;
1545
+ for (const a of _nullishCoalesce(leave.angles, () => ( []))) {
1546
+ const r = a * Math.PI / 180;
1547
+ const exact = (n) => Math.abs(n) < 1e-12 ? 0 : n;
1548
+ const dir = { x: exact(Math.cos(r)), y: exact(-Math.sin(r)) };
1549
+ const k = v.x * dir.x + v.y * dir.y;
1550
+ const point = { x: leave.at.x + dir.x * k, y: leave.at.y + dir.y * k };
1551
+ const d = distance(point, p);
1552
+ if (d <= tol && (!best || d < best.d)) best = { dir, point, d };
1553
+ }
1554
+ return best && { dir: best.dir, point: best.point };
1555
+ }
1556
+ var dotCondition = (r, d) => r[0] * d[0] + r[1] * d[1];
1557
+ var crossCondition = (r, d) => r[0] * d[1] - r[1] * d[0];
1558
+ function solveOnSegment(seg, base, cond) {
1559
+ const b = [base.x, base.y];
1560
+ const g = (t) => {
1561
+ const p = _chunkUG5DVXJKcjs.segmentPoint.call(void 0, seg, t);
1562
+ return cond([p[0] - b[0], p[1] - b[1]], _chunkUG5DVXJKcjs.segmentDerivative.call(void 0, seg, t));
1563
+ };
1564
+ const vals = [];
1565
+ let scale = 0;
1566
+ for (let i = 0; i <= CURVE_SAMPLES; i++) {
1567
+ const t = i / CURVE_SAMPLES;
1568
+ const p = _chunkUG5DVXJKcjs.segmentPoint.call(void 0, seg, t);
1569
+ const d = _chunkUG5DVXJKcjs.segmentDerivative.call(void 0, seg, t);
1570
+ const r = [p[0] - b[0], p[1] - b[1]];
1571
+ vals.push(cond(r, d));
1572
+ scale = Math.max(scale, Math.hypot(r[0], r[1]) * Math.hypot(d[0], d[1]));
1573
+ }
1574
+ if (!(scale > 0) || vals.every((v) => Math.abs(v) <= scale * 1e-12)) return [];
1575
+ const out = [];
1576
+ for (let i = 0; i < CURVE_SAMPLES; i++) {
1577
+ const ga = vals[i];
1578
+ const gb = vals[i + 1];
1579
+ if (!(Number.isFinite(ga) && Number.isFinite(gb)) || ga > 0 === gb > 0 && ga !== 0) continue;
1580
+ let lo = i / CURVE_SAMPLES;
1581
+ let hi = (i + 1) / CURVE_SAMPLES;
1582
+ let flo = ga;
1583
+ for (let k = 0; k < BISECT; k++) {
1584
+ const m = (lo + hi) / 2;
1585
+ const fm = g(m);
1586
+ if (fm === 0) {
1587
+ lo = hi = m;
1588
+ break;
1589
+ }
1590
+ if (fm > 0 === flo > 0) {
1591
+ lo = m;
1592
+ flo = fm;
1593
+ } else hi = m;
1594
+ }
1595
+ const t = (lo + hi) / 2;
1596
+ const p = _chunkUG5DVXJKcjs.segmentPoint.call(void 0, seg, t);
1597
+ const dv = _chunkUG5DVXJKcjs.segmentDerivative.call(void 0, seg, t);
1598
+ const r = Math.hypot(p[0] - b[0], p[1] - b[1]);
1599
+ const dl = Math.hypot(dv[0], dv[1]);
1600
+ if (!(r > 1e-9 && dl > 0) || Math.abs(cond([p[0] - b[0], p[1] - b[1]], dv)) > 1e-6 * r * dl) continue;
1601
+ 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] });
1602
+ }
1603
+ return out;
1604
+ }
1605
+
777
1606
  // src/vector/controller.ts
1607
+ var DEFAULT_VECTOR_SNAP = { grid: 0, objects: true, guides: true, threshold: 5, angles: VECTOR_CONSTRUCTION_ANGLES };
778
1608
  var VECTOR_CLIPBOARD_TYPE = "pixerate/vector";
779
1609
  var DEFAULT_DEFAULTS = {
780
1610
  fill: "#d4d4d8",
@@ -802,7 +1632,7 @@ function getEditablePathIds(state) {
802
1632
  }
803
1633
  }
804
1634
  }
805
- return [...ids].filter((id) => _optionalChain([findVectorNode, 'call', _8 => _8(state.document.nodes, id), 'optionalAccess', _9 => _9.type]) === "path");
1635
+ return [...ids].filter((id) => _optionalChain([findVectorNode, 'call', _27 => _27(state.document.nodes, id), 'optionalAccess', _28 => _28.type]) === "path");
806
1636
  }
807
1637
  function getVisibleAnchors(state) {
808
1638
  const out = [];
@@ -817,7 +1647,7 @@ function getVisibleHandles(state) {
817
1647
  const seen = /* @__PURE__ */ new Set();
818
1648
  const push = (node, s, i, handle) => {
819
1649
  const sp = node.subpaths[s];
820
- const a = _optionalChain([sp, 'optionalAccess', _10 => _10.anchors, 'access', _11 => _11[i]]);
1650
+ const a = _optionalChain([sp, 'optionalAccess', _29 => _29.anchors, 'access', _30 => _30[i]]);
821
1651
  if (!a || !a[handle]) return;
822
1652
  const key = `${node.id}:${s}:${i}:${handle}`;
823
1653
  if (seen.has(key)) return;
@@ -826,8 +1656,8 @@ function getVisibleHandles(state) {
826
1656
  };
827
1657
  if (state.tool === "pen" && state.pen) {
828
1658
  const node = findVectorNode(state.document.nodes, state.pen.nodeId);
829
- const sp = _optionalChain([node, 'optionalAccess', _12 => _12.type]) === "path" ? node.subpaths[state.pen.subpath] : void 0;
830
- if (_optionalChain([node, 'optionalAccess', _13 => _13.type]) === "path" && sp) {
1659
+ const sp = _optionalChain([node, 'optionalAccess', _31 => _31.type]) === "path" ? node.subpaths[state.pen.subpath] : void 0;
1660
+ if (_optionalChain([node, 'optionalAccess', _32 => _32.type]) === "path" && sp) {
831
1661
  const last = sp.anchors.length - 1;
832
1662
  push(node, state.pen.subpath, last, "in");
833
1663
  push(node, state.pen.subpath, last, "out");
@@ -853,8 +1683,26 @@ function getVisibleHandles(state) {
853
1683
  }
854
1684
  function getAnchor(doc, ref) {
855
1685
  const node = findVectorNode(doc.nodes, ref.nodeId);
856
- 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;
1686
+ 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;
1687
+ }
1688
+ function getAnchorWorld(doc, ref) {
1689
+ const a = getAnchor(doc, ref);
1690
+ if (!a) return void 0;
1691
+ const m = _nullishCoalesce(vectorWorldMatrix(doc.nodes, ref.nodeId), () => ( IDENTITY_MATRIX));
1692
+ const p = applyMatrix(m, a);
1693
+ return { x: p.x, y: p.y, ...a.in ? { in: applyMatrix(m, a.in) } : {}, ...a.out ? { out: applyMatrix(m, a.out) } : {}, ...a.type ? { type: a.type } : {} };
857
1694
  }
1695
+ function getVectorSelectionFrame(doc, ids) {
1696
+ const nodes = ids.map((id) => ({ node: findVectorNode(doc.nodes, id), parent: _nullishCoalesce(vectorParentMatrix(doc.nodes, id), () => ( IDENTITY_MATRIX)) })).filter((n) => n.node);
1697
+ if (!nodes.length) return null;
1698
+ const angles = nodes.map((n) => matrixRotation(multiplyMatrices(nodeMatrix(n.node), n.parent)));
1699
+ const same = angles.every((a) => Math.abs(normalizeAngle(a - angles[0])) < 1e-6);
1700
+ const angle = same ? angles[0] : 0;
1701
+ const unrotate = rotateMatrix(-angle);
1702
+ const b = unionBounds(nodes.map((n) => vectorNodeBounds(n.node, multiplyMatrices(n.parent, unrotate))));
1703
+ return b ? { ...b, angle } : null;
1704
+ }
1705
+ var normalizeAngle = (a) => ((a + 180) % 360 + 360) % 360 - 180;
858
1706
  function getTransformHandlePositions(box) {
859
1707
  const { x, y, width: w, height: h } = box;
860
1708
  return [
@@ -869,17 +1717,28 @@ function getTransformHandlePositions(box) {
869
1717
  { handle: "rotate", x: x + w / 2, y: y - VECTOR_ROTATE_HANDLE_OFFSET }
870
1718
  ];
871
1719
  }
872
- var RESIZE_CURSORS = {
873
- nw: "nwse-resize",
874
- se: "nwse-resize",
875
- ne: "nesw-resize",
876
- sw: "nesw-resize",
877
- n: "ns-resize",
878
- s: "ns-resize",
879
- e: "ew-resize",
880
- w: "ew-resize",
881
- rotate: "grab"
882
- };
1720
+ function getVectorSelectionBoxGeometry(frame, viewport) {
1721
+ const z = viewport.zoom;
1722
+ const box = { x: frame.x * z + viewport.x, y: frame.y * z + viewport.y, width: frame.width * z, height: frame.height * z };
1723
+ const rot = rotateMatrix(frame.angle, { x: viewport.x, y: viewport.y });
1724
+ const toScreen2 = (p) => frame.angle ? applyMatrix(rot, p) : { x: p.x, y: p.y };
1725
+ const handles = getTransformHandlePositions(box).map((h) => ({ ...h, screen: toScreen2(h) }));
1726
+ const corners = [
1727
+ { x: box.x, y: box.y },
1728
+ { x: box.x + box.width, y: box.y },
1729
+ { x: box.x + box.width, y: box.y + box.height },
1730
+ { x: box.x, y: box.y + box.height }
1731
+ ].map(toScreen2);
1732
+ const r = (n) => Math.round(n * 1e3) / 1e3;
1733
+ return { box, angle: frame.angle, transform: frame.angle ? `rotate(${r(frame.angle)} ${r(viewport.x)} ${r(viewport.y)})` : "", handles, corners };
1734
+ }
1735
+ var RESIZE_CURSOR_ORDER = ["ew-resize", "nwse-resize", "ns-resize", "nesw-resize"];
1736
+ var HANDLE_ANGLES = { e: 0, se: 45, s: 90, sw: 135, w: 180, nw: 225, n: 270, ne: 315 };
1737
+ function vectorResizeCursor(handle, angle = 0) {
1738
+ if (handle === "rotate") return "grab";
1739
+ const a = ((HANDLE_ANGLES[handle] + angle) % 180 + 180) % 180;
1740
+ return RESIZE_CURSOR_ORDER[Math.round(a / 45) % 4];
1741
+ }
883
1742
  var TOOL_LABELS = {
884
1743
  select: "Select",
885
1744
  direct: "Direct Select",
@@ -938,42 +1797,48 @@ function mapTree(nodes, fn) {
938
1797
  }
939
1798
  return changed ? out : nodes;
940
1799
  }
1800
+ var isRoot = (m) => m === IDENTITY_MATRIX || m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0;
941
1801
  var idCounter = 0;
942
1802
  var defaultCreateId = () => `vn-${Date.now().toString(36)}${(++idCounter).toString(36)}`;
943
- var VectorEditorController = (_class = class {
1803
+ var VectorEditorController = (_class2 = class {
944
1804
 
945
1805
 
946
- __init() {this.selection = []}
947
- __init2() {this.anchors = []}
948
- __init3() {this.editingId = null}
949
- __init4() {this.pen = null}
1806
+ __init7() {this.selection = []}
1807
+ __init8() {this.anchors = []}
1808
+ __init9() {this.editingId = null}
1809
+ __init10() {this.pen = null}
950
1810
 
951
1811
 
952
1812
 
953
1813
 
954
1814
 
955
- __init5() {this.hover = null}
956
- __init6() {this.pointer = null}
957
- __init7() {this.drag = null}
958
- __init8() {this.interaction = null}
959
- __init9() {this.spaceHeld = false}
960
- __init10() {this.clipboard = null}
1815
+ __init11() {this.hover = null}
1816
+ __init12() {this.pointer = null}
1817
+ __init13() {this.drag = null}
1818
+ __init14() {this.interaction = null}
1819
+ __init15() {this.guides = null}
1820
+ /** Whether the last `snapDrawn` landed on an object (guides may be hidden). */
1821
+ __init16() {this.snappedToObject = false}
1822
+ /** Drawing targets for hovering the pen and shape tools, kept while the document and the excluded path stay the same. */
1823
+ __init17() {this.hoverTargets = null}
1824
+ __init18() {this.spaceHeld = false}
1825
+ __init19() {this.clipboard = null}
961
1826
 
962
1827
 
963
- __init11() {this.listeners = /* @__PURE__ */ new Set()}
964
- __init12() {this.cleanups = []}
965
- __init13() {this.applying = false}
1828
+ __init20() {this.listeners = /* @__PURE__ */ new Set()}
1829
+ __init21() {this.cleanups = []}
1830
+ __init22() {this.applying = false}
966
1831
  /** Set while a commit records a change that was already applied, so `restore` still reports it. */
967
- __init14() {this.committing = false}
968
- __init15() {this.revision = 0}
1832
+ __init23() {this.committing = false}
1833
+ __init24() {this.revision = 0}
969
1834
 
970
- 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);
1835
+ constructor(options = {}) {;_class2.prototype.__init7.call(this);_class2.prototype.__init8.call(this);_class2.prototype.__init9.call(this);_class2.prototype.__init10.call(this);_class2.prototype.__init11.call(this);_class2.prototype.__init12.call(this);_class2.prototype.__init13.call(this);_class2.prototype.__init14.call(this);_class2.prototype.__init15.call(this);_class2.prototype.__init16.call(this);_class2.prototype.__init17.call(this);_class2.prototype.__init18.call(this);_class2.prototype.__init19.call(this);_class2.prototype.__init20.call(this);_class2.prototype.__init21.call(this);_class2.prototype.__init22.call(this);_class2.prototype.__init23.call(this);_class2.prototype.__init24.call(this);_class2.prototype.__init25.call(this);_class2.prototype.__init26.call(this);
971
1836
  this.history = _nullishCoalesce(options.history, () => ( new (0, _chunkQKLRAA4Tcjs.HistoryManager)()));
972
1837
  this.doc = options.document ? cloneVectorDocument(options.document) : { width: 800, height: 600, nodes: [] };
973
1838
  this.tool = _nullishCoalesce(options.tool, () => ( "select"));
974
1839
  this.defaults = { ...DEFAULT_DEFAULTS, ..._nullishCoalesce(options.defaults, () => ( {})) };
975
1840
  this.viewport = { x: 0, y: 0, zoom: 1, ..._nullishCoalesce(options.viewport, () => ( {})) };
976
- this.snap = { grid: 0, ..._nullishCoalesce(options.snap, () => ( {})) };
1841
+ this.snap = { ...DEFAULT_VECTOR_SNAP, ..._nullishCoalesce(options.snap, () => ( {})) };
977
1842
  this.hitRadius = _nullishCoalesce(options.hitRadius, () => ( 6));
978
1843
  this.createId = _nullishCoalesce(options.createId, () => ( defaultCreateId));
979
1844
  this.onChange = options.onChange;
@@ -995,9 +1860,9 @@ var VectorEditorController = (_class = class {
995
1860
  // State
996
1861
  // -------------------------------------------------------------------------
997
1862
  /** The current snapshot (stable until the next change). */
998
- __init16() {this.getState = () => this.state}
1863
+ __init25() {this.getState = () => this.state}
999
1864
  /** Calls `listener` after every change. Returns an unsubscribe function. */
1000
- __init17() {this.subscribe = (listener) => {
1865
+ __init26() {this.subscribe = (listener) => {
1001
1866
  this.listeners.add(listener);
1002
1867
  return () => {
1003
1868
  this.listeners.delete(listener);
@@ -1018,9 +1883,11 @@ var VectorEditorController = (_class = class {
1018
1883
  selectedAnchors: this.anchors,
1019
1884
  editingId: this.editingId,
1020
1885
  selectionBounds: this.boundsOf(this.selection),
1886
+ selectionFrame: this.frameOf(this.selectedRoots()),
1021
1887
  viewport: this.viewport,
1022
1888
  defaults: this.defaults,
1023
1889
  snap: this.snap,
1890
+ guides: this.guides,
1024
1891
  hitRadius: this.hitRadius,
1025
1892
  hover: this.hover,
1026
1893
  cursor: this.cursorFor(),
@@ -1037,7 +1904,7 @@ var VectorEditorController = (_class = class {
1037
1904
  notify(docChanged = false) {
1038
1905
  this.revision++;
1039
1906
  this.state = this.computeState();
1040
- if (docChanged) _optionalChain([this, 'access', _19 => _19.onChange, 'optionalCall', _20 => _20(this.doc)]);
1907
+ if (docChanged) _optionalChain([this, 'access', _38 => _38.onChange, 'optionalCall', _39 => _39(this.doc)]);
1041
1908
  const state = this.state;
1042
1909
  this.listeners.forEach((listener) => {
1043
1910
  try {
@@ -1048,23 +1915,39 @@ var VectorEditorController = (_class = class {
1048
1915
  });
1049
1916
  }
1050
1917
  boundsOf(ids) {
1051
- return unionBounds(ids.map((id) => this.getNode(id)).map((n) => n ? vectorNodeBounds(n) : null));
1918
+ return unionBounds(
1919
+ ids.map((id) => {
1920
+ const n = this.getNode(id);
1921
+ return n ? vectorNodeBounds(n, _nullishCoalesce(vectorParentMatrix(this.doc.nodes, id), () => ( IDENTITY_MATRIX))) : null;
1922
+ })
1923
+ );
1924
+ }
1925
+ frameOf(ids) {
1926
+ return getVectorSelectionFrame(this.doc, ids);
1927
+ }
1928
+ /** Local → document matrix of a node in `doc` (default: the current document). */
1929
+ worldOf(id, doc = this.doc) {
1930
+ return _nullishCoalesce(vectorWorldMatrix(doc.nodes, id), () => ( IDENTITY_MATRIX));
1931
+ }
1932
+ /** A document point in node `id`'s own space. */
1933
+ toLocal(id, p, doc = this.doc) {
1934
+ return applyMatrix(invertMatrix(this.worldOf(id, doc)), p);
1052
1935
  }
1053
1936
  cursorFor() {
1054
- if (_optionalChain([this, 'access', _21 => _21.drag, 'optionalAccess', _22 => _22.kind]) === "pan" || this.spaceHeld) return this.drag ? "grabbing" : "grab";
1055
- if (_optionalChain([this, 'access', _23 => _23.drag, 'optionalAccess', _24 => _24.kind]) === "rotate") return "grabbing";
1056
- if (_optionalChain([this, 'access', _25 => _25.drag, 'optionalAccess', _26 => _26.kind]) === "move") return "move";
1937
+ if (_optionalChain([this, 'access', _40 => _40.drag, 'optionalAccess', _41 => _41.kind]) === "pan" || this.spaceHeld) return this.drag ? "grabbing" : "grab";
1938
+ if (_optionalChain([this, 'access', _42 => _42.drag, 'optionalAccess', _43 => _43.kind]) === "rotate") return "grabbing";
1939
+ if (_optionalChain([this, 'access', _44 => _44.drag, 'optionalAccess', _45 => _45.kind]) === "move") return "move";
1057
1940
  const h = this.hover;
1058
1941
  switch (this.tool) {
1059
1942
  case "select":
1060
- if (_optionalChain([h, 'optionalAccess', _27 => _27.transformHandle])) return RESIZE_CURSORS[h.transformHandle];
1061
- return _optionalChain([h, 'optionalAccess', _28 => _28.nodeId]) ? "move" : "default";
1943
+ 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)));
1944
+ return _optionalChain([h, 'optionalAccess', _50 => _50.nodeId]) ? "move" : "default";
1062
1945
  case "direct":
1063
- if (_optionalChain([h, 'optionalAccess', _29 => _29.anchor]) || _optionalChain([h, 'optionalAccess', _30 => _30.handle])) return "pointer";
1064
- if (_optionalChain([h, 'optionalAccess', _31 => _31.segment])) return "copy";
1065
- return _optionalChain([h, 'optionalAccess', _32 => _32.nodeId]) ? "move" : "default";
1946
+ if (_optionalChain([h, 'optionalAccess', _51 => _51.anchor]) || _optionalChain([h, 'optionalAccess', _52 => _52.handle])) return "pointer";
1947
+ if (_optionalChain([h, 'optionalAccess', _53 => _53.segment])) return "copy";
1948
+ return _optionalChain([h, 'optionalAccess', _54 => _54.nodeId]) ? "move" : "default";
1066
1949
  case "pen":
1067
- return _optionalChain([h, 'optionalAccess', _33 => _33.anchor]) ? "pointer" : "crosshair";
1950
+ return _optionalChain([h, 'optionalAccess', _55 => _55.anchor]) ? "pointer" : "crosshair";
1068
1951
  default:
1069
1952
  return "crosshair";
1070
1953
  }
@@ -1084,7 +1967,7 @@ var VectorEditorController = (_class = class {
1084
1967
  this.selection = s.selection.filter((id) => this.getNode(id));
1085
1968
  this.anchors = s.anchors.filter((r) => getAnchor(this.doc, r));
1086
1969
  this.editingId = s.editingId && this.getNode(s.editingId) ? s.editingId : null;
1087
- this.pen = s.pen && _optionalChain([this, 'access', _34 => _34.getNode, 'call', _35 => _35(s.pen.nodeId), 'optionalAccess', _36 => _36.subpaths, 'optionalAccess', _37 => _37[s.pen.subpath]]) ? s.pen : null;
1970
+ 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;
1088
1971
  } finally {
1089
1972
  this.applying = false;
1090
1973
  }
@@ -1189,6 +2072,7 @@ var VectorEditorController = (_class = class {
1189
2072
  this.cancelDrag();
1190
2073
  this.tool = tool;
1191
2074
  this.hover = null;
2075
+ this.guides = null;
1192
2076
  if (tool !== "direct") {
1193
2077
  this.anchors = [];
1194
2078
  this.editingId = null;
@@ -1201,18 +2085,74 @@ var VectorEditorController = (_class = class {
1201
2085
  }
1202
2086
  setSnap(snap) {
1203
2087
  this.snap = { ...this.snap, ...snap };
2088
+ if (!this.snap.guides || !this.snap.objects) this.guides = null;
1204
2089
  this.notify();
1205
2090
  }
1206
- snapPoint(p) {
1207
- const g = this.snap.grid;
1208
- if (!g || g <= 0) return p;
1209
- return { x: Math.round(p.x / g) * g, y: Math.round(p.y / g) * g };
2091
+ /** Turns object snapping on or off (the toolbar's snapping toggle). */
2092
+ toggleSnapping(on = !this.snap.objects) {
2093
+ this.setSnap({ objects: on });
1210
2094
  }
1211
- snapDelta(dx, dy, origin) {
2095
+ gridValue(v) {
1212
2096
  const g = this.snap.grid;
1213
- if (!g || g <= 0 || !origin) return { dx, dy };
1214
- const target = this.snapPoint({ x: origin.x + dx, y: origin.y + dy });
1215
- return { dx: target.x - origin.x, dy: target.y - origin.y };
2097
+ return g > 0 ? Math.round(v / g) * g : v;
2098
+ }
2099
+ snapPoint(p) {
2100
+ return { x: this.gridValue(p.x), y: this.gridValue(p.y) };
2101
+ }
2102
+ /** The pull distance in document units. */
2103
+ snapTolerance() {
2104
+ return this.snap.threshold / this.viewport.zoom;
2105
+ }
2106
+ setGuides(guides) {
2107
+ this.guides = guides && this.snap.guides && (guides.lines.length || guides.labels.length) ? guides : null;
2108
+ }
2109
+ /**
2110
+ * Where a drawn or dragged point `p` goes: onto the smart guide `targets` (gathered by
2111
+ * `gather` when missing), else onto the grid, kept to `leave`'s constraint. Cmd / Ctrl
2112
+ * bypasses snapping.
2113
+ */
2114
+ snapDrawn(p, leave, input, targets) {
2115
+ this.snappedToObject = false;
2116
+ if (input.mod) {
2117
+ this.setGuides(null);
2118
+ return constrainToLeave(leave, p);
2119
+ }
2120
+ if (this.snap.objects) {
2121
+ const withAngles = leave ? { ...leave, angles: this.snap.angles } : null;
2122
+ const res = targets().snapFrom(p, withAngles, this.snapTolerance());
2123
+ if (res.kind) {
2124
+ this.snappedToObject = true;
2125
+ this.setGuides(res);
2126
+ 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) };
2127
+ }
2128
+ }
2129
+ this.setGuides(null);
2130
+ return constrainToLeave(leave, this.snapPoint(p));
2131
+ }
2132
+ /** Drawing targets for the pen and shape tools, reused while the document stays the same. */
2133
+ drawingTargets(exclude) {
2134
+ const key = _nullishCoalesce(exclude, () => ( ""));
2135
+ const cached = this.hoverTargets;
2136
+ if (cached && cached.doc === this.doc && cached.exclude === key) return cached.targets;
2137
+ const targets = VectorSnapTargets.forDrawing(this.doc, exclude ? [exclude] : []);
2138
+ this.hoverTargets = { doc: this.doc, exclude: key, targets };
2139
+ return targets;
2140
+ }
2141
+ /** Where the pointer at `p` would place a point with the pen or a shape tool (and the guides showing why). */
2142
+ hoverSnap(p, input) {
2143
+ if (this.tool === "pen") {
2144
+ 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;
2145
+ if (this.pen && sp && _optionalChain([this, 'access', _65 => _65.hover, 'optionalAccess', _66 => _66.anchor])) {
2146
+ this.setGuides(null);
2147
+ return p;
2148
+ }
2149
+ const last = this.pen && sp ? getAnchorWorld(this.doc, { nodeId: this.pen.nodeId, subpath: this.pen.subpath, index: sp.anchors.length - 1 }) : void 0;
2150
+ const leave = last ? { at: { x: last.x, y: last.y }, constrain: input.shift ? "angle" : null } : null;
2151
+ return this.snapDrawn(p, leave, input, () => this.drawingTargets(_nullishCoalesce(_optionalChain([this, 'access', _67 => _67.pen, 'optionalAccess', _68 => _68.nodeId]), () => ( null))));
2152
+ }
2153
+ if (this.tool !== "select" && this.tool !== "direct" && this.tool !== "pencil") return this.snapDrawn(p, null, input, () => this.drawingTargets(null));
2154
+ this.setGuides(null);
2155
+ return p;
1216
2156
  }
1217
2157
  // -------------------------------------------------------------------------
1218
2158
  // Document
@@ -1443,10 +2383,10 @@ var VectorEditorController = (_class = class {
1443
2383
  return this.selection.filter((id) => {
1444
2384
  let covered = false;
1445
2385
  const loc = locateVectorNode(this.doc.nodes, id);
1446
- let parent = _nullishCoalesce(_optionalChain([loc, 'optionalAccess', _38 => _38.parent]), () => ( null));
2386
+ let parent = _nullishCoalesce(_optionalChain([loc, 'optionalAccess', _69 => _69.parent]), () => ( null));
1447
2387
  while (parent) {
1448
2388
  if (this.selection.includes(parent.id)) covered = true;
1449
- parent = _nullishCoalesce(_optionalChain([locateVectorNode, 'call', _39 => _39(this.doc.nodes, parent.id), 'optionalAccess', _40 => _40.parent]), () => ( null));
2389
+ parent = _nullishCoalesce(_optionalChain([locateVectorNode, 'call', _70 => _70(this.doc.nodes, parent.id), 'optionalAccess', _71 => _71.parent]), () => ( null));
1450
2390
  }
1451
2391
  return !covered;
1452
2392
  });
@@ -1454,6 +2394,11 @@ var VectorEditorController = (_class = class {
1454
2394
  // -------------------------------------------------------------------------
1455
2395
  // Transforms
1456
2396
  // -------------------------------------------------------------------------
2397
+ /** `m`, given in a frame rotated by `angle`, as a document-space matrix. */
2398
+ inFrame(angle, m) {
2399
+ if (!angle) return m;
2400
+ return multiplyMatrices(multiplyMatrices(rotateMatrix(-angle), m), rotateMatrix(angle));
2401
+ }
1457
2402
  /** Applies a matrix to the selected (or given) nodes as one undo step. */
1458
2403
  transform(matrix, options = {}) {
1459
2404
  const ids = new Set(_nullishCoalesce(options.ids, () => ( this.selectedRoots())));
@@ -1463,8 +2408,25 @@ var VectorEditorController = (_class = class {
1463
2408
  });
1464
2409
  return { ok: true };
1465
2410
  }
2411
+ /** `doc` with nodes `ids` moved by `m`, given in document space. */
1466
2412
  transformed(doc, ids, m) {
1467
- const nodes = mapTree(doc.nodes, (n) => ids.has(n.id) && !n.locked ? transformVectorNode(n, m) : n);
2413
+ const visit = (list, parent) => {
2414
+ let changed = false;
2415
+ const out = list.map((n) => {
2416
+ let next = n;
2417
+ if (ids.has(n.id) && !n.locked) {
2418
+ const local = isRoot(parent) ? m : multiplyMatrices(multiplyMatrices(parent, m), invertMatrix(parent));
2419
+ next = transformVectorNode(n, local);
2420
+ } else if (n.type === "group") {
2421
+ const children = visit(n.children, multiplyMatrices(nodeMatrix(n), parent));
2422
+ if (children !== n.children) next = { ...n, children };
2423
+ }
2424
+ if (next !== n) changed = true;
2425
+ return next;
2426
+ });
2427
+ return changed ? out : list;
2428
+ };
2429
+ const nodes = visit(doc.nodes, IDENTITY_MATRIX);
1468
2430
  return nodes === doc.nodes ? doc : { ...doc, nodes };
1469
2431
  }
1470
2432
  translate(dx, dy, options = {}) {
@@ -1476,26 +2438,48 @@ var VectorEditorController = (_class = class {
1476
2438
  }
1477
2439
  return this.transform(translateMatrix(dx, dy), { ids: options.ids, label: "Move" });
1478
2440
  }
1479
- /** Scales around `origin` (default: the selection centre). */
2441
+ /** The selection frame's centre in document space. */
2442
+ frameCenter(frame) {
2443
+ return applyMatrix(rotateMatrix(frame.angle), { x: frame.x + frame.width / 2, y: frame.y + frame.height / 2 });
2444
+ }
2445
+ /**
2446
+ * Scales along the selection box's own axes (rotated with the selection when its nodes
2447
+ * share a rotation) around `origin` (default: the box centre, in document space).
2448
+ */
1480
2449
  scale(sx, sy = sx, options = {}) {
1481
- const b = this.boundsOf(_nullishCoalesce(options.ids, () => ( this.selectedRoots())));
1482
- if (!b) return { ok: false, message: "Nothing is selected." };
1483
- const origin = _nullishCoalesce(options.origin, () => ( { x: b.x + b.width / 2, y: b.y + b.height / 2 }));
1484
- return this.transform(scaleMatrix(sx, sy, origin), { ids: options.ids, label: "Scale" });
2450
+ const frame = this.frameOf(_nullishCoalesce(options.ids, () => ( this.selectedRoots())));
2451
+ if (!frame) return { ok: false, message: "Nothing is selected." };
2452
+ const origin = applyMatrix(rotateMatrix(-frame.angle), _nullishCoalesce(options.origin, () => ( this.frameCenter(frame))));
2453
+ return this.transform(this.inFrame(frame.angle, scaleMatrix(sx, sy, origin)), { ids: options.ids, label: "Scale" });
1485
2454
  }
1486
2455
  /** Rotates clockwise by `degrees` around `origin` (default: the selection centre). */
1487
2456
  rotate(degrees, options = {}) {
1488
- const b = this.boundsOf(_nullishCoalesce(options.ids, () => ( this.selectedRoots())));
1489
- if (!b) return { ok: false, message: "Nothing is selected." };
1490
- const origin = _nullishCoalesce(options.origin, () => ( { x: b.x + b.width / 2, y: b.y + b.height / 2 }));
2457
+ const frame = this.frameOf(_nullishCoalesce(options.ids, () => ( this.selectedRoots())));
2458
+ if (!frame) return { ok: false, message: "Nothing is selected." };
2459
+ const origin = _nullishCoalesce(options.origin, () => ( this.frameCenter(frame)));
1491
2460
  return this.transform(rotateMatrix(degrees, origin), { ids: options.ids, label: "Rotate" });
1492
2461
  }
2462
+ /** Sets each selected (or given) node's rotation to `degrees`, turning it around its own centre. */
2463
+ setRotation(degrees, options = {}) {
2464
+ const ids = _nullishCoalesce(options.ids, () => ( this.selectedRoots()));
2465
+ if (!ids.length) return { ok: false, message: "Nothing is selected." };
2466
+ this.change("Set Rotation", () => {
2467
+ for (const id of ids) {
2468
+ const frame = this.frameOf([id]);
2469
+ if (!frame) continue;
2470
+ const delta = normalizeAngle(degrees - frame.angle);
2471
+ if (Math.abs(delta) > 1e-9) this.doc = this.transformed(this.doc, /* @__PURE__ */ new Set([id]), rotateMatrix(delta, this.frameCenter(frame)));
2472
+ }
2473
+ });
2474
+ return { ok: true };
2475
+ }
2476
+ /** Mirrors along the selection box's own axes. */
1493
2477
  flip(axis, options = {}) {
1494
- const b = this.boundsOf(_nullishCoalesce(options.ids, () => ( this.selectedRoots())));
1495
- if (!b) return { ok: false, message: "Nothing is selected." };
1496
- const c = { x: b.x + b.width / 2, y: b.y + b.height / 2 };
2478
+ const frame = this.frameOf(_nullishCoalesce(options.ids, () => ( this.selectedRoots())));
2479
+ if (!frame) return { ok: false, message: "Nothing is selected." };
2480
+ const c = { x: frame.x + frame.width / 2, y: frame.y + frame.height / 2 };
1497
2481
  const m = axis === "horizontal" ? scaleMatrix(-1, 1, c) : scaleMatrix(1, -1, c);
1498
- return this.transform(m, { ids: options.ids, label: axis === "horizontal" ? "Flip Horizontal" : "Flip Vertical" });
2482
+ return this.transform(this.inFrame(frame.angle, m), { ids: options.ids, label: axis === "horizontal" ? "Flip Horizontal" : "Flip Vertical" });
1499
2483
  }
1500
2484
  /**
1501
2485
  * Aligns the selected nodes to their combined bounds, or to the artboard when one node is
@@ -1585,8 +2569,8 @@ var VectorEditorController = (_class = class {
1585
2569
  if (!ids.length) return null;
1586
2570
  const locs = ids.map((id2) => locateVectorNode(this.doc.nodes, id2));
1587
2571
  if (locs.some((l) => !l)) return null;
1588
- const parent = _nullishCoalesce(_optionalChain([locs, 'access', _41 => _41[0], 'access', _42 => _42.parent, 'optionalAccess', _43 => _43.id]), () => ( null));
1589
- if (locs.some((l) => (_nullishCoalesce(_optionalChain([l, 'access', _44 => _44.parent, 'optionalAccess', _45 => _45.id]), () => ( null))) !== parent)) return null;
2572
+ const parent = _nullishCoalesce(_optionalChain([locs, 'access', _72 => _72[0], 'access', _73 => _73.parent, 'optionalAccess', _74 => _74.id]), () => ( null));
2573
+ if (locs.some((l) => (_nullishCoalesce(_optionalChain([l, 'access', _75 => _75.parent, 'optionalAccess', _76 => _76.id]), () => ( null))) !== parent)) return null;
1590
2574
  const id = this.newId();
1591
2575
  this.change("Group", () => {
1592
2576
  const set = new Set(ids);
@@ -1609,7 +2593,7 @@ var VectorEditorController = (_class = class {
1609
2593
  }
1610
2594
  /** Replaces selected groups with their children. */
1611
2595
  ungroup(options = {}) {
1612
- const groups = (_nullishCoalesce(options.ids, () => ( this.selection))).map((id) => this.getNode(id)).filter((n) => _optionalChain([n, 'optionalAccess', _46 => _46.type]) === "group");
2596
+ const groups = (_nullishCoalesce(options.ids, () => ( this.selection))).map((id) => this.getNode(id)).filter((n) => _optionalChain([n, 'optionalAccess', _77 => _77.type]) === "group");
1613
2597
  if (!groups.length) return { ok: false, message: "Select a group to ungroup." };
1614
2598
  const set = new Set(groups.map((g) => g.id));
1615
2599
  this.change("Ungroup", () => {
@@ -1620,7 +2604,10 @@ var VectorEditorController = (_class = class {
1620
2604
  if (set.has(n.id) && n.type === "group") {
1621
2605
  changed = true;
1622
2606
  const opacity = n.opacity;
1623
- out.push(...n.children.map((c) => opacity !== void 0 && opacity < 1 ? { ...c, opacity: (_nullishCoalesce(c.opacity, () => ( 1))) * opacity } : c));
2607
+ const m = nodeMatrix(n);
2608
+ out.push(
2609
+ ...n.children.map((c) => n.transform ? transformVectorNode(c, m) : c).map((c) => opacity !== void 0 && opacity < 1 ? { ...c, opacity: (_nullishCoalesce(c.opacity, () => ( 1))) * opacity } : c)
2610
+ );
1624
2611
  } else if (n.type === "group") {
1625
2612
  const children = flatten(n.children);
1626
2613
  if (children !== n.children) changed = true;
@@ -1666,17 +2653,24 @@ var VectorEditorController = (_class = class {
1666
2653
  });
1667
2654
  const paths = [...new Set(order)].map((id2) => this.getNode(id2));
1668
2655
  if (paths.length < 2) return null;
1669
- const result = _chunkM4XE4UMTcjs.pathBoolean.call(void 0,
2656
+ const result = _chunkUG5DVXJKcjs.pathBoolean.call(void 0,
1670
2657
  BOOLEAN_OPS[op],
1671
- paths.map((p) => ({ path: subpathsToBezier(p.subpaths.filter((s) => s.anchors.length > 1).map((s) => ({ ...s, closed: true }))), fillRule: _nullishCoalesce(p.fillRule, () => ( "nonzero")) }))
2658
+ paths.map((p) => ({
2659
+ path: vectorPathWorldBezier(
2660
+ { ...p, subpaths: p.subpaths.filter((s) => s.anchors.length > 1).map((s) => ({ ...s, closed: true })) },
2661
+ _nullishCoalesce(vectorParentMatrix(this.doc.nodes, p.id), () => ( IDENTITY_MATRIX))
2662
+ ),
2663
+ fillRule: _nullishCoalesce(p.fillRule, () => ( "nonzero"))
2664
+ }))
1672
2665
  );
1673
- const subpaths = bezierToSubpaths(result.path);
1674
- if (result.error || !subpaths.length) return null;
1675
2666
  const base = paths[0];
2667
+ const anchorRoot = _nullishCoalesce(roots.find((r) => findVectorNode([this.getNode(r)], base.id)), () => ( base.id));
2668
+ const subpaths = bezierToSubpaths(this.intoParentSpace(anchorRoot, result.path));
2669
+ if (result.error || !subpaths.length) return null;
1676
2670
  const id = this.newId();
1677
- const node = { ...cloneVectorNode(base), id, name: BOOLEAN_LABELS[op], subpaths };
2671
+ const { transform: _t, ...baseRest } = cloneVectorNode(base);
2672
+ const node = { ...baseRest, id, name: BOOLEAN_LABELS[op], subpaths };
1678
2673
  const drop = /* @__PURE__ */ new Set([...paths.map((p) => p.id), ...roots]);
1679
- const anchorRoot = _nullishCoalesce(roots.find((r) => findVectorNode([this.getNode(r)], base.id)), () => ( base.id));
1680
2674
  this.change(BOOLEAN_LABELS[op], () => {
1681
2675
  this.setDoc(mapTree(this.doc.nodes, (n) => n.id === anchorRoot ? node : drop.has(n.id) ? null : n));
1682
2676
  this.selection = [id];
@@ -1694,9 +2688,11 @@ var VectorEditorController = (_class = class {
1694
2688
  });
1695
2689
  if (paths.length < 2) return null;
1696
2690
  const id = this.newId();
1697
- const node = { ...cloneVectorNode(paths[0]), id, name: "Compound Path", subpaths: paths.flatMap((p) => cloneVectorNode(p).subpaths) };
1698
- const drop = /* @__PURE__ */ new Set([...paths.map((p) => p.id), ...roots]);
1699
2691
  const first = _nullishCoalesce(roots.find((r) => findVectorNode([this.getNode(r)], paths[0].id)), () => ( paths[0].id));
2692
+ const subpaths = paths.flatMap((p) => bezierToSubpaths(this.intoParentSpace(first, vectorPathWorldBezier(p, _nullishCoalesce(vectorParentMatrix(this.doc.nodes, p.id), () => ( IDENTITY_MATRIX))))));
2693
+ const { transform: _t, ...baseRest } = cloneVectorNode(paths[0]);
2694
+ const node = { ...baseRest, id, name: "Compound Path", subpaths };
2695
+ const drop = /* @__PURE__ */ new Set([...paths.map((p) => p.id), ...roots]);
1700
2696
  this.change("Combine", () => {
1701
2697
  this.setDoc(mapTree(this.doc.nodes, (n) => n.id === first ? node : drop.has(n.id) ? null : n));
1702
2698
  this.selection = [id];
@@ -1704,9 +2700,14 @@ var VectorEditorController = (_class = class {
1704
2700
  });
1705
2701
  return id;
1706
2702
  }
2703
+ /** A document-space path mapped into the parent space of node `id`. */
2704
+ intoParentSpace(id, path) {
2705
+ const parent = _nullishCoalesce(vectorParentMatrix(this.doc.nodes, id), () => ( IDENTITY_MATRIX));
2706
+ return isRoot(parent) ? path : _chunkUG5DVXJKcjs.transformBezierPath.call(void 0, path, invertMatrix(parent));
2707
+ }
1707
2708
  /** Splits selected compound paths into one path per subpath. */
1708
2709
  breakApart(options = {}) {
1709
- const paths = (_nullishCoalesce(options.ids, () => ( this.selection))).map((id) => this.getNode(id)).filter((n) => _optionalChain([n, 'optionalAccess', _47 => _47.type]) === "path" && n.subpaths.length > 1);
2710
+ 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);
1710
2711
  if (!paths.length) return { ok: false, message: "Select a compound path to break apart." };
1711
2712
  const created = [];
1712
2713
  const set = new Set(paths.map((p) => p.id));
@@ -1743,23 +2744,30 @@ var VectorEditorController = (_class = class {
1743
2744
  });
1744
2745
  return nodes === doc.nodes ? doc : { ...doc, nodes };
1745
2746
  }
2747
+ /** Moves anchors by a document-space offset (converted into each node's own space). */
1746
2748
  movedAnchors(doc, refs, dx, dy) {
1747
- return this.updateAnchors(doc, refs, (a) => translateAnchor(a, dx, dy));
2749
+ const local = /* @__PURE__ */ new Map();
2750
+ for (const r of refs) if (!local.has(r.nodeId)) local.set(r.nodeId, applyMatrixLinear(invertMatrix(this.worldOf(r.nodeId, doc)), { x: dx, y: dy }));
2751
+ return this.updateAnchors(doc, refs, (a, ref) => {
2752
+ const d = local.get(ref.nodeId);
2753
+ return translateAnchor(a, d.x, d.y);
2754
+ });
1748
2755
  }
1749
- /** Moves one anchor to a position (handles follow). */
2756
+ /** Moves one anchor to a document position (handles follow). */
1750
2757
  moveAnchor(ref, to) {
1751
- const a = getAnchor(this.doc, ref);
2758
+ const a = getAnchorWorld(this.doc, ref);
1752
2759
  if (!a) return { ok: false, message: "There is no such anchor." };
1753
2760
  this.change("Move Point", () => {
1754
2761
  this.doc = this.movedAnchors(this.doc, [ref], to.x - a.x, to.y - a.y);
1755
2762
  });
1756
2763
  return { ok: true };
1757
2764
  }
1758
- /** Moves one handle (see `moveAnchorHandle` for how the opposite handle follows). */
2765
+ /** Moves one handle to a document position (see `moveAnchorHandle` for how the opposite handle follows). */
1759
2766
  moveHandle(ref, to, options = {}) {
1760
2767
  if (!getAnchor(this.doc, ref)) return { ok: false, message: "There is no such anchor." };
2768
+ const local = this.toLocal(ref.nodeId, to);
1761
2769
  this.change("Move Handle", () => {
1762
- this.doc = this.updateAnchors(this.doc, [ref], (a) => moveAnchorHandle(a, ref.handle, to, options.breakHandles));
2770
+ this.doc = this.updateAnchors(this.doc, [ref], (a) => moveAnchorHandle(a, ref.handle, local, options.breakHandles));
1763
2771
  });
1764
2772
  return { ok: true };
1765
2773
  }
@@ -1778,11 +2786,11 @@ var VectorEditorController = (_class = class {
1778
2786
  });
1779
2787
  return { ok: true };
1780
2788
  }
1781
- /** Adds an anchor on the nearest segment of `nodeId` to `point` (shape unchanged). */
2789
+ /** Adds an anchor on the nearest segment of `nodeId` to document point `point` (shape unchanged). */
1782
2790
  insertAnchorAt(nodeId, point) {
1783
2791
  const node = this.getNode(nodeId);
1784
- if (_optionalChain([node, 'optionalAccess', _48 => _48.type]) !== "path") return null;
1785
- const hit = nearestSegment(node, point);
2792
+ if (_optionalChain([node, 'optionalAccess', _79 => _79.type]) !== "path") return null;
2793
+ const hit = nearestSegment(node, this.toLocal(nodeId, point));
1786
2794
  if (!hit) return null;
1787
2795
  const { subpath, index } = insertAnchor(node.subpaths[hit.subpath], hit.segment, hit.t);
1788
2796
  const ref = { nodeId, subpath: hit.subpath, index };
@@ -1816,7 +2824,7 @@ var VectorEditorController = (_class = class {
1816
2824
  setClosed(closed, nodeId) {
1817
2825
  const targets = nodeId ? (() => {
1818
2826
  const n = this.getNode(nodeId);
1819
- return _optionalChain([n, 'optionalAccess', _49 => _49.type]) === "path" ? n.subpaths.map((_, s) => ({ nodeId, subpath: s })) : [];
2827
+ return _optionalChain([n, 'optionalAccess', _80 => _80.type]) === "path" ? n.subpaths.map((_, s) => ({ nodeId, subpath: s })) : [];
1820
2828
  })() : this.anchors.map((r) => ({ nodeId: r.nodeId, subpath: r.subpath }));
1821
2829
  if (!targets.length) return { ok: false, message: "Select a path or points." };
1822
2830
  this.change(closed ? "Close Path" : "Open Path", () => {
@@ -1843,7 +2851,7 @@ var VectorEditorController = (_class = class {
1843
2851
  finishPath() {
1844
2852
  if (!this.pen) return;
1845
2853
  const node = this.getNode(this.pen.nodeId);
1846
- const sp = _optionalChain([node, 'optionalAccess', _50 => _50.subpaths, 'access', _51 => _51[this.pen.subpath]]);
2854
+ const sp = _optionalChain([node, 'optionalAccess', _81 => _81.subpaths, 'access', _82 => _82[this.pen.subpath]]);
1847
2855
  const pen = this.pen;
1848
2856
  this.pen = null;
1849
2857
  this.pointer = null;
@@ -1887,7 +2895,7 @@ var VectorEditorController = (_class = class {
1887
2895
  if (trimmed.startsWith("{")) {
1888
2896
  try {
1889
2897
  const data = JSON.parse(trimmed);
1890
- if (_optionalChain([data, 'optionalAccess', _52 => _52.type]) === VECTOR_CLIPBOARD_TYPE && Array.isArray(data.nodes)) nodes = data.nodes;
2898
+ if (_optionalChain([data, 'optionalAccess', _83 => _83.type]) === VECTOR_CLIPBOARD_TYPE && Array.isArray(data.nodes)) nodes = data.nodes;
1891
2899
  } catch (e2) {
1892
2900
  nodes = null;
1893
2901
  }
@@ -1899,7 +2907,7 @@ var VectorEditorController = (_class = class {
1899
2907
  }
1900
2908
  }
1901
2909
  } else nodes = this.clipboard;
1902
- if (!_optionalChain([nodes, 'optionalAccess', _53 => _53.length])) return [];
2910
+ if (!_optionalChain([nodes, 'optionalAccess', _84 => _84.length])) return [];
1903
2911
  const offset = _nullishCoalesce(options.offset, () => ( (text && !text.trim().startsWith("{") ? { x: 0, y: 0 } : { x: 10, y: 10 })));
1904
2912
  const copies = nodes.map((n) => transformVectorNode(this.freshIds(n), translateMatrix(offset.x, offset.y)));
1905
2913
  this.change("Paste", () => {
@@ -1939,20 +2947,18 @@ var VectorEditorController = (_class = class {
1939
2947
  }
1940
2948
  hitTransformHandle(screen) {
1941
2949
  if (this.tool !== "select" || !this.selection.length) return null;
1942
- const b = this.boundsOf(this.selectedRoots());
1943
- if (!b) return null;
1944
- const tl = this.documentToScreen(b);
1945
- const box = { x: tl.x, y: tl.y, width: b.width * this.viewport.zoom, height: b.height * this.viewport.zoom };
2950
+ const frame = this.frameOf(this.selectedRoots());
2951
+ if (!frame) return null;
2952
+ const { box, handles } = getVectorSelectionBoxGeometry(frame, this.viewport);
1946
2953
  const r = this.hitRadius + 2;
1947
- const handles = getTransformHandlePositions(box);
1948
2954
  const usable = box.width < 2 * r || box.height < 2 * r ? handles.filter((h) => h.handle.length === 2 || h.handle === "rotate") : handles;
1949
- for (const h of usable) if (Math.abs(h.x - screen.x) <= r && Math.abs(h.y - screen.y) <= r) return h.handle;
2955
+ for (const h of usable) if (Math.hypot(h.screen.x - screen.x, h.screen.y - screen.y) <= r * Math.SQRT2) return h.handle;
1950
2956
  return null;
1951
2957
  }
1952
2958
  hitHandle(screen) {
1953
2959
  for (const ref of getVisibleHandles(this.view()).reverse()) {
1954
- const a = getAnchor(this.doc, ref);
1955
- const h = _optionalChain([a, 'optionalAccess', _54 => _54[ref.handle]]);
2960
+ const a = getAnchorWorld(this.doc, ref);
2961
+ const h = _optionalChain([a, 'optionalAccess', _85 => _85[ref.handle]]);
1956
2962
  if (!h) continue;
1957
2963
  const s = this.documentToScreen(h);
1958
2964
  if (Math.hypot(s.x - screen.x, s.y - screen.y) <= this.hitRadius) return ref;
@@ -1963,7 +2969,7 @@ var VectorEditorController = (_class = class {
1963
2969
  let best = null;
1964
2970
  let bestD = Infinity;
1965
2971
  for (const ref of refs) {
1966
- const a = getAnchor(this.doc, ref);
2972
+ const a = getAnchorWorld(this.doc, ref);
1967
2973
  if (!a) continue;
1968
2974
  const s = this.documentToScreen(a);
1969
2975
  const d = Math.hypot(s.x - screen.x, s.y - screen.y);
@@ -1977,18 +2983,18 @@ var VectorEditorController = (_class = class {
1977
2983
  /** Deepest unlocked visible path under `p`. */
1978
2984
  hitPath(p) {
1979
2985
  const tol = this.tolerance();
1980
- const search = (list) => {
2986
+ const search = (list, parent) => {
1981
2987
  for (let i = list.length - 1; i >= 0; i--) {
1982
2988
  const n = list[i];
1983
2989
  if (n.visible === false || n.locked) continue;
1984
2990
  if (n.type === "group") {
1985
- const found = search(n.children);
2991
+ const found = search(n.children, multiplyMatrices(nodeMatrix(n), parent));
1986
2992
  if (found) return found;
1987
- } else if (hitTestVectorNode(n, p, tol)) return n;
2993
+ } else if (hitTestVectorNode(n, p, tol, parent)) return n;
1988
2994
  }
1989
2995
  return null;
1990
2996
  };
1991
- return search(this.doc.nodes);
2997
+ return search(this.doc.nodes, IDENTITY_MATRIX);
1992
2998
  }
1993
2999
  /** Endpoints of open subpaths of unlocked visible paths (pen tool resumes from them). */
1994
3000
  openEndpoints() {
@@ -2019,8 +3025,11 @@ var VectorEditorController = (_class = class {
2019
3025
  if (anchor) return { nodeId: anchor.nodeId, anchor };
2020
3026
  for (const id of getEditablePathIds(this.view())) {
2021
3027
  const node = this.getNode(id);
2022
- const seg = nearestSegment(node, p);
2023
- if (seg && seg.distance <= this.tolerance()) return { nodeId: id, segment: { nodeId: id, subpath: seg.subpath, segment: seg.segment, t: seg.t, point: seg.point } };
3028
+ const seg = nearestSegment(node, this.toLocal(id, p));
3029
+ if (seg && seg.distance <= this.tolerance()) {
3030
+ const point = applyMatrix(this.worldOf(id), seg.point);
3031
+ return { nodeId: id, segment: { nodeId: id, subpath: seg.subpath, segment: seg.segment, t: seg.t, point } };
3032
+ }
2024
3033
  }
2025
3034
  const path = this.hitPath(p);
2026
3035
  return path ? { nodeId: path.id } : null;
@@ -2059,15 +3068,16 @@ var VectorEditorController = (_class = class {
2059
3068
  case "pencil":
2060
3069
  return this.downPencil(p);
2061
3070
  default:
2062
- return this.downShape(p);
3071
+ return this.downShape(p, input);
2063
3072
  }
2064
3073
  }
2065
3074
  downSelect(screen, p, input) {
2066
3075
  const handle = this.hitTransformHandle(screen);
2067
3076
  if (handle) {
2068
- const bounds = this.boundsOf(this.selectedRoots());
3077
+ const frame = this.frameOf(this.selectedRoots());
2069
3078
  const before = this.snapshot();
2070
- this.drag = handle === "rotate" ? { kind: "rotate", start: p, center: { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 }, before, base: this.doc } : { kind: "scale", handle, start: p, bounds, before, base: this.doc };
3079
+ const center = applyMatrix(rotateMatrix(frame.angle), { x: frame.x + frame.width / 2, y: frame.y + frame.height / 2 });
3080
+ this.drag = handle === "rotate" ? { kind: "rotate", start: p, center, before, base: this.doc } : { kind: "scale", handle, start: p, frame, before, base: this.doc };
2071
3081
  this.interaction = handle === "rotate" ? { type: "rotate", angle: 0 } : { type: "scale", handle, sx: 1, sy: 1 };
2072
3082
  this.notify();
2073
3083
  return;
@@ -2127,13 +3137,13 @@ var VectorEditorController = (_class = class {
2127
3137
  return;
2128
3138
  }
2129
3139
  } else if (!selected) this.anchors = [anchor];
2130
- this.drag = { kind: "anchors", start: p, before: this.snapshot(), base: this.doc, moved: false, clickRef: selected && !input.shift ? anchor : null };
3140
+ this.drag = { kind: "anchors", start: p, before: this.snapshot(), base: this.doc, moved: false, clickRef: selected && !input.shift ? anchor : null, lead: anchor };
2131
3141
  this.notify();
2132
3142
  return;
2133
3143
  }
2134
3144
  for (const id of getEditablePathIds(this.view())) {
2135
3145
  const node = this.getNode(id);
2136
- const seg = nearestSegment(node, p);
3146
+ const seg = nearestSegment(node, this.toLocal(id, p));
2137
3147
  if (!seg || seg.distance > this.tolerance()) continue;
2138
3148
  const sp = node.subpaths[seg.subpath];
2139
3149
  if (input.alt) {
@@ -2143,7 +3153,7 @@ var VectorEditorController = (_class = class {
2143
3153
  const ref = { nodeId: id, subpath: seg.subpath, index };
2144
3154
  this.anchors = [ref];
2145
3155
  this.editingId = id;
2146
- this.drag = { kind: "anchors", start: p, before, base: this.doc, moved: true, clickRef: null };
3156
+ this.drag = { kind: "anchors", start: p, before, base: this.doc, moved: true, clickRef: null, lead: ref };
2147
3157
  this.notify(true);
2148
3158
  return;
2149
3159
  }
@@ -2151,7 +3161,7 @@ var VectorEditorController = (_class = class {
2151
3161
  const b = { nodeId: id, subpath: seg.subpath, index: (seg.segment + 1) % sp.anchors.length };
2152
3162
  this.anchors = input.shift ? [...this.anchors.filter((r) => !sameRef(r, a) && !sameRef(r, b)), a, b] : [a, b];
2153
3163
  this.editingId = id;
2154
- this.drag = { kind: "anchors", start: p, before: this.snapshot(), base: this.doc, moved: false, clickRef: null };
3164
+ this.drag = { kind: "anchors", start: p, before: this.snapshot(), base: this.doc, moved: false, clickRef: null, lead: null };
2155
3165
  this.notify();
2156
3166
  return;
2157
3167
  }
@@ -2161,7 +3171,7 @@ var VectorEditorController = (_class = class {
2161
3171
  if (!input.shift) this.selection = [path.id];
2162
3172
  else if (!this.selection.includes(path.id)) this.selection = [...this.selection, path.id];
2163
3173
  this.anchors = path.subpaths.flatMap((sp, s) => sp.anchors.map((_, i) => ({ nodeId: path.id, subpath: s, index: i })));
2164
- this.drag = { kind: "anchors", start: p, before: this.snapshot(), base: this.doc, moved: false, clickRef: null };
3174
+ this.drag = { kind: "anchors", start: p, before: this.snapshot(), base: this.doc, moved: false, clickRef: null, lead: null };
2165
3175
  this.notify();
2166
3176
  return;
2167
3177
  }
@@ -2174,7 +3184,7 @@ var VectorEditorController = (_class = class {
2174
3184
  }
2175
3185
  downPen(screen, p, input) {
2176
3186
  const before = this.snapshot();
2177
- const sp = this.pen ? _optionalChain([this, 'access', _55 => _55.getNode, 'call', _56 => _56(this.pen.nodeId), 'optionalAccess', _57 => _57.subpaths, 'access', _58 => _58[this.pen.subpath]]) : void 0;
3187
+ const sp = this.pen ? _optionalChain([this, 'access', _86 => _86.getNode, 'call', _87 => _87(this.pen.nodeId), 'optionalAccess', _88 => _88.subpaths, 'access', _89 => _89[this.pen.subpath]]) : void 0;
2178
3188
  if (this.pen && sp) {
2179
3189
  const pen = this.pen;
2180
3190
  const first = { nodeId: pen.nodeId, subpath: pen.subpath, index: 0 };
@@ -2186,9 +3196,10 @@ var VectorEditorController = (_class = class {
2186
3196
  this.notify(true);
2187
3197
  return;
2188
3198
  }
2189
- let pt2 = this.snapPoint(p);
2190
- const last = sp.anchors[sp.anchors.length - 1];
2191
- if (input.shift) pt2 = constrain45(last, pt2);
3199
+ const last = getAnchorWorld(this.doc, { nodeId: pen.nodeId, subpath: pen.subpath, index: sp.anchors.length - 1 });
3200
+ const leave = { at: { x: last.x, y: last.y }, constrain: input.shift ? "angle" : null };
3201
+ let pt2 = this.snapDrawn(p, leave, input, () => this.drawingTargets(pen.nodeId));
3202
+ pt2 = this.toLocal(pen.nodeId, pt2);
2192
3203
  const node2 = this.getNode(pen.nodeId);
2193
3204
  const anchors = [...sp.anchors, { x: pt2.x, y: pt2.y }];
2194
3205
  this.replaceNode(pen.nodeId, { ...node2, subpaths: node2.subpaths.map((s, i) => i === pen.subpath ? { ...s, anchors } : s) });
@@ -2218,7 +3229,7 @@ var VectorEditorController = (_class = class {
2218
3229
  this.notify(this.doc !== before.doc);
2219
3230
  return;
2220
3231
  }
2221
- const pt = this.snapPoint(p);
3232
+ const pt = this.snapDrawn(p, null, input, () => this.drawingTargets(null));
2222
3233
  const node = { id: this.newId(), type: "path", name: "Path", ...this.styleFor(true), subpaths: [{ anchors: [{ x: pt.x, y: pt.y }], closed: false }] };
2223
3234
  this.setDoc([...this.doc.nodes, node]);
2224
3235
  this.pen = { nodeId: node.id, subpath: 0 };
@@ -2246,13 +3257,14 @@ var VectorEditorController = (_class = class {
2246
3257
  this.interaction = { type: "draw", tool: "pencil", nodeId: node.id, bounds: { x: p.x, y: p.y, width: 0, height: 0 } };
2247
3258
  this.notify(true);
2248
3259
  }
2249
- downShape(p) {
3260
+ downShape(p, input) {
2250
3261
  const before = this.snapshot();
2251
- const start = this.snapPoint(p);
3262
+ const targets = this.drawingTargets(null);
3263
+ const start = this.snapDrawn(p, null, input, () => targets);
2252
3264
  const kind = this.tool;
2253
3265
  const node = this.createShapeNode({ kind, x: start.x, y: start.y, width: 0, height: 0 });
2254
3266
  this.setDoc([...this.doc.nodes, node]);
2255
- this.drag = { kind: "shape", tool: this.tool, start, nodeId: node.id, before };
3267
+ this.drag = { kind: "shape", tool: this.tool, start, nodeId: node.id, before, targets };
2256
3268
  this.interaction = { type: "draw", tool: this.tool, nodeId: node.id, bounds: { x: start.x, y: start.y, width: 0, height: 0 } };
2257
3269
  this.notify(true);
2258
3270
  }
@@ -2263,9 +3275,11 @@ var VectorEditorController = (_class = class {
2263
3275
  if (!drag) {
2264
3276
  const hover = this.computeHover(screen);
2265
3277
  const pointerMatters = this.tool === "pen" && !!this.pen;
2266
- if (this.sameHover(hover, this.hover) && !pointerMatters) return;
3278
+ const guides = this.guides;
2267
3279
  this.hover = hover;
2268
- this.pointer = p;
3280
+ const pointer = this.hoverSnap(p, input);
3281
+ if (this.sameHover(hover, this.state.hover) && !pointerMatters && JSON.stringify(guides) === JSON.stringify(this.guides)) return;
3282
+ this.pointer = pointer;
2269
3283
  this.notify();
2270
3284
  return;
2271
3285
  }
@@ -2286,17 +3300,26 @@ var VectorEditorController = (_class = class {
2286
3300
  const created = [];
2287
3301
  drag.base = this.withDuplicates(drag.base, this.selectedRoots(), { x: 0, y: 0 }, created);
2288
3302
  this.selection = created;
3303
+ drag.targets = void 0;
2289
3304
  }
2290
- ({ dx, dy } = this.snapDelta(dx, dy, drag.origin));
3305
+ ({ dx, dy } = this.snapMove(drag, dx, dy, input));
2291
3306
  this.doc = this.transformed(drag.base, new Set(this.selectedRoots()), translateMatrix(dx, dy));
2292
3307
  this.interaction = { type: "move", dx, dy };
2293
3308
  this.notify(true);
2294
3309
  return;
2295
3310
  }
2296
3311
  case "scale": {
2297
- const { sx, sy, origin } = scaleFor(drag.handle, drag.bounds, p, input);
3312
+ const angle = drag.frame.angle;
3313
+ const q = angle ? applyMatrix(rotateMatrix(-angle), p) : p;
3314
+ let { sx, sy, origin } = scaleFor(drag.handle, drag.frame, q, input);
3315
+ if (this.snap.objects && !input.mod && drag.handle !== "rotate") {
3316
+ drag.targets ??= VectorSnapTargets.forResize(drag.base, this.selectedRoots());
3317
+ const res = drag.targets.snapScale(drag.frame, drag.handle, sx, sy, input, this.snapTolerance());
3318
+ ({ sx, sy } = res);
3319
+ this.setGuides(res);
3320
+ } else this.setGuides(null);
2298
3321
  const ids = new Set(this.selectedRoots());
2299
- this.doc = this.transformed(drag.base, ids, scaleMatrix(sx, sy, origin));
3322
+ this.doc = this.transformed(drag.base, ids, this.inFrame(angle, scaleMatrix(sx, sy, origin)));
2300
3323
  this.interaction = { type: "scale", handle: drag.handle, sx, sy };
2301
3324
  this.notify(true);
2302
3325
  return;
@@ -2316,7 +3339,7 @@ var VectorEditorController = (_class = class {
2316
3339
  this.interaction = { type: "marquee", bounds };
2317
3340
  if (drag.anchorsMode) {
2318
3341
  const inside = getVisibleAnchors(this.view()).filter((r) => {
2319
- const a = getAnchor(this.doc, r);
3342
+ const a = getAnchorWorld(this.doc, r);
2320
3343
  return a.x >= bounds.x && a.x <= bounds.x + bounds.width && a.y >= bounds.y && a.y <= bounds.y + bounds.height;
2321
3344
  });
2322
3345
  this.anchors = [...drag.baseAnchors, ...inside.filter((r) => !drag.baseAnchors.some((b) => sameRef(b, r)))];
@@ -2335,35 +3358,41 @@ var VectorEditorController = (_class = class {
2335
3358
  let dy = p.y - drag.start.y;
2336
3359
  if (!drag.moved && Math.hypot(dx, dy) * this.viewport.zoom < 2) return;
2337
3360
  drag.moved = true;
2338
- if (input.shift) Math.abs(dx) > Math.abs(dy) ? dy = 0 : dx = 0;
2339
- const lead = this.anchors.length ? getAnchor(drag.base, this.anchors[0]) : void 0;
2340
- ({ dx, dy } = this.snapDelta(dx, dy, _nullishCoalesce(lead, () => ( null))));
3361
+ const lead = this.leadAnchor(drag);
3362
+ if (lead) {
3363
+ const leave = input.shift ? { at: lead, constrain: "axis", curves: false } : null;
3364
+ const q = this.snapDrawn({ x: lead.x + dx, y: lead.y + dy }, leave, input, () => drag.targets ??= VectorSnapTargets.forAnchorDrag(drag.base, this.anchors));
3365
+ dx = q.x - lead.x;
3366
+ dy = q.y - lead.y;
3367
+ } else if (input.shift) Math.abs(dx) > Math.abs(dy) ? dy = 0 : dx = 0;
2341
3368
  this.doc = this.movedAnchors(drag.base, this.anchors, dx, dy);
2342
3369
  this.interaction = { type: "anchors", dx, dy };
2343
3370
  this.notify(true);
2344
3371
  return;
2345
3372
  }
2346
3373
  case "handle": {
2347
- const anchor = getAnchor(drag.base, drag.ref);
2348
- let pt = this.snapPoint(p);
2349
- if (input.shift && anchor) pt = constrain45(anchor, pt);
2350
- this.doc = this.updateAnchors(drag.base, [drag.ref], (a) => moveAnchorHandle(a, drag.ref.handle, pt, input.alt));
3374
+ const anchor = getAnchorWorld(drag.base, drag.ref);
3375
+ const leave = anchor ? { at: { x: anchor.x, y: anchor.y }, constrain: input.shift ? "angle" : null } : null;
3376
+ const pt = this.snapDrawn(p, leave, input, () => drag.targets ??= VectorSnapTargets.forHandle(drag.base, drag.ref.nodeId));
3377
+ const local = this.toLocal(drag.ref.nodeId, pt, drag.base);
3378
+ this.doc = this.updateAnchors(drag.base, [drag.ref], (a) => moveAnchorHandle(a, drag.ref.handle, local, input.alt));
2351
3379
  this.interaction = { type: "handle", ref: drag.ref };
2352
3380
  this.notify(true);
2353
3381
  return;
2354
3382
  }
2355
3383
  case "pull": {
2356
- const anchor = getAnchor(drag.base, drag.ref);
3384
+ const anchor = getAnchorWorld(drag.base, drag.ref);
2357
3385
  if (!anchor) return;
2358
3386
  if (Math.hypot(p.x - anchor.x, p.y - anchor.y) * this.viewport.zoom < 2) {
2359
3387
  this.doc = drag.base;
2360
3388
  this.notify(true);
2361
3389
  return;
2362
3390
  }
3391
+ this.guides = null;
2363
3392
  let pt = p;
2364
3393
  if (input.shift) pt = constrain45(anchor, pt);
2365
3394
  const side = drag.closing ? "in" : "out";
2366
- const pos = drag.closing ? { x: 2 * anchor.x - pt.x, y: 2 * anchor.y - pt.y } : pt;
3395
+ const pos = this.toLocal(drag.ref.nodeId, drag.closing ? { x: 2 * anchor.x - pt.x, y: 2 * anchor.y - pt.y } : pt, drag.base);
2367
3396
  this.doc = this.updateAnchors(drag.base, [drag.ref], (a) => {
2368
3397
  if (drag.outOnly || drag.altBreaks && input.alt) return moveAnchorHandle(a, side, pos, true);
2369
3398
  return moveAnchorHandle({ ...a, type: "symmetric" }, side, pos);
@@ -2373,8 +3402,10 @@ var VectorEditorController = (_class = class {
2373
3402
  }
2374
3403
  case "shape": {
2375
3404
  const node = this.getNode(drag.nodeId);
2376
- if (_optionalChain([node, 'optionalAccess', _59 => _59.type]) !== "path") return;
2377
- let end = this.snapPoint(p);
3405
+ if (_optionalChain([node, 'optionalAccess', _90 => _90.type]) !== "path") return;
3406
+ const leave = { at: drag.start, constrain: input.shift ? drag.tool === "line" ? "angle" : "square" : null, curves: false };
3407
+ let end = this.snapDrawn(p, leave, input, () => drag.targets ??= this.drawingTargets(null));
3408
+ 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);
2378
3409
  let start = drag.start;
2379
3410
  let bounds;
2380
3411
  if (drag.tool === "line") {
@@ -2405,7 +3436,7 @@ var VectorEditorController = (_class = class {
2405
3436
  if (Math.hypot(p.x - last[0], p.y - last[1]) * this.viewport.zoom < 1.5) return;
2406
3437
  drag.points.push([p.x, p.y]);
2407
3438
  const node = this.getNode(drag.nodeId);
2408
- if (_optionalChain([node, 'optionalAccess', _60 => _60.type]) !== "path") return;
3439
+ if (_optionalChain([node, 'optionalAccess', _91 => _91.type]) !== "path") return;
2409
3440
  this.replaceNode(drag.nodeId, { ...node, subpaths: [{ anchors: drag.points.map(([x, y]) => ({ x, y })), closed: false }] });
2410
3441
  const xs = drag.points.map((q) => q[0]);
2411
3442
  const ys = drag.points.map((q) => q[1]);
@@ -2420,12 +3451,64 @@ var VectorEditorController = (_class = class {
2420
3451
  }
2421
3452
  }
2422
3453
  }
3454
+ /** Snaps a move of the selection by (dx, dy): its bounds line up with targets or space evenly, else go to the grid. */
3455
+ snapMove(drag, dx, dy, input) {
3456
+ const origin = drag.origin;
3457
+ if (input.mod || !origin) {
3458
+ this.setGuides(null);
3459
+ return { dx, dy };
3460
+ }
3461
+ const axes = { x: !input.shift || dx !== 0, y: !input.shift || dy !== 0 };
3462
+ let res = null;
3463
+ if (this.snap.objects) {
3464
+ drag.targets ??= VectorSnapTargets.forMove(drag.base, this.selectedRoots());
3465
+ res = drag.targets.snapRect({ ...origin, x: origin.x + dx, y: origin.y + dy }, this.snapTolerance(), { spacing: true, axes });
3466
+ }
3467
+ this.setGuides(res);
3468
+ return {
3469
+ dx: _optionalChain([res, 'optionalAccess', _92 => _92.x]) ? dx + res.dx : axes.x ? this.gridValue(origin.x + dx) - origin.x : dx,
3470
+ dy: _optionalChain([res, 'optionalAccess', _93 => _93.y]) ? dy + res.dy : axes.y ? this.gridValue(origin.y + dy) - origin.y : dy
3471
+ };
3472
+ }
3473
+ /** The anchor a point drag snaps by, where it started: the grabbed one, else the moving one nearest the press. */
3474
+ leadAnchor(drag) {
3475
+ const ref = drag.lead && this.anchors.some((a) => sameRef(a, drag.lead)) ? drag.lead : null;
3476
+ if (ref) {
3477
+ const a = getAnchorWorld(drag.base, ref);
3478
+ return a ? { x: a.x, y: a.y } : null;
3479
+ }
3480
+ let best = null;
3481
+ for (const r of this.anchors) {
3482
+ const a = getAnchorWorld(drag.base, r);
3483
+ 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 };
3484
+ }
3485
+ return best;
3486
+ }
3487
+ /** A shape's dragged corner moved so its width or height matches another object's, when one is within reach. */
3488
+ matchShapeSize(targets, start, end, fromCentre = false) {
3489
+ const k = fromCentre ? 2 : 1;
3490
+ const tol = this.snapTolerance() * k;
3491
+ const out = { ...end };
3492
+ const guides = { lines: [], labels: [] };
3493
+ for (const axis of ["x", "y"]) {
3494
+ const d = end[axis] - start[axis];
3495
+ const len = Math.abs(d) > 1e-9 ? targets.matchSize(Math.abs(d) * k, tol) : null;
3496
+ if (len === null || len <= 0) continue;
3497
+ out[axis] = start[axis] + Math.sign(d) * len / k;
3498
+ const from = { ...out, [axis]: fromCentre ? 2 * start[axis] - out[axis] : start[axis] };
3499
+ guides.lines.push({ kind: "size", x1: from.x, y1: from.y, x2: out.x, y2: out.y });
3500
+ 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}` });
3501
+ }
3502
+ this.setGuides(guides);
3503
+ return out;
3504
+ }
2423
3505
  /** Primary button released. */
2424
3506
  pointerUp(screen, input = {}) {
2425
3507
  if (screen && this.drag && this.drag.kind !== "pan") this.pointerMove(screen, input);
2426
3508
  const drag = this.drag;
2427
3509
  this.drag = null;
2428
3510
  this.interaction = null;
3511
+ this.guides = null;
2429
3512
  if (!drag) return;
2430
3513
  switch (drag.kind) {
2431
3514
  case "pan":
@@ -2472,12 +3555,12 @@ var VectorEditorController = (_class = class {
2472
3555
  }
2473
3556
  case "pencil": {
2474
3557
  const node = this.getNode(drag.nodeId);
2475
- if (_optionalChain([node, 'optionalAccess', _61 => _61.type]) !== "path" || drag.points.length < 2) {
3558
+ if (_optionalChain([node, 'optionalAccess', _94 => _94.type]) !== "path" || drag.points.length < 2) {
2476
3559
  this.doc = drag.before.doc;
2477
3560
  this.notify(true);
2478
3561
  return;
2479
3562
  }
2480
- const fitted = _chunkM4XE4UMTcjs.fitCubics.call(void 0, drag.points, 1.5 / this.viewport.zoom, { cornerAngleDeg: 60 });
3563
+ const fitted = _chunkUG5DVXJKcjs.fitCubics.call(void 0, drag.points, 1.5 / this.viewport.zoom, { cornerAngleDeg: 60 });
2481
3564
  const anchors = [];
2482
3565
  for (const c of fitted) {
2483
3566
  if (!anchors.length) anchors.push({ x: c.p0[0], y: c.p0[1] });
@@ -2494,9 +3577,10 @@ var VectorEditorController = (_class = class {
2494
3577
  /** The pointer left the canvas. */
2495
3578
  pointerLeave() {
2496
3579
  if (this.drag) return;
2497
- if (!this.hover && !this.pointer) return;
3580
+ if (!this.hover && !this.pointer && !this.guides) return;
2498
3581
  this.hover = null;
2499
3582
  this.pointer = null;
3583
+ this.guides = null;
2500
3584
  this.notify();
2501
3585
  }
2502
3586
  /** Abandons the drag in progress (Escape, lost capture). */
@@ -2505,6 +3589,7 @@ var VectorEditorController = (_class = class {
2505
3589
  if (!drag) return;
2506
3590
  this.drag = null;
2507
3591
  this.interaction = null;
3592
+ this.guides = null;
2508
3593
  if (drag.kind === "pan") {
2509
3594
  this.notify();
2510
3595
  return;
@@ -2537,10 +3622,10 @@ var VectorEditorController = (_class = class {
2537
3622
  this.convertAnchors(a.in || a.out ? "corner" : "smooth", [anchor]);
2538
3623
  return;
2539
3624
  }
2540
- const id = _nullishCoalesce(_optionalChain([this, 'access', _62 => _62.hover, 'optionalAccess', _63 => _63.segment, 'optionalAccess', _64 => _64.nodeId]), () => ( this.editingId));
3625
+ const id = _nullishCoalesce(_optionalChain([this, 'access', _95 => _95.hover, 'optionalAccess', _96 => _96.segment, 'optionalAccess', _97 => _97.nodeId]), () => ( this.editingId));
2541
3626
  if (id) {
2542
3627
  const node = this.getNode(id);
2543
- const seg = node ? nearestSegment(node, p) : null;
3628
+ const seg = node ? nearestSegment(node, this.toLocal(id, p)) : null;
2544
3629
  if (seg && seg.distance <= this.tolerance()) this.insertAnchorAt(id, p);
2545
3630
  }
2546
3631
  return;
@@ -2615,7 +3700,7 @@ var VectorEditorController = (_class = class {
2615
3700
  this.finishPath();
2616
3701
  return true;
2617
3702
  }
2618
- if (this.tool === "select" && this.selection.length === 1 && _optionalChain([this, 'access', _65 => _65.getNode, 'call', _66 => _66(this.selection[0]), 'optionalAccess', _67 => _67.type]) === "path") {
3703
+ if (this.tool === "select" && this.selection.length === 1 && _optionalChain([this, 'access', _98 => _98.getNode, 'call', _99 => _99(this.selection[0]), 'optionalAccess', _100 => _100.type]) === "path") {
2619
3704
  this.tool = "direct";
2620
3705
  this.editingId = this.selection[0];
2621
3706
  this.notify();
@@ -2660,7 +3745,7 @@ var VectorEditorController = (_class = class {
2660
3745
  this.notify();
2661
3746
  }
2662
3747
  }
2663
- }, _class);
3748
+ }, _class2);
2664
3749
  function constrain45(origin, p) {
2665
3750
  const dx = p.x - origin.x;
2666
3751
  const dy = p.y - origin.y;
@@ -2692,11 +3777,12 @@ function scaleFor(handle, b, p, mods = {}) {
2692
3777
 
2693
3778
  // src/vector/render.ts
2694
3779
  var fmt = (n) => Math.round(n * 100) / 100;
3780
+ var fmtMatrix = (m) => `matrix(${m.map((n) => Math.round(n * 1e6) / 1e6).join(" ")})`;
2695
3781
  function renderNode(node, defs, idPrefix) {
2696
3782
  if (node.visible === false) return null;
2697
3783
  if (node.type === "group") {
2698
3784
  const children = node.children.map((c) => renderNode(c, defs, idPrefix)).filter((c) => !!c);
2699
- return { kind: "group", id: node.id, opacity: _nullishCoalesce(node.opacity, () => ( 1)), children };
3785
+ return { kind: "group", id: node.id, ...node.transform ? { transform: fmtMatrix(node.transform) } : {}, opacity: _nullishCoalesce(node.opacity, () => ( 1)), children };
2700
3786
  }
2701
3787
  const fill = vectorPaintToSvg(node.fill, `${idPrefix}fill-${node.id}`);
2702
3788
  const stroke = vectorPaintToSvg(node.stroke, `${idPrefix}stroke-${node.id}`);
@@ -2705,13 +3791,14 @@ function renderNode(node, defs, idPrefix) {
2705
3791
  return {
2706
3792
  kind: "path",
2707
3793
  id: node.id,
3794
+ ...node.transform ? { transform: fmtMatrix(node.transform) } : {},
2708
3795
  d: vectorPathData(node),
2709
3796
  fill: fill.value,
2710
3797
  stroke: stroke.value,
2711
3798
  strokeWidth: _nullishCoalesce(node.strokeWidth, () => ( 1)),
2712
3799
  ...node.strokeLinecap ? { strokeLinecap: node.strokeLinecap } : {},
2713
3800
  ...node.strokeLinejoin ? { strokeLinejoin: node.strokeLinejoin } : {},
2714
- ..._optionalChain([node, 'access', _68 => _68.strokeDasharray, 'optionalAccess', _69 => _69.length]) ? { strokeDasharray: node.strokeDasharray.join(" ") } : {},
3801
+ ..._optionalChain([node, 'access', _101 => _101.strokeDasharray, 'optionalAccess', _102 => _102.length]) ? { strokeDasharray: node.strokeDasharray.join(" ") } : {},
2715
3802
  fillRule: _nullishCoalesce(node.fillRule, () => ( "nonzero")),
2716
3803
  opacity: _nullishCoalesce(node.opacity, () => ( 1))
2717
3804
  };
@@ -2733,8 +3820,14 @@ function getVectorSceneRenderData(state, options = {}) {
2733
3820
  var toScreen = (state, p) => ({ x: p.x * state.viewport.zoom + state.viewport.x, y: p.y * state.viewport.zoom + state.viewport.y });
2734
3821
  function screenOutline(state, node) {
2735
3822
  const v = state.viewport;
2736
- const paths = node.type === "path" ? [node] : collect(node);
2737
- return paths.map((p) => _chunkM4XE4UMTcjs.bezierPathToSvg.call(void 0, _chunkM4XE4UMTcjs.transformBezierPath.call(void 0, subpathsToBezier(p.subpaths), [v.zoom, 0, 0, v.zoom, v.x, v.y]), { precision: 2 })).join(" ");
3823
+ const view = [v.zoom, 0, 0, v.zoom, v.x, v.y];
3824
+ const parent = _nullishCoalesce(vectorParentMatrix(state.document.nodes, node.id), () => ( IDENTITY_MATRIX));
3825
+ return collectWithMatrix(node, parent).map(({ path, parent: m }) => _chunkUG5DVXJKcjs.bezierPathToSvg.call(void 0, _chunkUG5DVXJKcjs.transformBezierPath.call(void 0, vectorPathWorldBezier(path, m), view), { precision: 2 })).join(" ");
3826
+ }
3827
+ function collectWithMatrix(node, parent) {
3828
+ if (node.type === "path") return [{ path: node, parent }];
3829
+ const m = multiplyMatrices(nodeMatrix(node), parent);
3830
+ return node.children.flatMap((c) => collectWithMatrix(c, m));
2738
3831
  }
2739
3832
  function collect(node) {
2740
3833
  if (node.type === "path") return [node];
@@ -2754,40 +3847,38 @@ function getVectorOverlayRenderData(state) {
2754
3847
  };
2755
3848
  for (const id of state.selection) outline(id, true);
2756
3849
  if (state.editingId) outline(state.editingId, true);
2757
- if (_optionalChain([state, 'access', _70 => _70.hover, 'optionalAccess', _71 => _71.nodeId]) && !state.interaction) outline(state.hover.nodeId, false);
3850
+ if (_optionalChain([state, 'access', _103 => _103.hover, 'optionalAccess', _104 => _104.nodeId]) && !state.interaction) outline(state.hover.nodeId, false);
2758
3851
  let selectionBox = null;
2759
- if (state.tool === "select" && state.selectionBounds && _optionalChain([state, 'access', _72 => _72.interaction, 'optionalAccess', _73 => _73.type]) !== "marquee") {
2760
- const b = state.selectionBounds;
2761
- const tl = toScreen(state, b);
2762
- const box = { x: tl.x, y: tl.y, width: b.width * z, height: b.height * z };
2763
- selectionBox = { ...box, handles: getTransformHandlePositions(box) };
3852
+ if (state.tool === "select" && state.selectionFrame && _optionalChain([state, 'access', _105 => _105.interaction, 'optionalAccess', _106 => _106.type]) !== "marquee") {
3853
+ const g = getVectorSelectionBoxGeometry(state.selectionFrame, state.viewport);
3854
+ selectionBox = { ...g.box, angle: g.angle, transform: g.transform, corners: g.corners, handles: g.handles };
2764
3855
  }
2765
3856
  const selected = (r) => state.selectedAnchors.some((a) => a.nodeId === r.nodeId && a.subpath === r.subpath && a.index === r.index);
2766
3857
  const sameAnchor = (a, b) => !!a && a.nodeId === b.nodeId && a.subpath === b.subpath && a.index === b.index;
2767
3858
  const anchors = getVisibleAnchors(state).map((ref) => {
2768
- const a = getAnchor(doc, ref);
3859
+ const a = getAnchorWorld(doc, ref);
2769
3860
  const s = toScreen(state, a);
2770
3861
  const closes = !!state.pen && state.pen.nodeId === ref.nodeId && state.pen.subpath === ref.subpath && ref.index === 0;
2771
- return { ref, x: s.x, y: s.y, type: _nullishCoalesce(a.type, () => ( inferAnchorType(a))), selected: selected(ref), hovered: sameAnchor(_optionalChain([state, 'access', _74 => _74.hover, 'optionalAccess', _75 => _75.anchor]), ref), closes };
3862
+ return { ref, x: s.x, y: s.y, type: _nullishCoalesce(a.type, () => ( inferAnchorType(a))), selected: selected(ref), hovered: sameAnchor(_optionalChain([state, 'access', _107 => _107.hover, 'optionalAccess', _108 => _108.anchor]), ref), closes };
2772
3863
  });
2773
- if (state.tool === "pen" && !state.pen && _optionalChain([state, 'access', _76 => _76.hover, 'optionalAccess', _77 => _77.anchor])) {
2774
- const a = getAnchor(doc, state.hover.anchor);
3864
+ if (state.tool === "pen" && !state.pen && _optionalChain([state, 'access', _109 => _109.hover, 'optionalAccess', _110 => _110.anchor])) {
3865
+ const a = getAnchorWorld(doc, state.hover.anchor);
2775
3866
  if (a) {
2776
3867
  const s = toScreen(state, a);
2777
3868
  anchors.push({ ref: state.hover.anchor, x: s.x, y: s.y, type: _nullishCoalesce(a.type, () => ( "corner")), selected: false, hovered: true, closes: false });
2778
3869
  }
2779
3870
  }
2780
3871
  const handles = getVisibleHandles(state).flatMap((ref) => {
2781
- const a = getAnchor(doc, ref);
2782
- const h = _optionalChain([a, 'optionalAccess', _78 => _78[ref.handle]]);
3872
+ const a = getAnchorWorld(doc, ref);
3873
+ const h = _optionalChain([a, 'optionalAccess', _111 => _111[ref.handle]]);
2783
3874
  if (!a || !h) return [];
2784
3875
  const s = toScreen(state, h);
2785
3876
  const sa = toScreen(state, a);
2786
- const hovered = !!_optionalChain([state, 'access', _79 => _79.hover, 'optionalAccess', _80 => _80.handle]) && sameAnchor(state.hover.handle, ref) && state.hover.handle.handle === ref.handle;
3877
+ const hovered = !!_optionalChain([state, 'access', _112 => _112.hover, 'optionalAccess', _113 => _113.handle]) && sameAnchor(state.hover.handle, ref) && state.hover.handle.handle === ref.handle;
2787
3878
  return [{ ref, x: s.x, y: s.y, anchorX: sa.x, anchorY: sa.y, hovered }];
2788
3879
  });
2789
3880
  let marquee = null;
2790
- if (_optionalChain([state, 'access', _81 => _81.interaction, 'optionalAccess', _82 => _82.type]) === "marquee") {
3881
+ if (_optionalChain([state, 'access', _114 => _114.interaction, 'optionalAccess', _115 => _115.type]) === "marquee") {
2791
3882
  const b = state.interaction.bounds;
2792
3883
  const tl = toScreen(state, b);
2793
3884
  marquee = { x: tl.x, y: tl.y, width: b.width * z, height: b.height * z };
@@ -2795,8 +3886,8 @@ function getVectorOverlayRenderData(state) {
2795
3886
  let penPreview = null;
2796
3887
  if (state.tool === "pen" && state.pen && state.pointer && !state.interaction) {
2797
3888
  const node = findVectorNode(doc.nodes, state.pen.nodeId);
2798
- const sp = _optionalChain([node, 'optionalAccess', _83 => _83.type]) === "path" ? node.subpaths[state.pen.subpath] : void 0;
2799
- const last = _optionalChain([sp, 'optionalAccess', _84 => _84.anchors, 'access', _85 => _85[sp.anchors.length - 1]]);
3889
+ const sp = _optionalChain([node, 'optionalAccess', _116 => _116.type]) === "path" ? node.subpaths[state.pen.subpath] : void 0;
3890
+ const last = sp ? getAnchorWorld(doc, { nodeId: state.pen.nodeId, subpath: state.pen.subpath, index: sp.anchors.length - 1 }) : void 0;
2800
3891
  if (last) {
2801
3892
  const a = toScreen(state, last);
2802
3893
  const p = toScreen(state, state.pointer);
@@ -2808,16 +3899,30 @@ function getVectorOverlayRenderData(state) {
2808
3899
  const artboard = { x: origin.x, y: origin.y, width: doc.width * z, height: doc.height * z };
2809
3900
  let status = null;
2810
3901
  const it = state.interaction;
2811
- if (_optionalChain([it, 'optionalAccess', _86 => _86.type]) === "rotate") status = `${Math.round(it.angle)}\xB0`;
2812
- else if (_optionalChain([it, 'optionalAccess', _87 => _87.type]) === "move" || _optionalChain([it, 'optionalAccess', _88 => _88.type]) === "anchors") status = `\u0394 ${fmt(it.dx)}, ${fmt(it.dy)}`;
2813
- else if (_optionalChain([it, 'optionalAccess', _89 => _89.type]) === "scale" || _optionalChain([it, 'optionalAccess', _90 => _90.type]) === "draw") {
2814
- const b = it.type === "draw" ? findVectorNode(doc.nodes, it.nodeId) ? vectorNodeBounds(findVectorNode(doc.nodes, it.nodeId)) : null : state.selectionBounds;
3902
+ if (_optionalChain([it, 'optionalAccess', _117 => _117.type]) === "rotate") status = `${Math.round(it.angle)}\xB0`;
3903
+ else if (_optionalChain([it, 'optionalAccess', _118 => _118.type]) === "move" || _optionalChain([it, 'optionalAccess', _119 => _119.type]) === "anchors") status = `\u0394 ${fmt(it.dx)}, ${fmt(it.dy)}`;
3904
+ else if (_optionalChain([it, 'optionalAccess', _120 => _120.type]) === "scale" || _optionalChain([it, 'optionalAccess', _121 => _121.type]) === "draw") {
3905
+ const b = it.type === "draw" ? findVectorNode(doc.nodes, it.nodeId) ? vectorNodeBounds(findVectorNode(doc.nodes, it.nodeId)) : null : state.selectionFrame;
2815
3906
  if (b) status = `W ${fmt(b.width)} \xD7 H ${fmt(b.height)}`;
2816
3907
  }
2817
- return { outlines, selectionBox, anchors, handles, marquee, penPreview, artboard, status };
3908
+ const guides = { lines: [], labels: [] };
3909
+ if (state.guides) {
3910
+ for (const l of state.guides.lines) {
3911
+ const a = toScreen(state, { x: l.x1, y: l.y1 });
3912
+ const b = toScreen(state, { x: l.x2, y: l.y2 });
3913
+ guides.lines.push({ kind: l.kind, x1: fmt(a.x), y1: fmt(a.y), x2: fmt(b.x), y2: fmt(b.y) });
3914
+ }
3915
+ for (const l of state.guides.labels) {
3916
+ const p = toScreen(state, l);
3917
+ guides.labels.push({ kind: l.kind, x: fmt(p.x), y: fmt(p.y), text: l.text });
3918
+ }
3919
+ }
3920
+ return { outlines, selectionBox, anchors, handles, marquee, penPreview, artboard, guides, status };
2818
3921
  }
2819
3922
  var VECTOR_OVERLAY_THEME = {
2820
3923
  accent: "#0ea5e9",
3924
+ /** Smart guide lines and labels. */
3925
+ guide: "#ff3dfc",
2821
3926
  handleFill: "#ffffff",
2822
3927
  marqueeFill: "rgba(14, 165, 233, 0.08)",
2823
3928
  transformHandleSize: 8,
@@ -2838,7 +3943,7 @@ function isVectorTextTarget(target) {
2838
3943
  if (!el || typeof el !== "object") return false;
2839
3944
  const tag = (_nullishCoalesce(el.tagName, () => ( ""))).toUpperCase();
2840
3945
  if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
2841
- return !!el.isContentEditable || _optionalChain([el, 'access', _91 => _91.getAttribute, 'optionalCall', _92 => _92("contenteditable")]) === "true";
3946
+ return !!el.isContentEditable || _optionalChain([el, 'access', _122 => _122.getAttribute, 'optionalCall', _123 => _123("contenteditable")]) === "true";
2842
3947
  }
2843
3948
  function getVectorSelectionStyle(state) {
2844
3949
  const paths = [];
@@ -2897,23 +4002,25 @@ function createVectorCommands(controller, options = {}) {
2897
4002
  const base = { group, ...options.scope ? { scope: options.scope } : {} };
2898
4003
  const hasSelection = () => controller.getState().selection.length ? true : "Nothing is selected.";
2899
4004
  const ids = (params) => {
2900
- const list2 = _nullishCoalesce(_optionalChain([params, 'optionalAccess', _93 => _93.ids]), () => ( controller.getState().selection));
4005
+ const list2 = _nullishCoalesce(_optionalChain([params, 'optionalAccess', _124 => _124.ids]), () => ( controller.getState().selection));
2901
4006
  for (const id of list2) if (!controller.getNode(id)) fail(`There is no node "${id}".`);
2902
4007
  if (!list2.length) fail("Nothing is selected; pass ids.");
2903
4008
  return list2;
2904
4009
  };
2905
- const enabledFor = (_, params) => _optionalChain([params, 'optionalAccess', _94 => _94.ids, 'optionalAccess', _95 => _95.length]) ? true : hasSelection();
4010
+ const enabledFor = (_, params) => _optionalChain([params, 'optionalAccess', _125 => _125.ids, 'optionalAccess', _126 => _126.length]) ? true : hasSelection();
2906
4011
  const list = () => {
2907
4012
  const state = controller.getState();
2908
4013
  const out = [];
2909
4014
  const visit = (nodes, depth) => {
2910
4015
  for (const n of nodes) {
2911
- const b = vectorNodeBounds(n);
4016
+ const parent = _nullishCoalesce(vectorParentMatrix(state.document.nodes, n.id), () => ( IDENTITY_MATRIX));
4017
+ const b = vectorNodeBounds(n, parent);
2912
4018
  out.push({
2913
4019
  id: n.id,
2914
4020
  type: n.type,
2915
4021
  ...n.name ? { name: n.name } : {},
2916
4022
  depth,
4023
+ rotation: Math.round(vectorNodeRotation(n, parent) * 100) / 100,
2917
4024
  bounds: b && { x: Math.round(b.x * 100) / 100, y: Math.round(b.y * 100) / 100, width: Math.round(b.width * 100) / 100, height: Math.round(b.height * 100) / 100 },
2918
4025
  visible: n.visible !== false,
2919
4026
  locked: !!n.locked,
@@ -2987,7 +4094,7 @@ function createVectorCommands(controller, options = {}) {
2987
4094
  run: (_ctx, params) => {
2988
4095
  const id = controller.addPath(params.d, { ...pickStyle(params), ...params.name ? { name: params.name } : {} });
2989
4096
  const node = controller.getNode(id);
2990
- if (_optionalChain([node, 'optionalAccess', _96 => _96.type]) === "path" && !node.subpaths.length) {
4097
+ if (_optionalChain([node, 'optionalAccess', _127 => _127.type]) === "path" && !node.subpaths.length) {
2991
4098
  controller.undo();
2992
4099
  fail("The path data has no points.");
2993
4100
  }
@@ -3015,10 +4122,18 @@ function createVectorCommands(controller, options = {}) {
3015
4122
  id: `${prefix}transform`,
3016
4123
  label: "Transform\u2026",
3017
4124
  keywords: ["move", "translate", "scale", "resize", "rotate", "flip"],
3018
- description: "Moves (dx, dy), scales (sx, sy) and/or rotates (degrees clockwise) nodes around their combined centre, in that order.",
4125
+ description: "Moves (dx, dy), scales (sx, sy, along the selection box axes) and/or rotates (`rotate`: by degrees clockwise around the combined centre; `angle`: to an absolute rotation, each node around its own centre), in that order.",
3019
4126
  params: {
3020
4127
  type: "object",
3021
- properties: { ids: IDS, dx: { type: "number" }, dy: { type: "number" }, sx: { type: "number" }, sy: { type: "number" }, rotate: { type: "number" } }
4128
+ properties: {
4129
+ ids: IDS,
4130
+ dx: { type: "number" },
4131
+ dy: { type: "number" },
4132
+ sx: { type: "number" },
4133
+ sy: { type: "number" },
4134
+ rotate: { type: "number" },
4135
+ angle: { type: "number" }
4136
+ }
3022
4137
  },
3023
4138
  enabled: enabledFor,
3024
4139
  undoable: true,
@@ -3027,6 +4142,7 @@ function createVectorCommands(controller, options = {}) {
3027
4142
  if (params.dx || params.dy) check(controller.translate(_nullishCoalesce(params.dx, () => ( 0)), _nullishCoalesce(params.dy, () => ( 0)), { ids: target }));
3028
4143
  if (params.sx !== void 0 || params.sy !== void 0) check(controller.scale(_nullishCoalesce(params.sx, () => ( 1)), _nullishCoalesce(_nullishCoalesce(params.sy, () => ( params.sx)), () => ( 1)), { ids: target }));
3029
4144
  if (params.rotate) check(controller.rotate(params.rotate, { ids: target }));
4145
+ if (params.angle !== void 0) check(controller.setRotation(params.angle, { ids: target }));
3030
4146
  return { ok: true };
3031
4147
  }
3032
4148
  },
@@ -3111,7 +4227,7 @@ function createVectorCommands(controller, options = {}) {
3111
4227
  enabled: enabledFor,
3112
4228
  undoable: true,
3113
4229
  run: (_ctx, params) => {
3114
- const id = controller.group({ ids: ids(params), name: _optionalChain([params, 'optionalAccess', _97 => _97.name]) });
4230
+ const id = controller.group({ ids: ids(params), name: _optionalChain([params, 'optionalAccess', _128 => _128.name]) });
3115
4231
  if (!id) fail("Only nodes that share a parent can be grouped.");
3116
4232
  return { id };
3117
4233
  }
@@ -3150,7 +4266,7 @@ function createVectorCommands(controller, options = {}) {
3150
4266
  keywords: ["download", "save", "markup"],
3151
4267
  description: "Returns the document as SVG markup.",
3152
4268
  params: { type: "object", properties: { precision: { type: "integer", minimum: 0, maximum: 8 } } },
3153
- run: (_ctx, params) => ({ svg: controller.toSvg({ precision: _optionalChain([params, 'optionalAccess', _98 => _98.precision]) }) })
4269
+ run: (_ctx, params) => ({ svg: controller.toSvg({ precision: _optionalChain([params, 'optionalAccess', _129 => _129.precision]) }) })
3154
4270
  },
3155
4271
  {
3156
4272
  ...base,
@@ -3239,5 +4355,23 @@ function createVectorCommands(controller, options = {}) {
3239
4355
 
3240
4356
 
3241
4357
 
3242
- 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.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.ellipseSubpath = ellipseSubpath; exports.findVectorNode = findVectorNode; exports.getAnchor = getAnchor; exports.getEditablePathIds = getEditablePathIds; exports.getTransformHandlePositions = getTransformHandlePositions; exports.getVectorOverlayRenderData = getVectorOverlayRenderData; exports.getVectorSceneRenderData = getVectorSceneRenderData; 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.isVectorTextTarget = isVectorTextTarget; exports.lineSubpath = lineSubpath; exports.locateVectorNode = locateVectorNode; exports.moveAnchorHandle = moveAnchorHandle; exports.multiplyMatrices = multiplyMatrices; exports.nearestSegment = nearestSegment; 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.vectorNodeToBezier = vectorNodeToBezier; exports.vectorPaintToHex = vectorPaintToHex; exports.vectorPaintToSvg = vectorPaintToSvg; exports.vectorPathData = vectorPathData; exports.vectorToolLabel = vectorToolLabel; exports.walkVectorNodes = walkVectorNodes;
4358
+
4359
+
4360
+
4361
+
4362
+
4363
+
4364
+
4365
+
4366
+
4367
+
4368
+
4369
+
4370
+
4371
+
4372
+
4373
+
4374
+
4375
+
4376
+ exports.DEFAULT_VECTOR_SNAP = DEFAULT_VECTOR_SNAP; exports.IDENTITY_MATRIX = IDENTITY_MATRIX; exports.KAPPA = KAPPA; exports.VECTOR_CLIPBOARD_TYPE = VECTOR_CLIPBOARD_TYPE; exports.VECTOR_CONSTRUCTION_ANGLES = VECTOR_CONSTRUCTION_ANGLES; 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.VectorSnapTargets = VectorSnapTargets; 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.constrainToLeave = constrainToLeave; 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;
3243
4377
  //# sourceMappingURL=index.cjs.map