@pixerate/editor 0.30.0 → 0.32.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.
@@ -0,0 +1,3243 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class;
2
+
3
+ var _chunkQKLRAA4Tcjs = require('../chunk-QKLRAA4T.cjs');
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+ var _chunkM4XE4UMTcjs = require('../chunk-M4XE4UMT.cjs');
19
+
20
+ // src/vector/types.ts
21
+ var VECTOR_TOOLS = ["select", "direct", "pen", "pencil", "rect", "ellipse", "polygon", "star", "line"];
22
+ var VECTOR_TRANSFORM_HANDLES = ["nw", "n", "ne", "e", "se", "s", "sw", "w", "rotate"];
23
+
24
+ // src/vector/geometry.ts
25
+ var EPS = 1e-9;
26
+ var cloneAnchor = (a) => ({
27
+ x: a.x,
28
+ y: a.y,
29
+ ...a.in ? { in: { x: a.in.x, y: a.in.y } } : {},
30
+ ...a.out ? { out: { x: a.out.x, y: a.out.y } } : {},
31
+ ...a.type && a.type !== "corner" ? { type: a.type } : {}
32
+ });
33
+ var cloneSubpath = (s) => ({ anchors: s.anchors.map(cloneAnchor), closed: s.closed });
34
+ function cloneVectorNode(node) {
35
+ return JSON.parse(JSON.stringify(node));
36
+ }
37
+ function cloneVectorDocument(doc) {
38
+ return JSON.parse(JSON.stringify(doc));
39
+ }
40
+ function hasPaint(paint) {
41
+ if (paint === void 0 || paint === null) return false;
42
+ if (typeof paint === "string") {
43
+ const p = paint.trim().toLowerCase();
44
+ return p !== "" && p !== "none" && p !== "transparent";
45
+ }
46
+ return true;
47
+ }
48
+ var handleOf = (a, side) => _nullishCoalesce(a[side], () => ( { x: a.x, y: a.y }));
49
+ var isOnAnchor = (a, h) => !h || Math.abs(h.x - a.x) < EPS && Math.abs(h.y - a.y) < EPS;
50
+ function subpathSegmentCount(sp) {
51
+ const n = sp.anchors.length;
52
+ if (n < 2) return 0;
53
+ return sp.closed ? n : n - 1;
54
+ }
55
+ function subpathSegment(sp, i) {
56
+ const a = sp.anchors[i];
57
+ const b = sp.anchors[(i + 1) % sp.anchors.length];
58
+ if (isOnAnchor(a, a.out) && isOnAnchor(b, b.in)) return [[a.x, a.y], [b.x, b.y]];
59
+ const c1 = handleOf(a, "out");
60
+ const c2 = handleOf(b, "in");
61
+ return [[a.x, a.y], [c1.x, c1.y], [c2.x, c2.y], [b.x, b.y]];
62
+ }
63
+ function subpathsToBezier(subpaths) {
64
+ const out = [];
65
+ for (const sp of subpaths) {
66
+ if (sp.anchors.length === 0) continue;
67
+ const first = sp.anchors[0];
68
+ out.push({ type: "M", x: first.x, y: first.y });
69
+ const count = subpathSegmentCount(sp);
70
+ for (let i = 0; i < count; i++) {
71
+ const seg = subpathSegment(sp, i);
72
+ if (sp.closed && i === count - 1 && seg.length === 2) break;
73
+ if (seg.length === 2) out.push({ type: "L", x: seg[1][0], y: seg[1][1] });
74
+ else out.push({ type: "C", x1: seg[1][0], y1: seg[1][1], x2: seg[2][0], y2: seg[2][1], x: seg[3][0], y: seg[3][1] });
75
+ }
76
+ if (sp.closed) out.push({ type: "Z" });
77
+ }
78
+ return out;
79
+ }
80
+ function vectorNodeToBezier(node) {
81
+ return node.type === "path" ? subpathsToBezier(node.subpaths) : [];
82
+ }
83
+ function vectorPathData(input, precision = 3) {
84
+ const subpaths = Array.isArray(input) ? input : input.subpaths;
85
+ return _chunkM4XE4UMTcjs.bezierPathToSvg.call(void 0, subpathsToBezier(subpaths), { precision });
86
+ }
87
+ function inferAnchorType(a) {
88
+ if (isOnAnchor(a, a.in) || isOnAnchor(a, a.out)) return "corner";
89
+ const ix = a.in.x - a.x;
90
+ const iy = a.in.y - a.y;
91
+ const ox = a.out.x - a.x;
92
+ const oy = a.out.y - a.y;
93
+ const li = Math.hypot(ix, iy);
94
+ const lo = Math.hypot(ox, oy);
95
+ const cross = (ix * oy - iy * ox) / (li * lo);
96
+ const dot = (ix * ox + iy * oy) / (li * lo);
97
+ if (Math.abs(cross) > 1e-3 || dot > 0) return "corner";
98
+ return Math.abs(li - lo) <= 1e-3 * Math.max(li, lo) ? "symmetric" : "smooth";
99
+ }
100
+ function bezierToSubpaths(input) {
101
+ const path = _chunkM4XE4UMTcjs.toBezierPath.call(void 0, input);
102
+ const out = [];
103
+ let current = null;
104
+ const last = () => current.anchors[current.anchors.length - 1];
105
+ for (const cmd of path) {
106
+ switch (cmd.type) {
107
+ case "M":
108
+ current = { anchors: [{ x: cmd.x, y: cmd.y }], closed: false };
109
+ out.push(current);
110
+ break;
111
+ case "L":
112
+ current.anchors.push({ x: cmd.x, y: cmd.y });
113
+ break;
114
+ case "Q": {
115
+ const p0 = last();
116
+ p0.out = { x: p0.x + 2 / 3 * (cmd.x1 - p0.x), y: p0.y + 2 / 3 * (cmd.y1 - p0.y) };
117
+ current.anchors.push({ x: cmd.x, y: cmd.y, in: { x: cmd.x + 2 / 3 * (cmd.x1 - cmd.x), y: cmd.y + 2 / 3 * (cmd.y1 - cmd.y) } });
118
+ break;
119
+ }
120
+ case "C":
121
+ last().out = { x: cmd.x1, y: cmd.y1 };
122
+ current.anchors.push({ x: cmd.x, y: cmd.y, in: { x: cmd.x2, y: cmd.y2 } });
123
+ break;
124
+ case "Z": {
125
+ if (!current) break;
126
+ current.closed = true;
127
+ const anchors = current.anchors;
128
+ if (anchors.length > 1) {
129
+ const first = anchors[0];
130
+ const end = anchors[anchors.length - 1];
131
+ if (Math.abs(end.x - first.x) < 1e-7 && Math.abs(end.y - first.y) < 1e-7) {
132
+ if (end.in) first.in = end.in;
133
+ anchors.pop();
134
+ }
135
+ }
136
+ current = null;
137
+ break;
138
+ }
139
+ }
140
+ }
141
+ for (const sp of out) {
142
+ for (const a of sp.anchors) {
143
+ if (isOnAnchor(a, a.in)) delete a.in;
144
+ if (isOnAnchor(a, a.out)) delete a.out;
145
+ const type = inferAnchorType(a);
146
+ if (type !== "corner") a.type = type;
147
+ }
148
+ }
149
+ return out.filter((sp) => sp.anchors.length > 0);
150
+ }
151
+ function walkVectorNodes(nodes, fn) {
152
+ const visit = (list, parent, depth) => {
153
+ for (const node of list) {
154
+ fn(node, parent, depth);
155
+ if (node.type === "group") visit(node.children, node, depth + 1);
156
+ }
157
+ };
158
+ visit(nodes, null, 0);
159
+ }
160
+ function findVectorNode(nodes, id) {
161
+ for (const node of nodes) {
162
+ if (node.id === id) return node;
163
+ if (node.type === "group") {
164
+ const found = findVectorNode(node.children, id);
165
+ if (found) return found;
166
+ }
167
+ }
168
+ return void 0;
169
+ }
170
+ function locateVectorNode(nodes, id) {
171
+ const search = (list, parent) => {
172
+ for (let i = 0; i < list.length; i++) {
173
+ const node = list[i];
174
+ if (node.id === id) return { list, index: i, parent };
175
+ if (node.type === "group") {
176
+ const found = search(node.children, node);
177
+ if (found) return found;
178
+ }
179
+ }
180
+ return null;
181
+ };
182
+ return search(nodes, null);
183
+ }
184
+ function collectPathNodes(node) {
185
+ if (node.type === "path") return [node];
186
+ const out = [];
187
+ walkVectorNodes(node.children, (n) => {
188
+ if (n.type === "path") out.push(n);
189
+ });
190
+ return out;
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));
195
+ if (!b) return null;
196
+ return { x: b.minX, y: b.minY, width: b.maxX - b.minX, height: b.maxY - b.minY };
197
+ }
198
+ function unionBounds(list) {
199
+ let minX = Infinity;
200
+ let minY = Infinity;
201
+ let maxX = -Infinity;
202
+ let maxY = -Infinity;
203
+ for (const b of list) {
204
+ if (!b) continue;
205
+ minX = Math.min(minX, b.x);
206
+ minY = Math.min(minY, b.y);
207
+ maxX = Math.max(maxX, b.x + b.width);
208
+ maxY = Math.max(maxY, b.y + b.height);
209
+ }
210
+ if (minX === Infinity) return null;
211
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
212
+ }
213
+ function boundsIntersect(a, b) {
214
+ return a.x <= b.x + b.width && b.x <= a.x + a.width && a.y <= b.y + b.height && b.y <= a.y + a.height;
215
+ }
216
+ function boundsFromPoints(a, b) {
217
+ 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
+ }
219
+ function hitTestVectorNode(node, p, tolerance = 0) {
220
+ 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);
223
+ if (path.length === 0) return false;
224
+ const point = [p.x, p.y];
225
+ if (hasPaint(node.fill) && _chunkM4XE4UMTcjs.pointInBezierPath.call(void 0, path, point, _nullishCoalesce(node.fillRule, () => ( "nonzero")))) return true;
226
+ const reach = (hasPaint(node.stroke) ? (_nullishCoalesce(node.strokeWidth, () => ( 1))) / 2 : 0) + tolerance;
227
+ return _chunkM4XE4UMTcjs.distanceToBezierPath.call(void 0, path, point) <= reach;
228
+ }
229
+ function hitTestVectorNodes(nodes, p, tolerance = 0) {
230
+ for (let i = nodes.length - 1; i >= 0; i--) {
231
+ const node = nodes[i];
232
+ if (node.visible === false || node.locked) continue;
233
+ if (hitTestVectorNode(node, p, tolerance)) return node;
234
+ }
235
+ return null;
236
+ }
237
+ function nearestSegment(node, p) {
238
+ let best = null;
239
+ node.subpaths.forEach((sp, s) => {
240
+ const count = subpathSegmentCount(sp);
241
+ for (let i = 0; i < count; i++) {
242
+ const seg = subpathSegment(sp, i);
243
+ const hit = _chunkM4XE4UMTcjs.segmentNearest.call(void 0, seg, [p.x, p.y]);
244
+ if (!best || hit.distance < best.distance) {
245
+ const pt = _chunkM4XE4UMTcjs.segmentPoint.call(void 0, seg, hit.t);
246
+ best = { subpath: s, segment: i, t: hit.t, point: { x: pt[0], y: pt[1] }, distance: hit.distance };
247
+ }
248
+ }
249
+ });
250
+ return best;
251
+ }
252
+ var IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0];
253
+ function multiplyMatrices(a, b) {
254
+ const [a0, a1, a2, a3, a4, a5] = a;
255
+ const [b0, b1, b2, b3, b4, b5] = b;
256
+ return [b0 * a0 + b2 * a1, b1 * a0 + b3 * a1, b0 * a2 + b2 * a3, b1 * a2 + b3 * a3, b0 * a4 + b2 * a5 + b4, b1 * a4 + b3 * a5 + b5];
257
+ }
258
+ var translateMatrix = (dx, dy) => [1, 0, 0, 1, dx, dy];
259
+ function scaleMatrix(sx, sy, origin = { x: 0, y: 0 }) {
260
+ return [sx, 0, 0, sy, origin.x - sx * origin.x, origin.y - sy * origin.y];
261
+ }
262
+ function rotateMatrix(degrees, origin = { x: 0, y: 0 }) {
263
+ const r = degrees * Math.PI / 180;
264
+ const cos = Math.cos(r);
265
+ const sin = Math.sin(r);
266
+ return [cos, sin, -sin, cos, origin.x - cos * origin.x + sin * origin.y, origin.y - sin * origin.x - cos * origin.y];
267
+ }
268
+ function applyMatrix(m, p) {
269
+ return { x: m[0] * p.x + m[2] * p.y + m[4], y: m[1] * p.x + m[3] * p.y + m[5] };
270
+ }
271
+ function transformVectorNode(node, m) {
272
+ if (node.type === "group") return { ...node, children: node.children.map((c) => transformVectorNode(c, m)) };
273
+ const path = node;
274
+ return {
275
+ ...path,
276
+ subpaths: path.subpaths.map((sp) => ({
277
+ closed: sp.closed,
278
+ anchors: sp.anchors.map((a) => {
279
+ const p = applyMatrix(m, a);
280
+ const next = { x: p.x, y: p.y };
281
+ if (a.in) next.in = applyMatrix(m, a.in);
282
+ if (a.out) next.out = applyMatrix(m, a.out);
283
+ if (a.type) next.type = a.type;
284
+ return next;
285
+ })
286
+ }))
287
+ };
288
+ }
289
+ function translateAnchor(a, dx, dy) {
290
+ const next = cloneAnchor(a);
291
+ next.x += dx;
292
+ next.y += dy;
293
+ if (next.in) next.in = { x: next.in.x + dx, y: next.in.y + dy };
294
+ if (next.out) next.out = { x: next.out.x + dx, y: next.out.y + dy };
295
+ return next;
296
+ }
297
+ function moveAnchorHandle(a, side, pos, breakHandles = false) {
298
+ const next = cloneAnchor(a);
299
+ next[side] = { x: pos.x, y: pos.y };
300
+ const other = side === "in" ? "out" : "in";
301
+ const type = breakHandles ? "corner" : _nullishCoalesce(a.type, () => ( "corner"));
302
+ if (breakHandles) delete next.type;
303
+ const dx = pos.x - a.x;
304
+ const dy = pos.y - a.y;
305
+ const len = Math.hypot(dx, dy);
306
+ if (type === "symmetric") next[other] = { x: a.x - dx, y: a.y - dy };
307
+ else if (type === "smooth" && len > EPS) {
308
+ const o = a[other];
309
+ const olen = o ? Math.hypot(o.x - a.x, o.y - a.y) : len;
310
+ next[other] = { x: a.x - dx / len * olen, y: a.y - dy / len * olen };
311
+ }
312
+ if (isOnAnchor(next, next.in)) delete next.in;
313
+ if (isOnAnchor(next, next.out)) delete next.out;
314
+ return next;
315
+ }
316
+ function convertAnchor(sp, index, type) {
317
+ const anchors = sp.anchors.map(cloneAnchor);
318
+ const a = anchors[index];
319
+ if (!a) return { anchors, closed: sp.closed };
320
+ if (type === "corner") {
321
+ delete a.in;
322
+ delete a.out;
323
+ delete a.type;
324
+ return { anchors, closed: sp.closed };
325
+ }
326
+ const n = anchors.length;
327
+ const prev = index > 0 ? anchors[index - 1] : sp.closed ? anchors[n - 1] : null;
328
+ const next = index < n - 1 ? anchors[index + 1] : sp.closed ? anchors[0] : null;
329
+ let dir = null;
330
+ let lenIn = 0;
331
+ let lenOut = 0;
332
+ if (!isOnAnchor(a, a.out)) dir = { x: a.out.x - a.x, y: a.out.y - a.y };
333
+ else if (!isOnAnchor(a, a.in)) dir = { x: a.x - a.in.x, y: a.y - a.in.y };
334
+ else if (prev && next) dir = { x: next.x - prev.x, y: next.y - prev.y };
335
+ else if (next) dir = { x: next.x - a.x, y: next.y - a.y };
336
+ else if (prev) dir = { x: a.x - prev.x, y: a.y - prev.y };
337
+ if (!dir || Math.hypot(dir.x, dir.y) < EPS) return { anchors, closed: sp.closed };
338
+ const dl = Math.hypot(dir.x, dir.y);
339
+ const ux = dir.x / dl;
340
+ const uy = dir.y / dl;
341
+ lenIn = a.in && !isOnAnchor(a, a.in) ? Math.hypot(a.in.x - a.x, a.in.y - a.y) : prev ? Math.hypot(prev.x - a.x, prev.y - a.y) / 3 : 0;
342
+ lenOut = a.out && !isOnAnchor(a, a.out) ? Math.hypot(a.out.x - a.x, a.out.y - a.y) : next ? Math.hypot(next.x - a.x, next.y - a.y) / 3 : 0;
343
+ if (type === "symmetric") lenIn = lenOut = (lenIn + lenOut) / 2 || Math.max(lenIn, lenOut);
344
+ if (lenIn > EPS) a.in = { x: a.x - ux * lenIn, y: a.y - uy * lenIn };
345
+ if (lenOut > EPS) a.out = { x: a.x + ux * lenOut, y: a.y + uy * lenOut };
346
+ a.type = type;
347
+ return { anchors, closed: sp.closed };
348
+ }
349
+ function insertAnchor(sp, segment, t) {
350
+ const anchors = sp.anchors.map(cloneAnchor);
351
+ const n = anchors.length;
352
+ const ia = segment;
353
+ const ib = (segment + 1) % n;
354
+ const seg = subpathSegment(sp, segment);
355
+ const [left, right] = _chunkM4XE4UMTcjs.splitSegment.call(void 0, seg, t);
356
+ const mid = left[left.length - 1];
357
+ const anchor = { x: mid[0], y: mid[1] };
358
+ if (seg.length === 4) {
359
+ anchors[ia].out = { x: left[1][0], y: left[1][1] };
360
+ anchor.in = { x: left[2][0], y: left[2][1] };
361
+ anchor.out = { x: right[1][0], y: right[1][1] };
362
+ anchor.type = "smooth";
363
+ anchors[ib].in = { x: right[2][0], y: right[2][1] };
364
+ for (const k of [ia, ib]) if (isOnAnchor(anchors[k], anchors[k].in)) delete anchors[k].in;
365
+ for (const k of [ia, ib]) if (isOnAnchor(anchors[k], anchors[k].out)) delete anchors[k].out;
366
+ }
367
+ const index = segment + 1;
368
+ anchors.splice(index, 0, anchor);
369
+ return { subpath: { anchors, closed: sp.closed }, index };
370
+ }
371
+ function removeAnchors(sp, indices) {
372
+ const drop = new Set(indices);
373
+ return { anchors: sp.anchors.filter((_, i) => !drop.has(i)).map(cloneAnchor), closed: sp.closed };
374
+ }
375
+ function reverseSubpath(sp) {
376
+ const anchors = sp.anchors.slice().reverse().map((a) => {
377
+ const next = { x: a.x, y: a.y };
378
+ if (a.out) next.in = { ...a.out };
379
+ if (a.in) next.out = { ...a.in };
380
+ if (a.type) next.type = a.type;
381
+ return next;
382
+ });
383
+ return { anchors, closed: sp.closed };
384
+ }
385
+ var KAPPA = 0.5522847498307936;
386
+ function rectSubpath(b, radius = 0) {
387
+ const { x, y, width: w, height: h } = b;
388
+ const r = Math.max(0, Math.min(radius, w / 2, h / 2));
389
+ if (r <= EPS) {
390
+ return { anchors: [{ x, y }, { x: x + w, y }, { x: x + w, y: y + h }, { x, y: y + h }], closed: true };
391
+ }
392
+ const k = r * KAPPA;
393
+ const anchors = [
394
+ { x: x + r, y, in: { x: x + r - k, y } },
395
+ { x: x + w - r, y, out: { x: x + w - r + k, y } },
396
+ { x: x + w, y: y + r, in: { x: x + w, y: y + r - k } },
397
+ { x: x + w, y: y + h - r, out: { x: x + w, y: y + h - r + k } },
398
+ { x: x + w - r, y: y + h, in: { x: x + w - r + k, y: y + h } },
399
+ { x: x + r, y: y + h, out: { x: x + r - k, y: y + h } },
400
+ { x, y: y + h - r, in: { x, y: y + h - r + k } },
401
+ { x, y: y + r, out: { x, y: y + r - k } }
402
+ ];
403
+ return { anchors, closed: true };
404
+ }
405
+ function ellipseSubpath(b) {
406
+ const rx = b.width / 2;
407
+ const ry = b.height / 2;
408
+ const cx = b.x + rx;
409
+ const cy = b.y + ry;
410
+ const kx = rx * KAPPA;
411
+ const ky = ry * KAPPA;
412
+ return {
413
+ closed: true,
414
+ anchors: [
415
+ { x: cx, y: cy - ry, in: { x: cx - kx, y: cy - ry }, out: { x: cx + kx, y: cy - ry }, type: "symmetric" },
416
+ { x: cx + rx, y: cy, in: { x: cx + rx, y: cy - ky }, out: { x: cx + rx, y: cy + ky }, type: "symmetric" },
417
+ { x: cx, y: cy + ry, in: { x: cx + kx, y: cy + ry }, out: { x: cx - kx, y: cy + ry }, type: "symmetric" },
418
+ { x: cx - rx, y: cy, in: { x: cx - rx, y: cy + ky }, out: { x: cx - rx, y: cy - ky }, type: "symmetric" }
419
+ ]
420
+ };
421
+ }
422
+ function polygonSubpath(b, sides = 6) {
423
+ const n = Math.max(3, Math.round(sides));
424
+ const cx = b.x + b.width / 2;
425
+ const cy = b.y + b.height / 2;
426
+ const anchors = [];
427
+ for (let i = 0; i < n; i++) {
428
+ const t = -Math.PI / 2 + i * 2 * Math.PI / n;
429
+ anchors.push({ x: cx + Math.cos(t) * b.width / 2, y: cy + Math.sin(t) * b.height / 2 });
430
+ }
431
+ return { anchors, closed: true };
432
+ }
433
+ function starSubpath(b, points = 5, innerRatio = 0.5) {
434
+ const n = Math.max(3, Math.round(points));
435
+ const cx = b.x + b.width / 2;
436
+ const cy = b.y + b.height / 2;
437
+ const anchors = [];
438
+ for (let i = 0; i < n * 2; i++) {
439
+ const t = -Math.PI / 2 + i * Math.PI / n;
440
+ const r = i % 2 === 0 ? 1 : innerRatio;
441
+ anchors.push({ x: cx + Math.cos(t) * r * b.width / 2, y: cy + Math.sin(t) * r * b.height / 2 });
442
+ }
443
+ return { anchors, closed: true };
444
+ }
445
+ function lineSubpath(a, b) {
446
+ return { anchors: [{ x: a.x, y: a.y }, { x: b.x, y: b.y }], closed: false };
447
+ }
448
+ function roundSubpaths(subpaths, precision = 3) {
449
+ const f = 10 ** precision;
450
+ const r = (n) => Math.round(n * f) / f;
451
+ const rp = (p) => ({ x: r(p.x), y: r(p.y) });
452
+ return subpaths.map((sp) => ({
453
+ closed: sp.closed,
454
+ anchors: sp.anchors.map((a) => ({ ...cloneAnchor(a), x: r(a.x), y: r(a.y), ...a.in ? { in: rp(a.in) } : {}, ...a.out ? { out: rp(a.out) } : {} }))
455
+ }));
456
+ }
457
+
458
+ // src/vector/svg.ts
459
+ var escapeAttr = (value) => value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
460
+ function vectorPaintToSvg(paint, gradientId) {
461
+ if (!hasPaint(paint)) return { value: "none", gradient: null };
462
+ if (typeof paint === "string") return { value: paint, gradient: null };
463
+ return { value: `url(#${gradientId})`, gradient: _chunkM4XE4UMTcjs.gradientToSvgDef.call(void 0, paint, gradientId) };
464
+ }
465
+ function gradientMarkup(def) {
466
+ const tag = def.type === "radial" ? "radialGradient" : "linearGradient";
467
+ const attrs = Object.entries(def.attrs).map(([k, v]) => ` ${k}="${escapeAttr(String(v))}"`).join("");
468
+ const stops = def.stops.map((s) => `<stop offset="${s.offset}" stop-color="${escapeAttr(s.stopColor)}"${s.stopOpacity !== void 0 ? ` stop-opacity="${s.stopOpacity}"` : ""}/>`).join("");
469
+ return `<${tag} id="${escapeAttr(def.id)}"${attrs}>${stops}</${tag}>`;
470
+ }
471
+ function vectorDocumentToSvg(doc, options = {}) {
472
+ const precision = _nullishCoalesce(options.precision, () => ( 3));
473
+ const prefix = _nullishCoalesce(options.idPrefix, () => ( "vg-"));
474
+ const pretty = options.pretty !== false;
475
+ const defs = [];
476
+ const lines = [];
477
+ const num = (n) => _chunkM4XE4UMTcjs.formatNumber.call(void 0, n, precision);
478
+ const indent = (depth) => pretty ? " ".repeat(depth) : "";
479
+ const common = (node) => {
480
+ let out2 = ` id="${escapeAttr(node.id)}"`;
481
+ if (node.opacity !== void 0 && node.opacity < 1) out2 += ` opacity="${num(node.opacity)}"`;
482
+ if (node.visible === false) out2 += ` display="none"`;
483
+ return out2;
484
+ };
485
+ const emit = (node, depth) => {
486
+ if (node.visible === false && !options.includeHidden) return;
487
+ if (node.type === "group") {
488
+ lines.push(`${indent(depth)}<g${common(node)}>`);
489
+ for (const child of node.children) emit(child, depth + 1);
490
+ lines.push(`${indent(depth)}</g>`);
491
+ return;
492
+ }
493
+ const fill = vectorPaintToSvg(node.fill, `${prefix}fill-${node.id}`);
494
+ const stroke = vectorPaintToSvg(node.stroke, `${prefix}stroke-${node.id}`);
495
+ if (fill.gradient) defs.push(gradientMarkup(fill.gradient));
496
+ if (stroke.gradient) defs.push(gradientMarkup(stroke.gradient));
497
+ let attrs = `${common(node)} d="${vectorPathData(node, precision)}" fill="${escapeAttr(fill.value)}"`;
498
+ if (node.fillRule === "evenodd") attrs += ` fill-rule="evenodd"`;
499
+ if (stroke.value !== "none") {
500
+ attrs += ` stroke="${escapeAttr(stroke.value)}" stroke-width="${num(_nullishCoalesce(node.strokeWidth, () => ( 1)))}"`;
501
+ if (node.strokeLinecap && node.strokeLinecap !== "butt") attrs += ` stroke-linecap="${node.strokeLinecap}"`;
502
+ if (node.strokeLinejoin && node.strokeLinejoin !== "miter") attrs += ` stroke-linejoin="${node.strokeLinejoin}"`;
503
+ if (_optionalChain([node, 'access', _2 => _2.strokeDasharray, 'optionalAccess', _3 => _3.length])) attrs += ` stroke-dasharray="${node.strokeDasharray.map(num).join(" ")}"`;
504
+ }
505
+ lines.push(`${indent(depth)}<path${attrs}/>`);
506
+ };
507
+ for (const node of doc.nodes) emit(node, 1);
508
+ const w = num(doc.width);
509
+ const h = num(doc.height);
510
+ const nl = pretty ? "\n" : "";
511
+ const out = [`<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">`];
512
+ if (defs.length) out.push(`${indent(1)}<defs>${defs.join("")}</defs>`);
513
+ if (doc.background && options.background !== false && hasPaint(doc.background)) {
514
+ out.push(`${indent(1)}<rect width="${w}" height="${h}" fill="${escapeAttr(doc.background)}"/>`);
515
+ }
516
+ out.push(...lines, "</svg>");
517
+ return out.join(nl);
518
+ }
519
+ function parseXml(source) {
520
+ const text = source.replace(/<!--[\s\S]*?-->/g, "").replace(/<\?[\s\S]*?\?>/g, "").replace(/<!\[CDATA\[[\s\S]*?\]\]>/g, "").replace(/<!DOCTYPE[^>[]*(\[[\s\S]*?\])?\s*>/gi, "");
521
+ const root = { name: "#root", attrs: {}, children: [] };
522
+ const stack = [root];
523
+ const tag = /<(\/?)([A-Za-z_][\w:.-]*)((?:\s+[^\s=/>]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+))?)*)\s*(\/?)>/g;
524
+ const attr = /([^\s=/>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
525
+ let m;
526
+ while (m = tag.exec(text)) {
527
+ const [, closing, rawName, rawAttrs, selfClosing] = m;
528
+ const name = rawName.replace(/^svg:/, "");
529
+ if (closing) {
530
+ for (let i = stack.length - 1; i > 0; i--) {
531
+ if (stack[i].name === name) {
532
+ stack.length = i;
533
+ break;
534
+ }
535
+ }
536
+ continue;
537
+ }
538
+ const attrs = {};
539
+ let a;
540
+ attr.lastIndex = 0;
541
+ while (a = attr.exec(rawAttrs)) attrs[a[1]] = decodeEntities(_nullishCoalesce(_nullishCoalesce(_nullishCoalesce(a[2], () => ( a[3])), () => ( a[4])), () => ( "")));
542
+ const el = { name, attrs, children: [] };
543
+ stack[stack.length - 1].children.push(el);
544
+ if (!selfClosing) stack.push(el);
545
+ }
546
+ const find = (el) => {
547
+ if (el.name === "svg") return el;
548
+ for (const c of el.children) {
549
+ const f = find(c);
550
+ if (f) return f;
551
+ }
552
+ return null;
553
+ };
554
+ return find(root);
555
+ }
556
+ function decodeEntities(s) {
557
+ return s.replace(/&#x([0-9a-f]+);/gi, (_, h) => String.fromCodePoint(parseInt(h, 16))).replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(parseInt(d, 10))).replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
558
+ }
559
+ var numberList = (s) => (_nullishCoalesce(s, () => ( ""))).trim().split(/[\s,]+|(?=-)(?<![eE])/).filter(Boolean).map(Number).filter((n) => Number.isFinite(n));
560
+ var length = (s, fallback = 0) => {
561
+ if (s === void 0) return fallback;
562
+ const n = parseFloat(s);
563
+ return Number.isFinite(n) ? n : fallback;
564
+ };
565
+ function parseSvgTransform(value) {
566
+ let m = IDENTITY_MATRIX;
567
+ if (!value) return m;
568
+ const re = /(matrix|translate|scale|rotate|skewX|skewY)\s*\(([^)]*)\)/g;
569
+ let t;
570
+ while (t = re.exec(value)) {
571
+ const n = numberList(t[2]);
572
+ let step = IDENTITY_MATRIX;
573
+ switch (t[1]) {
574
+ case "matrix":
575
+ if (n.length >= 6) step = [n[0], n[1], n[2], n[3], n[4], n[5]];
576
+ break;
577
+ case "translate":
578
+ step = translateMatrix(_nullishCoalesce(n[0], () => ( 0)), _nullishCoalesce(n[1], () => ( 0)));
579
+ break;
580
+ case "scale":
581
+ step = scaleMatrix(_nullishCoalesce(n[0], () => ( 1)), _nullishCoalesce(_nullishCoalesce(n[1], () => ( n[0])), () => ( 1)));
582
+ break;
583
+ case "rotate":
584
+ step = rotateMatrix(_nullishCoalesce(n[0], () => ( 0)), { x: _nullishCoalesce(n[1], () => ( 0)), y: _nullishCoalesce(n[2], () => ( 0)) });
585
+ break;
586
+ case "skewX":
587
+ step = [1, 0, Math.tan((_nullishCoalesce(n[0], () => ( 0))) * Math.PI / 180), 1, 0, 0];
588
+ break;
589
+ case "skewY":
590
+ step = [1, Math.tan((_nullishCoalesce(n[0], () => ( 0))) * Math.PI / 180), 0, 1, 0, 0];
591
+ break;
592
+ }
593
+ m = multiplyMatrices(step, m);
594
+ }
595
+ return m;
596
+ }
597
+ var STYLE_KEYS = {
598
+ fill: "fill",
599
+ stroke: "stroke",
600
+ "stroke-width": "strokeWidth",
601
+ "stroke-linecap": "strokeLinecap",
602
+ "stroke-linejoin": "strokeLinejoin",
603
+ "stroke-dasharray": "strokeDasharray",
604
+ "fill-rule": "fillRule"
605
+ };
606
+ function readStyle(el, parent) {
607
+ const out = { ...parent };
608
+ for (const [attr, key] of Object.entries(STYLE_KEYS)) if (el.attrs[attr] !== void 0) out[key] = el.attrs[attr].trim();
609
+ for (const decl of (_nullishCoalesce(el.attrs.style, () => ( ""))).split(";")) {
610
+ const i = decl.indexOf(":");
611
+ if (i < 0) continue;
612
+ const key = STYLE_KEYS[decl.slice(0, i).trim()];
613
+ if (key) out[key] = decl.slice(i + 1).replace(/!important/, "").trim();
614
+ }
615
+ return out;
616
+ }
617
+ function readStops(el) {
618
+ return el.children.filter((c) => c.name === "stop").map((c) => {
619
+ const style = Object.fromEntries(
620
+ (_nullishCoalesce(c.attrs.style, () => ( ""))).split(";").map((d) => d.split(":").map((s) => s.trim())).filter((d) => d.length === 2)
621
+ );
622
+ const raw = _nullishCoalesce(c.attrs.offset, () => ( "0"));
623
+ const offset = raw.endsWith("%") ? parseFloat(raw) / 100 : parseFloat(raw);
624
+ const opacity = _nullishCoalesce(style["stop-opacity"], () => ( c.attrs["stop-opacity"]));
625
+ return {
626
+ offset: Number.isFinite(offset) ? Math.max(0, Math.min(1, offset)) : 0,
627
+ color: _nullishCoalesce(_nullishCoalesce(style["stop-color"], () => ( c.attrs["stop-color"])), () => ( "#000")),
628
+ ...opacity !== void 0 ? { opacity: parseFloat(opacity) } : {}
629
+ };
630
+ });
631
+ }
632
+ var fraction = (s, fallback) => {
633
+ if (s === void 0) return fallback;
634
+ const n = parseFloat(s);
635
+ if (!Number.isFinite(n)) return fallback;
636
+ return s.trim().endsWith("%") ? n / 100 : n;
637
+ };
638
+ function collectGradients(el, raw) {
639
+ if ((el.name === "linearGradient" || el.name === "radialGradient") && el.attrs.id) raw.set(el.attrs.id, el);
640
+ for (const c of el.children) collectGradients(c, raw);
641
+ }
642
+ function resolveGradient(id, raw, seen = /* @__PURE__ */ new Set()) {
643
+ const el = raw.get(id);
644
+ if (!el || seen.has(id)) return null;
645
+ seen.add(id);
646
+ let stops = readStops(el);
647
+ const href = _nullishCoalesce(el.attrs.href, () => ( el.attrs["xlink:href"]));
648
+ if (!stops.length && _optionalChain([href, 'optionalAccess', _4 => _4.startsWith, 'call', _5 => _5("#")])) stops = _nullishCoalesce(_optionalChain([resolveGradient, 'call', _6 => _6(href.slice(1), raw, seen), 'optionalAccess', _7 => _7.stops]), () => ( []));
649
+ if (!stops.length) return null;
650
+ if (el.name === "radialGradient") {
651
+ return { type: "radial", cx: fraction(el.attrs.cx, 0.5), cy: fraction(el.attrs.cy, 0.5), r: fraction(el.attrs.r, 0.5), stops };
652
+ }
653
+ const x1 = fraction(el.attrs.x1, 0);
654
+ const y1 = fraction(el.attrs.y1, 0);
655
+ const x2 = fraction(el.attrs.x2, 1);
656
+ const y2 = fraction(el.attrs.y2, 0);
657
+ const angle = Math.round((Math.atan2(x2 - x1, -(y2 - y1)) * 180 / Math.PI + 360) % 360);
658
+ return { type: "linear", angle, stops };
659
+ }
660
+ function parseVectorSvg(svg, options = {}) {
661
+ const root = parseXml(svg);
662
+ if (!root) throw new Error("The markup has no <svg> element.");
663
+ let counter = 0;
664
+ const usedIds = /* @__PURE__ */ new Set();
665
+ const createId = _nullishCoalesce(options.createId, () => ( (() => `node-${++counter}`)));
666
+ const idFor = (el) => {
667
+ const id = el.attrs.id;
668
+ if (id && !usedIds.has(id)) {
669
+ usedIds.add(id);
670
+ return id;
671
+ }
672
+ let next = createId();
673
+ while (usedIds.has(next)) next = createId();
674
+ usedIds.add(next);
675
+ return next;
676
+ };
677
+ const rawGradients = /* @__PURE__ */ new Map();
678
+ collectGradients(root, rawGradients);
679
+ const paint = (value) => {
680
+ const v = value.trim();
681
+ const url = /^url\(\s*['"]?#([^'")\s]+)['"]?\s*\)/.exec(v);
682
+ if (url) return _nullishCoalesce(resolveGradient(url[1], rawGradients), () => ( null));
683
+ if (v === "none" || v === "") return null;
684
+ return v;
685
+ };
686
+ const viewBox = numberList(root.attrs.viewBox);
687
+ const width = viewBox.length === 4 ? viewBox[2] : length(root.attrs.width, 300);
688
+ const height = viewBox.length === 4 ? viewBox[3] : length(root.attrs.height, 150);
689
+ const rootMatrix = viewBox.length === 4 ? translateMatrix(-viewBox[0], -viewBox[1]) : IDENTITY_MATRIX;
690
+ const SKIP = /* @__PURE__ */ new Set(["defs", "clipPath", "mask", "symbol", "style", "title", "desc", "metadata", "pattern", "marker", "filter", "text", "image", "use", "linearGradient", "radialGradient", "script", "foreignObject"]);
691
+ const shapeOf = (el) => {
692
+ const a = el.attrs;
693
+ switch (el.name) {
694
+ case "path":
695
+ return a.d ? bezierToSubpaths(a.d) : null;
696
+ case "rect": {
697
+ const w = length(a.width);
698
+ const h = length(a.height);
699
+ if (w <= 0 || h <= 0) return null;
700
+ const r = length(_nullishCoalesce(a.rx, () => ( a.ry)), 0);
701
+ return [rectSubpath({ x: length(a.x), y: length(a.y), width: w, height: h }, r)];
702
+ }
703
+ case "circle": {
704
+ const r = length(a.r);
705
+ if (r <= 0) return null;
706
+ const cx = length(a.cx);
707
+ const cy = length(a.cy);
708
+ return [ellipseSubpath({ x: cx - r, y: cy - r, width: 2 * r, height: 2 * r })];
709
+ }
710
+ case "ellipse": {
711
+ const rx = length(a.rx);
712
+ const ry = length(a.ry);
713
+ if (rx <= 0 || ry <= 0) return null;
714
+ return [ellipseSubpath({ x: length(a.cx) - rx, y: length(a.cy) - ry, width: 2 * rx, height: 2 * ry })];
715
+ }
716
+ case "line":
717
+ return [lineSubpath({ x: length(a.x1), y: length(a.y1) }, { x: length(a.x2), y: length(a.y2) })];
718
+ case "polyline":
719
+ case "polygon": {
720
+ const n = numberList(a.points);
721
+ if (n.length < 4) return null;
722
+ const anchors = [];
723
+ for (let i = 0; i + 1 < n.length; i += 2) anchors.push({ x: n[i], y: n[i + 1] });
724
+ return [{ anchors, closed: el.name === "polygon" }];
725
+ }
726
+ }
727
+ return null;
728
+ };
729
+ const visit = (el, style, matrix) => {
730
+ const out = [];
731
+ for (const child of el.children) {
732
+ if (SKIP.has(child.name)) continue;
733
+ const m = multiplyMatrices(parseSvgTransform(child.attrs.transform), matrix);
734
+ const s = readStyle(child, style);
735
+ const opacity = child.attrs.opacity !== void 0 ? parseFloat(child.attrs.opacity) : void 0;
736
+ const hidden = child.attrs.display === "none" || child.attrs.visibility === "hidden";
737
+ const meta = {
738
+ ...child.attrs["inkscape:label"] || child.attrs["data-name"] ? { name: _nullishCoalesce(child.attrs["inkscape:label"], () => ( child.attrs["data-name"])) } : {},
739
+ ...opacity !== void 0 && Number.isFinite(opacity) && opacity < 1 ? { opacity } : {},
740
+ ...hidden ? { visible: false } : {}
741
+ };
742
+ if (child.name === "g" || child.name === "svg" || child.name === "a" || child.name === "switch") {
743
+ const children = visit(child, s, m);
744
+ if (!children.length) continue;
745
+ const group = { id: idFor(child), type: "group", ...meta, children };
746
+ out.push(group);
747
+ continue;
748
+ }
749
+ const subpaths = shapeOf(child);
750
+ if (!subpaths || !subpaths.length) continue;
751
+ let node = {
752
+ id: idFor(child),
753
+ type: "path",
754
+ ...meta,
755
+ subpaths,
756
+ fill: child.name === "line" ? null : paint(s.fill),
757
+ stroke: paint(s.stroke),
758
+ ...s.strokeWidth ? { strokeWidth: length(s.strokeWidth, 1) } : {},
759
+ ...s.strokeLinecap ? { strokeLinecap: s.strokeLinecap } : {},
760
+ ...s.strokeLinejoin ? { strokeLinejoin: s.strokeLinejoin } : {},
761
+ ...s.strokeDasharray && s.strokeDasharray !== "none" ? { strokeDasharray: numberList(s.strokeDasharray) } : {},
762
+ ...s.fillRule === "evenodd" ? { fillRule: "evenodd" } : {}
763
+ };
764
+ if (m !== IDENTITY_MATRIX) {
765
+ node = transformVectorNode(node, m);
766
+ const strokeScale = Math.sqrt(Math.abs(m[0] * m[3] - m[1] * m[2]));
767
+ if (hasPaint(node.stroke) && strokeScale > 0 && Math.abs(strokeScale - 1) > 1e-9) node.strokeWidth = (_nullishCoalesce(node.strokeWidth, () => ( 1))) * strokeScale;
768
+ }
769
+ out.push(node);
770
+ }
771
+ return out;
772
+ };
773
+ const nodes = visit(root, readStyle(root, { fill: "#000", stroke: "none", strokeWidth: "" }), rootMatrix);
774
+ return { width, height, nodes };
775
+ }
776
+
777
+ // src/vector/controller.ts
778
+ var VECTOR_CLIPBOARD_TYPE = "pixerate/vector";
779
+ var DEFAULT_DEFAULTS = {
780
+ fill: "#d4d4d8",
781
+ stroke: "#18181b",
782
+ strokeWidth: 2,
783
+ polygonSides: 6,
784
+ starPoints: 5,
785
+ starInnerRatio: 0.5,
786
+ cornerRadius: 0
787
+ };
788
+ var MIN_ZOOM = 0.05;
789
+ var MAX_ZOOM = 64;
790
+ var VECTOR_ROTATE_HANDLE_OFFSET = 24;
791
+ var sameRef = (a, b) => a.nodeId === b.nodeId && a.subpath === b.subpath && a.index === b.index;
792
+ function getEditablePathIds(state) {
793
+ if (state.tool !== "direct" && state.tool !== "pen") return [];
794
+ const ids = /* @__PURE__ */ new Set();
795
+ if (state.pen) ids.add(state.pen.nodeId);
796
+ if (state.tool === "direct") {
797
+ if (state.editingId) ids.add(state.editingId);
798
+ for (const id of state.selection) {
799
+ const node = findVectorNode(state.document.nodes, id);
800
+ if (node && !node.locked) {
801
+ for (const p of collectPathNodes(node)) if (!p.locked) ids.add(p.id);
802
+ }
803
+ }
804
+ }
805
+ return [...ids].filter((id) => _optionalChain([findVectorNode, 'call', _8 => _8(state.document.nodes, id), 'optionalAccess', _9 => _9.type]) === "path");
806
+ }
807
+ function getVisibleAnchors(state) {
808
+ const out = [];
809
+ for (const id of getEditablePathIds(state)) {
810
+ const node = findVectorNode(state.document.nodes, id);
811
+ node.subpaths.forEach((sp, s) => sp.anchors.forEach((_, i) => out.push({ nodeId: id, subpath: s, index: i })));
812
+ }
813
+ return out;
814
+ }
815
+ function getVisibleHandles(state) {
816
+ const out = [];
817
+ const seen = /* @__PURE__ */ new Set();
818
+ const push = (node, s, i, handle) => {
819
+ const sp = node.subpaths[s];
820
+ const a = _optionalChain([sp, 'optionalAccess', _10 => _10.anchors, 'access', _11 => _11[i]]);
821
+ if (!a || !a[handle]) return;
822
+ const key = `${node.id}:${s}:${i}:${handle}`;
823
+ if (seen.has(key)) return;
824
+ seen.add(key);
825
+ out.push({ nodeId: node.id, subpath: s, index: i, handle });
826
+ };
827
+ if (state.tool === "pen" && state.pen) {
828
+ const node = findVectorNode(state.document.nodes, state.pen.nodeId);
829
+ const sp = _optionalChain([node, 'optionalAccess', _12 => _12.type]) === "path" ? node.subpaths[state.pen.subpath] : void 0;
830
+ if (_optionalChain([node, 'optionalAccess', _13 => _13.type]) === "path" && sp) {
831
+ const last = sp.anchors.length - 1;
832
+ push(node, state.pen.subpath, last, "in");
833
+ push(node, state.pen.subpath, last, "out");
834
+ }
835
+ return out;
836
+ }
837
+ if (state.tool !== "direct") return out;
838
+ const editable = new Set(getEditablePathIds(state));
839
+ for (const ref of state.selectedAnchors) {
840
+ if (!editable.has(ref.nodeId)) continue;
841
+ const node = findVectorNode(state.document.nodes, ref.nodeId);
842
+ const sp = node.subpaths[ref.subpath];
843
+ if (!sp) continue;
844
+ const n = sp.anchors.length;
845
+ push(node, ref.subpath, ref.index, "in");
846
+ push(node, ref.subpath, ref.index, "out");
847
+ const prev = ref.index > 0 ? ref.index - 1 : sp.closed ? n - 1 : -1;
848
+ const next = ref.index < n - 1 ? ref.index + 1 : sp.closed ? 0 : -1;
849
+ if (prev >= 0) push(node, ref.subpath, prev, "out");
850
+ if (next >= 0) push(node, ref.subpath, next, "in");
851
+ }
852
+ return out;
853
+ }
854
+ function getAnchor(doc, ref) {
855
+ const node = findVectorNode(doc.nodes, ref.nodeId);
856
+ return _optionalChain([node, 'optionalAccess', _14 => _14.type]) === "path" ? _optionalChain([node, 'access', _15 => _15.subpaths, 'access', _16 => _16[ref.subpath], 'optionalAccess', _17 => _17.anchors, 'access', _18 => _18[ref.index]]) : void 0;
857
+ }
858
+ function getTransformHandlePositions(box) {
859
+ const { x, y, width: w, height: h } = box;
860
+ return [
861
+ { handle: "nw", x, y },
862
+ { handle: "n", x: x + w / 2, y },
863
+ { handle: "ne", x: x + w, y },
864
+ { handle: "e", x: x + w, y: y + h / 2 },
865
+ { handle: "se", x: x + w, y: y + h },
866
+ { handle: "s", x: x + w / 2, y: y + h },
867
+ { handle: "sw", x, y: y + h },
868
+ { handle: "w", x, y: y + h / 2 },
869
+ { handle: "rotate", x: x + w / 2, y: y - VECTOR_ROTATE_HANDLE_OFFSET }
870
+ ];
871
+ }
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
+ };
883
+ var TOOL_LABELS = {
884
+ select: "Select",
885
+ direct: "Direct Select",
886
+ pen: "Pen",
887
+ pencil: "Pencil",
888
+ rect: "Rectangle",
889
+ ellipse: "Ellipse",
890
+ polygon: "Polygon",
891
+ star: "Star",
892
+ line: "Line"
893
+ };
894
+ var TOOL_KEYS = { v: "select", a: "direct", p: "pen", n: "pencil", r: "rect", o: "ellipse", e: "ellipse", y: "polygon", s: "star", l: "line" };
895
+ function vectorToolLabel(tool) {
896
+ return TOOL_LABELS[tool];
897
+ }
898
+ var VECTOR_TOOL_SHORTCUTS = {
899
+ select: "V",
900
+ direct: "A",
901
+ pen: "P",
902
+ pencil: "N",
903
+ rect: "R",
904
+ ellipse: "O",
905
+ polygon: "Y",
906
+ star: "S",
907
+ line: "L"
908
+ };
909
+ var VECTOR_TOOL_GLYPHS = {
910
+ select: "\u2196",
911
+ direct: "\u2316",
912
+ pen: "\u2712",
913
+ pencil: "\u270E",
914
+ rect: "\u25AD",
915
+ ellipse: "\u25EF",
916
+ polygon: "\u2B21",
917
+ star: "\u2606",
918
+ line: "\u2571"
919
+ };
920
+ var BOOLEAN_OPS = {
921
+ union: "union",
922
+ subtract: "difference",
923
+ intersect: "intersection",
924
+ exclude: "xor"
925
+ };
926
+ var BOOLEAN_LABELS = { union: "Union", subtract: "Subtract", intersect: "Intersect", exclude: "Exclude" };
927
+ function mapTree(nodes, fn) {
928
+ let changed = false;
929
+ const out = [];
930
+ for (const node of nodes) {
931
+ let next = fn(node);
932
+ if (next && next.type === "group") {
933
+ const children = mapTree(next.children, fn);
934
+ if (children !== next.children) next = { ...next, children };
935
+ }
936
+ if (next !== node) changed = true;
937
+ if (next) out.push(next);
938
+ }
939
+ return changed ? out : nodes;
940
+ }
941
+ var idCounter = 0;
942
+ var defaultCreateId = () => `vn-${Date.now().toString(36)}${(++idCounter).toString(36)}`;
943
+ var VectorEditorController = (_class = class {
944
+
945
+
946
+ __init() {this.selection = []}
947
+ __init2() {this.anchors = []}
948
+ __init3() {this.editingId = null}
949
+ __init4() {this.pen = null}
950
+
951
+
952
+
953
+
954
+
955
+ __init5() {this.hover = null}
956
+ __init6() {this.pointer = null}
957
+ __init7() {this.drag = null}
958
+ __init8() {this.interaction = null}
959
+ __init9() {this.spaceHeld = false}
960
+ __init10() {this.clipboard = null}
961
+
962
+
963
+ __init11() {this.listeners = /* @__PURE__ */ new Set()}
964
+ __init12() {this.cleanups = []}
965
+ __init13() {this.applying = false}
966
+ /** Set while a commit records a change that was already applied, so `restore` still reports it. */
967
+ __init14() {this.committing = false}
968
+ __init15() {this.revision = 0}
969
+
970
+ constructor(options = {}) {;_class.prototype.__init.call(this);_class.prototype.__init2.call(this);_class.prototype.__init3.call(this);_class.prototype.__init4.call(this);_class.prototype.__init5.call(this);_class.prototype.__init6.call(this);_class.prototype.__init7.call(this);_class.prototype.__init8.call(this);_class.prototype.__init9.call(this);_class.prototype.__init10.call(this);_class.prototype.__init11.call(this);_class.prototype.__init12.call(this);_class.prototype.__init13.call(this);_class.prototype.__init14.call(this);_class.prototype.__init15.call(this);_class.prototype.__init16.call(this);_class.prototype.__init17.call(this);
971
+ this.history = _nullishCoalesce(options.history, () => ( new (0, _chunkQKLRAA4Tcjs.HistoryManager)()));
972
+ this.doc = options.document ? cloneVectorDocument(options.document) : { width: 800, height: 600, nodes: [] };
973
+ this.tool = _nullishCoalesce(options.tool, () => ( "select"));
974
+ this.defaults = { ...DEFAULT_DEFAULTS, ..._nullishCoalesce(options.defaults, () => ( {})) };
975
+ this.viewport = { x: 0, y: 0, zoom: 1, ..._nullishCoalesce(options.viewport, () => ( {})) };
976
+ this.snap = { grid: 0, ..._nullishCoalesce(options.snap, () => ( {})) };
977
+ this.hitRadius = _nullishCoalesce(options.hitRadius, () => ( 6));
978
+ this.createId = _nullishCoalesce(options.createId, () => ( defaultCreateId));
979
+ this.onChange = options.onChange;
980
+ this.state = this.computeState();
981
+ this.cleanups.push(
982
+ this.history.subscribe((h) => {
983
+ const s = this.state;
984
+ if (this.applying || s.canUndo === h.canUndo && s.canRedo === h.canRedo && s.undoLabel === h.undoName && s.redoLabel === h.redoName) return;
985
+ this.notify();
986
+ })
987
+ );
988
+ }
989
+ /** Stops listening to the history. */
990
+ destroy() {
991
+ this.cleanups.splice(0).forEach((fn) => fn());
992
+ this.listeners.clear();
993
+ }
994
+ // -------------------------------------------------------------------------
995
+ // State
996
+ // -------------------------------------------------------------------------
997
+ /** The current snapshot (stable until the next change). */
998
+ __init16() {this.getState = () => this.state}
999
+ /** Calls `listener` after every change. Returns an unsubscribe function. */
1000
+ __init17() {this.subscribe = (listener) => {
1001
+ this.listeners.add(listener);
1002
+ return () => {
1003
+ this.listeners.delete(listener);
1004
+ };
1005
+ }}
1006
+ getDocument() {
1007
+ return this.doc;
1008
+ }
1009
+ getNode(id) {
1010
+ return findVectorNode(this.doc.nodes, id);
1011
+ }
1012
+ computeState() {
1013
+ const h = this.history.getState();
1014
+ return {
1015
+ document: this.doc,
1016
+ tool: this.tool,
1017
+ selection: this.selection,
1018
+ selectedAnchors: this.anchors,
1019
+ editingId: this.editingId,
1020
+ selectionBounds: this.boundsOf(this.selection),
1021
+ viewport: this.viewport,
1022
+ defaults: this.defaults,
1023
+ snap: this.snap,
1024
+ hitRadius: this.hitRadius,
1025
+ hover: this.hover,
1026
+ cursor: this.cursorFor(),
1027
+ interaction: this.interaction,
1028
+ pen: this.pen,
1029
+ pointer: this.pointer,
1030
+ canUndo: h.canUndo,
1031
+ canRedo: h.canRedo,
1032
+ undoLabel: h.undoName,
1033
+ redoLabel: h.redoName,
1034
+ revision: this.revision
1035
+ };
1036
+ }
1037
+ notify(docChanged = false) {
1038
+ this.revision++;
1039
+ this.state = this.computeState();
1040
+ if (docChanged) _optionalChain([this, 'access', _19 => _19.onChange, 'optionalCall', _20 => _20(this.doc)]);
1041
+ const state = this.state;
1042
+ this.listeners.forEach((listener) => {
1043
+ try {
1044
+ listener(state);
1045
+ } catch (error) {
1046
+ console.error("[VectorEditorController] Error in state listener:", error);
1047
+ }
1048
+ });
1049
+ }
1050
+ boundsOf(ids) {
1051
+ return unionBounds(ids.map((id) => this.getNode(id)).map((n) => n ? vectorNodeBounds(n) : null));
1052
+ }
1053
+ cursorFor() {
1054
+ if (_optionalChain([this, 'access', _21 => _21.drag, 'optionalAccess', _22 => _22.kind]) === "pan" || this.spaceHeld) return this.drag ? "grabbing" : "grab";
1055
+ if (_optionalChain([this, 'access', _23 => _23.drag, 'optionalAccess', _24 => _24.kind]) === "rotate") return "grabbing";
1056
+ if (_optionalChain([this, 'access', _25 => _25.drag, 'optionalAccess', _26 => _26.kind]) === "move") return "move";
1057
+ const h = this.hover;
1058
+ switch (this.tool) {
1059
+ 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";
1062
+ 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";
1066
+ case "pen":
1067
+ return _optionalChain([h, 'optionalAccess', _33 => _33.anchor]) ? "pointer" : "crosshair";
1068
+ default:
1069
+ return "crosshair";
1070
+ }
1071
+ }
1072
+ // -------------------------------------------------------------------------
1073
+ // Undoable changes
1074
+ // -------------------------------------------------------------------------
1075
+ snapshot() {
1076
+ return { doc: this.doc, selection: this.selection, anchors: this.anchors, editingId: this.editingId, pen: this.pen };
1077
+ }
1078
+ restore(s) {
1079
+ const docChanged = s.doc !== this.doc || this.committing;
1080
+ this.committing = false;
1081
+ this.applying = true;
1082
+ try {
1083
+ this.doc = s.doc;
1084
+ this.selection = s.selection.filter((id) => this.getNode(id));
1085
+ this.anchors = s.anchors.filter((r) => getAnchor(this.doc, r));
1086
+ 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;
1088
+ } finally {
1089
+ this.applying = false;
1090
+ }
1091
+ this.notify(docChanged);
1092
+ }
1093
+ /** Records the change from `before` to the current state as one undo step. */
1094
+ commit(name, before) {
1095
+ const after = this.snapshot();
1096
+ if (before.doc === after.doc) {
1097
+ this.notify();
1098
+ return;
1099
+ }
1100
+ const command = {
1101
+ name,
1102
+ execute: () => this.restore(after),
1103
+ undo: () => this.restore(before),
1104
+ redo: () => this.restore(after)
1105
+ };
1106
+ this.committing = true;
1107
+ this.history.execute(command);
1108
+ this.committing = false;
1109
+ }
1110
+ /** Runs `fn` (which updates fields) and records it as one undo step. */
1111
+ change(name, fn) {
1112
+ const before = this.snapshot();
1113
+ fn();
1114
+ this.commit(name, before);
1115
+ }
1116
+ setDoc(nodes) {
1117
+ if (nodes !== this.doc.nodes) this.doc = { ...this.doc, nodes };
1118
+ }
1119
+ replaceNode(id, next) {
1120
+ this.setDoc(mapTree(this.doc.nodes, (n) => n.id === id ? next : n));
1121
+ }
1122
+ freshIds(node) {
1123
+ const copy = cloneVectorNode(node);
1124
+ const used = /* @__PURE__ */ new Set();
1125
+ walkVectorNodes(this.doc.nodes, (n) => used.add(n.id));
1126
+ const next = () => {
1127
+ let id = this.createId();
1128
+ while (used.has(id)) id = this.createId();
1129
+ used.add(id);
1130
+ return id;
1131
+ };
1132
+ const assign = (n) => {
1133
+ n.id = next();
1134
+ if (n.type === "group") n.children.forEach(assign);
1135
+ };
1136
+ assign(copy);
1137
+ return copy;
1138
+ }
1139
+ newId() {
1140
+ let id = this.createId();
1141
+ while (this.getNode(id)) id = this.createId();
1142
+ return id;
1143
+ }
1144
+ // -------------------------------------------------------------------------
1145
+ // Viewport
1146
+ // -------------------------------------------------------------------------
1147
+ screenToDocument(p) {
1148
+ const v = this.viewport;
1149
+ return { x: (p.x - v.x) / v.zoom, y: (p.y - v.y) / v.zoom };
1150
+ }
1151
+ documentToScreen(p) {
1152
+ const v = this.viewport;
1153
+ return { x: p.x * v.zoom + v.x, y: p.y * v.zoom + v.y };
1154
+ }
1155
+ setViewport(viewport) {
1156
+ const next = { ...this.viewport, ...viewport };
1157
+ next.zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, next.zoom));
1158
+ if (next.x === this.viewport.x && next.y === this.viewport.y && next.zoom === this.viewport.zoom) return;
1159
+ this.viewport = next;
1160
+ this.notify();
1161
+ }
1162
+ /** Zooms by `factor` keeping the document point under `screen` in place. */
1163
+ zoomAt(screen, factor) {
1164
+ const zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, this.viewport.zoom * factor));
1165
+ const doc = this.screenToDocument(screen);
1166
+ this.setViewport({ zoom, x: screen.x - doc.x * zoom, y: screen.y - doc.y * zoom });
1167
+ }
1168
+ panBy(dx, dy) {
1169
+ this.setViewport({ x: this.viewport.x + dx, y: this.viewport.y + dy });
1170
+ }
1171
+ /** Fits the artboard (or `bounds`) into a canvas of `size` screen pixels. */
1172
+ zoomToFit(size, options = {}) {
1173
+ const padding = _nullishCoalesce(options.padding, () => ( 40));
1174
+ const b = _nullishCoalesce(options.bounds, () => ( { x: 0, y: 0, width: this.doc.width, height: this.doc.height }));
1175
+ const zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, Math.min((size.width - padding * 2) / (b.width || 1), (size.height - padding * 2) / (b.height || 1))));
1176
+ this.setViewport({ zoom, x: (size.width - b.width * zoom) / 2 - b.x * zoom, y: (size.height - b.height * zoom) / 2 - b.y * zoom });
1177
+ }
1178
+ /** Zoom at the canvas centre-ish: positive `delta` zooms out (wheel convention). Use with `wheel`. */
1179
+ wheel(screen, delta, mods = {}) {
1180
+ if (mods.mod) this.zoomAt(screen, Math.exp(-delta.y * 0.01));
1181
+ else this.panBy(-(mods.shift ? delta.y : delta.x), -(mods.shift ? 0 : delta.y));
1182
+ }
1183
+ // -------------------------------------------------------------------------
1184
+ // Settings
1185
+ // -------------------------------------------------------------------------
1186
+ setTool(tool) {
1187
+ if (tool === this.tool) return;
1188
+ this.finishPath();
1189
+ this.cancelDrag();
1190
+ this.tool = tool;
1191
+ this.hover = null;
1192
+ if (tool !== "direct") {
1193
+ this.anchors = [];
1194
+ this.editingId = null;
1195
+ }
1196
+ this.notify();
1197
+ }
1198
+ setDefaults(defaults) {
1199
+ this.defaults = { ...this.defaults, ...defaults };
1200
+ this.notify();
1201
+ }
1202
+ setSnap(snap) {
1203
+ this.snap = { ...this.snap, ...snap };
1204
+ this.notify();
1205
+ }
1206
+ snapPoint(p) {
1207
+ const g = this.snap.grid;
1208
+ if (!g || g <= 0) return p;
1209
+ return { x: Math.round(p.x / g) * g, y: Math.round(p.y / g) * g };
1210
+ }
1211
+ snapDelta(dx, dy, origin) {
1212
+ const g = this.snap.grid;
1213
+ if (!g || g <= 0 || !origin) return { dx, dy };
1214
+ const target = this.snapPoint({ x: origin.x + dx, y: origin.y + dy });
1215
+ return { dx: target.x - origin.x, dy: target.y - origin.y };
1216
+ }
1217
+ // -------------------------------------------------------------------------
1218
+ // Document
1219
+ // -------------------------------------------------------------------------
1220
+ /** Replaces the document. Undoable unless `{ history: false }` (then history is cleared). */
1221
+ setDocument(doc, options = {}) {
1222
+ this.cancelDrag();
1223
+ const next = cloneVectorDocument(doc);
1224
+ if (options.history === false) {
1225
+ this.doc = next;
1226
+ this.selection = [];
1227
+ this.anchors = [];
1228
+ this.editingId = null;
1229
+ this.pen = null;
1230
+ this.history.clear();
1231
+ this.notify(true);
1232
+ return;
1233
+ }
1234
+ this.change(_nullishCoalesce(options.label, () => ( "Replace Document")), () => {
1235
+ this.doc = next;
1236
+ this.selection = [];
1237
+ this.anchors = [];
1238
+ this.editingId = null;
1239
+ this.pen = null;
1240
+ });
1241
+ }
1242
+ /** Changes the artboard size or background. */
1243
+ setArtboard(patch) {
1244
+ this.change("Change Artboard", () => {
1245
+ this.doc = { ...this.doc, ...patch };
1246
+ });
1247
+ }
1248
+ /** Replaces the document with parsed SVG markup (one undo step). */
1249
+ loadSvg(svg, options = {}) {
1250
+ try {
1251
+ const doc = parseVectorSvg(svg, { createId: () => this.createId() });
1252
+ this.setDocument(doc, { history: options.history, label: "Import SVG" });
1253
+ return { ok: true };
1254
+ } catch (error) {
1255
+ return { ok: false, message: error instanceof Error ? error.message : String(error) };
1256
+ }
1257
+ }
1258
+ /** Adds the nodes of SVG markup on top of the document, selected. */
1259
+ importSvg(svg) {
1260
+ try {
1261
+ const parsed = parseVectorSvg(svg);
1262
+ const nodes = parsed.nodes.map((n) => this.freshIds(n));
1263
+ if (!nodes.length) return { ok: false, message: "The SVG has no shapes the editor can read." };
1264
+ this.change("Import SVG", () => {
1265
+ this.setDoc([...this.doc.nodes, ...nodes]);
1266
+ this.selection = nodes.map((n) => n.id);
1267
+ this.anchors = [];
1268
+ });
1269
+ return { ok: true };
1270
+ } catch (error) {
1271
+ return { ok: false, message: error instanceof Error ? error.message : String(error) };
1272
+ }
1273
+ }
1274
+ toSvg(options) {
1275
+ return vectorDocumentToSvg(this.doc, options);
1276
+ }
1277
+ toJSON() {
1278
+ return cloneVectorDocument(this.doc);
1279
+ }
1280
+ /** Adds a node (ids are kept unless taken). Returns its id. */
1281
+ addNode(node, options = {}) {
1282
+ const taken = (() => {
1283
+ let clash = false;
1284
+ walkVectorNodes([node], (n) => {
1285
+ if (this.getNode(n.id)) clash = true;
1286
+ });
1287
+ return clash;
1288
+ })();
1289
+ const next = taken ? this.freshIds(node) : cloneVectorNode(node);
1290
+ this.change(_nullishCoalesce(options.label, () => ( `Add ${next.type === "group" ? "Group" : _nullishCoalesce(next.name, () => ( "Path"))}`)), () => {
1291
+ const insert = (list) => {
1292
+ const copy = list.slice();
1293
+ copy.splice(_nullishCoalesce(options.index, () => ( copy.length)), 0, next);
1294
+ return copy;
1295
+ };
1296
+ if (options.parentId) {
1297
+ this.setDoc(mapTree(this.doc.nodes, (n) => n.id === options.parentId && n.type === "group" ? { ...n, children: insert(n.children) } : n));
1298
+ } else this.setDoc(insert(this.doc.nodes));
1299
+ if (options.select !== false) {
1300
+ this.selection = [next.id];
1301
+ this.anchors = [];
1302
+ }
1303
+ });
1304
+ return next.id;
1305
+ }
1306
+ /** Builds a path node for a shape with the current defaults (not added). */
1307
+ createShapeNode(input) {
1308
+ const b = { x: input.x, y: input.y, width: input.width, height: input.height };
1309
+ const d = this.defaults;
1310
+ let subpath;
1311
+ switch (input.kind) {
1312
+ case "rect":
1313
+ subpath = rectSubpath(b, _nullishCoalesce(input.cornerRadius, () => ( d.cornerRadius)));
1314
+ break;
1315
+ case "ellipse":
1316
+ subpath = ellipseSubpath(b);
1317
+ break;
1318
+ case "polygon":
1319
+ subpath = polygonSubpath(b, _nullishCoalesce(input.sides, () => ( d.polygonSides)));
1320
+ break;
1321
+ case "star":
1322
+ subpath = starSubpath(b, _nullishCoalesce(input.points, () => ( d.starPoints)), _nullishCoalesce(input.innerRatio, () => ( d.starInnerRatio)));
1323
+ break;
1324
+ case "line":
1325
+ subpath = lineSubpath({ x: b.x, y: b.y }, { x: b.x + b.width, y: b.y + b.height });
1326
+ break;
1327
+ }
1328
+ const names = { rect: "Rectangle", ellipse: "Ellipse", polygon: "Polygon", star: "Star", line: "Line" };
1329
+ const style = this.styleFor(input.kind === "line");
1330
+ const own = {};
1331
+ for (const key of ["fill", "stroke", "strokeWidth", "strokeLinecap", "strokeLinejoin", "strokeDasharray", "fillRule"]) {
1332
+ if (input[key] !== void 0) own[key] = input[key];
1333
+ }
1334
+ return { id: this.newId(), type: "path", name: _nullishCoalesce(input.name, () => ( names[input.kind])), ...style, ...own, subpaths: [subpath] };
1335
+ }
1336
+ /** Adds a shape drawn with the current defaults. Returns its id. */
1337
+ addShape(input, options = {}) {
1338
+ const node = this.createShapeNode(input);
1339
+ return this.addNode(node, { label: `Add ${node.name}`, ...options });
1340
+ }
1341
+ /** Adds a path from SVG path data. Returns its id. */
1342
+ addPath(d, style = {}, options = {}) {
1343
+ const subpaths = bezierToSubpaths(d);
1344
+ const node = { id: this.newId(), type: "path", name: _nullishCoalesce(style.name, () => ( "Path")), ...this.styleFor(false), ...style, subpaths };
1345
+ return this.addNode(node, { label: "Add Path", ...options });
1346
+ }
1347
+ styleFor(open) {
1348
+ const d = this.defaults;
1349
+ const out = { fill: open ? null : _nullishCoalesce(d.fill, () => ( null)), stroke: _nullishCoalesce(d.stroke, () => ( null)), strokeWidth: _nullishCoalesce(d.strokeWidth, () => ( 1)) };
1350
+ if (d.strokeLinecap) out.strokeLinecap = d.strokeLinecap;
1351
+ if (d.strokeLinejoin) out.strokeLinejoin = d.strokeLinejoin;
1352
+ if (d.strokeDasharray) out.strokeDasharray = d.strokeDasharray;
1353
+ if (d.fillRule) out.fillRule = d.fillRule;
1354
+ return out;
1355
+ }
1356
+ /** Merges `patch` into a node (paths take style and geometry; groups name / visibility / opacity). */
1357
+ updateNode(id, patch, options = {}) {
1358
+ const node = this.getNode(id);
1359
+ if (!node) return { ok: false, message: `There is no node "${id}".` };
1360
+ const { id: _id, type: _type, ...rest } = patch;
1361
+ this.change(_nullishCoalesce(options.label, () => ( "Edit Node")), () => this.replaceNode(id, { ...node, ...rest }));
1362
+ return { ok: true };
1363
+ }
1364
+ /** Applies style (and opacity) to the selected (or given) nodes; groups pass it to every path inside. */
1365
+ setStyle(style, ids = this.selection, options = {}) {
1366
+ const targets = /* @__PURE__ */ new Set();
1367
+ for (const id of ids) {
1368
+ const node = this.getNode(id);
1369
+ if (!node) return { ok: false, message: `There is no node "${id}".` };
1370
+ for (const p of collectPathNodes(node)) targets.add(p.id);
1371
+ }
1372
+ if (!targets.size) return { ok: false, message: "Nothing is selected." };
1373
+ this.change(_nullishCoalesce(options.label, () => ( "Change Style")), () => {
1374
+ this.setDoc(mapTree(this.doc.nodes, (n) => targets.has(n.id) ? { ...n, ...style } : n));
1375
+ });
1376
+ return { ok: true };
1377
+ }
1378
+ rename(id, name) {
1379
+ return this.updateNode(id, { name }, { label: "Rename" });
1380
+ }
1381
+ setVisible(id, visible) {
1382
+ return this.updateNode(id, { visible }, { label: visible ? "Show" : "Hide" });
1383
+ }
1384
+ setLocked(id, locked) {
1385
+ const result = this.updateNode(id, { locked }, { label: locked ? "Lock" : "Unlock" });
1386
+ if (locked && result.ok && this.selection.includes(id)) {
1387
+ this.selection = this.selection.filter((s) => s !== id);
1388
+ this.notify();
1389
+ }
1390
+ return result;
1391
+ }
1392
+ /** Removes nodes by id. */
1393
+ removeNodes(ids, options = {}) {
1394
+ const drop = new Set(ids.filter((id) => this.getNode(id)));
1395
+ if (!drop.size) return { ok: false, message: "Nothing to delete." };
1396
+ this.change(_nullishCoalesce(options.label, () => ( "Delete")), () => {
1397
+ this.setDoc(mapTree(this.doc.nodes, (n) => drop.has(n.id) ? null : n));
1398
+ this.selection = this.selection.filter((id) => !drop.has(id) && this.getNode(id));
1399
+ this.anchors = this.anchors.filter((r) => getAnchor(this.doc, r));
1400
+ if (this.editingId && !this.getNode(this.editingId)) this.editingId = null;
1401
+ if (this.pen && !this.getNode(this.pen.nodeId)) this.pen = null;
1402
+ });
1403
+ return { ok: true };
1404
+ }
1405
+ // -------------------------------------------------------------------------
1406
+ // Selection
1407
+ // -------------------------------------------------------------------------
1408
+ select(ids, options = {}) {
1409
+ const valid = ids.filter((id) => this.getNode(id));
1410
+ let next;
1411
+ if (options.toggle) {
1412
+ const set = new Set(this.selection);
1413
+ for (const id of valid) set.has(id) ? set.delete(id) : set.add(id);
1414
+ next = [...set];
1415
+ } else next = options.add ? [.../* @__PURE__ */ new Set([...this.selection, ...valid])] : valid;
1416
+ this.selection = next;
1417
+ if (!options.add && !options.toggle) this.anchors = this.anchors.filter((r) => next.includes(r.nodeId));
1418
+ this.notify();
1419
+ }
1420
+ selectAll() {
1421
+ if (this.tool === "direct" && this.editingId) {
1422
+ this.anchors = getVisibleAnchors(this.view());
1423
+ } else {
1424
+ this.selection = this.doc.nodes.filter((n) => !n.locked && n.visible !== false).map((n) => n.id);
1425
+ }
1426
+ this.notify();
1427
+ }
1428
+ clearSelection() {
1429
+ if (!this.selection.length && !this.anchors.length && !this.editingId) return;
1430
+ this.selection = [];
1431
+ this.anchors = [];
1432
+ this.editingId = null;
1433
+ this.notify();
1434
+ }
1435
+ selectAnchors(refs, options = {}) {
1436
+ const valid = refs.filter((r) => getAnchor(this.doc, r));
1437
+ this.anchors = options.add ? [...this.anchors, ...valid.filter((r) => !this.anchors.some((a) => sameRef(a, r)))] : valid;
1438
+ if (valid.length && !this.editingId) this.editingId = valid[0].nodeId;
1439
+ this.notify();
1440
+ }
1441
+ /** Top-level ids of the selection (members of selected groups are covered by the group). */
1442
+ selectedRoots() {
1443
+ return this.selection.filter((id) => {
1444
+ let covered = false;
1445
+ const loc = locateVectorNode(this.doc.nodes, id);
1446
+ let parent = _nullishCoalesce(_optionalChain([loc, 'optionalAccess', _38 => _38.parent]), () => ( null));
1447
+ while (parent) {
1448
+ 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));
1450
+ }
1451
+ return !covered;
1452
+ });
1453
+ }
1454
+ // -------------------------------------------------------------------------
1455
+ // Transforms
1456
+ // -------------------------------------------------------------------------
1457
+ /** Applies a matrix to the selected (or given) nodes as one undo step. */
1458
+ transform(matrix, options = {}) {
1459
+ const ids = new Set(_nullishCoalesce(options.ids, () => ( this.selectedRoots())));
1460
+ if (!ids.size) return { ok: false, message: "Nothing is selected." };
1461
+ this.change(_nullishCoalesce(options.label, () => ( "Transform")), () => {
1462
+ this.doc = this.transformed(this.doc, ids, matrix);
1463
+ });
1464
+ return { ok: true };
1465
+ }
1466
+ transformed(doc, ids, m) {
1467
+ const nodes = mapTree(doc.nodes, (n) => ids.has(n.id) && !n.locked ? transformVectorNode(n, m) : n);
1468
+ return nodes === doc.nodes ? doc : { ...doc, nodes };
1469
+ }
1470
+ translate(dx, dy, options = {}) {
1471
+ if (this.tool === "direct" && this.anchors.length && !options.ids) {
1472
+ this.change("Move Points", () => {
1473
+ this.doc = this.movedAnchors(this.doc, this.anchors, dx, dy);
1474
+ });
1475
+ return { ok: true };
1476
+ }
1477
+ return this.transform(translateMatrix(dx, dy), { ids: options.ids, label: "Move" });
1478
+ }
1479
+ /** Scales around `origin` (default: the selection centre). */
1480
+ 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" });
1485
+ }
1486
+ /** Rotates clockwise by `degrees` around `origin` (default: the selection centre). */
1487
+ 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 }));
1491
+ return this.transform(rotateMatrix(degrees, origin), { ids: options.ids, label: "Rotate" });
1492
+ }
1493
+ 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 };
1497
+ 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" });
1499
+ }
1500
+ /**
1501
+ * Aligns the selected nodes to their combined bounds, or to the artboard when one node is
1502
+ * selected (or `to: 'artboard'`).
1503
+ */
1504
+ align(edge, options = {}) {
1505
+ const ids = _nullishCoalesce(options.ids, () => ( this.selectedRoots()));
1506
+ if (!ids.length) return { ok: false, message: "Nothing is selected." };
1507
+ const target = options.to === "artboard" || ids.length === 1 ? { x: 0, y: 0, width: this.doc.width, height: this.doc.height } : this.boundsOf(ids);
1508
+ this.change(`Align ${edge[0].toUpperCase()}${edge.slice(1)}`, () => {
1509
+ for (const id of ids) {
1510
+ const b = this.boundsOf([id]);
1511
+ if (!b) continue;
1512
+ let dx = 0;
1513
+ let dy = 0;
1514
+ if (edge === "left") dx = target.x - b.x;
1515
+ if (edge === "center") dx = target.x + target.width / 2 - (b.x + b.width / 2);
1516
+ if (edge === "right") dx = target.x + target.width - (b.x + b.width);
1517
+ if (edge === "top") dy = target.y - b.y;
1518
+ if (edge === "middle") dy = target.y + target.height / 2 - (b.y + b.height / 2);
1519
+ if (edge === "bottom") dy = target.y + target.height - (b.y + b.height);
1520
+ if (dx || dy) this.doc = this.transformed(this.doc, /* @__PURE__ */ new Set([id]), translateMatrix(dx, dy));
1521
+ }
1522
+ });
1523
+ return { ok: true };
1524
+ }
1525
+ /** Spaces three or more selected nodes evenly (equal gaps). */
1526
+ distribute(direction, options = {}) {
1527
+ const ids = _nullishCoalesce(options.ids, () => ( this.selectedRoots()));
1528
+ const items = ids.map((id) => ({ id, b: this.boundsOf([id]) })).filter((i) => !!i.b);
1529
+ if (items.length < 3) return { ok: false, message: "Select at least three objects to distribute." };
1530
+ const horizontal = direction === "horizontal";
1531
+ const pos = (b) => horizontal ? b.x : b.y;
1532
+ const size = (b) => horizontal ? b.width : b.height;
1533
+ items.sort((a, b) => pos(a.b) - pos(b.b));
1534
+ const first = items[0].b;
1535
+ const last = items[items.length - 1].b;
1536
+ const total = pos(last) + size(last) - pos(first);
1537
+ const gap = (total - items.reduce((s, i) => s + size(i.b), 0)) / (items.length - 1);
1538
+ this.change(horizontal ? "Distribute Horizontally" : "Distribute Vertically", () => {
1539
+ let cursor = pos(first);
1540
+ for (const item of items) {
1541
+ const d = cursor - pos(item.b);
1542
+ if (Math.abs(d) > 1e-9) this.doc = this.transformed(this.doc, /* @__PURE__ */ new Set([item.id]), horizontal ? translateMatrix(d, 0) : translateMatrix(0, d));
1543
+ cursor += size(item.b) + gap;
1544
+ }
1545
+ });
1546
+ return { ok: true };
1547
+ }
1548
+ // -------------------------------------------------------------------------
1549
+ // Structure
1550
+ // -------------------------------------------------------------------------
1551
+ /** Copies the selection (offset by `offset`, default 10, 10) on top of each original. */
1552
+ duplicate(options = {}) {
1553
+ const ids = _nullishCoalesce(options.ids, () => ( this.selectedRoots()));
1554
+ const offset = _nullishCoalesce(options.offset, () => ( { x: 10, y: 10 }));
1555
+ const created = [];
1556
+ if (!ids.length) return created;
1557
+ this.change("Duplicate", () => {
1558
+ this.doc = this.withDuplicates(this.doc, ids, offset, created);
1559
+ this.selection = created;
1560
+ this.anchors = [];
1561
+ });
1562
+ return created;
1563
+ }
1564
+ withDuplicates(doc, ids, offset, created) {
1565
+ const set = new Set(ids);
1566
+ const m = translateMatrix(offset.x, offset.y);
1567
+ const insert = (list) => {
1568
+ const out = [];
1569
+ for (const node of list) {
1570
+ const n = node.type === "group" ? { ...node, children: insert(node.children) } : node;
1571
+ out.push(n);
1572
+ if (set.has(node.id)) {
1573
+ const copy = transformVectorNode(this.freshIds(node), m);
1574
+ created.push(copy.id);
1575
+ out.push(copy);
1576
+ }
1577
+ }
1578
+ return out;
1579
+ };
1580
+ return { ...doc, nodes: insert(doc.nodes) };
1581
+ }
1582
+ /** Groups the selected nodes (they must share a parent). Returns the group id. */
1583
+ group(options = {}) {
1584
+ const ids = _nullishCoalesce(options.ids, () => ( this.selectedRoots()));
1585
+ if (!ids.length) return null;
1586
+ const locs = ids.map((id2) => locateVectorNode(this.doc.nodes, id2));
1587
+ 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;
1590
+ const id = this.newId();
1591
+ this.change("Group", () => {
1592
+ const set = new Set(ids);
1593
+ const regroup = (list) => {
1594
+ const members = list.filter((n) => set.has(n.id));
1595
+ const top = Math.max(...members.map((m) => list.indexOf(m)));
1596
+ const out = [];
1597
+ list.forEach((n, i) => {
1598
+ if (i === top) out.push({ id, type: "group", name: _nullishCoalesce(options.name, () => ( "Group")), children: members });
1599
+ if (!set.has(n.id)) out.push(n);
1600
+ });
1601
+ return out;
1602
+ };
1603
+ if (parent === null) this.setDoc(regroup(this.doc.nodes));
1604
+ else this.setDoc(mapTree(this.doc.nodes, (n) => n.id === parent && n.type === "group" ? { ...n, children: regroup(n.children) } : n));
1605
+ this.selection = [id];
1606
+ this.anchors = [];
1607
+ });
1608
+ return id;
1609
+ }
1610
+ /** Replaces selected groups with their children. */
1611
+ ungroup(options = {}) {
1612
+ const groups = (_nullishCoalesce(options.ids, () => ( this.selection))).map((id) => this.getNode(id)).filter((n) => _optionalChain([n, 'optionalAccess', _46 => _46.type]) === "group");
1613
+ if (!groups.length) return { ok: false, message: "Select a group to ungroup." };
1614
+ const set = new Set(groups.map((g) => g.id));
1615
+ this.change("Ungroup", () => {
1616
+ const flatten = (list) => {
1617
+ const out = [];
1618
+ let changed = false;
1619
+ for (const n of list) {
1620
+ if (set.has(n.id) && n.type === "group") {
1621
+ changed = true;
1622
+ const opacity = n.opacity;
1623
+ out.push(...n.children.map((c) => opacity !== void 0 && opacity < 1 ? { ...c, opacity: (_nullishCoalesce(c.opacity, () => ( 1))) * opacity } : c));
1624
+ } else if (n.type === "group") {
1625
+ const children = flatten(n.children);
1626
+ if (children !== n.children) changed = true;
1627
+ out.push(children !== n.children ? { ...n, children } : n);
1628
+ } else out.push(n);
1629
+ }
1630
+ return changed ? out : list;
1631
+ };
1632
+ this.setDoc(flatten(this.doc.nodes));
1633
+ this.selection = groups.flatMap((g) => g.children.map((c) => c.id));
1634
+ });
1635
+ return { ok: true };
1636
+ }
1637
+ /** Changes stacking order within each node's parent. */
1638
+ arrange(op, options = {}) {
1639
+ const set = new Set(_nullishCoalesce(options.ids, () => ( this.selectedRoots())));
1640
+ if (!set.size) return { ok: false, message: "Nothing is selected." };
1641
+ const reorder = (list) => {
1642
+ const out = list.map((n) => n.type === "group" ? { ...n, children: reorder(n.children) } : n);
1643
+ if (op === "front") return [...out.filter((n) => !set.has(n.id)), ...out.filter((n) => set.has(n.id))];
1644
+ if (op === "back") return [...out.filter((n) => set.has(n.id)), ...out.filter((n) => !set.has(n.id))];
1645
+ if (op === "forward") {
1646
+ for (let i = out.length - 2; i >= 0; i--) if (set.has(out[i].id) && !set.has(out[i + 1].id)) [out[i], out[i + 1]] = [out[i + 1], out[i]];
1647
+ } else {
1648
+ for (let i = 1; i < out.length; i++) if (set.has(out[i].id) && !set.has(out[i - 1].id)) [out[i], out[i - 1]] = [out[i - 1], out[i]];
1649
+ }
1650
+ return out;
1651
+ };
1652
+ const labels = { front: "Bring to Front", forward: "Bring Forward", backward: "Send Backward", back: "Send to Back" };
1653
+ this.change(labels[op], () => this.setDoc(reorder(this.doc.nodes)));
1654
+ return { ok: true };
1655
+ }
1656
+ /**
1657
+ * Combines the selected paths (two or more) into one with a boolean operation. `subtract`
1658
+ * removes the upper shapes from the lowest one. The result takes the lowest shape's style
1659
+ * and place.
1660
+ */
1661
+ booleanOp(op, options = {}) {
1662
+ const roots = _nullishCoalesce(options.ids, () => ( this.selectedRoots()));
1663
+ const order = [];
1664
+ walkVectorNodes(this.doc.nodes, (n) => {
1665
+ if (roots.includes(n.id)) for (const p of collectPathNodes(n)) order.push(p.id);
1666
+ });
1667
+ const paths = [...new Set(order)].map((id2) => this.getNode(id2));
1668
+ if (paths.length < 2) return null;
1669
+ const result = _chunkM4XE4UMTcjs.pathBoolean.call(void 0,
1670
+ 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")) }))
1672
+ );
1673
+ const subpaths = bezierToSubpaths(result.path);
1674
+ if (result.error || !subpaths.length) return null;
1675
+ const base = paths[0];
1676
+ const id = this.newId();
1677
+ const node = { ...cloneVectorNode(base), id, name: BOOLEAN_LABELS[op], subpaths };
1678
+ 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
+ this.change(BOOLEAN_LABELS[op], () => {
1681
+ this.setDoc(mapTree(this.doc.nodes, (n) => n.id === anchorRoot ? node : drop.has(n.id) ? null : n));
1682
+ this.selection = [id];
1683
+ this.anchors = [];
1684
+ this.editingId = null;
1685
+ });
1686
+ return id;
1687
+ }
1688
+ /** Joins the selected paths into one compound path (subpaths kept, lowest style). */
1689
+ combine(options = {}) {
1690
+ const roots = _nullishCoalesce(options.ids, () => ( this.selectedRoots()));
1691
+ const paths = [];
1692
+ walkVectorNodes(this.doc.nodes, (n) => {
1693
+ if (roots.includes(n.id)) paths.push(...collectPathNodes(n));
1694
+ });
1695
+ if (paths.length < 2) return null;
1696
+ 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
+ const first = _nullishCoalesce(roots.find((r) => findVectorNode([this.getNode(r)], paths[0].id)), () => ( paths[0].id));
1700
+ this.change("Combine", () => {
1701
+ this.setDoc(mapTree(this.doc.nodes, (n) => n.id === first ? node : drop.has(n.id) ? null : n));
1702
+ this.selection = [id];
1703
+ this.anchors = [];
1704
+ });
1705
+ return id;
1706
+ }
1707
+ /** Splits selected compound paths into one path per subpath. */
1708
+ 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);
1710
+ if (!paths.length) return { ok: false, message: "Select a compound path to break apart." };
1711
+ const created = [];
1712
+ const set = new Set(paths.map((p) => p.id));
1713
+ this.change("Break Apart", () => {
1714
+ const split = (list) => list.flatMap((n) => {
1715
+ if (n.type === "group") return [{ ...n, children: split(n.children) }];
1716
+ if (!set.has(n.id)) return [n];
1717
+ return n.subpaths.map((sp, i) => {
1718
+ const id = i === 0 ? n.id : this.newId();
1719
+ created.push(id);
1720
+ return { ...n, id, subpaths: [sp] };
1721
+ });
1722
+ });
1723
+ this.setDoc(split(this.doc.nodes));
1724
+ this.selection = created;
1725
+ });
1726
+ return { ok: true };
1727
+ }
1728
+ // -------------------------------------------------------------------------
1729
+ // Anchor editing
1730
+ // -------------------------------------------------------------------------
1731
+ updateAnchors(doc, refs, fn) {
1732
+ const byNode = /* @__PURE__ */ new Map();
1733
+ for (const r of refs) byNode.set(r.nodeId, [..._nullishCoalesce(byNode.get(r.nodeId), () => ( [])), r]);
1734
+ const nodes = mapTree(doc.nodes, (n) => {
1735
+ const list = byNode.get(n.id);
1736
+ if (!list || n.type !== "path" || n.locked) return n;
1737
+ const subpaths = n.subpaths.map((sp, s) => {
1738
+ const hits = list.filter((r) => r.subpath === s);
1739
+ if (!hits.length) return sp;
1740
+ return { closed: sp.closed, anchors: sp.anchors.map((a, i) => hits.some((r) => r.index === i) ? fn(a, { nodeId: n.id, subpath: s, index: i }) : a) };
1741
+ });
1742
+ return { ...n, subpaths };
1743
+ });
1744
+ return nodes === doc.nodes ? doc : { ...doc, nodes };
1745
+ }
1746
+ movedAnchors(doc, refs, dx, dy) {
1747
+ return this.updateAnchors(doc, refs, (a) => translateAnchor(a, dx, dy));
1748
+ }
1749
+ /** Moves one anchor to a position (handles follow). */
1750
+ moveAnchor(ref, to) {
1751
+ const a = getAnchor(this.doc, ref);
1752
+ if (!a) return { ok: false, message: "There is no such anchor." };
1753
+ this.change("Move Point", () => {
1754
+ this.doc = this.movedAnchors(this.doc, [ref], to.x - a.x, to.y - a.y);
1755
+ });
1756
+ return { ok: true };
1757
+ }
1758
+ /** Moves one handle (see `moveAnchorHandle` for how the opposite handle follows). */
1759
+ moveHandle(ref, to, options = {}) {
1760
+ if (!getAnchor(this.doc, ref)) return { ok: false, message: "There is no such anchor." };
1761
+ this.change("Move Handle", () => {
1762
+ this.doc = this.updateAnchors(this.doc, [ref], (a) => moveAnchorHandle(a, ref.handle, to, options.breakHandles));
1763
+ });
1764
+ return { ok: true };
1765
+ }
1766
+ /** Converts the selected (or given) anchors between corner, smooth and symmetric. */
1767
+ convertAnchors(type, refs = this.anchors) {
1768
+ if (!refs.length) return { ok: false, message: "Select points to convert." };
1769
+ this.change(type === "corner" ? "Make Corner" : "Make Smooth", () => {
1770
+ const nodes = mapTree(this.doc.nodes, (n) => {
1771
+ if (n.type !== "path") return n;
1772
+ const mine = refs.filter((r) => r.nodeId === n.id);
1773
+ if (!mine.length) return n;
1774
+ const subpaths = n.subpaths.map((sp, s) => mine.filter((r) => r.subpath === s).reduce((acc, r) => convertAnchor(acc, r.index, type), sp));
1775
+ return { ...n, subpaths };
1776
+ });
1777
+ this.setDoc(nodes);
1778
+ });
1779
+ return { ok: true };
1780
+ }
1781
+ /** Adds an anchor on the nearest segment of `nodeId` to `point` (shape unchanged). */
1782
+ insertAnchorAt(nodeId, point) {
1783
+ const node = this.getNode(nodeId);
1784
+ if (_optionalChain([node, 'optionalAccess', _48 => _48.type]) !== "path") return null;
1785
+ const hit = nearestSegment(node, point);
1786
+ if (!hit) return null;
1787
+ const { subpath, index } = insertAnchor(node.subpaths[hit.subpath], hit.segment, hit.t);
1788
+ const ref = { nodeId, subpath: hit.subpath, index };
1789
+ this.change("Add Point", () => {
1790
+ this.replaceNode(nodeId, { ...node, subpaths: node.subpaths.map((sp, i) => i === hit.subpath ? subpath : sp) });
1791
+ this.editingId = nodeId;
1792
+ this.anchors = [ref];
1793
+ });
1794
+ return ref;
1795
+ }
1796
+ /** Deletes the selected anchors; empty subpaths and paths go too. */
1797
+ deleteAnchors(refs = this.anchors) {
1798
+ if (!refs.length) return { ok: false, message: "Select points to delete." };
1799
+ this.change("Delete Points", () => {
1800
+ const nodes = mapTree(this.doc.nodes, (n) => {
1801
+ if (n.type !== "path") return n;
1802
+ const mine = refs.filter((r) => r.nodeId === n.id);
1803
+ if (!mine.length) return n;
1804
+ const subpaths = n.subpaths.map((sp, s) => removeAnchors(sp, mine.filter((r) => r.subpath === s).map((r) => r.index))).filter((sp) => sp.anchors.length > 1);
1805
+ return subpaths.length ? { ...n, subpaths } : null;
1806
+ });
1807
+ this.setDoc(nodes);
1808
+ this.anchors = [];
1809
+ this.selection = this.selection.filter((id) => this.getNode(id));
1810
+ if (this.editingId && !this.getNode(this.editingId)) this.editingId = null;
1811
+ if (this.pen && !this.getNode(this.pen.nodeId)) this.pen = null;
1812
+ });
1813
+ return { ok: true };
1814
+ }
1815
+ /** Opens or closes the subpaths that contain selected anchors (or all subpaths of `nodeId`). */
1816
+ setClosed(closed, nodeId) {
1817
+ const targets = nodeId ? (() => {
1818
+ const n = this.getNode(nodeId);
1819
+ return _optionalChain([n, 'optionalAccess', _49 => _49.type]) === "path" ? n.subpaths.map((_, s) => ({ nodeId, subpath: s })) : [];
1820
+ })() : this.anchors.map((r) => ({ nodeId: r.nodeId, subpath: r.subpath }));
1821
+ if (!targets.length) return { ok: false, message: "Select a path or points." };
1822
+ this.change(closed ? "Close Path" : "Open Path", () => {
1823
+ this.setDoc(
1824
+ mapTree(
1825
+ this.doc.nodes,
1826
+ (n) => n.type === "path" && targets.some((t) => t.nodeId === n.id) ? { ...n, subpaths: n.subpaths.map((sp, s) => targets.some((t) => t.nodeId === n.id && t.subpath === s) ? { ...sp, closed } : sp) } : n
1827
+ )
1828
+ );
1829
+ });
1830
+ return { ok: true };
1831
+ }
1832
+ /** Reverses the direction of every subpath of the selected paths. */
1833
+ reversePaths(ids = this.selection) {
1834
+ const set = new Set(ids.flatMap((id) => this.getNode(id) ? collectPathNodes(this.getNode(id)) : []).map((p) => p.id));
1835
+ if (!set.size) return { ok: false, message: "Select a path." };
1836
+ this.change("Reverse Path", () => {
1837
+ this.setDoc(mapTree(this.doc.nodes, (n) => n.type === "path" && set.has(n.id) ? { ...n, subpaths: n.subpaths.map(reverseSubpath) } : n));
1838
+ this.anchors = [];
1839
+ });
1840
+ return { ok: true };
1841
+ }
1842
+ /** Ends the pen path being drawn (a lone anchor is removed). */
1843
+ finishPath() {
1844
+ if (!this.pen) return;
1845
+ const node = this.getNode(this.pen.nodeId);
1846
+ const sp = _optionalChain([node, 'optionalAccess', _50 => _50.subpaths, 'access', _51 => _51[this.pen.subpath]]);
1847
+ const pen = this.pen;
1848
+ this.pen = null;
1849
+ this.pointer = null;
1850
+ if (node && sp && sp.anchors.length < 2) {
1851
+ const subpaths = node.subpaths.filter((_, i) => i !== pen.subpath);
1852
+ this.change("Delete Path", () => {
1853
+ this.replaceNode(node.id, subpaths.length ? { ...node, subpaths } : null);
1854
+ if (!subpaths.length) {
1855
+ this.selection = this.selection.filter((id) => id !== node.id);
1856
+ if (this.editingId === node.id) this.editingId = null;
1857
+ }
1858
+ this.anchors = [];
1859
+ });
1860
+ return;
1861
+ }
1862
+ this.notify();
1863
+ }
1864
+ // -------------------------------------------------------------------------
1865
+ // Clipboard
1866
+ // -------------------------------------------------------------------------
1867
+ /** Copies the selection. Returns JSON text for the system clipboard (or null). */
1868
+ copy() {
1869
+ const nodes = this.selectedRoots().map((id) => this.getNode(id)).filter((n) => !!n).map((n) => cloneVectorNode(n));
1870
+ if (!nodes.length) return null;
1871
+ this.clipboard = nodes;
1872
+ return JSON.stringify({ type: VECTOR_CLIPBOARD_TYPE, nodes });
1873
+ }
1874
+ cut() {
1875
+ const text = this.copy();
1876
+ if (text) this.removeNodes(this.selectedRoots(), { label: "Cut" });
1877
+ return text;
1878
+ }
1879
+ /**
1880
+ * Pastes `text` (from `copy()`, or SVG markup) or the internal clipboard, offset by 10, 10.
1881
+ * Returns the new ids.
1882
+ */
1883
+ paste(text, options = {}) {
1884
+ let nodes = null;
1885
+ if (text) {
1886
+ const trimmed = text.trim();
1887
+ if (trimmed.startsWith("{")) {
1888
+ try {
1889
+ const data = JSON.parse(trimmed);
1890
+ if (_optionalChain([data, 'optionalAccess', _52 => _52.type]) === VECTOR_CLIPBOARD_TYPE && Array.isArray(data.nodes)) nodes = data.nodes;
1891
+ } catch (e2) {
1892
+ nodes = null;
1893
+ }
1894
+ } else if (/<svg[\s>]/i.test(trimmed)) {
1895
+ try {
1896
+ nodes = parseVectorSvg(trimmed).nodes;
1897
+ } catch (e3) {
1898
+ nodes = null;
1899
+ }
1900
+ }
1901
+ } else nodes = this.clipboard;
1902
+ if (!_optionalChain([nodes, 'optionalAccess', _53 => _53.length])) return [];
1903
+ const offset = _nullishCoalesce(options.offset, () => ( (text && !text.trim().startsWith("{") ? { x: 0, y: 0 } : { x: 10, y: 10 })));
1904
+ const copies = nodes.map((n) => transformVectorNode(this.freshIds(n), translateMatrix(offset.x, offset.y)));
1905
+ this.change("Paste", () => {
1906
+ this.setDoc([...this.doc.nodes, ...copies]);
1907
+ this.selection = copies.map((n) => n.id);
1908
+ this.anchors = [];
1909
+ });
1910
+ if (!text) this.clipboard = copies.map((n) => cloneVectorNode(n));
1911
+ return copies.map((n) => n.id);
1912
+ }
1913
+ // -------------------------------------------------------------------------
1914
+ // Undo
1915
+ // -------------------------------------------------------------------------
1916
+ undo() {
1917
+ this.cancelDrag();
1918
+ this.pen = null;
1919
+ return this.history.undo();
1920
+ }
1921
+ redo() {
1922
+ this.cancelDrag();
1923
+ return this.history.redo();
1924
+ }
1925
+ /** Deletes selected anchors (direct tool) or nodes. */
1926
+ deleteSelection() {
1927
+ if (this.tool === "direct" && this.anchors.length) return this.deleteAnchors();
1928
+ return this.removeNodes(this.selectedRoots());
1929
+ }
1930
+ // -------------------------------------------------------------------------
1931
+ // Pointer input
1932
+ // -------------------------------------------------------------------------
1933
+ /** The fields the anchor / handle helpers read, without building a full state. */
1934
+ view() {
1935
+ return { document: this.doc, selection: this.selection, editingId: this.editingId, tool: this.tool, pen: this.pen, selectedAnchors: this.anchors };
1936
+ }
1937
+ tolerance() {
1938
+ return this.hitRadius / this.viewport.zoom;
1939
+ }
1940
+ hitTransformHandle(screen) {
1941
+ 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 };
1946
+ const r = this.hitRadius + 2;
1947
+ const handles = getTransformHandlePositions(box);
1948
+ 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;
1950
+ return null;
1951
+ }
1952
+ hitHandle(screen) {
1953
+ 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]]);
1956
+ if (!h) continue;
1957
+ const s = this.documentToScreen(h);
1958
+ if (Math.hypot(s.x - screen.x, s.y - screen.y) <= this.hitRadius) return ref;
1959
+ }
1960
+ return null;
1961
+ }
1962
+ hitAnchor(screen, refs) {
1963
+ let best = null;
1964
+ let bestD = Infinity;
1965
+ for (const ref of refs) {
1966
+ const a = getAnchor(this.doc, ref);
1967
+ if (!a) continue;
1968
+ const s = this.documentToScreen(a);
1969
+ const d = Math.hypot(s.x - screen.x, s.y - screen.y);
1970
+ if (d <= this.hitRadius && d < bestD) {
1971
+ best = ref;
1972
+ bestD = d;
1973
+ }
1974
+ }
1975
+ return best;
1976
+ }
1977
+ /** Deepest unlocked visible path under `p`. */
1978
+ hitPath(p) {
1979
+ const tol = this.tolerance();
1980
+ const search = (list) => {
1981
+ for (let i = list.length - 1; i >= 0; i--) {
1982
+ const n = list[i];
1983
+ if (n.visible === false || n.locked) continue;
1984
+ if (n.type === "group") {
1985
+ const found = search(n.children);
1986
+ if (found) return found;
1987
+ } else if (hitTestVectorNode(n, p, tol)) return n;
1988
+ }
1989
+ return null;
1990
+ };
1991
+ return search(this.doc.nodes);
1992
+ }
1993
+ /** Endpoints of open subpaths of unlocked visible paths (pen tool resumes from them). */
1994
+ openEndpoints() {
1995
+ const out = [];
1996
+ walkVectorNodes(this.doc.nodes, (n) => {
1997
+ if (n.type !== "path" || n.locked || n.visible === false) return;
1998
+ n.subpaths.forEach((sp, s) => {
1999
+ if (sp.closed || sp.anchors.length < 1) return;
2000
+ out.push({ nodeId: n.id, subpath: s, index: 0 });
2001
+ if (sp.anchors.length > 1) out.push({ nodeId: n.id, subpath: s, index: sp.anchors.length - 1 });
2002
+ });
2003
+ });
2004
+ return out;
2005
+ }
2006
+ computeHover(screen) {
2007
+ const p = this.screenToDocument(screen);
2008
+ switch (this.tool) {
2009
+ case "select": {
2010
+ const handle = this.hitTransformHandle(screen);
2011
+ if (handle) return { nodeId: null, transformHandle: handle };
2012
+ const node = hitTestVectorNodes(this.doc.nodes, p, this.tolerance());
2013
+ return node ? { nodeId: node.id } : null;
2014
+ }
2015
+ case "direct": {
2016
+ const handle = this.hitHandle(screen);
2017
+ if (handle) return { nodeId: handle.nodeId, handle };
2018
+ const anchor = this.hitAnchor(screen, getVisibleAnchors(this.view()));
2019
+ if (anchor) return { nodeId: anchor.nodeId, anchor };
2020
+ for (const id of getEditablePathIds(this.view())) {
2021
+ 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 } };
2024
+ }
2025
+ const path = this.hitPath(p);
2026
+ return path ? { nodeId: path.id } : null;
2027
+ }
2028
+ case "pen": {
2029
+ const refs = this.pen ? [{ nodeId: this.pen.nodeId, subpath: this.pen.subpath, index: 0 }] : this.openEndpoints();
2030
+ const anchor = this.hitAnchor(screen, refs);
2031
+ return anchor ? { nodeId: anchor.nodeId, anchor } : null;
2032
+ }
2033
+ default:
2034
+ return null;
2035
+ }
2036
+ }
2037
+ sameHover(a, b) {
2038
+ return JSON.stringify(a) === JSON.stringify(b);
2039
+ }
2040
+ /** Primary button down on the canvas at `screen` (pixels relative to the canvas). */
2041
+ pointerDown(screen, input = {}) {
2042
+ this.cancelDrag();
2043
+ if (input.button === 1 || this.spaceHeld) {
2044
+ this.drag = { kind: "pan", start: screen, viewport: this.viewport };
2045
+ this.interaction = { type: "pan" };
2046
+ this.notify();
2047
+ return;
2048
+ }
2049
+ if (input.button !== void 0 && input.button !== 0) return;
2050
+ const p = this.screenToDocument(screen);
2051
+ this.pointer = p;
2052
+ switch (this.tool) {
2053
+ case "select":
2054
+ return this.downSelect(screen, p, input);
2055
+ case "direct":
2056
+ return this.downDirect(screen, p, input);
2057
+ case "pen":
2058
+ return this.downPen(screen, p, input);
2059
+ case "pencil":
2060
+ return this.downPencil(p);
2061
+ default:
2062
+ return this.downShape(p);
2063
+ }
2064
+ }
2065
+ downSelect(screen, p, input) {
2066
+ const handle = this.hitTransformHandle(screen);
2067
+ if (handle) {
2068
+ const bounds = this.boundsOf(this.selectedRoots());
2069
+ 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 };
2071
+ this.interaction = handle === "rotate" ? { type: "rotate", angle: 0 } : { type: "scale", handle, sx: 1, sy: 1 };
2072
+ this.notify();
2073
+ return;
2074
+ }
2075
+ const node = hitTestVectorNodes(this.doc.nodes, p, this.tolerance());
2076
+ if (node) {
2077
+ const selected = this.selection.includes(node.id);
2078
+ if (input.shift) {
2079
+ this.selection = selected ? this.selection.filter((id) => id !== node.id) : [...this.selection, node.id];
2080
+ if (selected) {
2081
+ this.notify();
2082
+ return;
2083
+ }
2084
+ } else if (!selected) this.selection = [node.id];
2085
+ this.anchors = [];
2086
+ const before = this.snapshot();
2087
+ this.drag = {
2088
+ kind: "move",
2089
+ start: p,
2090
+ origin: this.boundsOf(this.selectedRoots()),
2091
+ before,
2092
+ base: this.doc,
2093
+ moved: false,
2094
+ duplicated: false,
2095
+ clickId: selected && !input.shift ? node.id : null
2096
+ };
2097
+ this.notify();
2098
+ return;
2099
+ }
2100
+ if (!input.shift) this.selection = [];
2101
+ this.drag = { kind: "marquee", start: p, base: this.selection, baseAnchors: [], anchorsMode: false };
2102
+ this.interaction = { type: "marquee", bounds: { x: p.x, y: p.y, width: 0, height: 0 } };
2103
+ this.notify();
2104
+ }
2105
+ downDirect(screen, p, input) {
2106
+ const handle = this.hitHandle(screen);
2107
+ if (handle) {
2108
+ this.drag = { kind: "handle", ref: handle, before: this.snapshot(), base: this.doc };
2109
+ this.interaction = { type: "handle", ref: handle };
2110
+ this.notify();
2111
+ return;
2112
+ }
2113
+ const anchor = this.hitAnchor(screen, getVisibleAnchors(this.view()));
2114
+ if (anchor) {
2115
+ const selected = this.anchors.some((a) => sameRef(a, anchor));
2116
+ this.editingId = anchor.nodeId;
2117
+ if (input.alt) {
2118
+ this.anchors = [anchor];
2119
+ this.drag = { kind: "pull", ref: anchor, before: this.snapshot(), base: this.doc, label: "Make Smooth", outOnly: false, closing: false, altBreaks: false };
2120
+ this.notify();
2121
+ return;
2122
+ }
2123
+ if (input.shift) {
2124
+ this.anchors = selected ? this.anchors.filter((a) => !sameRef(a, anchor)) : [...this.anchors, anchor];
2125
+ if (selected) {
2126
+ this.notify();
2127
+ return;
2128
+ }
2129
+ } else if (!selected) this.anchors = [anchor];
2130
+ this.drag = { kind: "anchors", start: p, before: this.snapshot(), base: this.doc, moved: false, clickRef: selected && !input.shift ? anchor : null };
2131
+ this.notify();
2132
+ return;
2133
+ }
2134
+ for (const id of getEditablePathIds(this.view())) {
2135
+ const node = this.getNode(id);
2136
+ const seg = nearestSegment(node, p);
2137
+ if (!seg || seg.distance > this.tolerance()) continue;
2138
+ const sp = node.subpaths[seg.subpath];
2139
+ if (input.alt) {
2140
+ const before = this.snapshot();
2141
+ const { subpath, index } = insertAnchor(sp, seg.segment, seg.t);
2142
+ this.replaceNode(id, { ...node, subpaths: node.subpaths.map((s, i) => i === seg.subpath ? subpath : s) });
2143
+ const ref = { nodeId: id, subpath: seg.subpath, index };
2144
+ this.anchors = [ref];
2145
+ this.editingId = id;
2146
+ this.drag = { kind: "anchors", start: p, before, base: this.doc, moved: true, clickRef: null };
2147
+ this.notify(true);
2148
+ return;
2149
+ }
2150
+ const a = { nodeId: id, subpath: seg.subpath, index: seg.segment };
2151
+ const b = { nodeId: id, subpath: seg.subpath, index: (seg.segment + 1) % sp.anchors.length };
2152
+ this.anchors = input.shift ? [...this.anchors.filter((r) => !sameRef(r, a) && !sameRef(r, b)), a, b] : [a, b];
2153
+ this.editingId = id;
2154
+ this.drag = { kind: "anchors", start: p, before: this.snapshot(), base: this.doc, moved: false, clickRef: null };
2155
+ this.notify();
2156
+ return;
2157
+ }
2158
+ const path = this.hitPath(p);
2159
+ if (path) {
2160
+ this.editingId = path.id;
2161
+ if (!input.shift) this.selection = [path.id];
2162
+ else if (!this.selection.includes(path.id)) this.selection = [...this.selection, path.id];
2163
+ this.anchors = path.subpaths.flatMap((sp, s) => sp.anchors.map((_, i) => ({ nodeId: path.id, subpath: s, index: i })));
2164
+ this.drag = { kind: "anchors", start: p, before: this.snapshot(), base: this.doc, moved: false, clickRef: null };
2165
+ this.notify();
2166
+ return;
2167
+ }
2168
+ if (!input.shift) {
2169
+ this.anchors = [];
2170
+ }
2171
+ this.drag = { kind: "marquee", start: p, base: this.selection, baseAnchors: this.anchors, anchorsMode: getEditablePathIds(this.view()).length > 0 };
2172
+ this.interaction = { type: "marquee", bounds: { x: p.x, y: p.y, width: 0, height: 0 } };
2173
+ this.notify();
2174
+ }
2175
+ downPen(screen, p, input) {
2176
+ 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;
2178
+ if (this.pen && sp) {
2179
+ const pen = this.pen;
2180
+ const first = { nodeId: pen.nodeId, subpath: pen.subpath, index: 0 };
2181
+ if (sp.anchors.length >= 2 && this.hitAnchor(screen, [first])) {
2182
+ const node3 = this.getNode(pen.nodeId);
2183
+ this.replaceNode(pen.nodeId, { ...node3, subpaths: node3.subpaths.map((s, i) => i === pen.subpath ? { ...s, closed: true } : s) });
2184
+ this.drag = { kind: "pull", ref: first, before, base: this.doc, label: "Close Path", outOnly: false, closing: true, altBreaks: true };
2185
+ this.interaction = { type: "pen", nodeId: pen.nodeId };
2186
+ this.notify(true);
2187
+ return;
2188
+ }
2189
+ let pt2 = this.snapPoint(p);
2190
+ const last = sp.anchors[sp.anchors.length - 1];
2191
+ if (input.shift) pt2 = constrain45(last, pt2);
2192
+ const node2 = this.getNode(pen.nodeId);
2193
+ const anchors = [...sp.anchors, { x: pt2.x, y: pt2.y }];
2194
+ this.replaceNode(pen.nodeId, { ...node2, subpaths: node2.subpaths.map((s, i) => i === pen.subpath ? { ...s, anchors } : s) });
2195
+ const ref2 = { nodeId: pen.nodeId, subpath: pen.subpath, index: anchors.length - 1 };
2196
+ this.anchors = [ref2];
2197
+ this.drag = { kind: "pull", ref: ref2, before, base: this.doc, label: "Add Point", outOnly: false, closing: false, altBreaks: true };
2198
+ this.interaction = { type: "pen", nodeId: pen.nodeId };
2199
+ this.notify(true);
2200
+ return;
2201
+ }
2202
+ const endpoint = this.hitAnchor(screen, this.openEndpoints());
2203
+ if (endpoint) {
2204
+ let node2 = this.getNode(endpoint.nodeId);
2205
+ let index = endpoint.index;
2206
+ if (index === 0 && node2.subpaths[endpoint.subpath].anchors.length > 1) {
2207
+ node2 = { ...node2, subpaths: node2.subpaths.map((s, i) => i === endpoint.subpath ? reverseSubpath(s) : s) };
2208
+ this.replaceNode(node2.id, node2);
2209
+ index = node2.subpaths[endpoint.subpath].anchors.length - 1;
2210
+ }
2211
+ this.pen = { nodeId: node2.id, subpath: endpoint.subpath };
2212
+ this.selection = [node2.id];
2213
+ this.editingId = node2.id;
2214
+ const ref2 = { nodeId: node2.id, subpath: endpoint.subpath, index };
2215
+ this.anchors = [ref2];
2216
+ this.drag = { kind: "pull", ref: ref2, before, base: this.doc, label: "Continue Path", outOnly: true, closing: false, altBreaks: true };
2217
+ this.interaction = { type: "pen", nodeId: node2.id };
2218
+ this.notify(this.doc !== before.doc);
2219
+ return;
2220
+ }
2221
+ const pt = this.snapPoint(p);
2222
+ const node = { id: this.newId(), type: "path", name: "Path", ...this.styleFor(true), subpaths: [{ anchors: [{ x: pt.x, y: pt.y }], closed: false }] };
2223
+ this.setDoc([...this.doc.nodes, node]);
2224
+ this.pen = { nodeId: node.id, subpath: 0 };
2225
+ this.selection = [node.id];
2226
+ this.editingId = node.id;
2227
+ const ref = { nodeId: node.id, subpath: 0, index: 0 };
2228
+ this.anchors = [ref];
2229
+ this.drag = { kind: "pull", ref, before, base: this.doc, label: "Start Path", outOnly: false, closing: false, altBreaks: true };
2230
+ this.interaction = { type: "pen", nodeId: node.id };
2231
+ this.notify(true);
2232
+ }
2233
+ downPencil(p) {
2234
+ const before = this.snapshot();
2235
+ const node = {
2236
+ id: this.newId(),
2237
+ type: "path",
2238
+ name: "Pencil",
2239
+ ...this.styleFor(true),
2240
+ strokeLinecap: _nullishCoalesce(this.defaults.strokeLinecap, () => ( "round")),
2241
+ strokeLinejoin: _nullishCoalesce(this.defaults.strokeLinejoin, () => ( "round")),
2242
+ subpaths: [{ anchors: [{ x: p.x, y: p.y }], closed: false }]
2243
+ };
2244
+ this.setDoc([...this.doc.nodes, node]);
2245
+ this.drag = { kind: "pencil", nodeId: node.id, points: [[p.x, p.y]], before };
2246
+ this.interaction = { type: "draw", tool: "pencil", nodeId: node.id, bounds: { x: p.x, y: p.y, width: 0, height: 0 } };
2247
+ this.notify(true);
2248
+ }
2249
+ downShape(p) {
2250
+ const before = this.snapshot();
2251
+ const start = this.snapPoint(p);
2252
+ const kind = this.tool;
2253
+ const node = this.createShapeNode({ kind, x: start.x, y: start.y, width: 0, height: 0 });
2254
+ this.setDoc([...this.doc.nodes, node]);
2255
+ this.drag = { kind: "shape", tool: this.tool, start, nodeId: node.id, before };
2256
+ this.interaction = { type: "draw", tool: this.tool, nodeId: node.id, bounds: { x: start.x, y: start.y, width: 0, height: 0 } };
2257
+ this.notify(true);
2258
+ }
2259
+ /** Pointer moved over the canvas (with or without a button down). */
2260
+ pointerMove(screen, input = {}) {
2261
+ const p = this.screenToDocument(screen);
2262
+ const drag = this.drag;
2263
+ if (!drag) {
2264
+ const hover = this.computeHover(screen);
2265
+ const pointerMatters = this.tool === "pen" && !!this.pen;
2266
+ if (this.sameHover(hover, this.hover) && !pointerMatters) return;
2267
+ this.hover = hover;
2268
+ this.pointer = p;
2269
+ this.notify();
2270
+ return;
2271
+ }
2272
+ this.pointer = p;
2273
+ switch (drag.kind) {
2274
+ case "pan":
2275
+ this.viewport = { ...drag.viewport, x: drag.viewport.x + screen.x - drag.start.x, y: drag.viewport.y + screen.y - drag.start.y };
2276
+ this.notify();
2277
+ return;
2278
+ case "move": {
2279
+ let dx = p.x - drag.start.x;
2280
+ let dy = p.y - drag.start.y;
2281
+ if (!drag.moved && Math.hypot(dx, dy) * this.viewport.zoom < 2) return;
2282
+ drag.moved = true;
2283
+ if (input.shift) Math.abs(dx) > Math.abs(dy) ? dy = 0 : dx = 0;
2284
+ if (input.alt && !drag.duplicated) {
2285
+ drag.duplicated = true;
2286
+ const created = [];
2287
+ drag.base = this.withDuplicates(drag.base, this.selectedRoots(), { x: 0, y: 0 }, created);
2288
+ this.selection = created;
2289
+ }
2290
+ ({ dx, dy } = this.snapDelta(dx, dy, drag.origin));
2291
+ this.doc = this.transformed(drag.base, new Set(this.selectedRoots()), translateMatrix(dx, dy));
2292
+ this.interaction = { type: "move", dx, dy };
2293
+ this.notify(true);
2294
+ return;
2295
+ }
2296
+ case "scale": {
2297
+ const { sx, sy, origin } = scaleFor(drag.handle, drag.bounds, p, input);
2298
+ const ids = new Set(this.selectedRoots());
2299
+ this.doc = this.transformed(drag.base, ids, scaleMatrix(sx, sy, origin));
2300
+ this.interaction = { type: "scale", handle: drag.handle, sx, sy };
2301
+ this.notify(true);
2302
+ return;
2303
+ }
2304
+ case "rotate": {
2305
+ const a0 = Math.atan2(drag.start.y - drag.center.y, drag.start.x - drag.center.x);
2306
+ const a1 = Math.atan2(p.y - drag.center.y, p.x - drag.center.x);
2307
+ let angle = (a1 - a0) * 180 / Math.PI;
2308
+ if (input.shift) angle = Math.round(angle / 15) * 15;
2309
+ this.doc = this.transformed(drag.base, new Set(this.selectedRoots()), rotateMatrix(angle, drag.center));
2310
+ this.interaction = { type: "rotate", angle };
2311
+ this.notify(true);
2312
+ return;
2313
+ }
2314
+ case "marquee": {
2315
+ const bounds = boundsFromPoints(drag.start, p);
2316
+ this.interaction = { type: "marquee", bounds };
2317
+ if (drag.anchorsMode) {
2318
+ const inside = getVisibleAnchors(this.view()).filter((r) => {
2319
+ const a = getAnchor(this.doc, r);
2320
+ return a.x >= bounds.x && a.x <= bounds.x + bounds.width && a.y >= bounds.y && a.y <= bounds.y + bounds.height;
2321
+ });
2322
+ this.anchors = [...drag.baseAnchors, ...inside.filter((r) => !drag.baseAnchors.some((b) => sameRef(b, r)))];
2323
+ } else {
2324
+ const hits = this.doc.nodes.filter((n) => !n.locked && n.visible !== false).filter((n) => {
2325
+ const b = vectorNodeBounds(n);
2326
+ return b && boundsIntersect(b, bounds);
2327
+ }).map((n) => n.id);
2328
+ this.selection = [.../* @__PURE__ */ new Set([...drag.base, ...hits])];
2329
+ }
2330
+ this.notify();
2331
+ return;
2332
+ }
2333
+ case "anchors": {
2334
+ let dx = p.x - drag.start.x;
2335
+ let dy = p.y - drag.start.y;
2336
+ if (!drag.moved && Math.hypot(dx, dy) * this.viewport.zoom < 2) return;
2337
+ drag.moved = true;
2338
+ if (input.shift) Math.abs(dx) > Math.abs(dy) ? dy = 0 : dx = 0;
2339
+ const lead = this.anchors.length ? getAnchor(drag.base, this.anchors[0]) : void 0;
2340
+ ({ dx, dy } = this.snapDelta(dx, dy, _nullishCoalesce(lead, () => ( null))));
2341
+ this.doc = this.movedAnchors(drag.base, this.anchors, dx, dy);
2342
+ this.interaction = { type: "anchors", dx, dy };
2343
+ this.notify(true);
2344
+ return;
2345
+ }
2346
+ case "handle": {
2347
+ const anchor = getAnchor(drag.base, drag.ref);
2348
+ let pt = this.snapPoint(p);
2349
+ if (input.shift && anchor) pt = constrain45(anchor, pt);
2350
+ this.doc = this.updateAnchors(drag.base, [drag.ref], (a) => moveAnchorHandle(a, drag.ref.handle, pt, input.alt));
2351
+ this.interaction = { type: "handle", ref: drag.ref };
2352
+ this.notify(true);
2353
+ return;
2354
+ }
2355
+ case "pull": {
2356
+ const anchor = getAnchor(drag.base, drag.ref);
2357
+ if (!anchor) return;
2358
+ if (Math.hypot(p.x - anchor.x, p.y - anchor.y) * this.viewport.zoom < 2) {
2359
+ this.doc = drag.base;
2360
+ this.notify(true);
2361
+ return;
2362
+ }
2363
+ let pt = p;
2364
+ if (input.shift) pt = constrain45(anchor, pt);
2365
+ const side = drag.closing ? "in" : "out";
2366
+ const pos = drag.closing ? { x: 2 * anchor.x - pt.x, y: 2 * anchor.y - pt.y } : pt;
2367
+ this.doc = this.updateAnchors(drag.base, [drag.ref], (a) => {
2368
+ if (drag.outOnly || drag.altBreaks && input.alt) return moveAnchorHandle(a, side, pos, true);
2369
+ return moveAnchorHandle({ ...a, type: "symmetric" }, side, pos);
2370
+ });
2371
+ this.notify(true);
2372
+ return;
2373
+ }
2374
+ case "shape": {
2375
+ const node = this.getNode(drag.nodeId);
2376
+ if (_optionalChain([node, 'optionalAccess', _59 => _59.type]) !== "path") return;
2377
+ let end = this.snapPoint(p);
2378
+ let start = drag.start;
2379
+ let bounds;
2380
+ if (drag.tool === "line") {
2381
+ if (input.shift) end = constrain45(start, end);
2382
+ if (input.alt) start = { x: 2 * start.x - end.x, y: 2 * start.y - end.y };
2383
+ bounds = { x: start.x, y: start.y, width: end.x - start.x, height: end.y - start.y };
2384
+ } else {
2385
+ let w = end.x - start.x;
2386
+ let h = end.y - start.y;
2387
+ if (input.shift) {
2388
+ const s = Math.max(Math.abs(w), Math.abs(h));
2389
+ w = Math.sign(w || 1) * s;
2390
+ h = Math.sign(h || 1) * s;
2391
+ }
2392
+ if (input.alt) start = { x: start.x - w, y: start.y - h };
2393
+ const x2 = drag.start.x + w;
2394
+ const y2 = drag.start.y + h;
2395
+ bounds = boundsFromPoints(start, { x: x2, y: y2 });
2396
+ }
2397
+ const shaped = this.createShapeNode({ kind: drag.tool, ...bounds });
2398
+ this.replaceNode(drag.nodeId, { ...node, subpaths: shaped.subpaths });
2399
+ this.interaction = { type: "draw", tool: drag.tool, nodeId: drag.nodeId, bounds };
2400
+ this.notify(true);
2401
+ return;
2402
+ }
2403
+ case "pencil": {
2404
+ const last = drag.points[drag.points.length - 1];
2405
+ if (Math.hypot(p.x - last[0], p.y - last[1]) * this.viewport.zoom < 1.5) return;
2406
+ drag.points.push([p.x, p.y]);
2407
+ const node = this.getNode(drag.nodeId);
2408
+ if (_optionalChain([node, 'optionalAccess', _60 => _60.type]) !== "path") return;
2409
+ this.replaceNode(drag.nodeId, { ...node, subpaths: [{ anchors: drag.points.map(([x, y]) => ({ x, y })), closed: false }] });
2410
+ const xs = drag.points.map((q) => q[0]);
2411
+ const ys = drag.points.map((q) => q[1]);
2412
+ this.interaction = {
2413
+ type: "draw",
2414
+ tool: "pencil",
2415
+ nodeId: drag.nodeId,
2416
+ bounds: { x: Math.min(...xs), y: Math.min(...ys), width: Math.max(...xs) - Math.min(...xs), height: Math.max(...ys) - Math.min(...ys) }
2417
+ };
2418
+ this.notify(true);
2419
+ return;
2420
+ }
2421
+ }
2422
+ }
2423
+ /** Primary button released. */
2424
+ pointerUp(screen, input = {}) {
2425
+ if (screen && this.drag && this.drag.kind !== "pan") this.pointerMove(screen, input);
2426
+ const drag = this.drag;
2427
+ this.drag = null;
2428
+ this.interaction = null;
2429
+ if (!drag) return;
2430
+ switch (drag.kind) {
2431
+ case "pan":
2432
+ case "marquee":
2433
+ this.notify();
2434
+ return;
2435
+ case "move":
2436
+ if (!drag.moved) {
2437
+ if (drag.clickId) this.selection = [drag.clickId];
2438
+ this.notify();
2439
+ return;
2440
+ }
2441
+ return this.commit(drag.duplicated ? "Duplicate" : "Move", drag.before);
2442
+ case "scale":
2443
+ return this.commit("Scale", drag.before);
2444
+ case "rotate":
2445
+ return this.commit("Rotate", drag.before);
2446
+ case "anchors":
2447
+ if (!drag.moved) {
2448
+ if (drag.clickRef) this.anchors = [drag.clickRef];
2449
+ if (drag.before.doc !== this.doc) return this.commit("Add Point", drag.before);
2450
+ this.notify();
2451
+ return;
2452
+ }
2453
+ return this.commit(drag.before.doc !== drag.base ? "Add Point" : "Move Points", drag.before);
2454
+ case "handle":
2455
+ return this.commit("Move Handle", drag.before);
2456
+ case "pull":
2457
+ if (drag.closing) this.pen = null;
2458
+ return this.commit(drag.label, drag.before);
2459
+ case "shape": {
2460
+ const node = this.getNode(drag.nodeId);
2461
+ const b = node ? vectorNodeBounds(node) : null;
2462
+ const min = 2 / this.viewport.zoom;
2463
+ if (!b || b.width < min && b.height < min) {
2464
+ this.doc = drag.before.doc;
2465
+ this.notify(true);
2466
+ return;
2467
+ }
2468
+ this.selection = [drag.nodeId];
2469
+ this.anchors = [];
2470
+ this.tool = "select";
2471
+ return this.commit(`Draw ${TOOL_LABELS[drag.tool]}`, drag.before);
2472
+ }
2473
+ case "pencil": {
2474
+ const node = this.getNode(drag.nodeId);
2475
+ if (_optionalChain([node, 'optionalAccess', _61 => _61.type]) !== "path" || drag.points.length < 2) {
2476
+ this.doc = drag.before.doc;
2477
+ this.notify(true);
2478
+ return;
2479
+ }
2480
+ const fitted = _chunkM4XE4UMTcjs.fitCubics.call(void 0, drag.points, 1.5 / this.viewport.zoom, { cornerAngleDeg: 60 });
2481
+ const anchors = [];
2482
+ for (const c of fitted) {
2483
+ if (!anchors.length) anchors.push({ x: c.p0[0], y: c.p0[1] });
2484
+ anchors[anchors.length - 1].out = { x: c.p1[0], y: c.p1[1] };
2485
+ anchors.push({ x: c.p3[0], y: c.p3[1], in: { x: c.p2[0], y: c.p2[1] } });
2486
+ }
2487
+ const subpaths = anchors.length > 1 ? bezierToSubpaths(subpathsToBezier([{ anchors, closed: false }])) : node.subpaths;
2488
+ this.replaceNode(drag.nodeId, { ...node, subpaths });
2489
+ this.selection = [drag.nodeId];
2490
+ return this.commit("Draw", drag.before);
2491
+ }
2492
+ }
2493
+ }
2494
+ /** The pointer left the canvas. */
2495
+ pointerLeave() {
2496
+ if (this.drag) return;
2497
+ if (!this.hover && !this.pointer) return;
2498
+ this.hover = null;
2499
+ this.pointer = null;
2500
+ this.notify();
2501
+ }
2502
+ /** Abandons the drag in progress (Escape, lost capture). */
2503
+ cancelDrag() {
2504
+ const drag = this.drag;
2505
+ if (!drag) return;
2506
+ this.drag = null;
2507
+ this.interaction = null;
2508
+ if (drag.kind === "pan") {
2509
+ this.notify();
2510
+ return;
2511
+ }
2512
+ if (drag.kind === "marquee") {
2513
+ this.selection = drag.base;
2514
+ this.anchors = drag.baseAnchors;
2515
+ this.notify();
2516
+ return;
2517
+ }
2518
+ this.restore(drag.before);
2519
+ }
2520
+ /** Double click: select tool enters path editing, direct tool toggles an anchor or adds one. */
2521
+ doubleClick(screen) {
2522
+ const p = this.screenToDocument(screen);
2523
+ if (this.tool === "select") {
2524
+ const path = this.hitPath(p);
2525
+ if (!path) return;
2526
+ this.tool = "direct";
2527
+ this.selection = [path.id];
2528
+ this.editingId = path.id;
2529
+ this.anchors = [];
2530
+ this.notify();
2531
+ return;
2532
+ }
2533
+ if (this.tool === "direct") {
2534
+ const anchor = this.hitAnchor(screen, getVisibleAnchors(this.view()));
2535
+ if (anchor) {
2536
+ const a = getAnchor(this.doc, anchor);
2537
+ this.convertAnchors(a.in || a.out ? "corner" : "smooth", [anchor]);
2538
+ return;
2539
+ }
2540
+ const id = _nullishCoalesce(_optionalChain([this, 'access', _62 => _62.hover, 'optionalAccess', _63 => _63.segment, 'optionalAccess', _64 => _64.nodeId]), () => ( this.editingId));
2541
+ if (id) {
2542
+ const node = this.getNode(id);
2543
+ const seg = node ? nearestSegment(node, p) : null;
2544
+ if (seg && seg.distance <= this.tolerance()) this.insertAnchorAt(id, p);
2545
+ }
2546
+ return;
2547
+ }
2548
+ if (this.tool === "pen") this.finishPath();
2549
+ }
2550
+ // -------------------------------------------------------------------------
2551
+ // Keyboard input
2552
+ // -------------------------------------------------------------------------
2553
+ /**
2554
+ * Handles editor shortcuts. Returns true when the key was used (call `preventDefault`).
2555
+ * Do not forward keys typed into text inputs.
2556
+ */
2557
+ keyDown(input) {
2558
+ const key = input.key;
2559
+ const lower = key.length === 1 ? key.toLowerCase() : key;
2560
+ if (key === " ") {
2561
+ if (!this.spaceHeld) {
2562
+ this.spaceHeld = true;
2563
+ this.notify();
2564
+ }
2565
+ return true;
2566
+ }
2567
+ if (input.mod) {
2568
+ switch (lower) {
2569
+ case "z":
2570
+ input.shift ? this.redo() : this.undo();
2571
+ return true;
2572
+ case "y":
2573
+ this.redo();
2574
+ return true;
2575
+ case "a":
2576
+ this.selectAll();
2577
+ return true;
2578
+ case "d":
2579
+ this.duplicate();
2580
+ return true;
2581
+ case "g":
2582
+ input.shift ? this.ungroup() : this.group();
2583
+ return true;
2584
+ case "]":
2585
+ this.arrange(input.alt ? "front" : "forward");
2586
+ return true;
2587
+ case "[":
2588
+ this.arrange(input.alt ? "back" : "backward");
2589
+ return true;
2590
+ case "=":
2591
+ case "+":
2592
+ this.setViewport({ zoom: this.viewport.zoom * 1.25 });
2593
+ return true;
2594
+ case "-":
2595
+ this.setViewport({ zoom: this.viewport.zoom / 1.25 });
2596
+ return true;
2597
+ case "0":
2598
+ this.setViewport({ zoom: 1 });
2599
+ return true;
2600
+ }
2601
+ return false;
2602
+ }
2603
+ switch (key) {
2604
+ case "Escape":
2605
+ if (this.drag) this.cancelDrag();
2606
+ else if (this.pen) this.finishPath();
2607
+ else if (this.tool === "direct" && this.anchors.length) {
2608
+ this.anchors = [];
2609
+ this.notify();
2610
+ } else if (this.tool !== "select") this.setTool("select");
2611
+ else this.clearSelection();
2612
+ return true;
2613
+ case "Enter":
2614
+ if (this.pen) {
2615
+ this.finishPath();
2616
+ return true;
2617
+ }
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") {
2619
+ this.tool = "direct";
2620
+ this.editingId = this.selection[0];
2621
+ this.notify();
2622
+ return true;
2623
+ }
2624
+ return false;
2625
+ case "Delete":
2626
+ case "Backspace":
2627
+ if (this.pen) {
2628
+ const node = this.getNode(this.pen.nodeId);
2629
+ const sp = node.subpaths[this.pen.subpath];
2630
+ if (sp.anchors.length > 2) this.deleteAnchors([{ nodeId: node.id, subpath: this.pen.subpath, index: sp.anchors.length - 1 }]);
2631
+ else {
2632
+ this.removeNodes([node.id], { label: "Delete Path" });
2633
+ this.pen = null;
2634
+ }
2635
+ return true;
2636
+ }
2637
+ return this.deleteSelection().ok;
2638
+ case "ArrowLeft":
2639
+ case "ArrowRight":
2640
+ case "ArrowUp":
2641
+ case "ArrowDown": {
2642
+ const step = input.shift ? 10 : 1;
2643
+ const dx = key === "ArrowLeft" ? -step : key === "ArrowRight" ? step : 0;
2644
+ const dy = key === "ArrowUp" ? -step : key === "ArrowDown" ? step : 0;
2645
+ return this.translate(dx, dy).ok;
2646
+ }
2647
+ }
2648
+ if (key.length === 1 && !input.alt) {
2649
+ const tool = TOOL_KEYS[lower];
2650
+ if (tool) {
2651
+ this.setTool(tool);
2652
+ return true;
2653
+ }
2654
+ }
2655
+ return false;
2656
+ }
2657
+ keyUp(input) {
2658
+ if (input.key === " " && this.spaceHeld) {
2659
+ this.spaceHeld = false;
2660
+ this.notify();
2661
+ }
2662
+ }
2663
+ }, _class);
2664
+ function constrain45(origin, p) {
2665
+ const dx = p.x - origin.x;
2666
+ const dy = p.y - origin.y;
2667
+ const len = Math.hypot(dx, dy);
2668
+ const a = Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) * (Math.PI / 4);
2669
+ return { x: origin.x + Math.cos(a) * len, y: origin.y + Math.sin(a) * len };
2670
+ }
2671
+ function scaleFor(handle, b, p, mods = {}) {
2672
+ const west = handle.includes("w");
2673
+ const east = handle.includes("e");
2674
+ const north = handle.startsWith("n");
2675
+ const south = handle.startsWith("s");
2676
+ const center = { x: b.x + b.width / 2, y: b.y + b.height / 2 };
2677
+ const origin = mods.alt ? center : { x: west ? b.x + b.width : east ? b.x : center.x, y: north ? b.y + b.height : south ? b.y : center.y };
2678
+ const reachX = mods.alt ? b.width / 2 : b.width;
2679
+ const reachY = mods.alt ? b.height / 2 : b.height;
2680
+ let sx = west || east ? reachX > 1e-9 ? (p.x - origin.x) * (west ? -1 : 1) / reachX : 1 : 1;
2681
+ let sy = north || south ? reachY > 1e-9 ? (p.y - origin.y) * (north ? -1 : 1) / reachY : 1 : 1;
2682
+ if (mods.shift) {
2683
+ if ((west || east) && (north || south)) {
2684
+ const s = Math.max(Math.abs(sx), Math.abs(sy));
2685
+ sx = Math.sign(sx || 1) * s;
2686
+ sy = Math.sign(sy || 1) * s;
2687
+ } else if (west || east) sy = Math.abs(sx);
2688
+ else sx = Math.abs(sy);
2689
+ }
2690
+ return { sx, sy, origin };
2691
+ }
2692
+
2693
+ // src/vector/render.ts
2694
+ var fmt = (n) => Math.round(n * 100) / 100;
2695
+ function renderNode(node, defs, idPrefix) {
2696
+ if (node.visible === false) return null;
2697
+ if (node.type === "group") {
2698
+ 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 };
2700
+ }
2701
+ const fill = vectorPaintToSvg(node.fill, `${idPrefix}fill-${node.id}`);
2702
+ const stroke = vectorPaintToSvg(node.stroke, `${idPrefix}stroke-${node.id}`);
2703
+ if (fill.gradient) defs.push(fill.gradient);
2704
+ if (stroke.gradient) defs.push(stroke.gradient);
2705
+ return {
2706
+ kind: "path",
2707
+ id: node.id,
2708
+ d: vectorPathData(node),
2709
+ fill: fill.value,
2710
+ stroke: stroke.value,
2711
+ strokeWidth: _nullishCoalesce(node.strokeWidth, () => ( 1)),
2712
+ ...node.strokeLinecap ? { strokeLinecap: node.strokeLinecap } : {},
2713
+ ...node.strokeLinejoin ? { strokeLinejoin: node.strokeLinejoin } : {},
2714
+ ..._optionalChain([node, 'access', _68 => _68.strokeDasharray, 'optionalAccess', _69 => _69.length]) ? { strokeDasharray: node.strokeDasharray.join(" ") } : {},
2715
+ fillRule: _nullishCoalesce(node.fillRule, () => ( "nonzero")),
2716
+ opacity: _nullishCoalesce(node.opacity, () => ( 1))
2717
+ };
2718
+ }
2719
+ function getVectorSceneRenderData(state, options = {}) {
2720
+ const defs = [];
2721
+ const prefix = _nullishCoalesce(options.idPrefix, () => ( "ve-"));
2722
+ const nodes = state.document.nodes.map((n) => renderNode(n, defs, prefix)).filter((n) => !!n);
2723
+ const v = state.viewport;
2724
+ return {
2725
+ transform: `translate(${fmt(v.x)} ${fmt(v.y)}) scale(${v.zoom})`,
2726
+ width: state.document.width,
2727
+ height: state.document.height,
2728
+ ...state.document.background ? { background: state.document.background } : {},
2729
+ defs,
2730
+ nodes
2731
+ };
2732
+ }
2733
+ var toScreen = (state, p) => ({ x: p.x * state.viewport.zoom + state.viewport.x, y: p.y * state.viewport.zoom + state.viewport.y });
2734
+ function screenOutline(state, node) {
2735
+ 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(" ");
2738
+ }
2739
+ function collect(node) {
2740
+ if (node.type === "path") return [node];
2741
+ return node.children.flatMap(collect);
2742
+ }
2743
+ function getVectorOverlayRenderData(state) {
2744
+ const doc = state.document;
2745
+ const z = state.viewport.zoom;
2746
+ const outlines = [];
2747
+ const seen = /* @__PURE__ */ new Set();
2748
+ const outline = (id, selected2) => {
2749
+ if (seen.has(id)) return;
2750
+ const node = findVectorNode(doc.nodes, id);
2751
+ if (!node || node.visible === false) return;
2752
+ seen.add(id);
2753
+ outlines.push({ id, d: screenOutline(state, node), selected: selected2 });
2754
+ };
2755
+ for (const id of state.selection) outline(id, true);
2756
+ 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);
2758
+ 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) };
2764
+ }
2765
+ const selected = (r) => state.selectedAnchors.some((a) => a.nodeId === r.nodeId && a.subpath === r.subpath && a.index === r.index);
2766
+ const sameAnchor = (a, b) => !!a && a.nodeId === b.nodeId && a.subpath === b.subpath && a.index === b.index;
2767
+ const anchors = getVisibleAnchors(state).map((ref) => {
2768
+ const a = getAnchor(doc, ref);
2769
+ const s = toScreen(state, a);
2770
+ 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 };
2772
+ });
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);
2775
+ if (a) {
2776
+ const s = toScreen(state, a);
2777
+ anchors.push({ ref: state.hover.anchor, x: s.x, y: s.y, type: _nullishCoalesce(a.type, () => ( "corner")), selected: false, hovered: true, closes: false });
2778
+ }
2779
+ }
2780
+ const handles = getVisibleHandles(state).flatMap((ref) => {
2781
+ const a = getAnchor(doc, ref);
2782
+ const h = _optionalChain([a, 'optionalAccess', _78 => _78[ref.handle]]);
2783
+ if (!a || !h) return [];
2784
+ const s = toScreen(state, h);
2785
+ 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;
2787
+ return [{ ref, x: s.x, y: s.y, anchorX: sa.x, anchorY: sa.y, hovered }];
2788
+ });
2789
+ let marquee = null;
2790
+ if (_optionalChain([state, 'access', _81 => _81.interaction, 'optionalAccess', _82 => _82.type]) === "marquee") {
2791
+ const b = state.interaction.bounds;
2792
+ const tl = toScreen(state, b);
2793
+ marquee = { x: tl.x, y: tl.y, width: b.width * z, height: b.height * z };
2794
+ }
2795
+ let penPreview = null;
2796
+ if (state.tool === "pen" && state.pen && state.pointer && !state.interaction) {
2797
+ 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]]);
2800
+ if (last) {
2801
+ const a = toScreen(state, last);
2802
+ const p = toScreen(state, state.pointer);
2803
+ const c = last.out ? toScreen(state, last.out) : null;
2804
+ penPreview = c ? `M${fmt(a.x)} ${fmt(a.y)}Q${fmt(c.x)} ${fmt(c.y)} ${fmt(p.x)} ${fmt(p.y)}` : `M${fmt(a.x)} ${fmt(a.y)}L${fmt(p.x)} ${fmt(p.y)}`;
2805
+ }
2806
+ }
2807
+ const origin = toScreen(state, { x: 0, y: 0 });
2808
+ const artboard = { x: origin.x, y: origin.y, width: doc.width * z, height: doc.height * z };
2809
+ let status = null;
2810
+ 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;
2815
+ if (b) status = `W ${fmt(b.width)} \xD7 H ${fmt(b.height)}`;
2816
+ }
2817
+ return { outlines, selectionBox, anchors, handles, marquee, penPreview, artboard, status };
2818
+ }
2819
+ var VECTOR_OVERLAY_THEME = {
2820
+ accent: "#0ea5e9",
2821
+ handleFill: "#ffffff",
2822
+ marqueeFill: "rgba(14, 165, 233, 0.08)",
2823
+ transformHandleSize: 8,
2824
+ anchorSize: 7,
2825
+ handleRadius: 3.5
2826
+ };
2827
+ function toVectorModifiers(event) {
2828
+ return { shift: !!event.shiftKey, alt: !!event.altKey, mod: !!(event.metaKey || event.ctrlKey) };
2829
+ }
2830
+ function toVectorPointerInput(event) {
2831
+ return { ...toVectorModifiers(event), button: event.button };
2832
+ }
2833
+ function toVectorKeyInput(event) {
2834
+ return { key: event.key, ...toVectorModifiers(event) };
2835
+ }
2836
+ function isVectorTextTarget(target) {
2837
+ const el = target;
2838
+ if (!el || typeof el !== "object") return false;
2839
+ const tag = (_nullishCoalesce(el.tagName, () => ( ""))).toUpperCase();
2840
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
2841
+ return !!el.isContentEditable || _optionalChain([el, 'access', _91 => _91.getAttribute, 'optionalCall', _92 => _92("contenteditable")]) === "true";
2842
+ }
2843
+ function getVectorSelectionStyle(state) {
2844
+ const paths = [];
2845
+ for (const id of state.selection) {
2846
+ const node = findVectorNode(state.document.nodes, id);
2847
+ if (node) paths.push(...collect(node));
2848
+ }
2849
+ if (!paths.length) return null;
2850
+ const first = paths[0];
2851
+ const same = (get) => paths.every((p) => JSON.stringify(_nullishCoalesce(get(p), () => ( null))) === JSON.stringify(_nullishCoalesce(get(first), () => ( null))));
2852
+ return {
2853
+ fill: _nullishCoalesce(first.fill, () => ( null)),
2854
+ stroke: _nullishCoalesce(first.stroke, () => ( null)),
2855
+ strokeWidth: _nullishCoalesce(first.strokeWidth, () => ( 1)),
2856
+ opacity: _nullishCoalesce(first.opacity, () => ( 1)),
2857
+ mixed: { fill: !same((p) => p.fill), stroke: !same((p) => p.stroke), strokeWidth: !same((p) => _nullishCoalesce(p.strokeWidth, () => ( 1))), opacity: !same((p) => _nullishCoalesce(p.opacity, () => ( 1))) },
2858
+ pathIds: paths.map((p) => p.id)
2859
+ };
2860
+ }
2861
+ function vectorPaintToHex(paint) {
2862
+ if (typeof paint !== "string") return null;
2863
+ const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(paint.trim());
2864
+ if (!m) return null;
2865
+ const hex = m[1].length === 3 ? m[1].replace(/./g, (c) => c + c) : m[1];
2866
+ return `#${hex.toLowerCase()}`;
2867
+ }
2868
+
2869
+ // src/vector/commands.ts
2870
+ var fail = (message) => {
2871
+ throw new Error(message);
2872
+ };
2873
+ var IDS = { type: "array", items: { type: "string" }, description: "Node ids (default: the selection)." };
2874
+ var PAINT = {
2875
+ description: 'A CSS colour, `null` for none, or a gradient `{ type: "linear", angle, stops: [{ offset, color }] }`.'
2876
+ };
2877
+ var STYLE_PROPS = {
2878
+ fill: PAINT,
2879
+ stroke: PAINT,
2880
+ strokeWidth: { type: "number", minimum: 0 },
2881
+ strokeLinecap: { type: "string", enum: ["butt", "round", "square"] },
2882
+ strokeLinejoin: { type: "string", enum: ["miter", "round", "bevel"] },
2883
+ strokeDasharray: { type: "array", items: { type: "number" } },
2884
+ fillRule: { type: "string", enum: ["nonzero", "evenodd"] }
2885
+ };
2886
+ var pickStyle = (params) => {
2887
+ const out = {};
2888
+ for (const key of Object.keys(STYLE_PROPS)) if (params[key] !== void 0) out[key] = params[key];
2889
+ return out;
2890
+ };
2891
+ var check = (result) => {
2892
+ if (!result.ok) fail(_nullishCoalesce(result.message, () => ( "The command failed.")));
2893
+ };
2894
+ function createVectorCommands(controller, options = {}) {
2895
+ const prefix = _nullishCoalesce(options.prefix, () => ( "vector."));
2896
+ const group = _nullishCoalesce(options.group, () => ( "Vector"));
2897
+ const base = { group, ...options.scope ? { scope: options.scope } : {} };
2898
+ const hasSelection = () => controller.getState().selection.length ? true : "Nothing is selected.";
2899
+ const ids = (params) => {
2900
+ const list2 = _nullishCoalesce(_optionalChain([params, 'optionalAccess', _93 => _93.ids]), () => ( controller.getState().selection));
2901
+ for (const id of list2) if (!controller.getNode(id)) fail(`There is no node "${id}".`);
2902
+ if (!list2.length) fail("Nothing is selected; pass ids.");
2903
+ return list2;
2904
+ };
2905
+ const enabledFor = (_, params) => _optionalChain([params, 'optionalAccess', _94 => _94.ids, 'optionalAccess', _95 => _95.length]) ? true : hasSelection();
2906
+ const list = () => {
2907
+ const state = controller.getState();
2908
+ const out = [];
2909
+ const visit = (nodes, depth) => {
2910
+ for (const n of nodes) {
2911
+ const b = vectorNodeBounds(n);
2912
+ out.push({
2913
+ id: n.id,
2914
+ type: n.type,
2915
+ ...n.name ? { name: n.name } : {},
2916
+ depth,
2917
+ 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
+ visible: n.visible !== false,
2919
+ locked: !!n.locked,
2920
+ selected: state.selection.includes(n.id),
2921
+ ...n.type === "path" ? { fill: _nullishCoalesce(n.fill, () => ( null)), stroke: _nullishCoalesce(n.stroke, () => ( null)), strokeWidth: _nullishCoalesce(n.strokeWidth, () => ( 1)), d: vectorPathData(n, 2) } : { children: n.children.length }
2922
+ });
2923
+ if (n.type === "group") visit(n.children, depth + 1);
2924
+ }
2925
+ };
2926
+ visit(state.document.nodes, 0);
2927
+ return { width: state.document.width, height: state.document.height, nodes: out, selection: state.selection, tool: state.tool };
2928
+ };
2929
+ return [
2930
+ {
2931
+ ...base,
2932
+ id: `${prefix}list`,
2933
+ label: "List Vector Nodes",
2934
+ readOnly: true,
2935
+ keywords: ["layers", "shapes", "document", "outline"],
2936
+ description: "Lists every node (paths and groups, depth first) with bounds, style, path data and selection.",
2937
+ run: list
2938
+ },
2939
+ {
2940
+ ...base,
2941
+ id: `${prefix}select`,
2942
+ label: "Select Nodes",
2943
+ keywords: ["pick", "choose"],
2944
+ description: "Selects nodes by id (empty list clears the selection).",
2945
+ params: { type: "object", properties: { ids: { type: "array", items: { type: "string" } }, add: { type: "boolean" } }, required: ["ids"] },
2946
+ run: (_ctx, params) => {
2947
+ for (const id of params.ids) if (!controller.getNode(id)) fail(`There is no node "${id}".`);
2948
+ if (!params.ids.length && !params.add) controller.clearSelection();
2949
+ else controller.select(params.ids, { add: params.add });
2950
+ return { selection: controller.getState().selection };
2951
+ }
2952
+ },
2953
+ {
2954
+ ...base,
2955
+ id: `${prefix}addShape`,
2956
+ label: "Add Shape\u2026",
2957
+ keywords: ["rectangle", "ellipse", "circle", "polygon", "star", "line", "draw"],
2958
+ description: "Adds a rectangle, ellipse, polygon, star or line in document units, using the current default style unless overridden.",
2959
+ params: {
2960
+ type: "object",
2961
+ properties: {
2962
+ kind: { type: "string", enum: ["rect", "ellipse", "polygon", "star", "line"] },
2963
+ x: { type: "number" },
2964
+ y: { type: "number" },
2965
+ width: { type: "number", description: "For a line: the x offset of its end." },
2966
+ height: { type: "number", description: "For a line: the y offset of its end." },
2967
+ cornerRadius: { type: "number", minimum: 0 },
2968
+ sides: { type: "integer", minimum: 3 },
2969
+ points: { type: "integer", minimum: 3 },
2970
+ innerRatio: { type: "number", minimum: 0, maximum: 1 },
2971
+ name: { type: "string" },
2972
+ ...STYLE_PROPS
2973
+ },
2974
+ required: ["kind", "x", "y", "width", "height"]
2975
+ },
2976
+ undoable: true,
2977
+ run: (_ctx, params) => ({ id: controller.addShape(params) })
2978
+ },
2979
+ {
2980
+ ...base,
2981
+ id: `${prefix}addPath`,
2982
+ label: "Add Path\u2026",
2983
+ keywords: ["svg", "path data", "bezier", "curve"],
2984
+ description: "Adds a path from SVG path data (`d`), in document units.",
2985
+ params: { type: "object", properties: { d: { type: "string" }, name: { type: "string" }, ...STYLE_PROPS }, required: ["d"] },
2986
+ undoable: true,
2987
+ run: (_ctx, params) => {
2988
+ const id = controller.addPath(params.d, { ...pickStyle(params), ...params.name ? { name: params.name } : {} });
2989
+ const node = controller.getNode(id);
2990
+ if (_optionalChain([node, 'optionalAccess', _96 => _96.type]) === "path" && !node.subpaths.length) {
2991
+ controller.undo();
2992
+ fail("The path data has no points.");
2993
+ }
2994
+ return { id };
2995
+ }
2996
+ },
2997
+ {
2998
+ ...base,
2999
+ id: `${prefix}setStyle`,
3000
+ label: "Set Style\u2026",
3001
+ keywords: ["fill", "stroke", "colour", "color", "gradient", "width"],
3002
+ description: "Sets fill, stroke and stroke settings of nodes (groups apply to every path inside).",
3003
+ params: { type: "object", properties: { ids: IDS, ...STYLE_PROPS } },
3004
+ enabled: enabledFor,
3005
+ undoable: true,
3006
+ run: (_ctx, params) => {
3007
+ const style = pickStyle(params);
3008
+ if (!Object.keys(style).length) fail("Pass at least one style property.");
3009
+ check(controller.setStyle(style, ids(params)));
3010
+ return { ok: true };
3011
+ }
3012
+ },
3013
+ {
3014
+ ...base,
3015
+ id: `${prefix}transform`,
3016
+ label: "Transform\u2026",
3017
+ 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.",
3019
+ params: {
3020
+ type: "object",
3021
+ properties: { ids: IDS, dx: { type: "number" }, dy: { type: "number" }, sx: { type: "number" }, sy: { type: "number" }, rotate: { type: "number" } }
3022
+ },
3023
+ enabled: enabledFor,
3024
+ undoable: true,
3025
+ run: (_ctx, params) => {
3026
+ const target = ids(params);
3027
+ if (params.dx || params.dy) check(controller.translate(_nullishCoalesce(params.dx, () => ( 0)), _nullishCoalesce(params.dy, () => ( 0)), { ids: target }));
3028
+ 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
+ if (params.rotate) check(controller.rotate(params.rotate, { ids: target }));
3030
+ return { ok: true };
3031
+ }
3032
+ },
3033
+ {
3034
+ ...base,
3035
+ id: `${prefix}delete`,
3036
+ label: "Delete",
3037
+ keywords: ["remove"],
3038
+ description: "Deletes nodes.",
3039
+ params: { type: "object", properties: { ids: IDS } },
3040
+ enabled: enabledFor,
3041
+ undoable: true,
3042
+ run: (_ctx, params) => {
3043
+ check(controller.removeNodes(ids(params)));
3044
+ return { ok: true };
3045
+ }
3046
+ },
3047
+ {
3048
+ ...base,
3049
+ id: `${prefix}boolean`,
3050
+ label: "Boolean Operation\u2026",
3051
+ keywords: ["union", "unite", "subtract", "intersect", "exclude", "combine", "pathfinder"],
3052
+ description: "Combines two or more paths with union, subtract (upper shapes from the lowest), intersect or exclude.",
3053
+ params: { type: "object", properties: { operation: { type: "string", enum: ["union", "subtract", "intersect", "exclude"] }, ids: IDS }, required: ["operation"] },
3054
+ undoable: true,
3055
+ run: (_ctx, params) => {
3056
+ const id = controller.booleanOp(params.operation, { ids: ids(params) });
3057
+ if (!id) fail("Select at least two overlapping paths (the result was empty).");
3058
+ return { id };
3059
+ }
3060
+ },
3061
+ {
3062
+ ...base,
3063
+ id: `${prefix}align`,
3064
+ label: "Align\u2026",
3065
+ keywords: ["left", "right", "center", "top", "bottom", "middle"],
3066
+ description: 'Aligns nodes to their combined bounds, or to the artboard (`to: "artboard"`, or when only one node is given).',
3067
+ params: {
3068
+ type: "object",
3069
+ properties: { edge: { type: "string", enum: ["left", "center", "right", "top", "middle", "bottom"] }, to: { type: "string", enum: ["selection", "artboard"] }, ids: IDS },
3070
+ required: ["edge"]
3071
+ },
3072
+ undoable: true,
3073
+ run: (_ctx, params) => {
3074
+ check(controller.align(params.edge, { ids: ids(params), to: params.to }));
3075
+ return { ok: true };
3076
+ }
3077
+ },
3078
+ {
3079
+ ...base,
3080
+ id: `${prefix}distribute`,
3081
+ label: "Distribute\u2026",
3082
+ keywords: ["space", "evenly", "gaps"],
3083
+ description: "Spaces three or more nodes with equal gaps.",
3084
+ params: { type: "object", properties: { direction: { type: "string", enum: ["horizontal", "vertical"] }, ids: IDS }, required: ["direction"] },
3085
+ undoable: true,
3086
+ run: (_ctx, params) => {
3087
+ check(controller.distribute(params.direction, { ids: ids(params) }));
3088
+ return { ok: true };
3089
+ }
3090
+ },
3091
+ {
3092
+ ...base,
3093
+ id: `${prefix}arrange`,
3094
+ label: "Arrange\u2026",
3095
+ keywords: ["order", "front", "back", "forward", "backward", "z-index", "stack"],
3096
+ description: "Changes stacking order within each node\u2019s parent.",
3097
+ params: { type: "object", properties: { order: { type: "string", enum: ["front", "forward", "backward", "back"] }, ids: IDS }, required: ["order"] },
3098
+ undoable: true,
3099
+ run: (_ctx, params) => {
3100
+ check(controller.arrange(params.order, { ids: ids(params) }));
3101
+ return { ok: true };
3102
+ }
3103
+ },
3104
+ {
3105
+ ...base,
3106
+ id: `${prefix}group`,
3107
+ label: "Group",
3108
+ shortcut: "Mod+G",
3109
+ description: "Groups nodes that share a parent.",
3110
+ params: { type: "object", properties: { ids: IDS, name: { type: "string" } } },
3111
+ enabled: enabledFor,
3112
+ undoable: true,
3113
+ run: (_ctx, params) => {
3114
+ const id = controller.group({ ids: ids(params), name: _optionalChain([params, 'optionalAccess', _97 => _97.name]) });
3115
+ if (!id) fail("Only nodes that share a parent can be grouped.");
3116
+ return { id };
3117
+ }
3118
+ },
3119
+ {
3120
+ ...base,
3121
+ id: `${prefix}ungroup`,
3122
+ label: "Ungroup",
3123
+ shortcut: "Mod+Shift+G",
3124
+ description: "Replaces groups with their children.",
3125
+ params: { type: "object", properties: { ids: IDS } },
3126
+ enabled: enabledFor,
3127
+ undoable: true,
3128
+ run: (_ctx, params) => {
3129
+ check(controller.ungroup({ ids: ids(params) }));
3130
+ return { selection: controller.getState().selection };
3131
+ }
3132
+ },
3133
+ {
3134
+ ...base,
3135
+ id: `${prefix}setTool`,
3136
+ label: "Choose Tool\u2026",
3137
+ keywords: ["pen", "pencil", "select", "rectangle", "ellipse"],
3138
+ description: "Switches the canvas tool.",
3139
+ params: { type: "object", properties: { tool: { type: "string", enum: [...VECTOR_TOOLS] } }, required: ["tool"] },
3140
+ run: (_ctx, params) => {
3141
+ controller.setTool(params.tool);
3142
+ return { tool: params.tool };
3143
+ }
3144
+ },
3145
+ {
3146
+ ...base,
3147
+ id: `${prefix}exportSvg`,
3148
+ label: "Export SVG",
3149
+ readOnly: true,
3150
+ keywords: ["download", "save", "markup"],
3151
+ description: "Returns the document as SVG markup.",
3152
+ 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]) }) })
3154
+ },
3155
+ {
3156
+ ...base,
3157
+ id: `${prefix}importSvg`,
3158
+ label: "Import SVG\u2026",
3159
+ keywords: ["open", "load", "paste", "markup"],
3160
+ description: "Adds the shapes of SVG markup on top of the document (or replaces it with `replace: true`).",
3161
+ params: { type: "object", properties: { svg: { type: "string" }, replace: { type: "boolean" } }, required: ["svg"] },
3162
+ undoable: true,
3163
+ run: (_ctx, params) => {
3164
+ check(params.replace ? controller.loadSvg(params.svg) : controller.importSvg(params.svg));
3165
+ return { nodes: controller.getState().document.nodes.length, selection: controller.getState().selection };
3166
+ }
3167
+ }
3168
+ ];
3169
+ }
3170
+
3171
+
3172
+
3173
+
3174
+
3175
+
3176
+
3177
+
3178
+
3179
+
3180
+
3181
+
3182
+
3183
+
3184
+
3185
+
3186
+
3187
+
3188
+
3189
+
3190
+
3191
+
3192
+
3193
+
3194
+
3195
+
3196
+
3197
+
3198
+
3199
+
3200
+
3201
+
3202
+
3203
+
3204
+
3205
+
3206
+
3207
+
3208
+
3209
+
3210
+
3211
+
3212
+
3213
+
3214
+
3215
+
3216
+
3217
+
3218
+
3219
+
3220
+
3221
+
3222
+
3223
+
3224
+
3225
+
3226
+
3227
+
3228
+
3229
+
3230
+
3231
+
3232
+
3233
+
3234
+
3235
+
3236
+
3237
+
3238
+
3239
+
3240
+
3241
+
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;
3243
+ //# sourceMappingURL=index.cjs.map