@vanduo-oss/vd3-cbun 1.1.0 → 1.3.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +26 -18
  3. package/SKILL.md +36 -8
  4. package/dist/charts/index.cjs +18 -8
  5. package/dist/charts/index.cjs.map +2 -2
  6. package/dist/charts/index.d.ts +1 -1
  7. package/dist/charts/index.js +18 -8
  8. package/dist/charts/index.js.map +2 -2
  9. package/dist/charts/vue.d.ts +78 -7
  10. package/dist/code-editor/index.cjs +5 -3
  11. package/dist/code-editor/index.cjs.map +2 -2
  12. package/dist/code-editor/index.js +5 -3
  13. package/dist/code-editor/index.js.map +2 -2
  14. package/dist/draw/core.d.ts +246 -0
  15. package/dist/draw/index.cjs +2248 -0
  16. package/dist/draw/index.cjs.map +7 -0
  17. package/dist/draw/index.d.ts +34 -0
  18. package/dist/draw/index.js +2225 -0
  19. package/dist/draw/index.js.map +7 -0
  20. package/dist/draw/shapes.d.ts +12 -0
  21. package/dist/draw/vd3-draw.css +301 -0
  22. package/dist/draw/vue.d.ts +68 -0
  23. package/dist/flowchart/index.cjs +4 -4
  24. package/dist/flowchart/index.cjs.map +2 -2
  25. package/dist/flowchart/index.js +4 -4
  26. package/dist/flowchart/index.js.map +2 -2
  27. package/dist/hex-grid/index.cjs +90 -62
  28. package/dist/hex-grid/index.cjs.map +3 -3
  29. package/dist/hex-grid/index.d.ts +1 -1
  30. package/dist/hex-grid/index.js +90 -62
  31. package/dist/hex-grid/index.js.map +3 -3
  32. package/dist/hex-grid/vue.d.ts +25 -1
  33. package/dist/index.d.ts +2 -0
  34. package/dist/index.js +5 -4
  35. package/dist/index.js.map +2 -2
  36. package/dist/meta.json +169 -45
  37. package/dist/music-player/index.cjs +3 -4
  38. package/dist/music-player/index.cjs.map +2 -2
  39. package/dist/music-player/index.d.ts +9 -1
  40. package/dist/music-player/index.js +3 -4
  41. package/dist/music-player/index.js.map +2 -2
  42. package/dist/music-player/vue.d.ts +49 -1
  43. package/package.json +9 -2
@@ -0,0 +1,2225 @@
1
+ // src/draw/vue.js
2
+ import { defineComponent, h, ref, onMounted, onBeforeUnmount, watch } from "vue";
3
+
4
+ // src/draw/shapes.js
5
+ var VD_DRAW_VERSION = "1.1.0";
6
+ var DRAW_TOOLS = Object.freeze([
7
+ "select",
8
+ "hand",
9
+ "draw",
10
+ "eraser",
11
+ "rectangle",
12
+ "ellipse",
13
+ "line",
14
+ "text",
15
+ "sticky"
16
+ ]);
17
+ var DRAW_SHAPE_TYPES = Object.freeze([
18
+ "rectangle",
19
+ "ellipse",
20
+ "line",
21
+ "freehand",
22
+ "text",
23
+ "sticky"
24
+ ]);
25
+ var BRUSH_PRESETS = Object.freeze({
26
+ pen: {
27
+ size: 6,
28
+ thinning: 0.55,
29
+ smoothing: 0.5,
30
+ streamline: 0.5,
31
+ taperStart: 0,
32
+ taperEnd: 14,
33
+ opacity: 1,
34
+ blend: "normal"
35
+ },
36
+ pencil: {
37
+ size: 4,
38
+ thinning: 0.7,
39
+ smoothing: 0.35,
40
+ streamline: 0.35,
41
+ taperStart: 4,
42
+ taperEnd: 10,
43
+ opacity: 0.92,
44
+ blend: "normal"
45
+ },
46
+ marker: {
47
+ size: 14,
48
+ thinning: 0.15,
49
+ smoothing: 0.55,
50
+ streamline: 0.5,
51
+ taperStart: 0,
52
+ taperEnd: 0,
53
+ opacity: 0.85,
54
+ blend: "normal"
55
+ },
56
+ highlighter: {
57
+ size: 22,
58
+ thinning: 0,
59
+ smoothing: 0.6,
60
+ streamline: 0.55,
61
+ taperStart: 0,
62
+ taperEnd: 0,
63
+ opacity: 0.4,
64
+ blend: "multiply"
65
+ },
66
+ calligraphy: {
67
+ size: 12,
68
+ thinning: 0.6,
69
+ smoothing: 0.4,
70
+ streamline: 0.3,
71
+ taperStart: 8,
72
+ taperEnd: 12,
73
+ opacity: 1,
74
+ nibAngle: -0.7,
75
+ blend: "normal"
76
+ }
77
+ });
78
+ var DEFAULT_BRUSH = "pen";
79
+ var POINT_TYPES = Object.freeze(["line", "freehand"]);
80
+ var MIN_SCALE = 0.2;
81
+ var MAX_SCALE = 4;
82
+ var DEFAULT_GRID_SIZE = 20;
83
+ var DEFAULT_STROKE_WIDTH = 2;
84
+ var MAX_SHAPES = 1e4;
85
+ var MAX_POINTS_PER_SHAPE = 1e5;
86
+ var idCounter = 0;
87
+ function createId(prefix = "sh") {
88
+ idCounter += 1;
89
+ return `${prefix}_${idCounter.toString(36)}${(idCounter * 2654435761).toString(36).slice(-4)}`;
90
+ }
91
+ function clamp(value, min, max) {
92
+ return Math.min(max, Math.max(min, value));
93
+ }
94
+ function round(value) {
95
+ return Math.round((Number(value) || 0) * 100) / 100;
96
+ }
97
+ function deepClone(value) {
98
+ if (typeof structuredClone === "function") {
99
+ try {
100
+ return structuredClone(value);
101
+ } catch {
102
+ }
103
+ }
104
+ return JSON.parse(JSON.stringify(value));
105
+ }
106
+ function roundPoint(p) {
107
+ const out = [round(p[0]), round(p[1])];
108
+ if (p.length >= 3 && p[2] != null) out.push(clamp(Number(p[2]) || 0, 0, 1));
109
+ return out;
110
+ }
111
+ function isPointShape(shape) {
112
+ return POINT_TYPES.includes(shape.type);
113
+ }
114
+ function shapeBounds(shape) {
115
+ if (isPointShape(shape)) {
116
+ const pts = shape.points || [];
117
+ if (!pts.length) return { x: shape.x || 0, y: shape.y || 0, w: 0, h: 0 };
118
+ let minX = Infinity;
119
+ let minY = Infinity;
120
+ let maxX = -Infinity;
121
+ let maxY = -Infinity;
122
+ for (const [px, py] of pts) {
123
+ if (px < minX) minX = px;
124
+ if (py < minY) minY = py;
125
+ if (px > maxX) maxX = px;
126
+ if (py > maxY) maxY = py;
127
+ }
128
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
129
+ }
130
+ return { x: shape.x || 0, y: shape.y || 0, w: shape.w || 0, h: shape.h || 0 };
131
+ }
132
+ function boundsOfShapes(shapes) {
133
+ if (!shapes || !shapes.length) return null;
134
+ let minX = Infinity;
135
+ let minY = Infinity;
136
+ let maxX = -Infinity;
137
+ let maxY = -Infinity;
138
+ for (const shape of shapes) {
139
+ const b = shapeBounds(shape);
140
+ minX = Math.min(minX, b.x);
141
+ minY = Math.min(minY, b.y);
142
+ maxX = Math.max(maxX, b.x + b.w);
143
+ maxY = Math.max(maxY, b.y + b.h);
144
+ }
145
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
146
+ }
147
+ function boundsIntersect(a, b) {
148
+ return !(a.x + a.w < b.x || b.x + b.w < a.x || a.y + a.h < b.y || b.y + b.h < a.y);
149
+ }
150
+ function distanceToShape(shape, x, y) {
151
+ if (isPointShape(shape)) {
152
+ const pts = shape.points || [];
153
+ if (pts.length === 1) return Math.hypot(pts[0][0] - x, pts[0][1] - y);
154
+ let min = Infinity;
155
+ for (let i = 1; i < pts.length; i += 1) {
156
+ min = Math.min(min, pointToSegment(x, y, pts[i - 1], pts[i]));
157
+ }
158
+ return min;
159
+ }
160
+ const b = shapeBounds(shape);
161
+ const dx = Math.max(b.x - x, 0, x - (b.x + b.w));
162
+ const dy = Math.max(b.y - y, 0, y - (b.y + b.h));
163
+ return Math.hypot(dx, dy);
164
+ }
165
+ function pointToSegment(px, py, a, b) {
166
+ const vx = b[0] - a[0];
167
+ const vy = b[1] - a[1];
168
+ const wx = px - a[0];
169
+ const wy = py - a[1];
170
+ const c1 = vx * wx + vy * wy;
171
+ if (c1 <= 0) return Math.hypot(px - a[0], py - a[1]);
172
+ const c2 = vx * vx + vy * vy;
173
+ if (c2 <= c1) return Math.hypot(px - b[0], py - b[1]);
174
+ const t = c1 / c2;
175
+ return Math.hypot(px - (a[0] + t * vx), py - (a[1] + t * vy));
176
+ }
177
+ function translateShape(shape, dx, dy) {
178
+ const next = deepClone(shape);
179
+ if (isPointShape(next)) {
180
+ next.points = (next.points || []).map((p) => roundPoint([p[0] + dx, p[1] + dy, p[2]]));
181
+ } else {
182
+ next.x = round((next.x || 0) + dx);
183
+ next.y = round((next.y || 0) + dy);
184
+ }
185
+ return next;
186
+ }
187
+ function scaleShape(shape, ox, oy, sx, sy) {
188
+ const next = deepClone(shape);
189
+ const avg = (Math.abs(sx) + Math.abs(sy)) * 0.5;
190
+ if (isPointShape(next)) {
191
+ next.points = (next.points || []).map(
192
+ (p) => roundPoint([ox + (p[0] - ox) * sx, oy + (p[1] - oy) * sy, p[2]])
193
+ );
194
+ if (typeof next.size === "number") next.size = round(Math.max(1, next.size * avg));
195
+ if (typeof next.strokeWidth === "number") next.strokeWidth = round(next.strokeWidth * avg);
196
+ } else {
197
+ next.x = round(ox + ((next.x || 0) - ox) * sx);
198
+ next.y = round(oy + ((next.y || 0) - oy) * sy);
199
+ next.w = round(Math.max(1, (next.w || 0) * sx));
200
+ next.h = round(Math.max(1, (next.h || 0) * sy));
201
+ }
202
+ return next;
203
+ }
204
+ function resizeHandlePositions(bounds) {
205
+ const { x, y, w, h: h2 } = bounds;
206
+ const mx = x + w / 2;
207
+ const my = y + h2 / 2;
208
+ return [
209
+ { key: "nw", x, y },
210
+ { key: "n", x: mx, y },
211
+ { key: "ne", x: x + w, y },
212
+ { key: "e", x: x + w, y: my },
213
+ { key: "se", x: x + w, y: y + h2 },
214
+ { key: "s", x: mx, y: y + h2 },
215
+ { key: "sw", x, y: y + h2 },
216
+ { key: "w", x, y: my }
217
+ ];
218
+ }
219
+ function pointsToPath(points) {
220
+ if (!points || !points.length) return "";
221
+ const [first, ...rest] = points;
222
+ let d = `M ${round(first[0])} ${round(first[1])}`;
223
+ for (const [px, py] of rest) d += ` L ${round(px)} ${round(py)}`;
224
+ return d;
225
+ }
226
+ function simplifyPoints(points, min = 1.2) {
227
+ if (!points || points.length <= 2) return (points || []).map(roundPoint);
228
+ const out = [roundPoint(points[0])];
229
+ for (let i = 1; i < points.length; i += 1) {
230
+ const p = points[i];
231
+ const last = out[out.length - 1];
232
+ if (Math.hypot(p[0] - last[0], p[1] - last[1]) >= min || i === points.length - 1)
233
+ out.push(roundPoint(p));
234
+ }
235
+ return out;
236
+ }
237
+ function streamlinePoints(points, streamline) {
238
+ const first = points[0];
239
+ const out = [[first[0], first[1], first[2] == null ? 0.5 : clamp(first[2], 0, 1)]];
240
+ if (points.length < 2) return out;
241
+ const factor = clamp(1 - (streamline == null ? 0.5 : streamline), 0.15, 1);
242
+ for (let i = 1; i < points.length; i += 1) {
243
+ const prev = out[out.length - 1];
244
+ const p = points[i];
245
+ out.push([
246
+ prev[0] + (p[0] - prev[0]) * factor,
247
+ prev[1] + (p[1] - prev[1]) * factor,
248
+ p[2] == null ? 0.5 : clamp(p[2], 0, 1)
249
+ ]);
250
+ }
251
+ return out;
252
+ }
253
+ function circlePolygon(cx, cy, r, segments = 16) {
254
+ const pts = [];
255
+ for (let i = 0; i < segments; i += 1) {
256
+ const a = i / segments * Math.PI * 2;
257
+ pts.push([cx + Math.cos(a) * r, cy + Math.sin(a) * r]);
258
+ }
259
+ return pts;
260
+ }
261
+ function velocityPressure(segmentLength) {
262
+ return clamp(1 - segmentLength / 28, 0.25, 1);
263
+ }
264
+ function strokeOutline(rawPoints, options = {}) {
265
+ const points = (rawPoints || []).filter((p) => Array.isArray(p) && p.length >= 2);
266
+ if (points.length === 0) return [];
267
+ const size = Math.max(1, options.size == null ? 8 : options.size);
268
+ const thinning = options.thinning == null ? 0.5 : options.thinning;
269
+ const taperStart = options.taperStart || 0;
270
+ const taperEnd = options.taperEnd || 0;
271
+ const nibAngle = options.nibAngle;
272
+ const pts = streamlinePoints(points, options.streamline);
273
+ if (pts.length === 1) return circlePolygon(pts[0][0], pts[0][1], size / 2);
274
+ const segLen = [];
275
+ let total = 0;
276
+ for (let i = 1; i < pts.length; i += 1) {
277
+ const d = Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]);
278
+ segLen.push(d);
279
+ total += d;
280
+ }
281
+ const hasPressure = points.some((p) => p.length >= 3 && p[2] != null && p[2] > 0 && p[2] !== 0.5);
282
+ const left = [];
283
+ const right = [];
284
+ let run = 0;
285
+ for (let i = 0; i < pts.length; i += 1) {
286
+ const p = pts[i];
287
+ const prev = pts[i - 1] || p;
288
+ const next = pts[i + 1] || p;
289
+ let dx = next[0] - prev[0];
290
+ let dy = next[1] - prev[1];
291
+ const len = Math.hypot(dx, dy) || 1;
292
+ dx /= len;
293
+ dy /= len;
294
+ const nx = -dy;
295
+ const ny = dx;
296
+ const pressure = hasPressure ? p[2] : velocityPressure(segLen[i - 1] == null ? segLen[i] || 0 : segLen[i - 1]);
297
+ let radius = size / 2 * (thinning === 0 ? 1 : 1 - thinning + thinning * pressure);
298
+ if (typeof nibAngle === "number") {
299
+ radius *= 0.35 + 0.65 * Math.abs(Math.sin(Math.atan2(dy, dx) - nibAngle));
300
+ }
301
+ if (i > 0) run += segLen[i - 1];
302
+ if (taperStart > 0) radius *= clamp(run / taperStart, 0, 1);
303
+ if (taperEnd > 0) radius *= clamp((total - run) / taperEnd, 0, 1);
304
+ radius = Math.max(0.1, radius);
305
+ left.push([p[0] + nx * radius, p[1] + ny * radius]);
306
+ right.push([p[0] - nx * radius, p[1] - ny * radius]);
307
+ }
308
+ return [...left, ...right.reverse()];
309
+ }
310
+ function pointsToBrushPath(outline) {
311
+ if (!outline || outline.length < 3) {
312
+ if (outline && outline.length) {
313
+ const [x, y] = outline[0];
314
+ return `M ${round(x - 1)} ${round(y)} a 1 1 0 1 0 2 0 a 1 1 0 1 0 -2 0 Z`;
315
+ }
316
+ return "";
317
+ }
318
+ let d = `M ${round(outline[0][0])} ${round(outline[0][1])}`;
319
+ for (let i = 1; i < outline.length; i += 1) {
320
+ const prev = outline[i - 1];
321
+ const cur = outline[i];
322
+ const mx = (prev[0] + cur[0]) / 2;
323
+ const my = (prev[1] + cur[1]) / 2;
324
+ d += ` Q ${round(prev[0])} ${round(prev[1])} ${round(mx)} ${round(my)}`;
325
+ }
326
+ return `${d} Z`;
327
+ }
328
+ function brushStrokePath(shape) {
329
+ const preset = BRUSH_PRESETS[shape.brush] || BRUSH_PRESETS[DEFAULT_BRUSH];
330
+ return pointsToBrushPath(
331
+ strokeOutline(shape.points || [], {
332
+ ...preset,
333
+ size: shape.size == null ? preset.size : shape.size
334
+ })
335
+ );
336
+ }
337
+ function coerceNumber(value, fallback = 0) {
338
+ const n = Number(value);
339
+ return Number.isFinite(n) ? n : fallback;
340
+ }
341
+ function coercePoints(raw) {
342
+ if (!Array.isArray(raw)) return [];
343
+ const pts = [];
344
+ const limit = Math.min(raw.length, MAX_POINTS_PER_SHAPE);
345
+ for (let i = 0; i < limit; i += 1) {
346
+ const p = raw[i];
347
+ if (Array.isArray(p) && p.length >= 2 && Number.isFinite(Number(p[0])) && Number.isFinite(Number(p[1]))) {
348
+ const pt = [round(Number(p[0])), round(Number(p[1]))];
349
+ if (p.length >= 3 && Number.isFinite(Number(p[2]))) pt.push(clamp(Number(p[2]), 0, 1));
350
+ pts.push(pt);
351
+ }
352
+ }
353
+ return pts;
354
+ }
355
+ function normalizeShape(raw, usedIds) {
356
+ if (!raw || typeof raw !== "object") return null;
357
+ const type = DRAW_SHAPE_TYPES.includes(raw.type) ? raw.type : "rectangle";
358
+ let id = typeof raw.id === "string" && raw.id ? raw.id : createId(type.slice(0, 2));
359
+ if (usedIds) {
360
+ while (usedIds.has(id)) id = createId(type.slice(0, 2));
361
+ usedIds.add(id);
362
+ }
363
+ const groupId = typeof raw.groupId === "string" && raw.groupId ? raw.groupId : void 0;
364
+ const color = typeof raw.color === "string" ? raw.color : typeof raw.stroke === "string" ? raw.stroke : void 0;
365
+ if (type === "freehand") {
366
+ const points = coercePoints(raw.points);
367
+ if (points.length < 1) return null;
368
+ const brush = BRUSH_PRESETS[raw.brush] ? raw.brush : DEFAULT_BRUSH;
369
+ const preset = BRUSH_PRESETS[brush];
370
+ const size = raw.size != null ? Math.max(1, coerceNumber(raw.size, preset.size)) : raw.strokeWidth != null ? Math.max(1, coerceNumber(raw.strokeWidth, DEFAULT_STROKE_WIDTH) * 2) : preset.size;
371
+ const shape2 = {
372
+ id,
373
+ type: "freehand",
374
+ brush,
375
+ points,
376
+ size: round(size),
377
+ opacity: raw.opacity == null ? preset.opacity : clamp(coerceNumber(raw.opacity, preset.opacity), 0, 1)
378
+ };
379
+ if (color) shape2.color = color;
380
+ if (groupId) shape2.groupId = groupId;
381
+ return shape2;
382
+ }
383
+ const base = {
384
+ id,
385
+ type,
386
+ strokeWidth: raw.strokeWidth == null ? DEFAULT_STROKE_WIDTH : coerceNumber(raw.strokeWidth, DEFAULT_STROKE_WIDTH),
387
+ opacity: raw.opacity == null ? 1 : clamp(coerceNumber(raw.opacity, 1), 0, 1)
388
+ };
389
+ if (color) base.color = color;
390
+ if (typeof raw.fill === "string") base.fill = raw.fill;
391
+ if (groupId) base.groupId = groupId;
392
+ if (type === "line") {
393
+ const points = coercePoints(raw.points);
394
+ if (points.length < 2) return null;
395
+ return {
396
+ ...base,
397
+ points,
398
+ arrowStart: Boolean(raw.arrowStart),
399
+ arrowEnd: raw.arrowEnd == null ? true : Boolean(raw.arrowEnd)
400
+ };
401
+ }
402
+ const shape = {
403
+ ...base,
404
+ x: round(coerceNumber(raw.x, 0)),
405
+ y: round(coerceNumber(raw.y, 0)),
406
+ w: round(Math.max(1, coerceNumber(raw.w, 100))),
407
+ h: round(Math.max(1, coerceNumber(raw.h, 80))),
408
+ rotation: coerceNumber(raw.rotation, 0)
409
+ };
410
+ if (type === "text" || type === "sticky")
411
+ shape.text = typeof raw.text === "string" ? raw.text : "";
412
+ return shape;
413
+ }
414
+ function normalizeViewport(raw) {
415
+ const vp = raw && typeof raw === "object" ? raw : {};
416
+ return {
417
+ x: coerceNumber(vp.x, 0),
418
+ y: coerceNumber(vp.y, 0),
419
+ scale: clamp(coerceNumber(vp.scale, 1), MIN_SCALE, MAX_SCALE)
420
+ };
421
+ }
422
+ function normalizeDocument(data) {
423
+ let raw = data;
424
+ if (typeof raw === "string") {
425
+ try {
426
+ raw = JSON.parse(raw);
427
+ } catch {
428
+ raw = {};
429
+ }
430
+ }
431
+ if (!raw || typeof raw !== "object") raw = {};
432
+ const usedIds = /* @__PURE__ */ new Set();
433
+ const rawShapes = Array.isArray(raw.shapes) ? raw.shapes : [];
434
+ const shapes = [];
435
+ const shapeLimit = Math.min(rawShapes.length, MAX_SHAPES);
436
+ for (let i = 0; i < shapeLimit; i += 1) {
437
+ const shape = normalizeShape(rawShapes[i], usedIds);
438
+ if (shape) shapes.push(shape);
439
+ }
440
+ const groupCounts = /* @__PURE__ */ new Map();
441
+ for (const s of shapes)
442
+ if (s.groupId) groupCounts.set(s.groupId, (groupCounts.get(s.groupId) || 0) + 1);
443
+ for (const s of shapes) if (s.groupId && groupCounts.get(s.groupId) < 2) delete s.groupId;
444
+ return {
445
+ version: VD_DRAW_VERSION,
446
+ viewport: normalizeViewport(raw.viewport),
447
+ shapes
448
+ };
449
+ }
450
+
451
+ // src/draw/core.js
452
+ var SVG_NS = "http://www.w3.org/2000/svg";
453
+ var WORLD_EXTENT = 8e3;
454
+ var HANDLE_SIZE = 8;
455
+ var SNAP_THRESHOLD = 6;
456
+ var DRAG_THRESHOLD = 3;
457
+ var ERASER_RADIUS = 12;
458
+ var COALESCE_REASONS = /* @__PURE__ */ new Set(["shape:style", "shape:text", "shape:nudge"]);
459
+ var ICON_PATHS = {
460
+ select: "M168,132.69,214.08,115l.33-.13A16,16,0,0,0,213,85.07L52.92,32.8A15.95,15.95,0,0,0,32.8,52.92L85.07,213a15.82,15.82,0,0,0,14.41,11l.78,0a15.84,15.84,0,0,0,14.61-9.59l.13-.33L132.69,168,184,219.31a16,16,0,0,0,22.63,0l12.68-12.68a16,16,0,0,0,0-22.63ZM195.31,208,144,156.69a16,16,0,0,0-26,4.93c0,.11-.09.22-.13.32l-17.65,46L48,48l159.85,52.2-45.95,17.64-.32.13a16,16,0,0,0-4.93,26h0L208,195.31Z",
461
+ hand: "M188,48a27.75,27.75,0,0,0-12,2.71V44a28,28,0,0,0-54.65-8.6A28,28,0,0,0,80,60v64l-3.82-6.13a28,28,0,0,0-48.6,27.82c16,33.77,28.93,57.72,43.72,72.69C86.24,233.54,103.2,240,128,240a88.1,88.1,0,0,0,88-88V76A28,28,0,0,0,188,48Zm12,104a72.08,72.08,0,0,1-72,72c-20.38,0-33.51-4.88-45.33-16.85C69.44,193.74,57.26,171,41.9,138.58a6.36,6.36,0,0,0-.3-.58,12,12,0,0,1,20.79-12,1.76,1.76,0,0,0,.14.23l18.67,30A8,8,0,0,0,96,152V60a12,12,0,0,1,24,0v60a8,8,0,0,0,16,0V44a12,12,0,0,1,24,0v76a8,8,0,0,0,16,0V76a12,12,0,0,1,24,0Z",
462
+ draw: "M232,32a8,8,0,0,0-8-8c-44.08,0-89.31,49.71-114.43,82.63A60,60,0,0,0,32,164c0,30.88-19.54,44.73-20.47,45.37A8,8,0,0,0,16,224H92a60,60,0,0,0,57.37-77.57C182.3,121.31,232,76.08,232,32ZM92,208H34.63C41.38,198.41,48,183.92,48,164a44,44,0,1,1,44,44Zm32.42-94.45q5.14-6.66,10.09-12.55A76.23,76.23,0,0,1,155,121.49q-5.9,4.94-12.55,10.09A60.54,60.54,0,0,0,124.42,113.55Zm42.7-2.68a92.57,92.57,0,0,0-22-22c31.78-34.53,55.75-45,69.9-47.91C212.17,55.12,201.65,79.09,167.12,110.87Z",
463
+ eraser: "M225,80.4,183.6,39a24,24,0,0,0-33.94,0L31,157.66a24,24,0,0,0,0,33.94l30.06,30.06A8,8,0,0,0,66.74,224H216a8,8,0,0,0,0-16h-84.7L225,114.34A24,24,0,0,0,225,80.4ZM108.68,208H70.05L42.33,180.28a8,8,0,0,1,0-11.31L96,115.31,148.69,168Zm105-105L160,156.69,107.31,104,161,50.34a8,8,0,0,1,11.32,0l41.38,41.38a8,8,0,0,1,0,11.31Z",
464
+ rectangle: "M208,32H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32Zm0,176H48V48H208V208Z",
465
+ ellipse: "M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Z",
466
+ line: "M200,64V168a8,8,0,0,1-16,0V83.31L69.66,197.66a8,8,0,0,1-11.32-11.32L172.69,72H88a8,8,0,0,1,0-16H192A8,8,0,0,1,200,64Z",
467
+ text: "M208,56V88a8,8,0,0,1-16,0V64H136V192h24a8,8,0,0,1,0,16H96a8,8,0,0,1,0-16h24V64H64V88a8,8,0,0,1-16,0V56a8,8,0,0,1,8-8H200A8,8,0,0,1,208,56Z",
468
+ sticky: "M88,96a8,8,0,0,1,8-8h64a8,8,0,0,1,0,16H96A8,8,0,0,1,88,96Zm8,40h64a8,8,0,0,0,0-16H96a8,8,0,0,0,0,16Zm32,16H96a8,8,0,0,0,0,16h32a8,8,0,0,0,0-16ZM224,48V156.69A15.86,15.86,0,0,1,219.31,168L168,219.31A15.86,15.86,0,0,1,156.69,224H48a16,16,0,0,1-16-16V48A16,16,0,0,1,48,32H208A16,16,0,0,1,224,48ZM48,208H152V160a8,8,0,0,1,8-8h48V48H48Zm120-40v28.7L196.69,168Z",
469
+ undo: "M224,128a96,96,0,0,1-94.71,96H128A95.38,95.38,0,0,1,62.1,197.8a8,8,0,0,1,11-11.63A80,80,0,1,0,71.43,71.39a3.07,3.07,0,0,1-.26.25L44.59,96H72a8,8,0,0,1,0,16H24a8,8,0,0,1-8-8V56a8,8,0,0,1,16,0V85.8L60.25,60A96,96,0,0,1,224,128Z",
470
+ redo: "M240,56v48a8,8,0,0,1-8,8H184a8,8,0,0,1,0-16H211.4L184.81,71.64l-.25-.24a80,80,0,1,0-1.67,114.78,8,8,0,0,1,11,11.63A95.44,95.44,0,0,1,128,224h-1.32A96,96,0,1,1,195.75,60L224,85.8V56a8,8,0,1,1,16,0Z",
471
+ duplicate: "M216,32H88a8,8,0,0,0-8,8V80H40a8,8,0,0,0-8,8V216a8,8,0,0,0,8,8H168a8,8,0,0,0,8-8V176h40a8,8,0,0,0,8-8V40A8,8,0,0,0,216,32ZM160,208H48V96H160Zm48-48H176V88a8,8,0,0,0-8-8H96V48H208Z",
472
+ delete: "M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z",
473
+ grid: "M200,40H56A16,16,0,0,0,40,56V200a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V56A16,16,0,0,0,200,40Zm0,80H136V56h64ZM120,56v64H56V56ZM56,136h64v64H56Zm144,64H136V136h64v64Z"
474
+ };
475
+ var TOOL_BUTTONS = [
476
+ { tool: "select", label: "Select" },
477
+ { tool: "hand", label: "Pan" },
478
+ { tool: "draw", label: "Brush" },
479
+ { tool: "eraser", label: "Eraser" },
480
+ { tool: "rectangle", label: "Rectangle" },
481
+ { tool: "ellipse", label: "Ellipse" },
482
+ { tool: "line", label: "Arrow" },
483
+ { tool: "text", label: "Text" },
484
+ { tool: "sticky", label: "Sticky note" }
485
+ ];
486
+ var ACTION_BUTTONS = [
487
+ { action: "undo", label: "Undo" },
488
+ { action: "redo", label: "Redo" },
489
+ { action: "duplicate", label: "Duplicate" },
490
+ { action: "delete", label: "Delete / clear" }
491
+ ];
492
+ var BRUSH_ORDER = ["pen", "pencil", "marker", "highlighter", "calligraphy"];
493
+ var BRUSH_LABELS = {
494
+ pen: "Pen",
495
+ pencil: "Pencil",
496
+ marker: "Marker",
497
+ highlighter: "Highlighter",
498
+ calligraphy: "Calligraphy"
499
+ };
500
+ var SWATCHES = [
501
+ "#1f2720",
502
+ "#495057",
503
+ "#e03131",
504
+ "#f08c00",
505
+ "#f2c200",
506
+ "#2f9e44",
507
+ "#1971c2",
508
+ "#7048e8",
509
+ "#e64980",
510
+ "#ffffff"
511
+ ];
512
+ function createSvgEl(name, attrs) {
513
+ const el = document.createElementNS(SVG_NS, name);
514
+ if (attrs) for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, String(v));
515
+ return el;
516
+ }
517
+ function createIcon(name) {
518
+ const svg = createSvgEl("svg", {
519
+ viewBox: "0 0 256 256",
520
+ "aria-hidden": "true",
521
+ focusable: "false"
522
+ });
523
+ svg.classList.add("vd-draw-icon");
524
+ svg.setAttribute("fill", "currentColor");
525
+ svg.appendChild(createSvgEl("path", { d: ICON_PATHS[name] || "" }));
526
+ return svg;
527
+ }
528
+ function clearChildren(node) {
529
+ if (node) node.replaceChildren();
530
+ }
531
+ function hasWindow() {
532
+ return typeof window !== "undefined" && typeof document !== "undefined";
533
+ }
534
+ function toHex6(color) {
535
+ if (typeof color !== "string") return null;
536
+ const value = color.trim();
537
+ if (/^#[0-9a-fA-F]{6}$/.test(value)) return value.toLowerCase();
538
+ const short = /^#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])$/.exec(value);
539
+ if (short)
540
+ return `#${short[1]}${short[1]}${short[2]}${short[2]}${short[3]}${short[3]}`.toLowerCase();
541
+ return null;
542
+ }
543
+ var VdDraw = class {
544
+ constructor(options = {}) {
545
+ const opts = options || {};
546
+ this.element = this._resolveElement(opts.element ?? opts.target);
547
+ if (!this.element) throw new Error("VdDraw: a target `element` is required");
548
+ this.readonly = Boolean(opts.readonly);
549
+ this.tool = DRAW_TOOLS.includes(opts.tool) ? opts.tool : "draw";
550
+ this.gridSize = Number.isFinite(opts.gridSize) ? opts.gridSize : DEFAULT_GRID_SIZE;
551
+ this.showGrid = opts.showGrid == null ? true : Boolean(opts.showGrid);
552
+ this.snap = opts.snap == null ? true : Boolean(opts.snap);
553
+ this.autoFit = Boolean(opts.autoFit);
554
+ this.historyEnabled = opts.history == null ? true : Boolean(opts.history);
555
+ this.historyLimit = Number.isFinite(opts.historyLimit) ? opts.historyLimit : 100;
556
+ this.style = {
557
+ color: typeof opts.color === "string" ? opts.color : "#1f2720",
558
+ opacity: Number.isFinite(opts.opacity) ? clamp(opts.opacity, 0.05, 1) : 1,
559
+ size: Number.isFinite(opts.brushSize) ? opts.brushSize : BRUSH_PRESETS[DEFAULT_BRUSH].size,
560
+ brush: BRUSH_PRESETS[opts.brush] ? opts.brush : DEFAULT_BRUSH
561
+ };
562
+ this.recentColors = [];
563
+ this.documentData = normalizeDocument(opts.data);
564
+ this.selectedIds = /* @__PURE__ */ new Set();
565
+ this.interaction = null;
566
+ this.clipboard = [];
567
+ this.textEditor = null;
568
+ this.destroyed = false;
569
+ this.history = [];
570
+ this.historyIndex = -1;
571
+ this.isApplyingHistory = false;
572
+ this.lastReason = null;
573
+ this.lastTargetKey = null;
574
+ this.listeners = /* @__PURE__ */ new Map();
575
+ this._buildShell();
576
+ if (typeof opts.color !== "string") this.style.color = this._resolveInk();
577
+ this._bindEvents();
578
+ this._resetHistory();
579
+ this.render();
580
+ this._syncStylePanel();
581
+ this._scheduleReady();
582
+ }
583
+ _resolveElement(target) {
584
+ if (!target) return null;
585
+ if (typeof target === "string") return hasWindow() ? document.querySelector(target) : null;
586
+ return target;
587
+ }
588
+ _resolveInk() {
589
+ if (!hasWindow()) return "#1f2720";
590
+ try {
591
+ const v = getComputedStyle(this.svg).getPropertyValue("--vd-draw-ink").trim();
592
+ return v || "#1f2720";
593
+ } catch {
594
+ return "#1f2720";
595
+ }
596
+ }
597
+ // ── Shell ──────────────────────────────────────────────────────────────
598
+ _buildShell() {
599
+ const host = this.element;
600
+ host.classList.add("vd-draw-host");
601
+ clearChildren(host);
602
+ const shell = document.createElement("div");
603
+ shell.className = "vd-draw-shell";
604
+ this.toolbarEl = document.createElement("div");
605
+ this.toolbarEl.className = "vd-draw-toolbar";
606
+ this.toolbarEl.setAttribute("role", "toolbar");
607
+ this.toolbarEl.setAttribute("aria-label", "Drawing tools");
608
+ this.panelEl = document.createElement("div");
609
+ this.panelEl.className = "vd-draw-panel";
610
+ if (!this.readonly) {
611
+ this._buildToolbar();
612
+ this._buildStylePanel();
613
+ }
614
+ this.canvasEl = document.createElement("div");
615
+ this.canvasEl.className = "vd-draw-canvas";
616
+ this.canvasEl.tabIndex = 0;
617
+ this.svg = createSvgEl("svg", { class: "vd-draw-svg" });
618
+ this.svg.setAttribute("width", "100%");
619
+ this.svg.setAttribute("height", "100%");
620
+ const defs = createSvgEl("defs");
621
+ const gs = this.gridSize;
622
+ this.gridPattern = createSvgEl("pattern", {
623
+ id: this._svgId("grid"),
624
+ width: gs,
625
+ height: gs,
626
+ patternUnits: "userSpaceOnUse"
627
+ });
628
+ this.gridPatternPath = createSvgEl("path", {
629
+ d: `M ${gs} 0 L 0 0 0 ${gs}`,
630
+ fill: "none",
631
+ stroke: "var(--vd-draw-grid)",
632
+ "stroke-width": 1
633
+ });
634
+ this.gridPattern.appendChild(this.gridPatternPath);
635
+ defs.appendChild(this.gridPattern);
636
+ const marker = createSvgEl("marker", {
637
+ id: this._svgId("arrow"),
638
+ viewBox: "0 0 10 10",
639
+ refX: 8,
640
+ refY: 5,
641
+ markerWidth: 7,
642
+ markerHeight: 7,
643
+ orient: "auto-start-reverse"
644
+ });
645
+ marker.appendChild(
646
+ createSvgEl("path", { d: "M 0 0 L 10 5 L 0 10 z", fill: "var(--vd-draw-shape-stroke)" })
647
+ );
648
+ defs.appendChild(marker);
649
+ this.svg.appendChild(defs);
650
+ this.world = createSvgEl("g", { class: "vd-draw-world" });
651
+ this.gridLayer = createSvgEl("rect", {
652
+ class: "vd-draw-grid-rect",
653
+ x: -WORLD_EXTENT,
654
+ y: -WORLD_EXTENT,
655
+ width: WORLD_EXTENT * 2,
656
+ height: WORLD_EXTENT * 2,
657
+ fill: `url(#${this._svgId("grid")})`
658
+ });
659
+ if (!this.showGrid) this.gridLayer.style.display = "none";
660
+ this.shapesLayer = createSvgEl("g", { class: "vd-draw-shapes" });
661
+ this.guidesLayer = createSvgEl("g", { class: "vd-draw-guides" });
662
+ this.overlayLayer = createSvgEl("g", { class: "vd-draw-overlay" });
663
+ this.marqueeLayer = createSvgEl("g", { class: "vd-draw-marquee" });
664
+ this.world.append(
665
+ this.gridLayer,
666
+ this.shapesLayer,
667
+ this.guidesLayer,
668
+ this.overlayLayer,
669
+ this.marqueeLayer
670
+ );
671
+ this.svg.appendChild(this.world);
672
+ this.canvasEl.appendChild(this.svg);
673
+ this.textLayer = document.createElement("div");
674
+ this.textLayer.className = "vd-draw-text-layer";
675
+ this.canvasEl.appendChild(this.textLayer);
676
+ shell.append(this.toolbarEl, this.panelEl, this.canvasEl);
677
+ host.appendChild(shell);
678
+ }
679
+ _buildToolbar() {
680
+ clearChildren(this.toolbarEl);
681
+ const makeBtn = (name, label, attr, value, active) => {
682
+ const btn = document.createElement("button");
683
+ btn.type = "button";
684
+ btn.className = "vd-draw-tool";
685
+ btn.setAttribute(attr, value);
686
+ btn.setAttribute("aria-label", label);
687
+ btn.setAttribute("title", label);
688
+ if (active) btn.setAttribute("aria-pressed", "true");
689
+ btn.appendChild(createIcon(name));
690
+ return btn;
691
+ };
692
+ const tools = document.createElement("div");
693
+ tools.className = "vd-draw-toolbar-group";
694
+ for (const t of TOOL_BUTTONS)
695
+ tools.appendChild(makeBtn(t.tool, t.label, "data-tool", t.tool, t.tool === this.tool));
696
+ const actions = document.createElement("div");
697
+ actions.className = "vd-draw-toolbar-group";
698
+ for (const a of ACTION_BUTTONS)
699
+ actions.appendChild(makeBtn(a.action, a.label, "data-action", a.action, false));
700
+ this.toolbarEl.append(tools, actions);
701
+ }
702
+ _panelGroup(labelText, className = "") {
703
+ const group = document.createElement("div");
704
+ group.className = `vd-draw-panel-group${className ? ` ${className}` : ""}`;
705
+ const label = document.createElement("span");
706
+ label.className = "vd-draw-panel-label";
707
+ label.textContent = labelText;
708
+ group.appendChild(label);
709
+ return group;
710
+ }
711
+ _rangeInput(min, max, step, styleKey) {
712
+ const input = document.createElement("input");
713
+ input.type = "range";
714
+ input.min = String(min);
715
+ input.max = String(max);
716
+ input.step = String(step);
717
+ input.setAttribute("data-style", styleKey);
718
+ input.setAttribute("aria-label", styleKey);
719
+ return input;
720
+ }
721
+ _buildStylePanel() {
722
+ clearChildren(this.panelEl);
723
+ const brushGroup = this._panelGroup("Brush");
724
+ for (const key of BRUSH_ORDER) {
725
+ const btn = document.createElement("button");
726
+ btn.type = "button";
727
+ btn.className = "vd-draw-brush";
728
+ btn.setAttribute("data-brush", key);
729
+ btn.setAttribute("title", BRUSH_LABELS[key]);
730
+ btn.textContent = BRUSH_LABELS[key];
731
+ brushGroup.appendChild(btn);
732
+ }
733
+ const colorGroup = this._panelGroup("Color", "vd-draw-swatches-group");
734
+ for (const color of SWATCHES) {
735
+ const sw = document.createElement("button");
736
+ sw.type = "button";
737
+ sw.className = "vd-draw-swatch";
738
+ sw.setAttribute("data-swatch", color);
739
+ sw.setAttribute("aria-label", `Color ${color}`);
740
+ sw.setAttribute("title", color);
741
+ sw.style.setProperty("--sw", color);
742
+ colorGroup.appendChild(sw);
743
+ }
744
+ this.colorInput = document.createElement("input");
745
+ this.colorInput.type = "color";
746
+ this.colorInput.className = "vd-draw-color-input";
747
+ this.colorInput.setAttribute("data-style", "color");
748
+ this.colorInput.setAttribute("aria-label", "Custom color");
749
+ this.colorInput.setAttribute("title", "Custom color");
750
+ colorGroup.appendChild(this.colorInput);
751
+ const sizeGroup = this._panelGroup("Size");
752
+ this.sizeInput = this._rangeInput(1, 40, 1, "size");
753
+ this.sizeValue = document.createElement("span");
754
+ this.sizeValue.className = "vd-draw-value";
755
+ sizeGroup.append(this.sizeInput, this.sizeValue);
756
+ const opacityGroup = this._panelGroup("Opacity");
757
+ this.opacityInput = this._rangeInput(0.1, 1, 0.05, "opacity");
758
+ this.opacityValue = document.createElement("span");
759
+ this.opacityValue.className = "vd-draw-value";
760
+ opacityGroup.append(this.opacityInput, this.opacityValue);
761
+ const gridGroup = this._panelGroup("Grid");
762
+ this.gridToggle = document.createElement("button");
763
+ this.gridToggle.type = "button";
764
+ this.gridToggle.className = "vd-draw-tool";
765
+ this.gridToggle.setAttribute("data-grid", "toggle");
766
+ this.gridToggle.setAttribute("aria-label", "Toggle grid");
767
+ this.gridToggle.setAttribute("title", "Toggle grid");
768
+ this.gridToggle.appendChild(createIcon("grid"));
769
+ this.gridSizeInput = this._rangeInput(4, 128, 4, "grid");
770
+ this.gridSizeInput.title = "Grid cell size";
771
+ gridGroup.append(this.gridToggle, this.gridSizeInput);
772
+ this.recentGroup = this._panelGroup("Recent", "vd-draw-swatches-group");
773
+ this.recentEl = document.createElement("span");
774
+ this.recentEl.className = "vd-draw-recent";
775
+ this.recentGroup.appendChild(this.recentEl);
776
+ this.panelEl.append(
777
+ brushGroup,
778
+ colorGroup,
779
+ sizeGroup,
780
+ opacityGroup,
781
+ gridGroup,
782
+ this.recentGroup
783
+ );
784
+ }
785
+ _svgId(suffix) {
786
+ if (!this._idBase) this._idBase = createId("draw");
787
+ return `${this._idBase}-${suffix}`;
788
+ }
789
+ _syncToolbar() {
790
+ if (this.readonly || !this.toolbarEl) return;
791
+ for (const btn of this.toolbarEl.querySelectorAll("[data-tool]")) {
792
+ if (btn.getAttribute("data-tool") === this.tool) btn.setAttribute("aria-pressed", "true");
793
+ else btn.removeAttribute("aria-pressed");
794
+ }
795
+ }
796
+ _syncStylePanel() {
797
+ if (this.readonly || !this.panelEl) return;
798
+ for (const btn of this.panelEl.querySelectorAll("[data-brush]")) {
799
+ if (btn.getAttribute("data-brush") === this.style.brush)
800
+ btn.setAttribute("aria-pressed", "true");
801
+ else btn.removeAttribute("aria-pressed");
802
+ }
803
+ if (this.colorInput) {
804
+ const hex = toHex6(this.style.color);
805
+ if (hex) this.colorInput.value = hex;
806
+ }
807
+ if (this.sizeInput) this.sizeInput.value = String(this.style.size);
808
+ if (this.sizeValue) this.sizeValue.textContent = `${Math.round(this.style.size)}px`;
809
+ if (this.opacityInput) this.opacityInput.value = String(this.style.opacity);
810
+ if (this.opacityValue)
811
+ this.opacityValue.textContent = `${Math.round(this.style.opacity * 100)}%`;
812
+ if (this.gridToggle) {
813
+ if (this.showGrid) this.gridToggle.setAttribute("aria-pressed", "true");
814
+ else this.gridToggle.removeAttribute("aria-pressed");
815
+ }
816
+ if (this.gridSizeInput) this.gridSizeInput.value = String(this.gridSize);
817
+ for (const sw of this.panelEl.querySelectorAll(".vd-draw-swatch[data-swatch]")) {
818
+ if (sw.getAttribute("data-swatch") === this.style.color)
819
+ sw.setAttribute("aria-pressed", "true");
820
+ else sw.removeAttribute("aria-pressed");
821
+ }
822
+ if (this.recentEl && this.recentGroup) {
823
+ clearChildren(this.recentEl);
824
+ for (const color of this.recentColors) {
825
+ const sw = document.createElement("button");
826
+ sw.type = "button";
827
+ sw.className = "vd-draw-swatch";
828
+ sw.setAttribute("data-swatch", color);
829
+ sw.setAttribute("aria-label", `Recent color ${color}`);
830
+ sw.setAttribute("title", color);
831
+ sw.style.setProperty("--sw", color);
832
+ this.recentEl.appendChild(sw);
833
+ }
834
+ this.recentGroup.style.display = this.recentColors.length ? "" : "none";
835
+ }
836
+ }
837
+ // ── Events ─────────────────────────────────────────────────────────────
838
+ _bindEvents() {
839
+ this._onPointerDown = this._handlePointerDown.bind(this);
840
+ this._onPointerMove = this._handlePointerMove.bind(this);
841
+ this._onPointerUp = this._handlePointerUp.bind(this);
842
+ this._onWheel = this._handleWheel.bind(this);
843
+ this._onKeyDown = this._handleKeyDown.bind(this);
844
+ this._onToolbarClick = this._handleToolbarClick.bind(this);
845
+ this._onPanelClick = this._handlePanelClick.bind(this);
846
+ this._onPanelInput = this._handlePanelInput.bind(this);
847
+ this._onResize = this._handleResize.bind(this);
848
+ this.canvasEl.addEventListener("pointerdown", this._onPointerDown);
849
+ this.canvasEl.addEventListener("pointermove", this._onPointerMove);
850
+ this.canvasEl.addEventListener("pointerup", this._onPointerUp);
851
+ this.canvasEl.addEventListener("pointercancel", this._onPointerUp);
852
+ this.canvasEl.addEventListener("wheel", this._onWheel, { passive: false });
853
+ this.canvasEl.addEventListener("keydown", this._onKeyDown);
854
+ this.toolbarEl.addEventListener("click", this._onToolbarClick);
855
+ this.panelEl.addEventListener("click", this._onPanelClick);
856
+ this.panelEl.addEventListener("input", this._onPanelInput);
857
+ window.addEventListener("pointerup", this._onPointerUp);
858
+ window.addEventListener("resize", this._onResize);
859
+ }
860
+ _unbindEvents() {
861
+ this.canvasEl.removeEventListener("pointerdown", this._onPointerDown);
862
+ this.canvasEl.removeEventListener("pointermove", this._onPointerMove);
863
+ this.canvasEl.removeEventListener("pointerup", this._onPointerUp);
864
+ this.canvasEl.removeEventListener("pointercancel", this._onPointerUp);
865
+ this.canvasEl.removeEventListener("wheel", this._onWheel);
866
+ this.canvasEl.removeEventListener("keydown", this._onKeyDown);
867
+ this.toolbarEl.removeEventListener("click", this._onToolbarClick);
868
+ this.panelEl.removeEventListener("click", this._onPanelClick);
869
+ this.panelEl.removeEventListener("input", this._onPanelInput);
870
+ window.removeEventListener("pointerup", this._onPointerUp);
871
+ window.removeEventListener("resize", this._onResize);
872
+ }
873
+ on(event, callback) {
874
+ if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
875
+ this.listeners.get(event).add(callback);
876
+ return this;
877
+ }
878
+ off(event, callback) {
879
+ this.listeners.get(event)?.delete(callback);
880
+ return this;
881
+ }
882
+ emit(event, payload) {
883
+ const set = this.listeners.get(event);
884
+ if (set) for (const cb of set) cb(payload);
885
+ }
886
+ // ── Coordinate transforms ───────────────────────────────────────────────
887
+ _clientToLocal(clientX, clientY) {
888
+ const rect = this.canvasEl.getBoundingClientRect();
889
+ return { x: clientX - rect.left, y: clientY - rect.top };
890
+ }
891
+ _localToWorld(lx, ly) {
892
+ const vp = this.documentData.viewport;
893
+ return { x: (lx - vp.x) / vp.scale, y: (ly - vp.y) / vp.scale };
894
+ }
895
+ _clientToWorld(clientX, clientY) {
896
+ const local = this._clientToLocal(clientX, clientY);
897
+ return this._localToWorld(local.x, local.y);
898
+ }
899
+ scaleAround(factor, lx, ly) {
900
+ const vp = this.documentData.viewport;
901
+ const next = clamp(vp.scale * factor, MIN_SCALE, MAX_SCALE);
902
+ const applied = next / vp.scale;
903
+ vp.x = lx - (lx - vp.x) * applied;
904
+ vp.y = ly - (ly - vp.y) * applied;
905
+ vp.scale = next;
906
+ return this;
907
+ }
908
+ // ── Viewport ─────────────────────────────────────────────────────────────
909
+ setViewport(patch) {
910
+ const vp = this.documentData.viewport;
911
+ if (patch && typeof patch === "object") {
912
+ if (Number.isFinite(patch.x)) vp.x = patch.x;
913
+ if (Number.isFinite(patch.y)) vp.y = patch.y;
914
+ if (Number.isFinite(patch.scale)) vp.scale = clamp(patch.scale, MIN_SCALE, MAX_SCALE);
915
+ }
916
+ this.render({ scene: false });
917
+ this._emitViewportChange("viewport:set");
918
+ return this;
919
+ }
920
+ zoomIn() {
921
+ return this._zoomAtCentre(1.2);
922
+ }
923
+ zoomOut() {
924
+ return this._zoomAtCentre(1 / 1.2);
925
+ }
926
+ _zoomAtCentre(factor) {
927
+ const rect = this.canvasEl.getBoundingClientRect();
928
+ this.scaleAround(factor, rect.width / 2, rect.height / 2);
929
+ this.render({ scene: false });
930
+ this._emitViewportChange("viewport:zoom");
931
+ return this;
932
+ }
933
+ resetView() {
934
+ this.documentData.viewport = { x: 0, y: 0, scale: 1 };
935
+ this.render({ scene: false });
936
+ this._emitViewportChange("viewport:reset");
937
+ return this;
938
+ }
939
+ fitView() {
940
+ const bounds = boundsOfShapes(this.documentData.shapes);
941
+ const rect = this.canvasEl.getBoundingClientRect();
942
+ if (!bounds || bounds.w === 0 || bounds.h === 0 || !rect.width || !rect.height) return this;
943
+ const pad = 40;
944
+ const scale = clamp(
945
+ Math.min((rect.width - pad * 2) / bounds.w, (rect.height - pad * 2) / bounds.h),
946
+ MIN_SCALE,
947
+ MAX_SCALE
948
+ );
949
+ const vp = this.documentData.viewport;
950
+ vp.scale = scale;
951
+ vp.x = (rect.width - bounds.w * scale) / 2 - bounds.x * scale;
952
+ vp.y = (rect.height - bounds.h * scale) / 2 - bounds.y * scale;
953
+ this.render({ scene: false });
954
+ this._emitViewportChange("viewport:fit");
955
+ return this;
956
+ }
957
+ _emitViewportChange(reason) {
958
+ this.emit("viewport", {
959
+ reason,
960
+ viewport: { ...this.documentData.viewport },
961
+ document: this.toJSON()
962
+ });
963
+ }
964
+ // ── Grid ─────────────────────────────────────────────────────────────────
965
+ setGridSize(size) {
966
+ if (!Number.isFinite(size)) return this;
967
+ this.gridSize = clamp(Math.round(size), 4, 128);
968
+ if (this.gridPattern) {
969
+ this.gridPattern.setAttribute("width", String(this.gridSize));
970
+ this.gridPattern.setAttribute("height", String(this.gridSize));
971
+ this.gridPatternPath.setAttribute("d", `M ${this.gridSize} 0 L 0 0 0 ${this.gridSize}`);
972
+ }
973
+ this._syncStylePanel();
974
+ return this;
975
+ }
976
+ setGridVisible(visible) {
977
+ this.showGrid = Boolean(visible);
978
+ if (this.gridLayer) this.gridLayer.style.display = this.showGrid ? "" : "none";
979
+ this._syncStylePanel();
980
+ return this;
981
+ }
982
+ toggleGrid() {
983
+ return this.setGridVisible(!this.showGrid);
984
+ }
985
+ // ── Tool + current style ─────────────────────────────────────────────────
986
+ setTool(tool) {
987
+ if (!DRAW_TOOLS.includes(tool)) return this;
988
+ this.tool = tool;
989
+ this._syncToolbar();
990
+ return this;
991
+ }
992
+ setColor(color) {
993
+ if (typeof color !== "string") return this;
994
+ this.style.color = color;
995
+ this._pushRecent(color);
996
+ this._syncStylePanel();
997
+ return this;
998
+ }
999
+ setOpacity(opacity) {
1000
+ if (Number.isFinite(opacity)) this.style.opacity = clamp(opacity, 0.05, 1);
1001
+ this._syncStylePanel();
1002
+ return this;
1003
+ }
1004
+ setBrushSize(size) {
1005
+ if (Number.isFinite(size)) this.style.size = clamp(size, 1, 200);
1006
+ this._syncStylePanel();
1007
+ return this;
1008
+ }
1009
+ setBrush(brush) {
1010
+ if (BRUSH_PRESETS[brush]) this.style.brush = brush;
1011
+ this._syncStylePanel();
1012
+ return this;
1013
+ }
1014
+ _pushRecent(color) {
1015
+ this.recentColors = [color, ...this.recentColors.filter((c) => c !== color)].slice(0, 8);
1016
+ }
1017
+ _shapeStrokeWidth() {
1018
+ return clamp(round(this.style.size / 3), 1, 14);
1019
+ }
1020
+ // ── Shape CRUD ─────────────────────────────────────────────────────────
1021
+ getShape(id) {
1022
+ return this.documentData.shapes.find((s) => s.id === id) || null;
1023
+ }
1024
+ addShape(partial = {}) {
1025
+ const type = DRAW_SHAPE_TYPES.includes(partial.type) ? partial.type : "rectangle";
1026
+ const s = this.style;
1027
+ let shape;
1028
+ if (type === "freehand") {
1029
+ const points = Array.isArray(partial.points) ? partial.points.map((p) => this._roundPoint(p)) : [];
1030
+ shape = {
1031
+ id: partial.id || createId("fh"),
1032
+ type: "freehand",
1033
+ brush: BRUSH_PRESETS[partial.brush] ? partial.brush : s.brush,
1034
+ color: partial.color || s.color,
1035
+ size: partial.size == null ? s.size : partial.size,
1036
+ opacity: partial.opacity == null ? s.opacity : partial.opacity,
1037
+ points
1038
+ };
1039
+ } else if (type === "line") {
1040
+ const points = Array.isArray(partial.points) ? partial.points.map(([x, y]) => [round(x), round(y)]) : [];
1041
+ shape = {
1042
+ id: partial.id || createId("ln"),
1043
+ type: "line",
1044
+ points,
1045
+ color: partial.color || s.color,
1046
+ strokeWidth: partial.strokeWidth == null ? this._shapeStrokeWidth() : partial.strokeWidth,
1047
+ opacity: partial.opacity == null ? s.opacity : partial.opacity,
1048
+ arrowStart: Boolean(partial.arrowStart),
1049
+ arrowEnd: partial.arrowEnd == null ? true : Boolean(partial.arrowEnd)
1050
+ };
1051
+ } else {
1052
+ shape = {
1053
+ id: partial.id || createId(type.slice(0, 2)),
1054
+ type,
1055
+ x: round(partial.x ?? 0),
1056
+ y: round(partial.y ?? 0),
1057
+ w: round(Math.max(1, partial.w ?? 100)),
1058
+ h: round(Math.max(1, partial.h ?? 80)),
1059
+ rotation: partial.rotation ?? 0,
1060
+ color: partial.color || s.color,
1061
+ strokeWidth: partial.strokeWidth == null ? this._shapeStrokeWidth() : partial.strokeWidth,
1062
+ opacity: partial.opacity == null ? s.opacity : partial.opacity
1063
+ };
1064
+ if (partial.fill) shape.fill = partial.fill;
1065
+ if (type === "text" || type === "sticky")
1066
+ shape.text = typeof partial.text === "string" ? partial.text : "";
1067
+ }
1068
+ this.documentData.shapes.push(shape);
1069
+ this.render();
1070
+ this._emitChange("shape:add", { shape: deepClone(shape), shapeId: shape.id });
1071
+ return shape;
1072
+ }
1073
+ _roundPoint(p) {
1074
+ const out = [round(p[0]), round(p[1])];
1075
+ if (p.length >= 3 && p[2] != null) out.push(clamp(Number(p[2]) || 0, 0, 1));
1076
+ return out;
1077
+ }
1078
+ updateShape(id, patch, options = {}) {
1079
+ const shape = this.getShape(id);
1080
+ if (!shape || !patch) return null;
1081
+ Object.assign(shape, patch);
1082
+ this.render();
1083
+ this._emitChange(options.reason || "shape:update", { shapeId: id }, options.reason ? id : null);
1084
+ return shape;
1085
+ }
1086
+ removeShape(id) {
1087
+ const idx = this.documentData.shapes.findIndex((s) => s.id === id);
1088
+ if (idx === -1) return false;
1089
+ this.documentData.shapes.splice(idx, 1);
1090
+ this.selectedIds.delete(id);
1091
+ this.render();
1092
+ this._emitChange("shape:delete", { shapeId: id });
1093
+ return true;
1094
+ }
1095
+ // ── Selection ────────────────────────────────────────────────────────────
1096
+ getSelectedShapes() {
1097
+ return this.documentData.shapes.filter((s) => this.selectedIds.has(s.id));
1098
+ }
1099
+ select(ids, { additive = false } = {}) {
1100
+ const list = Array.isArray(ids) ? ids : ids == null ? [] : [ids];
1101
+ if (!additive) this.selectedIds.clear();
1102
+ for (const id of list) {
1103
+ const shape = this.getShape(id);
1104
+ if (shape?.groupId) {
1105
+ for (const s of this.documentData.shapes)
1106
+ if (s.groupId === shape.groupId) this.selectedIds.add(s.id);
1107
+ } else if (shape) {
1108
+ this.selectedIds.add(id);
1109
+ }
1110
+ }
1111
+ this.render({ scene: false });
1112
+ this._emitSelect();
1113
+ return this;
1114
+ }
1115
+ selectAll() {
1116
+ this.selectedIds = new Set(this.documentData.shapes.map((s) => s.id));
1117
+ this.render({ scene: false });
1118
+ this._emitSelect();
1119
+ return this;
1120
+ }
1121
+ deselect() {
1122
+ if (this.selectedIds.size === 0) return this;
1123
+ this.selectedIds.clear();
1124
+ this.render({ scene: false });
1125
+ this._emitSelect();
1126
+ return this;
1127
+ }
1128
+ selectInBounds(box, { additive = false } = {}) {
1129
+ if (!additive) this.selectedIds.clear();
1130
+ for (const shape of this.documentData.shapes) {
1131
+ if (boundsIntersect(shapeBounds(shape), box)) this.selectedIds.add(shape.id);
1132
+ }
1133
+ for (const shape of [...this.getSelectedShapes()]) {
1134
+ if (shape.groupId) {
1135
+ for (const s of this.documentData.shapes)
1136
+ if (s.groupId === shape.groupId) this.selectedIds.add(s.id);
1137
+ }
1138
+ }
1139
+ this.render({ scene: false });
1140
+ this._emitSelect();
1141
+ return this;
1142
+ }
1143
+ _emitSelect() {
1144
+ this.emit("select", {
1145
+ ids: [...this.selectedIds],
1146
+ shapes: this.getSelectedShapes().map((s) => deepClone(s))
1147
+ });
1148
+ }
1149
+ _syncSelectionValidity() {
1150
+ const present = new Set(this.documentData.shapes.map((s) => s.id));
1151
+ for (const id of [...this.selectedIds]) if (!present.has(id)) this.selectedIds.delete(id);
1152
+ }
1153
+ // ── Manipulation ─────────────────────────────────────────────────────────
1154
+ _replaceShape(next) {
1155
+ const idx = this.documentData.shapes.findIndex((s) => s.id === next.id);
1156
+ if (idx !== -1) this.documentData.shapes[idx] = next;
1157
+ }
1158
+ nudge(dx, dy) {
1159
+ const selected = this.getSelectedShapes();
1160
+ if (!selected.length) return this;
1161
+ for (const shape of selected) this._replaceShape(translateShape(shape, dx, dy));
1162
+ this.render();
1163
+ this._emitChange(
1164
+ "shape:nudge",
1165
+ { shapeIds: [...this.selectedIds] },
1166
+ `nudge:${[...this.selectedIds].sort().join(",")}`
1167
+ );
1168
+ return this;
1169
+ }
1170
+ setSelectionBounds(target) {
1171
+ const selected = this.getSelectedShapes();
1172
+ const cur = boundsOfShapes(selected);
1173
+ if (!cur || cur.w === 0 || cur.h === 0 || !target) return this;
1174
+ const sx = target.w / cur.w;
1175
+ const sy = target.h / cur.h;
1176
+ for (const shape of selected) {
1177
+ const scaled = scaleShape(shape, cur.x, cur.y, sx, sy);
1178
+ this._replaceShape(translateShape(scaled, target.x - cur.x, target.y - cur.y));
1179
+ }
1180
+ this.render();
1181
+ this._emitChange("shape:resize", { shapeIds: [...this.selectedIds] });
1182
+ return this;
1183
+ }
1184
+ deleteSelection() {
1185
+ if (this.selectedIds.size === 0) return false;
1186
+ const ids = [...this.selectedIds];
1187
+ this.documentData.shapes = this.documentData.shapes.filter((s) => !this.selectedIds.has(s.id));
1188
+ this.selectedIds.clear();
1189
+ this.render();
1190
+ this._emitChange("shape:delete", { shapeIds: ids });
1191
+ return true;
1192
+ }
1193
+ setStyle(patch) {
1194
+ const selected = this.getSelectedShapes();
1195
+ if (!selected.length || !patch) return this;
1196
+ const allowed = ["color", "fill", "strokeWidth", "opacity", "size", "brush"];
1197
+ for (const shape of selected) {
1198
+ for (const key of allowed) if (key in patch) shape[key] = patch[key];
1199
+ }
1200
+ this.render();
1201
+ this._emitChange(
1202
+ "shape:style",
1203
+ { shapeIds: [...this.selectedIds] },
1204
+ `style:${[...this.selectedIds].sort().join(",")}`
1205
+ );
1206
+ return this;
1207
+ }
1208
+ // ── Z-order ────────────────────────────────────────────────────────────
1209
+ _reorder(mutator) {
1210
+ if (this.selectedIds.size === 0) return this;
1211
+ mutator();
1212
+ this.render();
1213
+ this._emitChange("shape:reorder", { shapeIds: [...this.selectedIds] });
1214
+ return this;
1215
+ }
1216
+ bringToFront() {
1217
+ return this._reorder(() => {
1218
+ const sel = this.documentData.shapes.filter((s) => this.selectedIds.has(s.id));
1219
+ const rest = this.documentData.shapes.filter((s) => !this.selectedIds.has(s.id));
1220
+ this.documentData.shapes = [...rest, ...sel];
1221
+ });
1222
+ }
1223
+ sendToBack() {
1224
+ return this._reorder(() => {
1225
+ const sel = this.documentData.shapes.filter((s) => this.selectedIds.has(s.id));
1226
+ const rest = this.documentData.shapes.filter((s) => !this.selectedIds.has(s.id));
1227
+ this.documentData.shapes = [...sel, ...rest];
1228
+ });
1229
+ }
1230
+ bringForward() {
1231
+ return this._reorder(() => {
1232
+ const arr = this.documentData.shapes;
1233
+ for (let i = arr.length - 2; i >= 0; i -= 1) {
1234
+ if (this.selectedIds.has(arr[i].id) && !this.selectedIds.has(arr[i + 1].id))
1235
+ [arr[i], arr[i + 1]] = [arr[i + 1], arr[i]];
1236
+ }
1237
+ });
1238
+ }
1239
+ sendBackward() {
1240
+ return this._reorder(() => {
1241
+ const arr = this.documentData.shapes;
1242
+ for (let i = 1; i < arr.length; i += 1) {
1243
+ if (this.selectedIds.has(arr[i].id) && !this.selectedIds.has(arr[i - 1].id))
1244
+ [arr[i], arr[i - 1]] = [arr[i - 1], arr[i]];
1245
+ }
1246
+ });
1247
+ }
1248
+ // ── Grouping ─────────────────────────────────────────────────────────────
1249
+ group() {
1250
+ const selected = this.getSelectedShapes();
1251
+ if (selected.length < 2) return this;
1252
+ const groupId = createId("grp");
1253
+ for (const shape of selected) shape.groupId = groupId;
1254
+ this.render();
1255
+ this._emitChange("shape:group", { groupId, shapeIds: [...this.selectedIds] });
1256
+ return this;
1257
+ }
1258
+ ungroup() {
1259
+ const selected = this.getSelectedShapes();
1260
+ let changed = false;
1261
+ for (const shape of selected) {
1262
+ if (shape.groupId) {
1263
+ delete shape.groupId;
1264
+ changed = true;
1265
+ }
1266
+ }
1267
+ if (!changed) return this;
1268
+ this.render();
1269
+ this._emitChange("shape:ungroup", { shapeIds: [...this.selectedIds] });
1270
+ return this;
1271
+ }
1272
+ // ── Clipboard ────────────────────────────────────────────────────────────
1273
+ copy() {
1274
+ this.clipboard = this.getSelectedShapes().map((s) => deepClone(s));
1275
+ return this;
1276
+ }
1277
+ cut() {
1278
+ this.copy();
1279
+ this.deleteSelection();
1280
+ return this;
1281
+ }
1282
+ paste({ offset = 16 } = {}) {
1283
+ if (!this.clipboard.length) return this;
1284
+ const groupRemap = /* @__PURE__ */ new Map();
1285
+ const newIds = [];
1286
+ for (const src of this.clipboard) {
1287
+ const copy = translateShape(deepClone(src), offset, offset);
1288
+ copy.id = createId(copy.type.slice(0, 2));
1289
+ if (copy.groupId) {
1290
+ if (!groupRemap.has(copy.groupId)) groupRemap.set(copy.groupId, createId("grp"));
1291
+ copy.groupId = groupRemap.get(copy.groupId);
1292
+ }
1293
+ this.documentData.shapes.push(copy);
1294
+ newIds.push(copy.id);
1295
+ }
1296
+ this.selectedIds = new Set(newIds);
1297
+ this.render();
1298
+ this._emitChange("shape:paste", { shapeIds: newIds });
1299
+ this._emitSelect();
1300
+ return this;
1301
+ }
1302
+ duplicate() {
1303
+ this.copy();
1304
+ this.paste();
1305
+ return this;
1306
+ }
1307
+ // ── Snapping ─────────────────────────────────────────────────────────────
1308
+ _computeSnap(movingBounds, movingIds) {
1309
+ if (!this.snap) return { dx: 0, dy: 0, guides: [] };
1310
+ const threshold = SNAP_THRESHOLD / this.documentData.viewport.scale;
1311
+ const targetsX = [
1312
+ movingBounds.x,
1313
+ movingBounds.x + movingBounds.w / 2,
1314
+ movingBounds.x + movingBounds.w
1315
+ ];
1316
+ const targetsY = [
1317
+ movingBounds.y,
1318
+ movingBounds.y + movingBounds.h / 2,
1319
+ movingBounds.y + movingBounds.h
1320
+ ];
1321
+ let bestX = null;
1322
+ let bestY = null;
1323
+ const guides = [];
1324
+ for (const other of this.documentData.shapes) {
1325
+ if (movingIds.has(other.id)) continue;
1326
+ const b = shapeBounds(other);
1327
+ for (const t of targetsX)
1328
+ for (const l of [b.x, b.x + b.w / 2, b.x + b.w]) {
1329
+ const d = l - t;
1330
+ if (Math.abs(d) <= threshold && (!bestX || Math.abs(d) < Math.abs(bestX.d)))
1331
+ bestX = { d, at: l };
1332
+ }
1333
+ for (const t of targetsY)
1334
+ for (const l of [b.y, b.y + b.h / 2, b.y + b.h]) {
1335
+ const d = l - t;
1336
+ if (Math.abs(d) <= threshold && (!bestY || Math.abs(d) < Math.abs(bestY.d)))
1337
+ bestY = { d, at: l };
1338
+ }
1339
+ }
1340
+ if (bestX) guides.push({ x1: bestX.at, y1: -WORLD_EXTENT, x2: bestX.at, y2: WORLD_EXTENT });
1341
+ if (bestY) guides.push({ x1: -WORLD_EXTENT, y1: bestY.at, x2: WORLD_EXTENT, y2: bestY.at });
1342
+ return { dx: bestX ? bestX.d : 0, dy: bestY ? bestY.d : 0, guides };
1343
+ }
1344
+ // ── Export ───────────────────────────────────────────────────────────────
1345
+ _resolvedColors() {
1346
+ const cs = hasWindow() ? getComputedStyle(this.svg) : null;
1347
+ const read = (name, fallback) => {
1348
+ const v = cs?.getPropertyValue(name)?.trim();
1349
+ return v || fallback;
1350
+ };
1351
+ return {
1352
+ ink: read("--vd-draw-ink", "#1f2720"),
1353
+ shapeStroke: read("--vd-draw-shape-stroke", "#245f52"),
1354
+ text: read("--vd-draw-text", "#1f2720"),
1355
+ sticky: read("--vd-draw-sticky-fill", "#fdf3c4")
1356
+ };
1357
+ }
1358
+ toSVG() {
1359
+ const shapes = this.documentData.shapes;
1360
+ const bounds = boundsOfShapes(shapes) || { x: 0, y: 0, w: 100, h: 100 };
1361
+ const pad = 20;
1362
+ const vb = {
1363
+ x: bounds.x - pad,
1364
+ y: bounds.y - pad,
1365
+ w: Math.max(1, bounds.w) + pad * 2,
1366
+ h: Math.max(1, bounds.h) + pad * 2
1367
+ };
1368
+ const colors = this._resolvedColors();
1369
+ const svg = createSvgEl("svg", {
1370
+ xmlns: SVG_NS,
1371
+ width: round(vb.w),
1372
+ height: round(vb.h),
1373
+ viewBox: `${round(vb.x)} ${round(vb.y)} ${round(vb.w)} ${round(vb.h)}`
1374
+ });
1375
+ for (const shape of shapes) {
1376
+ const el = this._renderShapeEl(shape, { standalone: true, colors });
1377
+ if (el) svg.appendChild(el);
1378
+ }
1379
+ return new XMLSerializer().serializeToString(svg);
1380
+ }
1381
+ toPNG({ scale = 2 } = {}) {
1382
+ const markup = this.toSVG();
1383
+ const bounds = boundsOfShapes(this.documentData.shapes) || { w: 100, h: 100 };
1384
+ const pad = 20;
1385
+ const w = Math.max(1, (bounds.w || 100) + pad * 2);
1386
+ const h2 = Math.max(1, (bounds.h || 100) + pad * 2);
1387
+ return new Promise((resolve, reject) => {
1388
+ try {
1389
+ const img = new Image();
1390
+ const url = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(markup)}`;
1391
+ img.onload = () => {
1392
+ const canvas = document.createElement("canvas");
1393
+ canvas.width = Math.ceil(w * scale);
1394
+ canvas.height = Math.ceil(h2 * scale);
1395
+ const ctx = canvas.getContext("2d");
1396
+ if (!ctx) {
1397
+ reject(new Error("VdDraw: 2D canvas context unavailable"));
1398
+ return;
1399
+ }
1400
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
1401
+ resolve(canvas.toDataURL("image/png"));
1402
+ };
1403
+ img.onerror = () => reject(new Error("VdDraw: failed to rasterize SVG"));
1404
+ img.src = url;
1405
+ } catch (err) {
1406
+ reject(err);
1407
+ }
1408
+ });
1409
+ }
1410
+ // ── History ──────────────────────────────────────────────────────────────
1411
+ _resetHistory() {
1412
+ this.history = [this.toJSON()];
1413
+ this.historyIndex = 0;
1414
+ this.lastReason = null;
1415
+ this.lastTargetKey = null;
1416
+ }
1417
+ _recordHistory(reason, targetKey) {
1418
+ if (!this.historyEnabled || this.isApplyingHistory) return;
1419
+ const snapshot = this.toJSON();
1420
+ const coalesce = COALESCE_REASONS.has(reason) && reason === this.lastReason && targetKey != null && targetKey === this.lastTargetKey && this.historyIndex >= 0 && // Only coalesce onto the TOP of the stack. Without this guard a
1421
+ // coalescing edit made right after an undo overwrites an earlier
1422
+ // (non-top) snapshot, corrupting the redo branch and losing base state.
1423
+ // Mirrors the flowchart core's canCoalesce guard.
1424
+ this.historyIndex === this.history.length - 1;
1425
+ if (coalesce) {
1426
+ this.history[this.historyIndex] = snapshot;
1427
+ } else {
1428
+ this.history = this.history.slice(0, this.historyIndex + 1);
1429
+ this.history.push(snapshot);
1430
+ this.historyIndex = this.history.length - 1;
1431
+ if (this.history.length > this.historyLimit + 1) {
1432
+ this.history.shift();
1433
+ this.historyIndex -= 1;
1434
+ }
1435
+ }
1436
+ this.lastReason = reason;
1437
+ this.lastTargetKey = targetKey ?? null;
1438
+ }
1439
+ _emitChange(reason, extra = {}, targetKey = null) {
1440
+ this._recordHistory(reason, targetKey);
1441
+ this._syncSelectionValidity();
1442
+ this.emit("change", { reason, document: this.toJSON(), ...extra });
1443
+ }
1444
+ canUndo() {
1445
+ return this.historyIndex > 0;
1446
+ }
1447
+ canRedo() {
1448
+ return this.historyIndex < this.history.length - 1;
1449
+ }
1450
+ undo() {
1451
+ if (!this.canUndo()) return this;
1452
+ this.historyIndex -= 1;
1453
+ this._applySnapshot(this.history[this.historyIndex], "undo");
1454
+ return this;
1455
+ }
1456
+ redo() {
1457
+ if (!this.canRedo()) return this;
1458
+ this.historyIndex += 1;
1459
+ this._applySnapshot(this.history[this.historyIndex], "redo");
1460
+ return this;
1461
+ }
1462
+ clearHistory() {
1463
+ this._resetHistory();
1464
+ this.emit("history", { reason: "clear", canUndo: false, canRedo: false });
1465
+ return this;
1466
+ }
1467
+ _applySnapshot(snapshot, reason) {
1468
+ this.isApplyingHistory = true;
1469
+ const viewport = { ...this.documentData.viewport };
1470
+ this.documentData = deepClone(snapshot);
1471
+ this.documentData.viewport = viewport;
1472
+ this._syncSelectionValidity();
1473
+ this.render();
1474
+ this.emit("change", { reason, document: this.toJSON() });
1475
+ this.emit("history", { reason, canUndo: this.canUndo(), canRedo: this.canRedo() });
1476
+ this.isApplyingHistory = false;
1477
+ }
1478
+ // ── Serialization ──────────────────────────────────────────────────────
1479
+ toJSON() {
1480
+ return deepClone({
1481
+ version: VD_DRAW_VERSION,
1482
+ viewport: this.documentData.viewport,
1483
+ shapes: this.documentData.shapes
1484
+ });
1485
+ }
1486
+ load(data) {
1487
+ this.documentData = normalizeDocument(data);
1488
+ this.selectedIds.clear();
1489
+ this._resetHistory();
1490
+ this.render();
1491
+ this._emitChange("load");
1492
+ this._emitSelect();
1493
+ return this;
1494
+ }
1495
+ clear() {
1496
+ if (!this.documentData.shapes.length) return this;
1497
+ this.documentData.shapes = [];
1498
+ this.selectedIds.clear();
1499
+ this.render();
1500
+ this._emitChange("clear");
1501
+ return this;
1502
+ }
1503
+ // ── Text editing ─────────────────────────────────────────────────────────
1504
+ startTextEdit(id) {
1505
+ const shape = this.getShape(id);
1506
+ if (!shape || shape.type !== "text" && shape.type !== "sticky" || this.readonly) return false;
1507
+ this.stopTextEdit({ commit: false });
1508
+ const editor = document.createElement("textarea");
1509
+ editor.className = "vd-draw-text-editor";
1510
+ editor.value = shape.text || "";
1511
+ this.textEditor = { id, el: editor };
1512
+ this._positionTextEditor(shape, editor);
1513
+ this.textLayer.appendChild(editor);
1514
+ editor.focus();
1515
+ editor.addEventListener("blur", () => this.stopTextEdit({ commit: true }));
1516
+ editor.addEventListener("keydown", (e) => {
1517
+ if (e.key === "Escape") this.stopTextEdit({ commit: false });
1518
+ });
1519
+ return true;
1520
+ }
1521
+ _positionTextEditor(shape, editor) {
1522
+ const vp = this.documentData.viewport;
1523
+ editor.style.left = `${vp.x + shape.x * vp.scale}px`;
1524
+ editor.style.top = `${vp.y + shape.y * vp.scale}px`;
1525
+ editor.style.width = `${shape.w * vp.scale}px`;
1526
+ editor.style.height = `${shape.h * vp.scale}px`;
1527
+ }
1528
+ stopTextEdit({ commit = true } = {}) {
1529
+ if (!this.textEditor) return;
1530
+ const { id, el } = this.textEditor;
1531
+ const value = el.value;
1532
+ this.textEditor = null;
1533
+ el.remove();
1534
+ if (commit) {
1535
+ const shape = this.getShape(id);
1536
+ if (shape && shape.text !== value) {
1537
+ shape.text = value;
1538
+ this.render();
1539
+ this._emitChange("shape:text", { shapeId: id }, `text:${id}`);
1540
+ }
1541
+ }
1542
+ }
1543
+ // ── Pointer interaction ──────────────────────────────────────────────────
1544
+ _capture(pointerId) {
1545
+ if (typeof this.canvasEl.setPointerCapture === "function") {
1546
+ try {
1547
+ this.canvasEl.setPointerCapture(pointerId);
1548
+ } catch {
1549
+ }
1550
+ }
1551
+ }
1552
+ _handleToolbarClick(event) {
1553
+ const toolBtn = event.target.closest("[data-tool]");
1554
+ if (toolBtn) {
1555
+ this.setTool(toolBtn.getAttribute("data-tool"));
1556
+ return;
1557
+ }
1558
+ const actionBtn = event.target.closest("[data-action]");
1559
+ if (!actionBtn) return;
1560
+ const action = actionBtn.getAttribute("data-action");
1561
+ if (action === "undo") this.undo();
1562
+ else if (action === "redo") this.redo();
1563
+ else if (action === "duplicate") this.duplicate();
1564
+ else if (action === "delete") {
1565
+ if (this.selectedIds.size) this.deleteSelection();
1566
+ else this.clear();
1567
+ }
1568
+ }
1569
+ _handlePanelClick(event) {
1570
+ const brushBtn = event.target.closest("[data-brush]");
1571
+ if (brushBtn) {
1572
+ this.setBrush(brushBtn.getAttribute("data-brush"));
1573
+ if (this.tool !== "draw") this.setTool("draw");
1574
+ return;
1575
+ }
1576
+ const gridBtn = event.target.closest("[data-grid]");
1577
+ if (gridBtn) {
1578
+ this.toggleGrid();
1579
+ return;
1580
+ }
1581
+ const swatch = event.target.closest("[data-swatch]");
1582
+ if (swatch) this.setColor(swatch.getAttribute("data-swatch"));
1583
+ }
1584
+ _handlePanelInput(event) {
1585
+ const kind = event.target.getAttribute?.("data-style");
1586
+ if (kind === "color") this.setColor(event.target.value);
1587
+ else if (kind === "size") this.setBrushSize(Number(event.target.value));
1588
+ else if (kind === "opacity") this.setOpacity(Number(event.target.value));
1589
+ else if (kind === "grid") this.setGridSize(Number(event.target.value));
1590
+ }
1591
+ _handlePointerDown(event) {
1592
+ if (this.destroyed || event.button != null && event.button !== 0) return;
1593
+ this.canvasEl.focus();
1594
+ this.stopTextEdit({ commit: true });
1595
+ const world = this._clientToWorld(event.clientX, event.clientY);
1596
+ const shapeTarget = event.target.closest("[data-shape-id]");
1597
+ const handleTarget = event.target.closest("[data-handle]");
1598
+ if (this.tool === "hand") {
1599
+ this.interaction = this._beginPan(event);
1600
+ this._capture(event.pointerId);
1601
+ return;
1602
+ }
1603
+ if (this.tool === "eraser" && !this.readonly) {
1604
+ this.interaction = { kind: "erase", pointerId: event.pointerId, erased: /* @__PURE__ */ new Set() };
1605
+ this._capture(event.pointerId);
1606
+ this._applyErase(world);
1607
+ return;
1608
+ }
1609
+ if (this.tool === "select") {
1610
+ if (handleTarget && this.selectedIds.size) {
1611
+ this.interaction = {
1612
+ kind: "resize",
1613
+ pointerId: event.pointerId,
1614
+ handle: handleTarget.getAttribute("data-handle"),
1615
+ startBounds: boundsOfShapes(this.getSelectedShapes()),
1616
+ originals: this.getSelectedShapes().map((s) => deepClone(s)),
1617
+ start: world,
1618
+ moved: false
1619
+ };
1620
+ this._capture(event.pointerId);
1621
+ return;
1622
+ }
1623
+ if (shapeTarget) {
1624
+ const id = shapeTarget.getAttribute("data-shape-id");
1625
+ if (!this.selectedIds.has(id)) this.select(id, { additive: event.shiftKey });
1626
+ this.interaction = {
1627
+ kind: "move",
1628
+ pointerId: event.pointerId,
1629
+ start: world,
1630
+ originals: this.getSelectedShapes().map((s) => deepClone(s)),
1631
+ moved: false
1632
+ };
1633
+ this._capture(event.pointerId);
1634
+ return;
1635
+ }
1636
+ if (!event.shiftKey) this.deselect();
1637
+ this.interaction = {
1638
+ kind: "marquee",
1639
+ pointerId: event.pointerId,
1640
+ start: world,
1641
+ additive: event.shiftKey
1642
+ };
1643
+ this._capture(event.pointerId);
1644
+ return;
1645
+ }
1646
+ if (this.readonly) return;
1647
+ this.interaction = this._beginCreate(this.tool, world, event);
1648
+ this._capture(event.pointerId);
1649
+ }
1650
+ _beginPan(event) {
1651
+ return {
1652
+ kind: "pan",
1653
+ pointerId: event.pointerId,
1654
+ startClientX: event.clientX,
1655
+ startClientY: event.clientY,
1656
+ startX: this.documentData.viewport.x,
1657
+ startY: this.documentData.viewport.y
1658
+ };
1659
+ }
1660
+ _beginCreate(tool, world, event) {
1661
+ const pressure = event.pressure || 0.5;
1662
+ if (tool === "draw") {
1663
+ const shape2 = {
1664
+ id: createId("fh"),
1665
+ type: "freehand",
1666
+ brush: this.style.brush,
1667
+ color: this.style.color,
1668
+ size: this.style.size,
1669
+ opacity: this.style.opacity,
1670
+ points: [[round(world.x), round(world.y), pressure]]
1671
+ };
1672
+ this.documentData.shapes.push(shape2);
1673
+ return { kind: "freehand", pointerId: event.pointerId, shapeId: shape2.id };
1674
+ }
1675
+ if (tool === "line") {
1676
+ const shape2 = {
1677
+ id: createId("ln"),
1678
+ type: "line",
1679
+ points: [
1680
+ [round(world.x), round(world.y)],
1681
+ [round(world.x), round(world.y)]
1682
+ ],
1683
+ arrowEnd: true,
1684
+ arrowStart: false,
1685
+ color: this.style.color,
1686
+ strokeWidth: this._shapeStrokeWidth(),
1687
+ opacity: this.style.opacity
1688
+ };
1689
+ this.documentData.shapes.push(shape2);
1690
+ return { kind: "create-line", pointerId: event.pointerId, shapeId: shape2.id };
1691
+ }
1692
+ const type = tool;
1693
+ const shape = {
1694
+ id: createId(type.slice(0, 2)),
1695
+ type,
1696
+ x: round(world.x),
1697
+ y: round(world.y),
1698
+ w: 1,
1699
+ h: 1,
1700
+ rotation: 0,
1701
+ color: this.style.color,
1702
+ strokeWidth: this._shapeStrokeWidth(),
1703
+ opacity: this.style.opacity
1704
+ };
1705
+ if (type === "text" || type === "sticky") shape.text = "";
1706
+ this.documentData.shapes.push(shape);
1707
+ return { kind: "create-box", pointerId: event.pointerId, shapeId: shape.id, start: world };
1708
+ }
1709
+ _applyErase(world) {
1710
+ const it = this.interaction;
1711
+ if (!it || it.kind !== "erase") return;
1712
+ const threshold = ERASER_RADIUS / this.documentData.viewport.scale;
1713
+ let changed = false;
1714
+ for (const shape of this.documentData.shapes) {
1715
+ if (it.erased.has(shape.id)) continue;
1716
+ if (distanceToShape(shape, world.x, world.y) <= threshold) {
1717
+ it.erased.add(shape.id);
1718
+ changed = true;
1719
+ }
1720
+ }
1721
+ if (changed) this.render({ scene: true, overlay: false });
1722
+ }
1723
+ _handlePointerMove(event) {
1724
+ const it = this.interaction;
1725
+ if (!it) return;
1726
+ if (it.kind === "pan") {
1727
+ const vp = this.documentData.viewport;
1728
+ vp.x = it.startX + (event.clientX - it.startClientX);
1729
+ vp.y = it.startY + (event.clientY - it.startClientY);
1730
+ this.render({ scene: false });
1731
+ return;
1732
+ }
1733
+ const world = this._clientToWorld(event.clientX, event.clientY);
1734
+ if (it.kind === "erase") {
1735
+ this._applyErase(world);
1736
+ return;
1737
+ }
1738
+ if (it.kind === "move") {
1739
+ const dx = world.x - it.start.x;
1740
+ const dy = world.y - it.start.y;
1741
+ if (Math.hypot(dx, dy) * this.documentData.viewport.scale > DRAG_THRESHOLD) it.moved = true;
1742
+ for (const orig of it.originals) this._replaceShape(translateShape(orig, dx, dy));
1743
+ const movingIds = new Set(it.originals.map((s) => s.id));
1744
+ const snap = this._computeSnap(boundsOfShapes(this.getSelectedShapes()), movingIds);
1745
+ if (snap.dx || snap.dy)
1746
+ for (const orig of it.originals)
1747
+ this._replaceShape(translateShape(orig, dx + snap.dx, dy + snap.dy));
1748
+ this._renderGuides(snap.guides);
1749
+ this.render({ scene: true, guides: false });
1750
+ return;
1751
+ }
1752
+ if (it.kind === "resize") {
1753
+ const t = this._resizeBounds(
1754
+ it.startBounds,
1755
+ it.handle,
1756
+ world.x - it.start.x,
1757
+ world.y - it.start.y
1758
+ );
1759
+ const sx = it.startBounds.w === 0 ? 1 : t.w / it.startBounds.w;
1760
+ const sy = it.startBounds.h === 0 ? 1 : t.h / it.startBounds.h;
1761
+ for (const orig of it.originals) {
1762
+ const scaled = scaleShape(orig, it.startBounds.x, it.startBounds.y, sx, sy);
1763
+ this._replaceShape(translateShape(scaled, t.x - it.startBounds.x, t.y - it.startBounds.y));
1764
+ }
1765
+ it.moved = true;
1766
+ this.render();
1767
+ return;
1768
+ }
1769
+ if (it.kind === "marquee") {
1770
+ it.current = world;
1771
+ this._renderMarquee(it.start, world);
1772
+ return;
1773
+ }
1774
+ if (it.kind === "freehand") {
1775
+ const shape = this.getShape(it.shapeId);
1776
+ if (shape) {
1777
+ shape.points.push([round(world.x), round(world.y), event.pressure || 0.5]);
1778
+ this.render({ scene: true });
1779
+ }
1780
+ return;
1781
+ }
1782
+ if (it.kind === "create-line") {
1783
+ const shape = this.getShape(it.shapeId);
1784
+ if (shape) {
1785
+ shape.points[1] = [round(world.x), round(world.y)];
1786
+ this.render({ scene: true });
1787
+ }
1788
+ return;
1789
+ }
1790
+ if (it.kind === "create-box") {
1791
+ const shape = this.getShape(it.shapeId);
1792
+ if (shape) {
1793
+ shape.x = round(Math.min(it.start.x, world.x));
1794
+ shape.y = round(Math.min(it.start.y, world.y));
1795
+ shape.w = round(Math.max(1, Math.abs(world.x - it.start.x)));
1796
+ shape.h = round(Math.max(1, Math.abs(world.y - it.start.y)));
1797
+ this.render({ scene: true });
1798
+ }
1799
+ }
1800
+ }
1801
+ _resizeBounds(start, handle, dx, dy) {
1802
+ let { x, y, w, h: h2 } = start;
1803
+ if (handle.includes("e")) w = Math.max(1, start.w + dx);
1804
+ if (handle.includes("s")) h2 = Math.max(1, start.h + dy);
1805
+ if (handle.includes("w")) {
1806
+ w = Math.max(1, start.w - dx);
1807
+ x = start.x + (start.w - w);
1808
+ }
1809
+ if (handle.includes("n")) {
1810
+ h2 = Math.max(1, start.h - dy);
1811
+ y = start.y + (start.h - h2);
1812
+ }
1813
+ return { x, y, w, h: h2 };
1814
+ }
1815
+ _handlePointerUp(event) {
1816
+ const it = this.interaction;
1817
+ if (!it) return;
1818
+ this.interaction = null;
1819
+ if (typeof this.canvasEl.releasePointerCapture === "function" && this.canvasEl.hasPointerCapture?.(event.pointerId)) {
1820
+ try {
1821
+ this.canvasEl.releasePointerCapture(event.pointerId);
1822
+ } catch {
1823
+ }
1824
+ }
1825
+ this._renderGuides([]);
1826
+ clearChildren(this.marqueeLayer);
1827
+ if (it.kind === "pan") {
1828
+ this._emitViewportChange("viewport:pan");
1829
+ return;
1830
+ }
1831
+ if (it.kind === "erase") {
1832
+ if (it.erased.size) {
1833
+ const ids = [...it.erased];
1834
+ this.documentData.shapes = this.documentData.shapes.filter((s) => !it.erased.has(s.id));
1835
+ this.render();
1836
+ this._emitChange("shape:erase", { shapeIds: ids });
1837
+ }
1838
+ return;
1839
+ }
1840
+ if (it.kind === "move" && it.moved) {
1841
+ this.render();
1842
+ this._emitChange("shape:move", { shapeIds: [...this.selectedIds] });
1843
+ return;
1844
+ }
1845
+ if (it.kind === "resize" && it.moved) {
1846
+ this.render();
1847
+ this._emitChange("shape:resize", { shapeIds: [...this.selectedIds] });
1848
+ return;
1849
+ }
1850
+ if (it.kind === "marquee") {
1851
+ const box = this._boxFromPoints(it.start, it.current || it.start);
1852
+ this.selectInBounds(box, { additive: it.additive });
1853
+ return;
1854
+ }
1855
+ if (it.kind === "freehand" || it.kind === "create-line" || it.kind === "create-box") {
1856
+ const shape = this.getShape(it.shapeId);
1857
+ if (!shape) return;
1858
+ if (it.kind === "freehand") shape.points = simplifyPoints(shape.points);
1859
+ const b = shapeBounds(shape);
1860
+ if (it.kind !== "freehand" && b.w < 2 && b.h < 2 && shape.type !== "text" && shape.type !== "sticky") {
1861
+ this.documentData.shapes = this.documentData.shapes.filter((s) => s.id !== it.shapeId);
1862
+ this.render();
1863
+ return;
1864
+ }
1865
+ if (shape.type === "text" || shape.type === "sticky") {
1866
+ if (b.w < 2 && b.h < 2) {
1867
+ shape.w = shape.type === "sticky" ? 160 : 120;
1868
+ shape.h = shape.type === "sticky" ? 120 : 40;
1869
+ }
1870
+ }
1871
+ if (it.kind !== "freehand") {
1872
+ this.select(shape.id);
1873
+ this.setTool("select");
1874
+ }
1875
+ this.render();
1876
+ this._emitChange("shape:add", { shape: deepClone(shape), shapeId: shape.id });
1877
+ if (shape.type === "text" || shape.type === "sticky") this.startTextEdit(shape.id);
1878
+ }
1879
+ }
1880
+ _boxFromPoints(a, b) {
1881
+ return {
1882
+ x: Math.min(a.x, b.x),
1883
+ y: Math.min(a.y, b.y),
1884
+ w: Math.abs(a.x - b.x),
1885
+ h: Math.abs(a.y - b.y)
1886
+ };
1887
+ }
1888
+ _handleWheel(event) {
1889
+ event.preventDefault();
1890
+ const local = this._clientToLocal(event.clientX, event.clientY);
1891
+ const factor = event.deltaY < 0 ? 1.1 : 1 / 1.1;
1892
+ this.scaleAround(factor, local.x, local.y);
1893
+ this.render({ scene: false });
1894
+ this._emitViewportChange("viewport:zoom");
1895
+ }
1896
+ _handleKeyDown(event) {
1897
+ if (this.textEditor) return;
1898
+ const meta = event.metaKey || event.ctrlKey;
1899
+ if (meta && event.key.toLowerCase() === "z") {
1900
+ event.preventDefault();
1901
+ if (event.shiftKey) this.redo();
1902
+ else this.undo();
1903
+ return;
1904
+ }
1905
+ if (meta && event.key.toLowerCase() === "y") {
1906
+ event.preventDefault();
1907
+ this.redo();
1908
+ return;
1909
+ }
1910
+ if (meta && event.key.toLowerCase() === "a") {
1911
+ event.preventDefault();
1912
+ this.selectAll();
1913
+ return;
1914
+ }
1915
+ if (meta && event.key.toLowerCase() === "c") {
1916
+ this.copy();
1917
+ return;
1918
+ }
1919
+ if (meta && event.key.toLowerCase() === "v") {
1920
+ this.paste();
1921
+ return;
1922
+ }
1923
+ if (meta && event.key.toLowerCase() === "d") {
1924
+ event.preventDefault();
1925
+ this.duplicate();
1926
+ return;
1927
+ }
1928
+ if (this.readonly) return;
1929
+ if (event.key === "Delete" || event.key === "Backspace") {
1930
+ event.preventDefault();
1931
+ this.deleteSelection();
1932
+ return;
1933
+ }
1934
+ const nudgeMap = {
1935
+ ArrowLeft: [-1, 0],
1936
+ ArrowRight: [1, 0],
1937
+ ArrowUp: [0, -1],
1938
+ ArrowDown: [0, 1]
1939
+ };
1940
+ if (nudgeMap[event.key] && this.selectedIds.size) {
1941
+ event.preventDefault();
1942
+ const step = event.shiftKey ? 10 : 1;
1943
+ const [dx, dy] = nudgeMap[event.key];
1944
+ this.nudge(dx * step, dy * step);
1945
+ }
1946
+ }
1947
+ _handleResize() {
1948
+ if (this.destroyed) return;
1949
+ this.render({ scene: false });
1950
+ }
1951
+ // ── Rendering ────────────────────────────────────────────────────────────
1952
+ render(flags = {}) {
1953
+ if (this.destroyed) return;
1954
+ const { scene = true, overlay = true } = flags;
1955
+ const vp = this.documentData.viewport;
1956
+ this.world.setAttribute("transform", `matrix(${vp.scale} 0 0 ${vp.scale} ${vp.x} ${vp.y})`);
1957
+ if (scene) {
1958
+ clearChildren(this.shapesLayer);
1959
+ const erasing = this.interaction && this.interaction.kind === "erase" ? this.interaction.erased : null;
1960
+ for (const shape of this.documentData.shapes) {
1961
+ if (erasing && erasing.has(shape.id)) continue;
1962
+ const el = this._renderShapeEl(shape, {});
1963
+ if (el) this.shapesLayer.appendChild(el);
1964
+ }
1965
+ }
1966
+ if (overlay) this._renderOverlay();
1967
+ }
1968
+ // Colors are applied via inline `style` (which wins over the CSS class rules
1969
+ // and serializes self-contained), so a picked color always renders.
1970
+ _renderShapeEl(shape, { standalone = false, colors = null }) {
1971
+ let el = null;
1972
+ const setOpacity = (node) => {
1973
+ if (shape.opacity != null && shape.opacity !== 1)
1974
+ node.setAttribute("opacity", String(shape.opacity));
1975
+ };
1976
+ if (shape.type === "freehand") {
1977
+ el = createSvgEl("path", { d: brushStrokePath(shape) });
1978
+ el.classList.add("vd-draw-ink");
1979
+ el.style.stroke = "none";
1980
+ const fill = shape.color || (standalone && colors ? colors.ink : "");
1981
+ if (fill) el.style.fill = fill;
1982
+ if (shape.opacity != null && shape.opacity !== 1)
1983
+ el.style.fillOpacity = String(shape.opacity);
1984
+ const preset = BRUSH_PRESETS[shape.brush];
1985
+ if (preset && preset.blend && preset.blend !== "normal") el.style.mixBlendMode = preset.blend;
1986
+ } else if (shape.type === "rectangle") {
1987
+ el = createSvgEl("rect", { x: shape.x, y: shape.y, width: shape.w, height: shape.h, rx: 4 });
1988
+ el.classList.add("vd-draw-shape");
1989
+ this._applyShapeStroke(el, shape, standalone, colors);
1990
+ setOpacity(el);
1991
+ } else if (shape.type === "ellipse") {
1992
+ el = createSvgEl("ellipse", {
1993
+ cx: shape.x + shape.w / 2,
1994
+ cy: shape.y + shape.h / 2,
1995
+ rx: shape.w / 2,
1996
+ ry: shape.h / 2
1997
+ });
1998
+ el.classList.add("vd-draw-shape");
1999
+ this._applyShapeStroke(el, shape, standalone, colors);
2000
+ setOpacity(el);
2001
+ } else if (shape.type === "line") {
2002
+ el = createSvgEl("path", { d: pointsToPath(shape.points) });
2003
+ el.classList.add("vd-draw-shape");
2004
+ this._applyShapeStroke(el, shape, standalone, colors);
2005
+ setOpacity(el);
2006
+ if (shape.arrowEnd) el.setAttribute("marker-end", `url(#${this._svgId("arrow")})`);
2007
+ if (shape.arrowStart) el.setAttribute("marker-start", `url(#${this._svgId("arrow")})`);
2008
+ } else if (shape.type === "text" || shape.type === "sticky") {
2009
+ el = createSvgEl("g");
2010
+ setOpacity(el);
2011
+ if (shape.type === "sticky") {
2012
+ const bg = createSvgEl("rect", {
2013
+ x: shape.x,
2014
+ y: shape.y,
2015
+ width: shape.w,
2016
+ height: shape.h,
2017
+ rx: 4
2018
+ });
2019
+ bg.classList.add("vd-draw-sticky");
2020
+ if (shape.fill) bg.style.fill = shape.fill;
2021
+ else if (standalone && colors) bg.style.fill = colors.sticky;
2022
+ el.appendChild(bg);
2023
+ }
2024
+ const text = createSvgEl("text", { x: shape.x + 6, y: shape.y + 18 });
2025
+ text.classList.add("vd-draw-text");
2026
+ const fill = shape.color || (standalone && colors ? colors.text : "");
2027
+ if (fill) text.style.fill = fill;
2028
+ text.textContent = shape.text || "";
2029
+ el.appendChild(text);
2030
+ }
2031
+ if (el && !standalone) el.setAttribute("data-shape-id", shape.id);
2032
+ return el;
2033
+ }
2034
+ _applyShapeStroke(el, shape, standalone, colors) {
2035
+ const stroke = shape.color || (standalone && colors ? colors.shapeStroke : "");
2036
+ if (stroke) el.style.stroke = stroke;
2037
+ if (shape.strokeWidth != null) el.setAttribute("stroke-width", String(shape.strokeWidth));
2038
+ el.style.fill = shape.fill ? shape.fill : "none";
2039
+ }
2040
+ _renderOverlay() {
2041
+ clearChildren(this.overlayLayer);
2042
+ const selected = this.getSelectedShapes();
2043
+ if (!selected.length || this.readonly) return;
2044
+ const bounds = boundsOfShapes(selected);
2045
+ if (!bounds) return;
2046
+ const box = createSvgEl("rect", {
2047
+ class: "vd-draw-selection-box",
2048
+ x: bounds.x,
2049
+ y: bounds.y,
2050
+ width: bounds.w,
2051
+ height: bounds.h,
2052
+ fill: "none"
2053
+ });
2054
+ box.setAttribute("vector-effect", "non-scaling-stroke");
2055
+ this.overlayLayer.appendChild(box);
2056
+ const scale = this.documentData.viewport.scale || 1;
2057
+ const r = HANDLE_SIZE / scale / 2;
2058
+ for (const handle of resizeHandlePositions(bounds)) {
2059
+ const dot = createSvgEl("rect", {
2060
+ class: "vd-draw-handle",
2061
+ x: handle.x - r,
2062
+ y: handle.y - r,
2063
+ width: r * 2,
2064
+ height: r * 2,
2065
+ "data-handle": handle.key
2066
+ });
2067
+ dot.setAttribute("vector-effect", "non-scaling-stroke");
2068
+ this.overlayLayer.appendChild(dot);
2069
+ }
2070
+ }
2071
+ _renderMarquee(a, b) {
2072
+ clearChildren(this.marqueeLayer);
2073
+ const box = this._boxFromPoints(a, b);
2074
+ const rect = createSvgEl("rect", {
2075
+ class: "vd-draw-marquee-rect",
2076
+ x: box.x,
2077
+ y: box.y,
2078
+ width: box.w,
2079
+ height: box.h
2080
+ });
2081
+ rect.setAttribute("vector-effect", "non-scaling-stroke");
2082
+ this.marqueeLayer.appendChild(rect);
2083
+ }
2084
+ _renderGuides(guides) {
2085
+ clearChildren(this.guidesLayer);
2086
+ for (const g of guides || []) {
2087
+ const line = createSvgEl("line", {
2088
+ class: "vd-draw-guide",
2089
+ x1: g.x1,
2090
+ y1: g.y1,
2091
+ x2: g.x2,
2092
+ y2: g.y2
2093
+ });
2094
+ line.setAttribute("vector-effect", "non-scaling-stroke");
2095
+ this.guidesLayer.appendChild(line);
2096
+ }
2097
+ }
2098
+ // ── Ready + lifecycle ────────────────────────────────────────────────────
2099
+ _scheduleReady() {
2100
+ const fire = () => {
2101
+ if (this.destroyed || this._ready) return;
2102
+ this._ready = true;
2103
+ if (this.autoFit) this.fitView();
2104
+ this.emit("ready", this);
2105
+ };
2106
+ if (typeof requestAnimationFrame === "function") requestAnimationFrame(fire);
2107
+ else setTimeout(fire, 0);
2108
+ }
2109
+ destroy() {
2110
+ if (this.destroyed) return;
2111
+ this.destroyed = true;
2112
+ this.stopTextEdit({ commit: false });
2113
+ this._unbindEvents();
2114
+ this.listeners.clear();
2115
+ if (this.element) this.element.replaceChildren();
2116
+ }
2117
+ };
2118
+
2119
+ // src/draw/vue.js
2120
+ var FORWARDED_EVENTS = ["change", "select", "viewport", "ready"];
2121
+ var VdDraw2 = defineComponent({
2122
+ name: "VdDraw",
2123
+ props: {
2124
+ /** Drawing document — `{ shapes, viewport }`. */
2125
+ data: { type: Object, default: () => ({}) },
2126
+ /** Render as a non-editable viewer (no toolbar, no editing). */
2127
+ readonly: { type: Boolean, default: false },
2128
+ /** Active tool — updated live without recreating the editor. */
2129
+ tool: { type: String, default: "draw" },
2130
+ /** Background grid size in px. */
2131
+ gridSize: { type: Number, default: void 0 },
2132
+ /** Show the background grid — updated live (default true). */
2133
+ showGrid: { type: Boolean, default: true },
2134
+ /** Snap shapes to nearby edges/centres while moving/resizing. */
2135
+ snap: { type: Boolean, default: true },
2136
+ /** Fit the view to content once the editor reports a measurable size. */
2137
+ autoFit: { type: Boolean, default: false },
2138
+ /** Enable the built-in undo/redo history (default true). */
2139
+ history: { type: Boolean, default: true },
2140
+ /** Maximum number of history entries to retain. */
2141
+ historyLimit: { type: Number, default: void 0 }
2142
+ },
2143
+ emits: ["change", "select", "viewport", "ready"],
2144
+ setup(props, { emit, expose }) {
2145
+ const el = ref(null);
2146
+ let instance = null;
2147
+ const create = () => {
2148
+ instance = new VdDraw({
2149
+ element: el.value,
2150
+ data: props.data,
2151
+ readonly: props.readonly,
2152
+ tool: props.tool,
2153
+ gridSize: props.gridSize,
2154
+ showGrid: props.showGrid,
2155
+ snap: props.snap,
2156
+ autoFit: props.autoFit,
2157
+ history: props.history,
2158
+ historyLimit: props.historyLimit
2159
+ });
2160
+ FORWARDED_EVENTS.forEach((name) => {
2161
+ instance.on(name, (payload) => emit(name, payload));
2162
+ });
2163
+ };
2164
+ onMounted(() => {
2165
+ if (typeof window === "undefined" || !el.value) return;
2166
+ create();
2167
+ });
2168
+ watch(
2169
+ () => props.data,
2170
+ (next) => {
2171
+ if (instance && typeof instance.load === "function") instance.load(next);
2172
+ },
2173
+ { deep: true }
2174
+ );
2175
+ watch(
2176
+ () => props.tool,
2177
+ (next) => instance?.setTool(next)
2178
+ );
2179
+ watch(
2180
+ () => props.showGrid,
2181
+ (next) => instance?.setGridVisible(next)
2182
+ );
2183
+ watch(
2184
+ () => [
2185
+ props.readonly,
2186
+ props.gridSize,
2187
+ props.snap,
2188
+ props.autoFit,
2189
+ props.history,
2190
+ props.historyLimit
2191
+ ],
2192
+ () => {
2193
+ if (!instance) return;
2194
+ instance.destroy();
2195
+ create();
2196
+ }
2197
+ );
2198
+ onBeforeUnmount(() => {
2199
+ if (instance) {
2200
+ instance.destroy();
2201
+ instance = null;
2202
+ }
2203
+ });
2204
+ expose({
2205
+ getInstance: () => instance,
2206
+ setTool: (tool) => instance?.setTool(tool),
2207
+ undo: () => instance?.undo(),
2208
+ redo: () => instance?.redo(),
2209
+ canUndo: () => Boolean(instance?.canUndo()),
2210
+ canRedo: () => Boolean(instance?.canRedo()),
2211
+ toSVG: () => instance?.toSVG(),
2212
+ toPNG: (options) => instance?.toPNG(options)
2213
+ });
2214
+ return () => h("div", { ref: el, class: "vd-draw" });
2215
+ }
2216
+ });
2217
+ export {
2218
+ BRUSH_PRESETS,
2219
+ DRAW_SHAPE_TYPES,
2220
+ DRAW_TOOLS,
2221
+ VD_DRAW_VERSION,
2222
+ VdDraw2 as VdDraw,
2223
+ VdDraw as VdDrawCore
2224
+ };
2225
+ //# sourceMappingURL=index.js.map