@pixerate/editor 0.32.0 → 0.33.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.
@@ -189,9 +189,20 @@ function collectPathNodes(node) {
189
189
  });
190
190
  return out;
191
191
  }
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));
192
+ function nodeMatrix(node) {
193
+ return _nullishCoalesce(node.transform, () => ( IDENTITY_MATRIX));
194
+ }
195
+ function vectorPathWorldBezier(node, parent = IDENTITY_MATRIX) {
196
+ const m = multiplyMatrices(nodeMatrix(node), parent);
197
+ const path = subpathsToBezier(node.subpaths);
198
+ return isIdentityMatrix(m) ? path : _chunkM4XE4UMTcjs.transformBezierPath.call(void 0, path, m);
199
+ }
200
+ function vectorNodeBounds(node, parent = IDENTITY_MATRIX) {
201
+ if (node.type === "group") {
202
+ const m = multiplyMatrices(nodeMatrix(node), parent);
203
+ return unionBounds(node.children.map((c) => vectorNodeBounds(c, m)));
204
+ }
205
+ const b = _chunkM4XE4UMTcjs.bezierPathBounds.call(void 0, vectorPathWorldBezier(node, parent));
195
206
  if (!b) return null;
196
207
  return { x: b.minX, y: b.minY, width: b.maxX - b.minX, height: b.maxY - b.minY };
197
208
  }
@@ -216,10 +227,13 @@ function boundsIntersect(a, b) {
216
227
  function boundsFromPoints(a, b) {
217
228
  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
229
  }
219
- function hitTestVectorNode(node, p, tolerance = 0) {
230
+ function hitTestVectorNode(node, p, tolerance = 0, parent = IDENTITY_MATRIX) {
220
231
  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);
232
+ if (node.type === "group") {
233
+ const m = multiplyMatrices(nodeMatrix(node), parent);
234
+ return node.children.some((c) => hitTestVectorNode(c, p, tolerance, m));
235
+ }
236
+ const path = vectorPathWorldBezier(node, parent);
223
237
  if (path.length === 0) return false;
224
238
  const point = [p.x, p.y];
225
239
  if (hasPaint(node.fill) && _chunkM4XE4UMTcjs.pointInBezierPath.call(void 0, path, point, _nullishCoalesce(node.fillRule, () => ( "nonzero")))) return true;
@@ -268,7 +282,38 @@ function rotateMatrix(degrees, origin = { x: 0, y: 0 }) {
268
282
  function applyMatrix(m, p) {
269
283
  return { x: m[0] * p.x + m[2] * p.y + m[4], y: m[1] * p.x + m[3] * p.y + m[5] };
270
284
  }
271
- function transformVectorNode(node, m) {
285
+ function isIdentityMatrix(m, eps = 1e-12) {
286
+ 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;
287
+ }
288
+ function invertMatrix(m) {
289
+ const [a, b, c, d, e, f] = m;
290
+ const det = a * d - b * c;
291
+ if (!det || !Number.isFinite(det)) return IDENTITY_MATRIX;
292
+ return [d / det, -b / det, -c / det, a / det, (c * f - d * e) / det, (b * e - a * f) / det];
293
+ }
294
+ function applyMatrixLinear(m, v) {
295
+ return { x: m[0] * v.x + m[2] * v.y, y: m[1] * v.x + m[3] * v.y };
296
+ }
297
+ function matrixRotation(m) {
298
+ const [a, b, c, d] = m;
299
+ const mirrored = a * d - b * c < 0;
300
+ const deg = (mirrored ? Math.atan2(-b - c, d - a) : Math.atan2(b - c, a + d)) * 180 / Math.PI;
301
+ return Math.abs(deg) < 1e-9 ? 0 : deg;
302
+ }
303
+ function decomposeRigid(m) {
304
+ const [a, b, c, d, e, f] = m;
305
+ const deg = matrixRotation(m);
306
+ if (deg === 0) return { rigid: IDENTITY_MATRIX, residual: m };
307
+ const r = deg * Math.PI / 180;
308
+ const cos = Math.cos(r);
309
+ const sin = Math.sin(r);
310
+ return {
311
+ rigid: [cos, sin, -sin, cos, e, f],
312
+ residual: [cos * a + sin * b, -sin * a + cos * b, cos * c + sin * d, -sin * c + cos * d, 0, 0]
313
+ };
314
+ }
315
+ function bakeIntoGeometry(node, m) {
316
+ if (isIdentityMatrix(m)) return node;
272
317
  if (node.type === "group") return { ...node, children: node.children.map((c) => transformVectorNode(c, m)) };
273
318
  const path = node;
274
319
  return {
@@ -286,6 +331,36 @@ function transformVectorNode(node, m) {
286
331
  }))
287
332
  };
288
333
  }
334
+ function transformVectorNode(node, m) {
335
+ const total = multiplyMatrices(nodeMatrix(node), m);
336
+ const { rigid, residual } = decomposeRigid(total);
337
+ const { transform: _old, ...rest } = node;
338
+ const baked = bakeIntoGeometry(rest, residual);
339
+ return isIdentityMatrix(rigid) ? baked : { ...baked, transform: rigid };
340
+ }
341
+ function vectorNodeRotation(node, parent = IDENTITY_MATRIX) {
342
+ return matrixRotation(multiplyMatrices(nodeMatrix(node), parent));
343
+ }
344
+ function vectorWorldMatrix(nodes, id) {
345
+ const search = (list, parent) => {
346
+ for (const n of list) {
347
+ const m = multiplyMatrices(nodeMatrix(n), parent);
348
+ if (n.id === id) return m;
349
+ if (n.type === "group") {
350
+ const found = search(n.children, m);
351
+ if (found) return found;
352
+ }
353
+ }
354
+ return null;
355
+ };
356
+ return search(nodes, IDENTITY_MATRIX);
357
+ }
358
+ function vectorParentMatrix(nodes, id) {
359
+ const node = findVectorNode(nodes, id);
360
+ const world = vectorWorldMatrix(nodes, id);
361
+ if (!node || !world) return null;
362
+ return multiplyMatrices(invertMatrix(nodeMatrix(node)), world);
363
+ }
289
364
  function translateAnchor(a, dx, dy) {
290
365
  const next = cloneAnchor(a);
291
366
  next.x += dx;
@@ -478,6 +553,7 @@ function vectorDocumentToSvg(doc, options = {}) {
478
553
  const indent = (depth) => pretty ? " ".repeat(depth) : "";
479
554
  const common = (node) => {
480
555
  let out2 = ` id="${escapeAttr(node.id)}"`;
556
+ if (node.transform) out2 += ` transform="matrix(${node.transform.map((n) => _chunkM4XE4UMTcjs.formatNumber.call(void 0, n, 6)).join(" ")})"`;
481
557
  if (node.opacity !== void 0 && node.opacity < 1) out2 += ` opacity="${num(node.opacity)}"`;
482
558
  if (node.visible === false) out2 += ` display="none"`;
483
559
  return out2;
@@ -855,6 +931,24 @@ function getAnchor(doc, ref) {
855
931
  const node = findVectorNode(doc.nodes, ref.nodeId);
856
932
  return _optionalChain([node, 'optionalAccess', _14 => _14.type]) === "path" ? _optionalChain([node, 'access', _15 => _15.subpaths, 'access', _16 => _16[ref.subpath], 'optionalAccess', _17 => _17.anchors, 'access', _18 => _18[ref.index]]) : void 0;
857
933
  }
934
+ function getAnchorWorld(doc, ref) {
935
+ const a = getAnchor(doc, ref);
936
+ if (!a) return void 0;
937
+ const m = _nullishCoalesce(vectorWorldMatrix(doc.nodes, ref.nodeId), () => ( IDENTITY_MATRIX));
938
+ const p = applyMatrix(m, a);
939
+ 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 } : {} };
940
+ }
941
+ function getVectorSelectionFrame(doc, ids) {
942
+ const nodes = ids.map((id) => ({ node: findVectorNode(doc.nodes, id), parent: _nullishCoalesce(vectorParentMatrix(doc.nodes, id), () => ( IDENTITY_MATRIX)) })).filter((n) => n.node);
943
+ if (!nodes.length) return null;
944
+ const angles = nodes.map((n) => matrixRotation(multiplyMatrices(nodeMatrix(n.node), n.parent)));
945
+ const same = angles.every((a) => Math.abs(normalizeAngle(a - angles[0])) < 1e-6);
946
+ const angle = same ? angles[0] : 0;
947
+ const unrotate = rotateMatrix(-angle);
948
+ const b = unionBounds(nodes.map((n) => vectorNodeBounds(n.node, multiplyMatrices(n.parent, unrotate))));
949
+ return b ? { ...b, angle } : null;
950
+ }
951
+ var normalizeAngle = (a) => ((a + 180) % 360 + 360) % 360 - 180;
858
952
  function getTransformHandlePositions(box) {
859
953
  const { x, y, width: w, height: h } = box;
860
954
  return [
@@ -869,17 +963,28 @@ function getTransformHandlePositions(box) {
869
963
  { handle: "rotate", x: x + w / 2, y: y - VECTOR_ROTATE_HANDLE_OFFSET }
870
964
  ];
871
965
  }
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
- };
966
+ function getVectorSelectionBoxGeometry(frame, viewport) {
967
+ const z = viewport.zoom;
968
+ const box = { x: frame.x * z + viewport.x, y: frame.y * z + viewport.y, width: frame.width * z, height: frame.height * z };
969
+ const rot = rotateMatrix(frame.angle, { x: viewport.x, y: viewport.y });
970
+ const toScreen2 = (p) => frame.angle ? applyMatrix(rot, p) : { x: p.x, y: p.y };
971
+ const handles = getTransformHandlePositions(box).map((h) => ({ ...h, screen: toScreen2(h) }));
972
+ const corners = [
973
+ { x: box.x, y: box.y },
974
+ { x: box.x + box.width, y: box.y },
975
+ { x: box.x + box.width, y: box.y + box.height },
976
+ { x: box.x, y: box.y + box.height }
977
+ ].map(toScreen2);
978
+ const r = (n) => Math.round(n * 1e3) / 1e3;
979
+ return { box, angle: frame.angle, transform: frame.angle ? `rotate(${r(frame.angle)} ${r(viewport.x)} ${r(viewport.y)})` : "", handles, corners };
980
+ }
981
+ var RESIZE_CURSOR_ORDER = ["ew-resize", "nwse-resize", "ns-resize", "nesw-resize"];
982
+ var HANDLE_ANGLES = { e: 0, se: 45, s: 90, sw: 135, w: 180, nw: 225, n: 270, ne: 315 };
983
+ function vectorResizeCursor(handle, angle = 0) {
984
+ if (handle === "rotate") return "grab";
985
+ const a = ((HANDLE_ANGLES[handle] + angle) % 180 + 180) % 180;
986
+ return RESIZE_CURSOR_ORDER[Math.round(a / 45) % 4];
987
+ }
883
988
  var TOOL_LABELS = {
884
989
  select: "Select",
885
990
  direct: "Direct Select",
@@ -938,6 +1043,7 @@ function mapTree(nodes, fn) {
938
1043
  }
939
1044
  return changed ? out : nodes;
940
1045
  }
1046
+ 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
1047
  var idCounter = 0;
942
1048
  var defaultCreateId = () => `vn-${Date.now().toString(36)}${(++idCounter).toString(36)}`;
943
1049
  var VectorEditorController = (_class = class {
@@ -1018,6 +1124,7 @@ var VectorEditorController = (_class = class {
1018
1124
  selectedAnchors: this.anchors,
1019
1125
  editingId: this.editingId,
1020
1126
  selectionBounds: this.boundsOf(this.selection),
1127
+ selectionFrame: this.frameOf(this.selectedRoots()),
1021
1128
  viewport: this.viewport,
1022
1129
  defaults: this.defaults,
1023
1130
  snap: this.snap,
@@ -1048,7 +1155,23 @@ var VectorEditorController = (_class = class {
1048
1155
  });
1049
1156
  }
1050
1157
  boundsOf(ids) {
1051
- return unionBounds(ids.map((id) => this.getNode(id)).map((n) => n ? vectorNodeBounds(n) : null));
1158
+ return unionBounds(
1159
+ ids.map((id) => {
1160
+ const n = this.getNode(id);
1161
+ return n ? vectorNodeBounds(n, _nullishCoalesce(vectorParentMatrix(this.doc.nodes, id), () => ( IDENTITY_MATRIX))) : null;
1162
+ })
1163
+ );
1164
+ }
1165
+ frameOf(ids) {
1166
+ return getVectorSelectionFrame(this.doc, ids);
1167
+ }
1168
+ /** Local → document matrix of a node in `doc` (default: the current document). */
1169
+ worldOf(id, doc = this.doc) {
1170
+ return _nullishCoalesce(vectorWorldMatrix(doc.nodes, id), () => ( IDENTITY_MATRIX));
1171
+ }
1172
+ /** A document point in node `id`'s own space. */
1173
+ toLocal(id, p, doc = this.doc) {
1174
+ return applyMatrix(invertMatrix(this.worldOf(id, doc)), p);
1052
1175
  }
1053
1176
  cursorFor() {
1054
1177
  if (_optionalChain([this, 'access', _21 => _21.drag, 'optionalAccess', _22 => _22.kind]) === "pan" || this.spaceHeld) return this.drag ? "grabbing" : "grab";
@@ -1057,14 +1180,14 @@ var VectorEditorController = (_class = class {
1057
1180
  const h = this.hover;
1058
1181
  switch (this.tool) {
1059
1182
  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";
1183
+ if (_optionalChain([h, 'optionalAccess', _27 => _27.transformHandle])) return vectorResizeCursor(h.transformHandle, _nullishCoalesce(_optionalChain([this, 'access', _28 => _28.frameOf, 'call', _29 => _29(this.selectedRoots()), 'optionalAccess', _30 => _30.angle]), () => ( 0)));
1184
+ return _optionalChain([h, 'optionalAccess', _31 => _31.nodeId]) ? "move" : "default";
1062
1185
  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";
1186
+ if (_optionalChain([h, 'optionalAccess', _32 => _32.anchor]) || _optionalChain([h, 'optionalAccess', _33 => _33.handle])) return "pointer";
1187
+ if (_optionalChain([h, 'optionalAccess', _34 => _34.segment])) return "copy";
1188
+ return _optionalChain([h, 'optionalAccess', _35 => _35.nodeId]) ? "move" : "default";
1066
1189
  case "pen":
1067
- return _optionalChain([h, 'optionalAccess', _33 => _33.anchor]) ? "pointer" : "crosshair";
1190
+ return _optionalChain([h, 'optionalAccess', _36 => _36.anchor]) ? "pointer" : "crosshair";
1068
1191
  default:
1069
1192
  return "crosshair";
1070
1193
  }
@@ -1084,7 +1207,7 @@ var VectorEditorController = (_class = class {
1084
1207
  this.selection = s.selection.filter((id) => this.getNode(id));
1085
1208
  this.anchors = s.anchors.filter((r) => getAnchor(this.doc, r));
1086
1209
  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;
1210
+ this.pen = s.pen && _optionalChain([this, 'access', _37 => _37.getNode, 'call', _38 => _38(s.pen.nodeId), 'optionalAccess', _39 => _39.subpaths, 'optionalAccess', _40 => _40[s.pen.subpath]]) ? s.pen : null;
1088
1211
  } finally {
1089
1212
  this.applying = false;
1090
1213
  }
@@ -1443,10 +1566,10 @@ var VectorEditorController = (_class = class {
1443
1566
  return this.selection.filter((id) => {
1444
1567
  let covered = false;
1445
1568
  const loc = locateVectorNode(this.doc.nodes, id);
1446
- let parent = _nullishCoalesce(_optionalChain([loc, 'optionalAccess', _38 => _38.parent]), () => ( null));
1569
+ let parent = _nullishCoalesce(_optionalChain([loc, 'optionalAccess', _41 => _41.parent]), () => ( null));
1447
1570
  while (parent) {
1448
1571
  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));
1572
+ parent = _nullishCoalesce(_optionalChain([locateVectorNode, 'call', _42 => _42(this.doc.nodes, parent.id), 'optionalAccess', _43 => _43.parent]), () => ( null));
1450
1573
  }
1451
1574
  return !covered;
1452
1575
  });
@@ -1454,6 +1577,11 @@ var VectorEditorController = (_class = class {
1454
1577
  // -------------------------------------------------------------------------
1455
1578
  // Transforms
1456
1579
  // -------------------------------------------------------------------------
1580
+ /** `m`, given in a frame rotated by `angle`, as a document-space matrix. */
1581
+ inFrame(angle, m) {
1582
+ if (!angle) return m;
1583
+ return multiplyMatrices(multiplyMatrices(rotateMatrix(-angle), m), rotateMatrix(angle));
1584
+ }
1457
1585
  /** Applies a matrix to the selected (or given) nodes as one undo step. */
1458
1586
  transform(matrix, options = {}) {
1459
1587
  const ids = new Set(_nullishCoalesce(options.ids, () => ( this.selectedRoots())));
@@ -1463,8 +1591,25 @@ var VectorEditorController = (_class = class {
1463
1591
  });
1464
1592
  return { ok: true };
1465
1593
  }
1594
+ /** `doc` with nodes `ids` moved by `m`, given in document space. */
1466
1595
  transformed(doc, ids, m) {
1467
- const nodes = mapTree(doc.nodes, (n) => ids.has(n.id) && !n.locked ? transformVectorNode(n, m) : n);
1596
+ const visit = (list, parent) => {
1597
+ let changed = false;
1598
+ const out = list.map((n) => {
1599
+ let next = n;
1600
+ if (ids.has(n.id) && !n.locked) {
1601
+ const local = isRoot(parent) ? m : multiplyMatrices(multiplyMatrices(parent, m), invertMatrix(parent));
1602
+ next = transformVectorNode(n, local);
1603
+ } else if (n.type === "group") {
1604
+ const children = visit(n.children, multiplyMatrices(nodeMatrix(n), parent));
1605
+ if (children !== n.children) next = { ...n, children };
1606
+ }
1607
+ if (next !== n) changed = true;
1608
+ return next;
1609
+ });
1610
+ return changed ? out : list;
1611
+ };
1612
+ const nodes = visit(doc.nodes, IDENTITY_MATRIX);
1468
1613
  return nodes === doc.nodes ? doc : { ...doc, nodes };
1469
1614
  }
1470
1615
  translate(dx, dy, options = {}) {
@@ -1476,26 +1621,48 @@ var VectorEditorController = (_class = class {
1476
1621
  }
1477
1622
  return this.transform(translateMatrix(dx, dy), { ids: options.ids, label: "Move" });
1478
1623
  }
1479
- /** Scales around `origin` (default: the selection centre). */
1624
+ /** The selection frame's centre in document space. */
1625
+ frameCenter(frame) {
1626
+ return applyMatrix(rotateMatrix(frame.angle), { x: frame.x + frame.width / 2, y: frame.y + frame.height / 2 });
1627
+ }
1628
+ /**
1629
+ * Scales along the selection box's own axes (rotated with the selection when its nodes
1630
+ * share a rotation) around `origin` (default: the box centre, in document space).
1631
+ */
1480
1632
  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" });
1633
+ const frame = this.frameOf(_nullishCoalesce(options.ids, () => ( this.selectedRoots())));
1634
+ if (!frame) return { ok: false, message: "Nothing is selected." };
1635
+ const origin = applyMatrix(rotateMatrix(-frame.angle), _nullishCoalesce(options.origin, () => ( this.frameCenter(frame))));
1636
+ return this.transform(this.inFrame(frame.angle, scaleMatrix(sx, sy, origin)), { ids: options.ids, label: "Scale" });
1485
1637
  }
1486
1638
  /** Rotates clockwise by `degrees` around `origin` (default: the selection centre). */
1487
1639
  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 }));
1640
+ const frame = this.frameOf(_nullishCoalesce(options.ids, () => ( this.selectedRoots())));
1641
+ if (!frame) return { ok: false, message: "Nothing is selected." };
1642
+ const origin = _nullishCoalesce(options.origin, () => ( this.frameCenter(frame)));
1491
1643
  return this.transform(rotateMatrix(degrees, origin), { ids: options.ids, label: "Rotate" });
1492
1644
  }
1645
+ /** Sets each selected (or given) node's rotation to `degrees`, turning it around its own centre. */
1646
+ setRotation(degrees, options = {}) {
1647
+ const ids = _nullishCoalesce(options.ids, () => ( this.selectedRoots()));
1648
+ if (!ids.length) return { ok: false, message: "Nothing is selected." };
1649
+ this.change("Set Rotation", () => {
1650
+ for (const id of ids) {
1651
+ const frame = this.frameOf([id]);
1652
+ if (!frame) continue;
1653
+ const delta = normalizeAngle(degrees - frame.angle);
1654
+ if (Math.abs(delta) > 1e-9) this.doc = this.transformed(this.doc, /* @__PURE__ */ new Set([id]), rotateMatrix(delta, this.frameCenter(frame)));
1655
+ }
1656
+ });
1657
+ return { ok: true };
1658
+ }
1659
+ /** Mirrors along the selection box's own axes. */
1493
1660
  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 };
1661
+ const frame = this.frameOf(_nullishCoalesce(options.ids, () => ( this.selectedRoots())));
1662
+ if (!frame) return { ok: false, message: "Nothing is selected." };
1663
+ const c = { x: frame.x + frame.width / 2, y: frame.y + frame.height / 2 };
1497
1664
  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" });
1665
+ return this.transform(this.inFrame(frame.angle, m), { ids: options.ids, label: axis === "horizontal" ? "Flip Horizontal" : "Flip Vertical" });
1499
1666
  }
1500
1667
  /**
1501
1668
  * Aligns the selected nodes to their combined bounds, or to the artboard when one node is
@@ -1585,8 +1752,8 @@ var VectorEditorController = (_class = class {
1585
1752
  if (!ids.length) return null;
1586
1753
  const locs = ids.map((id2) => locateVectorNode(this.doc.nodes, id2));
1587
1754
  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;
1755
+ const parent = _nullishCoalesce(_optionalChain([locs, 'access', _44 => _44[0], 'access', _45 => _45.parent, 'optionalAccess', _46 => _46.id]), () => ( null));
1756
+ if (locs.some((l) => (_nullishCoalesce(_optionalChain([l, 'access', _47 => _47.parent, 'optionalAccess', _48 => _48.id]), () => ( null))) !== parent)) return null;
1590
1757
  const id = this.newId();
1591
1758
  this.change("Group", () => {
1592
1759
  const set = new Set(ids);
@@ -1609,7 +1776,7 @@ var VectorEditorController = (_class = class {
1609
1776
  }
1610
1777
  /** Replaces selected groups with their children. */
1611
1778
  ungroup(options = {}) {
1612
- const groups = (_nullishCoalesce(options.ids, () => ( this.selection))).map((id) => this.getNode(id)).filter((n) => _optionalChain([n, 'optionalAccess', _46 => _46.type]) === "group");
1779
+ const groups = (_nullishCoalesce(options.ids, () => ( this.selection))).map((id) => this.getNode(id)).filter((n) => _optionalChain([n, 'optionalAccess', _49 => _49.type]) === "group");
1613
1780
  if (!groups.length) return { ok: false, message: "Select a group to ungroup." };
1614
1781
  const set = new Set(groups.map((g) => g.id));
1615
1782
  this.change("Ungroup", () => {
@@ -1620,7 +1787,10 @@ var VectorEditorController = (_class = class {
1620
1787
  if (set.has(n.id) && n.type === "group") {
1621
1788
  changed = true;
1622
1789
  const opacity = n.opacity;
1623
- out.push(...n.children.map((c) => opacity !== void 0 && opacity < 1 ? { ...c, opacity: (_nullishCoalesce(c.opacity, () => ( 1))) * opacity } : c));
1790
+ const m = nodeMatrix(n);
1791
+ out.push(
1792
+ ...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)
1793
+ );
1624
1794
  } else if (n.type === "group") {
1625
1795
  const children = flatten(n.children);
1626
1796
  if (children !== n.children) changed = true;
@@ -1668,15 +1838,22 @@ var VectorEditorController = (_class = class {
1668
1838
  if (paths.length < 2) return null;
1669
1839
  const result = _chunkM4XE4UMTcjs.pathBoolean.call(void 0,
1670
1840
  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")) }))
1841
+ paths.map((p) => ({
1842
+ path: vectorPathWorldBezier(
1843
+ { ...p, subpaths: p.subpaths.filter((s) => s.anchors.length > 1).map((s) => ({ ...s, closed: true })) },
1844
+ _nullishCoalesce(vectorParentMatrix(this.doc.nodes, p.id), () => ( IDENTITY_MATRIX))
1845
+ ),
1846
+ fillRule: _nullishCoalesce(p.fillRule, () => ( "nonzero"))
1847
+ }))
1672
1848
  );
1673
- const subpaths = bezierToSubpaths(result.path);
1674
- if (result.error || !subpaths.length) return null;
1675
1849
  const base = paths[0];
1850
+ const anchorRoot = _nullishCoalesce(roots.find((r) => findVectorNode([this.getNode(r)], base.id)), () => ( base.id));
1851
+ const subpaths = bezierToSubpaths(this.intoParentSpace(anchorRoot, result.path));
1852
+ if (result.error || !subpaths.length) return null;
1676
1853
  const id = this.newId();
1677
- const node = { ...cloneVectorNode(base), id, name: BOOLEAN_LABELS[op], subpaths };
1854
+ const { transform: _t, ...baseRest } = cloneVectorNode(base);
1855
+ const node = { ...baseRest, id, name: BOOLEAN_LABELS[op], subpaths };
1678
1856
  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
1857
  this.change(BOOLEAN_LABELS[op], () => {
1681
1858
  this.setDoc(mapTree(this.doc.nodes, (n) => n.id === anchorRoot ? node : drop.has(n.id) ? null : n));
1682
1859
  this.selection = [id];
@@ -1694,9 +1871,11 @@ var VectorEditorController = (_class = class {
1694
1871
  });
1695
1872
  if (paths.length < 2) return null;
1696
1873
  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
1874
  const first = _nullishCoalesce(roots.find((r) => findVectorNode([this.getNode(r)], paths[0].id)), () => ( paths[0].id));
1875
+ const subpaths = paths.flatMap((p) => bezierToSubpaths(this.intoParentSpace(first, vectorPathWorldBezier(p, _nullishCoalesce(vectorParentMatrix(this.doc.nodes, p.id), () => ( IDENTITY_MATRIX))))));
1876
+ const { transform: _t, ...baseRest } = cloneVectorNode(paths[0]);
1877
+ const node = { ...baseRest, id, name: "Compound Path", subpaths };
1878
+ const drop = /* @__PURE__ */ new Set([...paths.map((p) => p.id), ...roots]);
1700
1879
  this.change("Combine", () => {
1701
1880
  this.setDoc(mapTree(this.doc.nodes, (n) => n.id === first ? node : drop.has(n.id) ? null : n));
1702
1881
  this.selection = [id];
@@ -1704,9 +1883,14 @@ var VectorEditorController = (_class = class {
1704
1883
  });
1705
1884
  return id;
1706
1885
  }
1886
+ /** A document-space path mapped into the parent space of node `id`. */
1887
+ intoParentSpace(id, path) {
1888
+ const parent = _nullishCoalesce(vectorParentMatrix(this.doc.nodes, id), () => ( IDENTITY_MATRIX));
1889
+ return isRoot(parent) ? path : _chunkM4XE4UMTcjs.transformBezierPath.call(void 0, path, invertMatrix(parent));
1890
+ }
1707
1891
  /** Splits selected compound paths into one path per subpath. */
1708
1892
  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);
1893
+ const paths = (_nullishCoalesce(options.ids, () => ( this.selection))).map((id) => this.getNode(id)).filter((n) => _optionalChain([n, 'optionalAccess', _50 => _50.type]) === "path" && n.subpaths.length > 1);
1710
1894
  if (!paths.length) return { ok: false, message: "Select a compound path to break apart." };
1711
1895
  const created = [];
1712
1896
  const set = new Set(paths.map((p) => p.id));
@@ -1743,23 +1927,30 @@ var VectorEditorController = (_class = class {
1743
1927
  });
1744
1928
  return nodes === doc.nodes ? doc : { ...doc, nodes };
1745
1929
  }
1930
+ /** Moves anchors by a document-space offset (converted into each node's own space). */
1746
1931
  movedAnchors(doc, refs, dx, dy) {
1747
- return this.updateAnchors(doc, refs, (a) => translateAnchor(a, dx, dy));
1932
+ const local = /* @__PURE__ */ new Map();
1933
+ 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 }));
1934
+ return this.updateAnchors(doc, refs, (a, ref) => {
1935
+ const d = local.get(ref.nodeId);
1936
+ return translateAnchor(a, d.x, d.y);
1937
+ });
1748
1938
  }
1749
- /** Moves one anchor to a position (handles follow). */
1939
+ /** Moves one anchor to a document position (handles follow). */
1750
1940
  moveAnchor(ref, to) {
1751
- const a = getAnchor(this.doc, ref);
1941
+ const a = getAnchorWorld(this.doc, ref);
1752
1942
  if (!a) return { ok: false, message: "There is no such anchor." };
1753
1943
  this.change("Move Point", () => {
1754
1944
  this.doc = this.movedAnchors(this.doc, [ref], to.x - a.x, to.y - a.y);
1755
1945
  });
1756
1946
  return { ok: true };
1757
1947
  }
1758
- /** Moves one handle (see `moveAnchorHandle` for how the opposite handle follows). */
1948
+ /** Moves one handle to a document position (see `moveAnchorHandle` for how the opposite handle follows). */
1759
1949
  moveHandle(ref, to, options = {}) {
1760
1950
  if (!getAnchor(this.doc, ref)) return { ok: false, message: "There is no such anchor." };
1951
+ const local = this.toLocal(ref.nodeId, to);
1761
1952
  this.change("Move Handle", () => {
1762
- this.doc = this.updateAnchors(this.doc, [ref], (a) => moveAnchorHandle(a, ref.handle, to, options.breakHandles));
1953
+ this.doc = this.updateAnchors(this.doc, [ref], (a) => moveAnchorHandle(a, ref.handle, local, options.breakHandles));
1763
1954
  });
1764
1955
  return { ok: true };
1765
1956
  }
@@ -1778,11 +1969,11 @@ var VectorEditorController = (_class = class {
1778
1969
  });
1779
1970
  return { ok: true };
1780
1971
  }
1781
- /** Adds an anchor on the nearest segment of `nodeId` to `point` (shape unchanged). */
1972
+ /** Adds an anchor on the nearest segment of `nodeId` to document point `point` (shape unchanged). */
1782
1973
  insertAnchorAt(nodeId, point) {
1783
1974
  const node = this.getNode(nodeId);
1784
- if (_optionalChain([node, 'optionalAccess', _48 => _48.type]) !== "path") return null;
1785
- const hit = nearestSegment(node, point);
1975
+ if (_optionalChain([node, 'optionalAccess', _51 => _51.type]) !== "path") return null;
1976
+ const hit = nearestSegment(node, this.toLocal(nodeId, point));
1786
1977
  if (!hit) return null;
1787
1978
  const { subpath, index } = insertAnchor(node.subpaths[hit.subpath], hit.segment, hit.t);
1788
1979
  const ref = { nodeId, subpath: hit.subpath, index };
@@ -1816,7 +2007,7 @@ var VectorEditorController = (_class = class {
1816
2007
  setClosed(closed, nodeId) {
1817
2008
  const targets = nodeId ? (() => {
1818
2009
  const n = this.getNode(nodeId);
1819
- return _optionalChain([n, 'optionalAccess', _49 => _49.type]) === "path" ? n.subpaths.map((_, s) => ({ nodeId, subpath: s })) : [];
2010
+ return _optionalChain([n, 'optionalAccess', _52 => _52.type]) === "path" ? n.subpaths.map((_, s) => ({ nodeId, subpath: s })) : [];
1820
2011
  })() : this.anchors.map((r) => ({ nodeId: r.nodeId, subpath: r.subpath }));
1821
2012
  if (!targets.length) return { ok: false, message: "Select a path or points." };
1822
2013
  this.change(closed ? "Close Path" : "Open Path", () => {
@@ -1843,7 +2034,7 @@ var VectorEditorController = (_class = class {
1843
2034
  finishPath() {
1844
2035
  if (!this.pen) return;
1845
2036
  const node = this.getNode(this.pen.nodeId);
1846
- const sp = _optionalChain([node, 'optionalAccess', _50 => _50.subpaths, 'access', _51 => _51[this.pen.subpath]]);
2037
+ const sp = _optionalChain([node, 'optionalAccess', _53 => _53.subpaths, 'access', _54 => _54[this.pen.subpath]]);
1847
2038
  const pen = this.pen;
1848
2039
  this.pen = null;
1849
2040
  this.pointer = null;
@@ -1887,7 +2078,7 @@ var VectorEditorController = (_class = class {
1887
2078
  if (trimmed.startsWith("{")) {
1888
2079
  try {
1889
2080
  const data = JSON.parse(trimmed);
1890
- if (_optionalChain([data, 'optionalAccess', _52 => _52.type]) === VECTOR_CLIPBOARD_TYPE && Array.isArray(data.nodes)) nodes = data.nodes;
2081
+ if (_optionalChain([data, 'optionalAccess', _55 => _55.type]) === VECTOR_CLIPBOARD_TYPE && Array.isArray(data.nodes)) nodes = data.nodes;
1891
2082
  } catch (e2) {
1892
2083
  nodes = null;
1893
2084
  }
@@ -1899,7 +2090,7 @@ var VectorEditorController = (_class = class {
1899
2090
  }
1900
2091
  }
1901
2092
  } else nodes = this.clipboard;
1902
- if (!_optionalChain([nodes, 'optionalAccess', _53 => _53.length])) return [];
2093
+ if (!_optionalChain([nodes, 'optionalAccess', _56 => _56.length])) return [];
1903
2094
  const offset = _nullishCoalesce(options.offset, () => ( (text && !text.trim().startsWith("{") ? { x: 0, y: 0 } : { x: 10, y: 10 })));
1904
2095
  const copies = nodes.map((n) => transformVectorNode(this.freshIds(n), translateMatrix(offset.x, offset.y)));
1905
2096
  this.change("Paste", () => {
@@ -1939,20 +2130,18 @@ var VectorEditorController = (_class = class {
1939
2130
  }
1940
2131
  hitTransformHandle(screen) {
1941
2132
  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 };
2133
+ const frame = this.frameOf(this.selectedRoots());
2134
+ if (!frame) return null;
2135
+ const { box, handles } = getVectorSelectionBoxGeometry(frame, this.viewport);
1946
2136
  const r = this.hitRadius + 2;
1947
- const handles = getTransformHandlePositions(box);
1948
2137
  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;
2138
+ 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
2139
  return null;
1951
2140
  }
1952
2141
  hitHandle(screen) {
1953
2142
  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]]);
2143
+ const a = getAnchorWorld(this.doc, ref);
2144
+ const h = _optionalChain([a, 'optionalAccess', _57 => _57[ref.handle]]);
1956
2145
  if (!h) continue;
1957
2146
  const s = this.documentToScreen(h);
1958
2147
  if (Math.hypot(s.x - screen.x, s.y - screen.y) <= this.hitRadius) return ref;
@@ -1963,7 +2152,7 @@ var VectorEditorController = (_class = class {
1963
2152
  let best = null;
1964
2153
  let bestD = Infinity;
1965
2154
  for (const ref of refs) {
1966
- const a = getAnchor(this.doc, ref);
2155
+ const a = getAnchorWorld(this.doc, ref);
1967
2156
  if (!a) continue;
1968
2157
  const s = this.documentToScreen(a);
1969
2158
  const d = Math.hypot(s.x - screen.x, s.y - screen.y);
@@ -1977,18 +2166,18 @@ var VectorEditorController = (_class = class {
1977
2166
  /** Deepest unlocked visible path under `p`. */
1978
2167
  hitPath(p) {
1979
2168
  const tol = this.tolerance();
1980
- const search = (list) => {
2169
+ const search = (list, parent) => {
1981
2170
  for (let i = list.length - 1; i >= 0; i--) {
1982
2171
  const n = list[i];
1983
2172
  if (n.visible === false || n.locked) continue;
1984
2173
  if (n.type === "group") {
1985
- const found = search(n.children);
2174
+ const found = search(n.children, multiplyMatrices(nodeMatrix(n), parent));
1986
2175
  if (found) return found;
1987
- } else if (hitTestVectorNode(n, p, tol)) return n;
2176
+ } else if (hitTestVectorNode(n, p, tol, parent)) return n;
1988
2177
  }
1989
2178
  return null;
1990
2179
  };
1991
- return search(this.doc.nodes);
2180
+ return search(this.doc.nodes, IDENTITY_MATRIX);
1992
2181
  }
1993
2182
  /** Endpoints of open subpaths of unlocked visible paths (pen tool resumes from them). */
1994
2183
  openEndpoints() {
@@ -2019,8 +2208,11 @@ var VectorEditorController = (_class = class {
2019
2208
  if (anchor) return { nodeId: anchor.nodeId, anchor };
2020
2209
  for (const id of getEditablePathIds(this.view())) {
2021
2210
  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 } };
2211
+ const seg = nearestSegment(node, this.toLocal(id, p));
2212
+ if (seg && seg.distance <= this.tolerance()) {
2213
+ const point = applyMatrix(this.worldOf(id), seg.point);
2214
+ return { nodeId: id, segment: { nodeId: id, subpath: seg.subpath, segment: seg.segment, t: seg.t, point } };
2215
+ }
2024
2216
  }
2025
2217
  const path = this.hitPath(p);
2026
2218
  return path ? { nodeId: path.id } : null;
@@ -2065,9 +2257,10 @@ var VectorEditorController = (_class = class {
2065
2257
  downSelect(screen, p, input) {
2066
2258
  const handle = this.hitTransformHandle(screen);
2067
2259
  if (handle) {
2068
- const bounds = this.boundsOf(this.selectedRoots());
2260
+ const frame = this.frameOf(this.selectedRoots());
2069
2261
  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 };
2262
+ const center = applyMatrix(rotateMatrix(frame.angle), { x: frame.x + frame.width / 2, y: frame.y + frame.height / 2 });
2263
+ this.drag = handle === "rotate" ? { kind: "rotate", start: p, center, before, base: this.doc } : { kind: "scale", handle, start: p, frame, before, base: this.doc };
2071
2264
  this.interaction = handle === "rotate" ? { type: "rotate", angle: 0 } : { type: "scale", handle, sx: 1, sy: 1 };
2072
2265
  this.notify();
2073
2266
  return;
@@ -2133,7 +2326,7 @@ var VectorEditorController = (_class = class {
2133
2326
  }
2134
2327
  for (const id of getEditablePathIds(this.view())) {
2135
2328
  const node = this.getNode(id);
2136
- const seg = nearestSegment(node, p);
2329
+ const seg = nearestSegment(node, this.toLocal(id, p));
2137
2330
  if (!seg || seg.distance > this.tolerance()) continue;
2138
2331
  const sp = node.subpaths[seg.subpath];
2139
2332
  if (input.alt) {
@@ -2174,7 +2367,7 @@ var VectorEditorController = (_class = class {
2174
2367
  }
2175
2368
  downPen(screen, p, input) {
2176
2369
  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;
2370
+ const sp = this.pen ? _optionalChain([this, 'access', _58 => _58.getNode, 'call', _59 => _59(this.pen.nodeId), 'optionalAccess', _60 => _60.subpaths, 'access', _61 => _61[this.pen.subpath]]) : void 0;
2178
2371
  if (this.pen && sp) {
2179
2372
  const pen = this.pen;
2180
2373
  const first = { nodeId: pen.nodeId, subpath: pen.subpath, index: 0 };
@@ -2187,8 +2380,9 @@ var VectorEditorController = (_class = class {
2187
2380
  return;
2188
2381
  }
2189
2382
  let pt2 = this.snapPoint(p);
2190
- const last = sp.anchors[sp.anchors.length - 1];
2383
+ const last = getAnchorWorld(this.doc, { nodeId: pen.nodeId, subpath: pen.subpath, index: sp.anchors.length - 1 });
2191
2384
  if (input.shift) pt2 = constrain45(last, pt2);
2385
+ pt2 = this.toLocal(pen.nodeId, pt2);
2192
2386
  const node2 = this.getNode(pen.nodeId);
2193
2387
  const anchors = [...sp.anchors, { x: pt2.x, y: pt2.y }];
2194
2388
  this.replaceNode(pen.nodeId, { ...node2, subpaths: node2.subpaths.map((s, i) => i === pen.subpath ? { ...s, anchors } : s) });
@@ -2294,9 +2488,11 @@ var VectorEditorController = (_class = class {
2294
2488
  return;
2295
2489
  }
2296
2490
  case "scale": {
2297
- const { sx, sy, origin } = scaleFor(drag.handle, drag.bounds, p, input);
2491
+ const angle = drag.frame.angle;
2492
+ const q = angle ? applyMatrix(rotateMatrix(-angle), p) : p;
2493
+ const { sx, sy, origin } = scaleFor(drag.handle, drag.frame, q, input);
2298
2494
  const ids = new Set(this.selectedRoots());
2299
- this.doc = this.transformed(drag.base, ids, scaleMatrix(sx, sy, origin));
2495
+ this.doc = this.transformed(drag.base, ids, this.inFrame(angle, scaleMatrix(sx, sy, origin)));
2300
2496
  this.interaction = { type: "scale", handle: drag.handle, sx, sy };
2301
2497
  this.notify(true);
2302
2498
  return;
@@ -2316,7 +2512,7 @@ var VectorEditorController = (_class = class {
2316
2512
  this.interaction = { type: "marquee", bounds };
2317
2513
  if (drag.anchorsMode) {
2318
2514
  const inside = getVisibleAnchors(this.view()).filter((r) => {
2319
- const a = getAnchor(this.doc, r);
2515
+ const a = getAnchorWorld(this.doc, r);
2320
2516
  return a.x >= bounds.x && a.x <= bounds.x + bounds.width && a.y >= bounds.y && a.y <= bounds.y + bounds.height;
2321
2517
  });
2322
2518
  this.anchors = [...drag.baseAnchors, ...inside.filter((r) => !drag.baseAnchors.some((b) => sameRef(b, r)))];
@@ -2336,7 +2532,7 @@ var VectorEditorController = (_class = class {
2336
2532
  if (!drag.moved && Math.hypot(dx, dy) * this.viewport.zoom < 2) return;
2337
2533
  drag.moved = true;
2338
2534
  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;
2535
+ const lead = this.anchors.length ? getAnchorWorld(drag.base, this.anchors[0]) : void 0;
2340
2536
  ({ dx, dy } = this.snapDelta(dx, dy, _nullishCoalesce(lead, () => ( null))));
2341
2537
  this.doc = this.movedAnchors(drag.base, this.anchors, dx, dy);
2342
2538
  this.interaction = { type: "anchors", dx, dy };
@@ -2344,16 +2540,17 @@ var VectorEditorController = (_class = class {
2344
2540
  return;
2345
2541
  }
2346
2542
  case "handle": {
2347
- const anchor = getAnchor(drag.base, drag.ref);
2543
+ const anchor = getAnchorWorld(drag.base, drag.ref);
2348
2544
  let pt = this.snapPoint(p);
2349
2545
  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));
2546
+ const local = this.toLocal(drag.ref.nodeId, pt, drag.base);
2547
+ this.doc = this.updateAnchors(drag.base, [drag.ref], (a) => moveAnchorHandle(a, drag.ref.handle, local, input.alt));
2351
2548
  this.interaction = { type: "handle", ref: drag.ref };
2352
2549
  this.notify(true);
2353
2550
  return;
2354
2551
  }
2355
2552
  case "pull": {
2356
- const anchor = getAnchor(drag.base, drag.ref);
2553
+ const anchor = getAnchorWorld(drag.base, drag.ref);
2357
2554
  if (!anchor) return;
2358
2555
  if (Math.hypot(p.x - anchor.x, p.y - anchor.y) * this.viewport.zoom < 2) {
2359
2556
  this.doc = drag.base;
@@ -2363,7 +2560,7 @@ var VectorEditorController = (_class = class {
2363
2560
  let pt = p;
2364
2561
  if (input.shift) pt = constrain45(anchor, pt);
2365
2562
  const side = drag.closing ? "in" : "out";
2366
- const pos = drag.closing ? { x: 2 * anchor.x - pt.x, y: 2 * anchor.y - pt.y } : pt;
2563
+ 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
2564
  this.doc = this.updateAnchors(drag.base, [drag.ref], (a) => {
2368
2565
  if (drag.outOnly || drag.altBreaks && input.alt) return moveAnchorHandle(a, side, pos, true);
2369
2566
  return moveAnchorHandle({ ...a, type: "symmetric" }, side, pos);
@@ -2373,7 +2570,7 @@ var VectorEditorController = (_class = class {
2373
2570
  }
2374
2571
  case "shape": {
2375
2572
  const node = this.getNode(drag.nodeId);
2376
- if (_optionalChain([node, 'optionalAccess', _59 => _59.type]) !== "path") return;
2573
+ if (_optionalChain([node, 'optionalAccess', _62 => _62.type]) !== "path") return;
2377
2574
  let end = this.snapPoint(p);
2378
2575
  let start = drag.start;
2379
2576
  let bounds;
@@ -2405,7 +2602,7 @@ var VectorEditorController = (_class = class {
2405
2602
  if (Math.hypot(p.x - last[0], p.y - last[1]) * this.viewport.zoom < 1.5) return;
2406
2603
  drag.points.push([p.x, p.y]);
2407
2604
  const node = this.getNode(drag.nodeId);
2408
- if (_optionalChain([node, 'optionalAccess', _60 => _60.type]) !== "path") return;
2605
+ if (_optionalChain([node, 'optionalAccess', _63 => _63.type]) !== "path") return;
2409
2606
  this.replaceNode(drag.nodeId, { ...node, subpaths: [{ anchors: drag.points.map(([x, y]) => ({ x, y })), closed: false }] });
2410
2607
  const xs = drag.points.map((q) => q[0]);
2411
2608
  const ys = drag.points.map((q) => q[1]);
@@ -2472,7 +2669,7 @@ var VectorEditorController = (_class = class {
2472
2669
  }
2473
2670
  case "pencil": {
2474
2671
  const node = this.getNode(drag.nodeId);
2475
- if (_optionalChain([node, 'optionalAccess', _61 => _61.type]) !== "path" || drag.points.length < 2) {
2672
+ if (_optionalChain([node, 'optionalAccess', _64 => _64.type]) !== "path" || drag.points.length < 2) {
2476
2673
  this.doc = drag.before.doc;
2477
2674
  this.notify(true);
2478
2675
  return;
@@ -2537,10 +2734,10 @@ var VectorEditorController = (_class = class {
2537
2734
  this.convertAnchors(a.in || a.out ? "corner" : "smooth", [anchor]);
2538
2735
  return;
2539
2736
  }
2540
- const id = _nullishCoalesce(_optionalChain([this, 'access', _62 => _62.hover, 'optionalAccess', _63 => _63.segment, 'optionalAccess', _64 => _64.nodeId]), () => ( this.editingId));
2737
+ const id = _nullishCoalesce(_optionalChain([this, 'access', _65 => _65.hover, 'optionalAccess', _66 => _66.segment, 'optionalAccess', _67 => _67.nodeId]), () => ( this.editingId));
2541
2738
  if (id) {
2542
2739
  const node = this.getNode(id);
2543
- const seg = node ? nearestSegment(node, p) : null;
2740
+ const seg = node ? nearestSegment(node, this.toLocal(id, p)) : null;
2544
2741
  if (seg && seg.distance <= this.tolerance()) this.insertAnchorAt(id, p);
2545
2742
  }
2546
2743
  return;
@@ -2615,7 +2812,7 @@ var VectorEditorController = (_class = class {
2615
2812
  this.finishPath();
2616
2813
  return true;
2617
2814
  }
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") {
2815
+ if (this.tool === "select" && this.selection.length === 1 && _optionalChain([this, 'access', _68 => _68.getNode, 'call', _69 => _69(this.selection[0]), 'optionalAccess', _70 => _70.type]) === "path") {
2619
2816
  this.tool = "direct";
2620
2817
  this.editingId = this.selection[0];
2621
2818
  this.notify();
@@ -2692,11 +2889,12 @@ function scaleFor(handle, b, p, mods = {}) {
2692
2889
 
2693
2890
  // src/vector/render.ts
2694
2891
  var fmt = (n) => Math.round(n * 100) / 100;
2892
+ var fmtMatrix = (m) => `matrix(${m.map((n) => Math.round(n * 1e6) / 1e6).join(" ")})`;
2695
2893
  function renderNode(node, defs, idPrefix) {
2696
2894
  if (node.visible === false) return null;
2697
2895
  if (node.type === "group") {
2698
2896
  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 };
2897
+ return { kind: "group", id: node.id, ...node.transform ? { transform: fmtMatrix(node.transform) } : {}, opacity: _nullishCoalesce(node.opacity, () => ( 1)), children };
2700
2898
  }
2701
2899
  const fill = vectorPaintToSvg(node.fill, `${idPrefix}fill-${node.id}`);
2702
2900
  const stroke = vectorPaintToSvg(node.stroke, `${idPrefix}stroke-${node.id}`);
@@ -2705,13 +2903,14 @@ function renderNode(node, defs, idPrefix) {
2705
2903
  return {
2706
2904
  kind: "path",
2707
2905
  id: node.id,
2906
+ ...node.transform ? { transform: fmtMatrix(node.transform) } : {},
2708
2907
  d: vectorPathData(node),
2709
2908
  fill: fill.value,
2710
2909
  stroke: stroke.value,
2711
2910
  strokeWidth: _nullishCoalesce(node.strokeWidth, () => ( 1)),
2712
2911
  ...node.strokeLinecap ? { strokeLinecap: node.strokeLinecap } : {},
2713
2912
  ...node.strokeLinejoin ? { strokeLinejoin: node.strokeLinejoin } : {},
2714
- ..._optionalChain([node, 'access', _68 => _68.strokeDasharray, 'optionalAccess', _69 => _69.length]) ? { strokeDasharray: node.strokeDasharray.join(" ") } : {},
2913
+ ..._optionalChain([node, 'access', _71 => _71.strokeDasharray, 'optionalAccess', _72 => _72.length]) ? { strokeDasharray: node.strokeDasharray.join(" ") } : {},
2715
2914
  fillRule: _nullishCoalesce(node.fillRule, () => ( "nonzero")),
2716
2915
  opacity: _nullishCoalesce(node.opacity, () => ( 1))
2717
2916
  };
@@ -2733,8 +2932,14 @@ function getVectorSceneRenderData(state, options = {}) {
2733
2932
  var toScreen = (state, p) => ({ x: p.x * state.viewport.zoom + state.viewport.x, y: p.y * state.viewport.zoom + state.viewport.y });
2734
2933
  function screenOutline(state, node) {
2735
2934
  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(" ");
2935
+ const view = [v.zoom, 0, 0, v.zoom, v.x, v.y];
2936
+ const parent = _nullishCoalesce(vectorParentMatrix(state.document.nodes, node.id), () => ( IDENTITY_MATRIX));
2937
+ return collectWithMatrix(node, parent).map(({ path, parent: m }) => _chunkM4XE4UMTcjs.bezierPathToSvg.call(void 0, _chunkM4XE4UMTcjs.transformBezierPath.call(void 0, vectorPathWorldBezier(path, m), view), { precision: 2 })).join(" ");
2938
+ }
2939
+ function collectWithMatrix(node, parent) {
2940
+ if (node.type === "path") return [{ path: node, parent }];
2941
+ const m = multiplyMatrices(nodeMatrix(node), parent);
2942
+ return node.children.flatMap((c) => collectWithMatrix(c, m));
2738
2943
  }
2739
2944
  function collect(node) {
2740
2945
  if (node.type === "path") return [node];
@@ -2754,40 +2959,38 @@ function getVectorOverlayRenderData(state) {
2754
2959
  };
2755
2960
  for (const id of state.selection) outline(id, true);
2756
2961
  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);
2962
+ if (_optionalChain([state, 'access', _73 => _73.hover, 'optionalAccess', _74 => _74.nodeId]) && !state.interaction) outline(state.hover.nodeId, false);
2758
2963
  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) };
2964
+ if (state.tool === "select" && state.selectionFrame && _optionalChain([state, 'access', _75 => _75.interaction, 'optionalAccess', _76 => _76.type]) !== "marquee") {
2965
+ const g = getVectorSelectionBoxGeometry(state.selectionFrame, state.viewport);
2966
+ selectionBox = { ...g.box, angle: g.angle, transform: g.transform, corners: g.corners, handles: g.handles };
2764
2967
  }
2765
2968
  const selected = (r) => state.selectedAnchors.some((a) => a.nodeId === r.nodeId && a.subpath === r.subpath && a.index === r.index);
2766
2969
  const sameAnchor = (a, b) => !!a && a.nodeId === b.nodeId && a.subpath === b.subpath && a.index === b.index;
2767
2970
  const anchors = getVisibleAnchors(state).map((ref) => {
2768
- const a = getAnchor(doc, ref);
2971
+ const a = getAnchorWorld(doc, ref);
2769
2972
  const s = toScreen(state, a);
2770
2973
  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 };
2974
+ return { ref, x: s.x, y: s.y, type: _nullishCoalesce(a.type, () => ( inferAnchorType(a))), selected: selected(ref), hovered: sameAnchor(_optionalChain([state, 'access', _77 => _77.hover, 'optionalAccess', _78 => _78.anchor]), ref), closes };
2772
2975
  });
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);
2976
+ if (state.tool === "pen" && !state.pen && _optionalChain([state, 'access', _79 => _79.hover, 'optionalAccess', _80 => _80.anchor])) {
2977
+ const a = getAnchorWorld(doc, state.hover.anchor);
2775
2978
  if (a) {
2776
2979
  const s = toScreen(state, a);
2777
2980
  anchors.push({ ref: state.hover.anchor, x: s.x, y: s.y, type: _nullishCoalesce(a.type, () => ( "corner")), selected: false, hovered: true, closes: false });
2778
2981
  }
2779
2982
  }
2780
2983
  const handles = getVisibleHandles(state).flatMap((ref) => {
2781
- const a = getAnchor(doc, ref);
2782
- const h = _optionalChain([a, 'optionalAccess', _78 => _78[ref.handle]]);
2984
+ const a = getAnchorWorld(doc, ref);
2985
+ const h = _optionalChain([a, 'optionalAccess', _81 => _81[ref.handle]]);
2783
2986
  if (!a || !h) return [];
2784
2987
  const s = toScreen(state, h);
2785
2988
  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;
2989
+ const hovered = !!_optionalChain([state, 'access', _82 => _82.hover, 'optionalAccess', _83 => _83.handle]) && sameAnchor(state.hover.handle, ref) && state.hover.handle.handle === ref.handle;
2787
2990
  return [{ ref, x: s.x, y: s.y, anchorX: sa.x, anchorY: sa.y, hovered }];
2788
2991
  });
2789
2992
  let marquee = null;
2790
- if (_optionalChain([state, 'access', _81 => _81.interaction, 'optionalAccess', _82 => _82.type]) === "marquee") {
2993
+ if (_optionalChain([state, 'access', _84 => _84.interaction, 'optionalAccess', _85 => _85.type]) === "marquee") {
2791
2994
  const b = state.interaction.bounds;
2792
2995
  const tl = toScreen(state, b);
2793
2996
  marquee = { x: tl.x, y: tl.y, width: b.width * z, height: b.height * z };
@@ -2795,8 +2998,8 @@ function getVectorOverlayRenderData(state) {
2795
2998
  let penPreview = null;
2796
2999
  if (state.tool === "pen" && state.pen && state.pointer && !state.interaction) {
2797
3000
  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]]);
3001
+ const sp = _optionalChain([node, 'optionalAccess', _86 => _86.type]) === "path" ? node.subpaths[state.pen.subpath] : void 0;
3002
+ const last = sp ? getAnchorWorld(doc, { nodeId: state.pen.nodeId, subpath: state.pen.subpath, index: sp.anchors.length - 1 }) : void 0;
2800
3003
  if (last) {
2801
3004
  const a = toScreen(state, last);
2802
3005
  const p = toScreen(state, state.pointer);
@@ -2808,10 +3011,10 @@ function getVectorOverlayRenderData(state) {
2808
3011
  const artboard = { x: origin.x, y: origin.y, width: doc.width * z, height: doc.height * z };
2809
3012
  let status = null;
2810
3013
  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;
3014
+ if (_optionalChain([it, 'optionalAccess', _87 => _87.type]) === "rotate") status = `${Math.round(it.angle)}\xB0`;
3015
+ else if (_optionalChain([it, 'optionalAccess', _88 => _88.type]) === "move" || _optionalChain([it, 'optionalAccess', _89 => _89.type]) === "anchors") status = `\u0394 ${fmt(it.dx)}, ${fmt(it.dy)}`;
3016
+ else if (_optionalChain([it, 'optionalAccess', _90 => _90.type]) === "scale" || _optionalChain([it, 'optionalAccess', _91 => _91.type]) === "draw") {
3017
+ const b = it.type === "draw" ? findVectorNode(doc.nodes, it.nodeId) ? vectorNodeBounds(findVectorNode(doc.nodes, it.nodeId)) : null : state.selectionFrame;
2815
3018
  if (b) status = `W ${fmt(b.width)} \xD7 H ${fmt(b.height)}`;
2816
3019
  }
2817
3020
  return { outlines, selectionBox, anchors, handles, marquee, penPreview, artboard, status };
@@ -2838,7 +3041,7 @@ function isVectorTextTarget(target) {
2838
3041
  if (!el || typeof el !== "object") return false;
2839
3042
  const tag = (_nullishCoalesce(el.tagName, () => ( ""))).toUpperCase();
2840
3043
  if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
2841
- return !!el.isContentEditable || _optionalChain([el, 'access', _91 => _91.getAttribute, 'optionalCall', _92 => _92("contenteditable")]) === "true";
3044
+ return !!el.isContentEditable || _optionalChain([el, 'access', _92 => _92.getAttribute, 'optionalCall', _93 => _93("contenteditable")]) === "true";
2842
3045
  }
2843
3046
  function getVectorSelectionStyle(state) {
2844
3047
  const paths = [];
@@ -2897,23 +3100,25 @@ function createVectorCommands(controller, options = {}) {
2897
3100
  const base = { group, ...options.scope ? { scope: options.scope } : {} };
2898
3101
  const hasSelection = () => controller.getState().selection.length ? true : "Nothing is selected.";
2899
3102
  const ids = (params) => {
2900
- const list2 = _nullishCoalesce(_optionalChain([params, 'optionalAccess', _93 => _93.ids]), () => ( controller.getState().selection));
3103
+ const list2 = _nullishCoalesce(_optionalChain([params, 'optionalAccess', _94 => _94.ids]), () => ( controller.getState().selection));
2901
3104
  for (const id of list2) if (!controller.getNode(id)) fail(`There is no node "${id}".`);
2902
3105
  if (!list2.length) fail("Nothing is selected; pass ids.");
2903
3106
  return list2;
2904
3107
  };
2905
- const enabledFor = (_, params) => _optionalChain([params, 'optionalAccess', _94 => _94.ids, 'optionalAccess', _95 => _95.length]) ? true : hasSelection();
3108
+ const enabledFor = (_, params) => _optionalChain([params, 'optionalAccess', _95 => _95.ids, 'optionalAccess', _96 => _96.length]) ? true : hasSelection();
2906
3109
  const list = () => {
2907
3110
  const state = controller.getState();
2908
3111
  const out = [];
2909
3112
  const visit = (nodes, depth) => {
2910
3113
  for (const n of nodes) {
2911
- const b = vectorNodeBounds(n);
3114
+ const parent = _nullishCoalesce(vectorParentMatrix(state.document.nodes, n.id), () => ( IDENTITY_MATRIX));
3115
+ const b = vectorNodeBounds(n, parent);
2912
3116
  out.push({
2913
3117
  id: n.id,
2914
3118
  type: n.type,
2915
3119
  ...n.name ? { name: n.name } : {},
2916
3120
  depth,
3121
+ rotation: Math.round(vectorNodeRotation(n, parent) * 100) / 100,
2917
3122
  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
3123
  visible: n.visible !== false,
2919
3124
  locked: !!n.locked,
@@ -2987,7 +3192,7 @@ function createVectorCommands(controller, options = {}) {
2987
3192
  run: (_ctx, params) => {
2988
3193
  const id = controller.addPath(params.d, { ...pickStyle(params), ...params.name ? { name: params.name } : {} });
2989
3194
  const node = controller.getNode(id);
2990
- if (_optionalChain([node, 'optionalAccess', _96 => _96.type]) === "path" && !node.subpaths.length) {
3195
+ if (_optionalChain([node, 'optionalAccess', _97 => _97.type]) === "path" && !node.subpaths.length) {
2991
3196
  controller.undo();
2992
3197
  fail("The path data has no points.");
2993
3198
  }
@@ -3015,10 +3220,18 @@ function createVectorCommands(controller, options = {}) {
3015
3220
  id: `${prefix}transform`,
3016
3221
  label: "Transform\u2026",
3017
3222
  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.",
3223
+ 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
3224
  params: {
3020
3225
  type: "object",
3021
- properties: { ids: IDS, dx: { type: "number" }, dy: { type: "number" }, sx: { type: "number" }, sy: { type: "number" }, rotate: { type: "number" } }
3226
+ properties: {
3227
+ ids: IDS,
3228
+ dx: { type: "number" },
3229
+ dy: { type: "number" },
3230
+ sx: { type: "number" },
3231
+ sy: { type: "number" },
3232
+ rotate: { type: "number" },
3233
+ angle: { type: "number" }
3234
+ }
3022
3235
  },
3023
3236
  enabled: enabledFor,
3024
3237
  undoable: true,
@@ -3027,6 +3240,7 @@ function createVectorCommands(controller, options = {}) {
3027
3240
  if (params.dx || params.dy) check(controller.translate(_nullishCoalesce(params.dx, () => ( 0)), _nullishCoalesce(params.dy, () => ( 0)), { ids: target }));
3028
3241
  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
3242
  if (params.rotate) check(controller.rotate(params.rotate, { ids: target }));
3243
+ if (params.angle !== void 0) check(controller.setRotation(params.angle, { ids: target }));
3030
3244
  return { ok: true };
3031
3245
  }
3032
3246
  },
@@ -3111,7 +3325,7 @@ function createVectorCommands(controller, options = {}) {
3111
3325
  enabled: enabledFor,
3112
3326
  undoable: true,
3113
3327
  run: (_ctx, params) => {
3114
- const id = controller.group({ ids: ids(params), name: _optionalChain([params, 'optionalAccess', _97 => _97.name]) });
3328
+ const id = controller.group({ ids: ids(params), name: _optionalChain([params, 'optionalAccess', _98 => _98.name]) });
3115
3329
  if (!id) fail("Only nodes that share a parent can be grouped.");
3116
3330
  return { id };
3117
3331
  }
@@ -3150,7 +3364,7 @@ function createVectorCommands(controller, options = {}) {
3150
3364
  keywords: ["download", "save", "markup"],
3151
3365
  description: "Returns the document as SVG markup.",
3152
3366
  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]) }) })
3367
+ run: (_ctx, params) => ({ svg: controller.toSvg({ precision: _optionalChain([params, 'optionalAccess', _99 => _99.precision]) }) })
3154
3368
  },
3155
3369
  {
3156
3370
  ...base,
@@ -3239,5 +3453,19 @@ function createVectorCommands(controller, options = {}) {
3239
3453
 
3240
3454
 
3241
3455
 
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;
3456
+
3457
+
3458
+
3459
+
3460
+
3461
+
3462
+
3463
+
3464
+
3465
+
3466
+
3467
+
3468
+
3469
+
3470
+ exports.IDENTITY_MATRIX = IDENTITY_MATRIX; exports.KAPPA = KAPPA; exports.VECTOR_CLIPBOARD_TYPE = VECTOR_CLIPBOARD_TYPE; exports.VECTOR_OVERLAY_THEME = VECTOR_OVERLAY_THEME; exports.VECTOR_ROTATE_HANDLE_OFFSET = VECTOR_ROTATE_HANDLE_OFFSET; exports.VECTOR_TOOLS = VECTOR_TOOLS; exports.VECTOR_TOOL_GLYPHS = VECTOR_TOOL_GLYPHS; exports.VECTOR_TOOL_SHORTCUTS = VECTOR_TOOL_SHORTCUTS; exports.VECTOR_TRANSFORM_HANDLES = VECTOR_TRANSFORM_HANDLES; exports.VectorEditorController = VectorEditorController; exports.applyMatrix = applyMatrix; exports.applyMatrixLinear = applyMatrixLinear; exports.bezierToSubpaths = bezierToSubpaths; exports.boundsFromPoints = boundsFromPoints; exports.boundsIntersect = boundsIntersect; exports.cloneAnchor = cloneAnchor; exports.cloneSubpath = cloneSubpath; exports.cloneVectorDocument = cloneVectorDocument; exports.cloneVectorNode = cloneVectorNode; exports.collectPathNodes = collectPathNodes; exports.convertAnchor = convertAnchor; exports.createVectorCommands = createVectorCommands; exports.decomposeRigid = decomposeRigid; exports.ellipseSubpath = ellipseSubpath; exports.findVectorNode = findVectorNode; exports.getAnchor = getAnchor; exports.getAnchorWorld = getAnchorWorld; exports.getEditablePathIds = getEditablePathIds; exports.getTransformHandlePositions = getTransformHandlePositions; exports.getVectorOverlayRenderData = getVectorOverlayRenderData; exports.getVectorSceneRenderData = getVectorSceneRenderData; exports.getVectorSelectionBoxGeometry = getVectorSelectionBoxGeometry; exports.getVectorSelectionFrame = getVectorSelectionFrame; exports.getVectorSelectionStyle = getVectorSelectionStyle; exports.getVisibleAnchors = getVisibleAnchors; exports.getVisibleHandles = getVisibleHandles; exports.hasPaint = hasPaint; exports.hitTestVectorNode = hitTestVectorNode; exports.hitTestVectorNodes = hitTestVectorNodes; exports.inferAnchorType = inferAnchorType; exports.insertAnchor = insertAnchor; exports.invertMatrix = invertMatrix; exports.isIdentityMatrix = isIdentityMatrix; exports.isVectorTextTarget = isVectorTextTarget; exports.lineSubpath = lineSubpath; exports.locateVectorNode = locateVectorNode; exports.matrixRotation = matrixRotation; exports.moveAnchorHandle = moveAnchorHandle; exports.multiplyMatrices = multiplyMatrices; exports.nearestSegment = nearestSegment; exports.nodeMatrix = nodeMatrix; exports.parseSvgTransform = parseSvgTransform; exports.parseVectorSvg = parseVectorSvg; exports.polygonSubpath = polygonSubpath; exports.rectSubpath = rectSubpath; exports.removeAnchors = removeAnchors; exports.reverseSubpath = reverseSubpath; exports.rotateMatrix = rotateMatrix; exports.roundSubpaths = roundSubpaths; exports.scaleFor = scaleFor; exports.scaleMatrix = scaleMatrix; exports.starSubpath = starSubpath; exports.subpathSegment = subpathSegment; exports.subpathSegmentCount = subpathSegmentCount; exports.subpathsToBezier = subpathsToBezier; exports.toVectorKeyInput = toVectorKeyInput; exports.toVectorModifiers = toVectorModifiers; exports.toVectorPointerInput = toVectorPointerInput; exports.transformVectorNode = transformVectorNode; exports.translateAnchor = translateAnchor; exports.translateMatrix = translateMatrix; exports.unionBounds = unionBounds; exports.vectorDocumentToSvg = vectorDocumentToSvg; exports.vectorNodeBounds = vectorNodeBounds; exports.vectorNodeRotation = vectorNodeRotation; exports.vectorNodeToBezier = vectorNodeToBezier; exports.vectorPaintToHex = vectorPaintToHex; exports.vectorPaintToSvg = vectorPaintToSvg; exports.vectorParentMatrix = vectorParentMatrix; exports.vectorPathData = vectorPathData; exports.vectorPathWorldBezier = vectorPathWorldBezier; exports.vectorResizeCursor = vectorResizeCursor; exports.vectorToolLabel = vectorToolLabel; exports.vectorWorldMatrix = vectorWorldMatrix; exports.walkVectorNodes = walkVectorNodes;
3243
3471
  //# sourceMappingURL=index.cjs.map