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