@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.
- package/dist/vector/index.cjs +361 -133
- package/dist/vector/index.cjs.map +1 -1
- package/dist/vector/index.d.cts +143 -24
- package/dist/vector/index.d.ts +143 -24
- package/dist/vector/index.js +319 -91
- package/dist/vector/index.js.map +1 -1
- package/package.json +1 -1
package/dist/vector/index.js
CHANGED
|
@@ -189,9 +189,20 @@ function collectPathNodes(node) {
|
|
|
189
189
|
});
|
|
190
190
|
return out;
|
|
191
191
|
}
|
|
192
|
-
function
|
|
193
|
-
|
|
194
|
-
|
|
192
|
+
function nodeMatrix(node) {
|
|
193
|
+
return 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 : transformBezierPath(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 = bezierPathBounds(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")
|
|
222
|
-
|
|
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) && pointInBezierPath(path, point, 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
|
|
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) => formatNumber(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 node?.type === "path" ? node.subpaths[ref.subpath]?.anchors[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 = 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: 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
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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 {
|
|
@@ -1018,6 +1124,7 @@ var VectorEditorController = 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 {
|
|
|
1048
1155
|
});
|
|
1049
1156
|
}
|
|
1050
1157
|
boundsOf(ids) {
|
|
1051
|
-
return unionBounds(
|
|
1158
|
+
return unionBounds(
|
|
1159
|
+
ids.map((id) => {
|
|
1160
|
+
const n = this.getNode(id);
|
|
1161
|
+
return n ? vectorNodeBounds(n, 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 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 (this.drag?.kind === "pan" || this.spaceHeld) return this.drag ? "grabbing" : "grab";
|
|
@@ -1057,7 +1180,7 @@ var VectorEditorController = class {
|
|
|
1057
1180
|
const h = this.hover;
|
|
1058
1181
|
switch (this.tool) {
|
|
1059
1182
|
case "select":
|
|
1060
|
-
if (h?.transformHandle) return
|
|
1183
|
+
if (h?.transformHandle) return vectorResizeCursor(h.transformHandle, this.frameOf(this.selectedRoots())?.angle ?? 0);
|
|
1061
1184
|
return h?.nodeId ? "move" : "default";
|
|
1062
1185
|
case "direct":
|
|
1063
1186
|
if (h?.anchor || h?.handle) return "pointer";
|
|
@@ -1454,6 +1577,11 @@ var VectorEditorController = 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(options.ids ?? this.selectedRoots());
|
|
@@ -1463,8 +1591,25 @@ var VectorEditorController = 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
|
|
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 {
|
|
|
1476
1621
|
}
|
|
1477
1622
|
return this.transform(translateMatrix(dx, dy), { ids: options.ids, label: "Move" });
|
|
1478
1623
|
}
|
|
1479
|
-
/**
|
|
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
|
|
1482
|
-
if (!
|
|
1483
|
-
const origin = options.origin ??
|
|
1484
|
-
return this.transform(scaleMatrix(sx, sy, origin), { ids: options.ids, label: "Scale" });
|
|
1633
|
+
const frame = this.frameOf(options.ids ?? this.selectedRoots());
|
|
1634
|
+
if (!frame) return { ok: false, message: "Nothing is selected." };
|
|
1635
|
+
const origin = applyMatrix(rotateMatrix(-frame.angle), 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
|
|
1489
|
-
if (!
|
|
1490
|
-
const origin = options.origin ??
|
|
1640
|
+
const frame = this.frameOf(options.ids ?? this.selectedRoots());
|
|
1641
|
+
if (!frame) return { ok: false, message: "Nothing is selected." };
|
|
1642
|
+
const origin = 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 = 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
|
|
1495
|
-
if (!
|
|
1496
|
-
const c = { x:
|
|
1661
|
+
const frame = this.frameOf(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
|
|
@@ -1620,7 +1787,10 @@ var VectorEditorController = class {
|
|
|
1620
1787
|
if (set.has(n.id) && n.type === "group") {
|
|
1621
1788
|
changed = true;
|
|
1622
1789
|
const opacity = n.opacity;
|
|
1623
|
-
|
|
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: (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 {
|
|
|
1668
1838
|
if (paths.length < 2) return null;
|
|
1669
1839
|
const result = pathBoolean(
|
|
1670
1840
|
BOOLEAN_OPS[op],
|
|
1671
|
-
paths.map((p) => ({
|
|
1841
|
+
paths.map((p) => ({
|
|
1842
|
+
path: vectorPathWorldBezier(
|
|
1843
|
+
{ ...p, subpaths: p.subpaths.filter((s) => s.anchors.length > 1).map((s) => ({ ...s, closed: true })) },
|
|
1844
|
+
vectorParentMatrix(this.doc.nodes, p.id) ?? IDENTITY_MATRIX
|
|
1845
|
+
),
|
|
1846
|
+
fillRule: 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 = 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
|
|
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 = 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 {
|
|
|
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 = 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, 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,6 +1883,11 @@ var VectorEditorController = 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 = vectorParentMatrix(this.doc.nodes, id) ?? IDENTITY_MATRIX;
|
|
1889
|
+
return isRoot(parent) ? path : transformBezierPath(path, invertMatrix(parent));
|
|
1890
|
+
}
|
|
1707
1891
|
/** Splits selected compound paths into one path per subpath. */
|
|
1708
1892
|
breakApart(options = {}) {
|
|
1709
1893
|
const paths = (options.ids ?? this.selection).map((id) => this.getNode(id)).filter((n) => n?.type === "path" && n.subpaths.length > 1);
|
|
@@ -1743,23 +1927,30 @@ var VectorEditorController = 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
|
-
|
|
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 =
|
|
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,
|
|
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 {
|
|
|
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
1975
|
if (node?.type !== "path") return null;
|
|
1785
|
-
const hit = nearestSegment(node, point);
|
|
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 };
|
|
@@ -1939,19 +2130,17 @@ var VectorEditorController = class {
|
|
|
1939
2130
|
}
|
|
1940
2131
|
hitTransformHandle(screen) {
|
|
1941
2132
|
if (this.tool !== "select" || !this.selection.length) return null;
|
|
1942
|
-
const
|
|
1943
|
-
if (!
|
|
1944
|
-
const
|
|
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.
|
|
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 =
|
|
2143
|
+
const a = getAnchorWorld(this.doc, ref);
|
|
1955
2144
|
const h = a?.[ref.handle];
|
|
1956
2145
|
if (!h) continue;
|
|
1957
2146
|
const s = this.documentToScreen(h);
|
|
@@ -1963,7 +2152,7 @@ var VectorEditorController = class {
|
|
|
1963
2152
|
let best = null;
|
|
1964
2153
|
let bestD = Infinity;
|
|
1965
2154
|
for (const ref of refs) {
|
|
1966
|
-
const a =
|
|
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 {
|
|
|
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 {
|
|
|
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())
|
|
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 {
|
|
|
2065
2257
|
downSelect(screen, p, input) {
|
|
2066
2258
|
const handle = this.hitTransformHandle(screen);
|
|
2067
2259
|
if (handle) {
|
|
2068
|
-
const
|
|
2260
|
+
const frame = this.frameOf(this.selectedRoots());
|
|
2069
2261
|
const before = this.snapshot();
|
|
2070
|
-
|
|
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 {
|
|
|
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) {
|
|
@@ -2187,8 +2380,9 @@ var VectorEditorController = class {
|
|
|
2187
2380
|
return;
|
|
2188
2381
|
}
|
|
2189
2382
|
let pt2 = this.snapPoint(p);
|
|
2190
|
-
const last =
|
|
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 {
|
|
|
2294
2488
|
return;
|
|
2295
2489
|
}
|
|
2296
2490
|
case "scale": {
|
|
2297
|
-
const
|
|
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 {
|
|
|
2316
2512
|
this.interaction = { type: "marquee", bounds };
|
|
2317
2513
|
if (drag.anchorsMode) {
|
|
2318
2514
|
const inside = getVisibleAnchors(this.view()).filter((r) => {
|
|
2319
|
-
const a =
|
|
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 {
|
|
|
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 ?
|
|
2535
|
+
const lead = this.anchors.length ? getAnchorWorld(drag.base, this.anchors[0]) : void 0;
|
|
2340
2536
|
({ dx, dy } = this.snapDelta(dx, dy, 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 {
|
|
|
2344
2540
|
return;
|
|
2345
2541
|
}
|
|
2346
2542
|
case "handle": {
|
|
2347
|
-
const anchor =
|
|
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
|
-
|
|
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 =
|
|
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 {
|
|
|
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);
|
|
@@ -2540,7 +2737,7 @@ var VectorEditorController = class {
|
|
|
2540
2737
|
const id = this.hover?.segment?.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;
|
|
@@ -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: node.opacity ?? 1, children };
|
|
2897
|
+
return { kind: "group", id: node.id, ...node.transform ? { transform: fmtMatrix(node.transform) } : {}, opacity: 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,6 +2903,7 @@ 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,
|
|
@@ -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
|
|
2737
|
-
|
|
2935
|
+
const view = [v.zoom, 0, 0, v.zoom, v.x, v.y];
|
|
2936
|
+
const parent = vectorParentMatrix(state.document.nodes, node.id) ?? IDENTITY_MATRIX;
|
|
2937
|
+
return collectWithMatrix(node, parent).map(({ path, parent: m }) => bezierPathToSvg(transformBezierPath(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];
|
|
@@ -2756,29 +2961,27 @@ function getVectorOverlayRenderData(state) {
|
|
|
2756
2961
|
if (state.editingId) outline(state.editingId, true);
|
|
2757
2962
|
if (state.hover?.nodeId && !state.interaction) outline(state.hover.nodeId, false);
|
|
2758
2963
|
let selectionBox = null;
|
|
2759
|
-
if (state.tool === "select" && state.
|
|
2760
|
-
const
|
|
2761
|
-
|
|
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 && state.interaction?.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 =
|
|
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
2974
|
return { ref, x: s.x, y: s.y, type: a.type ?? inferAnchorType(a), selected: selected(ref), hovered: sameAnchor(state.hover?.anchor, ref), closes };
|
|
2772
2975
|
});
|
|
2773
2976
|
if (state.tool === "pen" && !state.pen && state.hover?.anchor) {
|
|
2774
|
-
const a =
|
|
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: a.type ?? "corner", selected: false, hovered: true, closes: false });
|
|
2778
2981
|
}
|
|
2779
2982
|
}
|
|
2780
2983
|
const handles = getVisibleHandles(state).flatMap((ref) => {
|
|
2781
|
-
const a =
|
|
2984
|
+
const a = getAnchorWorld(doc, ref);
|
|
2782
2985
|
const h = a?.[ref.handle];
|
|
2783
2986
|
if (!a || !h) return [];
|
|
2784
2987
|
const s = toScreen(state, h);
|
|
@@ -2796,7 +2999,7 @@ function getVectorOverlayRenderData(state) {
|
|
|
2796
2999
|
if (state.tool === "pen" && state.pen && state.pointer && !state.interaction) {
|
|
2797
3000
|
const node = findVectorNode(doc.nodes, state.pen.nodeId);
|
|
2798
3001
|
const sp = node?.type === "path" ? node.subpaths[state.pen.subpath] : void 0;
|
|
2799
|
-
const last = sp
|
|
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);
|
|
@@ -2811,7 +3014,7 @@ function getVectorOverlayRenderData(state) {
|
|
|
2811
3014
|
if (it?.type === "rotate") status = `${Math.round(it.angle)}\xB0`;
|
|
2812
3015
|
else if (it?.type === "move" || it?.type === "anchors") status = `\u0394 ${fmt(it.dx)}, ${fmt(it.dy)}`;
|
|
2813
3016
|
else if (it?.type === "scale" || it?.type === "draw") {
|
|
2814
|
-
const b = it.type === "draw" ? findVectorNode(doc.nodes, it.nodeId) ? vectorNodeBounds(findVectorNode(doc.nodes, it.nodeId)) : null : state.
|
|
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 };
|
|
@@ -2908,12 +3111,14 @@ function createVectorCommands(controller, options = {}) {
|
|
|
2908
3111
|
const out = [];
|
|
2909
3112
|
const visit = (nodes, depth) => {
|
|
2910
3113
|
for (const n of nodes) {
|
|
2911
|
-
const
|
|
3114
|
+
const parent = 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,
|
|
@@ -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
|
|
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: {
|
|
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(params.dx ?? 0, params.dy ?? 0, { ids: target }));
|
|
3028
3241
|
if (params.sx !== void 0 || params.sy !== void 0) check(controller.scale(params.sx ?? 1, 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
|
},
|
|
@@ -3179,6 +3393,7 @@ export {
|
|
|
3179
3393
|
VECTOR_TRANSFORM_HANDLES,
|
|
3180
3394
|
VectorEditorController,
|
|
3181
3395
|
applyMatrix,
|
|
3396
|
+
applyMatrixLinear,
|
|
3182
3397
|
bezierToSubpaths,
|
|
3183
3398
|
boundsFromPoints,
|
|
3184
3399
|
boundsIntersect,
|
|
@@ -3189,13 +3404,17 @@ export {
|
|
|
3189
3404
|
collectPathNodes,
|
|
3190
3405
|
convertAnchor,
|
|
3191
3406
|
createVectorCommands,
|
|
3407
|
+
decomposeRigid,
|
|
3192
3408
|
ellipseSubpath,
|
|
3193
3409
|
findVectorNode,
|
|
3194
3410
|
getAnchor,
|
|
3411
|
+
getAnchorWorld,
|
|
3195
3412
|
getEditablePathIds,
|
|
3196
3413
|
getTransformHandlePositions,
|
|
3197
3414
|
getVectorOverlayRenderData,
|
|
3198
3415
|
getVectorSceneRenderData,
|
|
3416
|
+
getVectorSelectionBoxGeometry,
|
|
3417
|
+
getVectorSelectionFrame,
|
|
3199
3418
|
getVectorSelectionStyle,
|
|
3200
3419
|
getVisibleAnchors,
|
|
3201
3420
|
getVisibleHandles,
|
|
@@ -3204,12 +3423,16 @@ export {
|
|
|
3204
3423
|
hitTestVectorNodes,
|
|
3205
3424
|
inferAnchorType,
|
|
3206
3425
|
insertAnchor,
|
|
3426
|
+
invertMatrix,
|
|
3427
|
+
isIdentityMatrix,
|
|
3207
3428
|
isVectorTextTarget,
|
|
3208
3429
|
lineSubpath,
|
|
3209
3430
|
locateVectorNode,
|
|
3431
|
+
matrixRotation,
|
|
3210
3432
|
moveAnchorHandle,
|
|
3211
3433
|
multiplyMatrices,
|
|
3212
3434
|
nearestSegment,
|
|
3435
|
+
nodeMatrix,
|
|
3213
3436
|
parseSvgTransform,
|
|
3214
3437
|
parseVectorSvg,
|
|
3215
3438
|
polygonSubpath,
|
|
@@ -3233,11 +3456,16 @@ export {
|
|
|
3233
3456
|
unionBounds,
|
|
3234
3457
|
vectorDocumentToSvg,
|
|
3235
3458
|
vectorNodeBounds,
|
|
3459
|
+
vectorNodeRotation,
|
|
3236
3460
|
vectorNodeToBezier,
|
|
3237
3461
|
vectorPaintToHex,
|
|
3238
3462
|
vectorPaintToSvg,
|
|
3463
|
+
vectorParentMatrix,
|
|
3239
3464
|
vectorPathData,
|
|
3465
|
+
vectorPathWorldBezier,
|
|
3466
|
+
vectorResizeCursor,
|
|
3240
3467
|
vectorToolLabel,
|
|
3468
|
+
vectorWorldMatrix,
|
|
3241
3469
|
walkVectorNodes
|
|
3242
3470
|
};
|
|
3243
3471
|
//# sourceMappingURL=index.js.map
|