@vanduo-oss/vd3-cbun 1.1.0 → 1.2.0

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