drawfn 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1956 @@
1
+ import { getStroke } from 'perfect-freehand';
2
+
3
+ // src/renderer.ts
4
+
5
+ // src/utils.ts
6
+ function generateId() {
7
+ return `e_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
8
+ }
9
+ function screenToCanvas(screenPoint, camera, canvas) {
10
+ const rect = canvas.getBoundingClientRect();
11
+ const x = (screenPoint.x - rect.left - camera.x) / camera.zoom;
12
+ const y = (screenPoint.y - rect.top - camera.y) / camera.zoom;
13
+ return { x, y };
14
+ }
15
+ function canvasToScreen(canvasPoint, camera) {
16
+ return {
17
+ x: canvasPoint.x * camera.zoom + camera.x,
18
+ y: canvasPoint.y * camera.zoom + camera.y
19
+ };
20
+ }
21
+ function getElementBounds(element) {
22
+ return {
23
+ x: element.x,
24
+ y: element.y,
25
+ w: element.w,
26
+ h: element.h
27
+ };
28
+ }
29
+ function boundsIntersect(a, b) {
30
+ 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);
31
+ }
32
+ function getViewportBounds(camera, canvas) {
33
+ const w = canvas.width / camera.zoom;
34
+ const h = canvas.height / camera.zoom;
35
+ return {
36
+ x: -camera.x / camera.zoom,
37
+ y: -camera.y / camera.zoom,
38
+ w,
39
+ h
40
+ };
41
+ }
42
+ function rotatePoint(point, center, angle) {
43
+ const cos = Math.cos(angle);
44
+ const sin = Math.sin(angle);
45
+ const dx = point.x - center.x;
46
+ const dy = point.y - center.y;
47
+ return {
48
+ x: center.x + dx * cos - dy * sin,
49
+ y: center.y + dx * sin + dy * cos
50
+ };
51
+ }
52
+
53
+ // src/renderer.ts
54
+ var Renderer = class {
55
+ constructor(canvas, getImageBitmap) {
56
+ this.imageCache = /* @__PURE__ */ new Map();
57
+ this.canvas = canvas;
58
+ const ctx = canvas.getContext("2d");
59
+ if (!ctx) throw new Error("Failed to get 2D context");
60
+ this.ctx = ctx;
61
+ this.getImageBitmap = getImageBitmap;
62
+ }
63
+ render(scene, options = {}) {
64
+ const { camera, background, elements } = scene;
65
+ const { selectedIds = /* @__PURE__ */ new Set(), hoveredId = null } = options;
66
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
67
+ this.ctx.fillStyle = background.color;
68
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
69
+ if (background.grid) {
70
+ this.renderGrid(camera, background.grid);
71
+ }
72
+ this.ctx.save();
73
+ this.ctx.translate(camera.x, camera.y);
74
+ this.ctx.scale(camera.zoom, camera.zoom);
75
+ const viewport = getViewportBounds(camera, this.canvas);
76
+ for (const element of elements) {
77
+ if (element.locked && !selectedIds.has(element.id)) continue;
78
+ const bounds = getElementBounds(element);
79
+ if (!boundsIntersect(viewport, bounds)) continue;
80
+ this.renderElement(element);
81
+ if (selectedIds.has(element.id)) {
82
+ this.renderSelectionBox(element, camera);
83
+ } else if (hoveredId === element.id) {
84
+ this.renderHoverHighlight(element);
85
+ }
86
+ }
87
+ this.ctx.restore();
88
+ if (options.marquee) {
89
+ this.renderMarquee(options.marquee, camera);
90
+ }
91
+ }
92
+ renderGrid(camera, grid) {
93
+ const { size, color, alpha } = grid;
94
+ this.ctx.save();
95
+ const startX = Math.floor(-camera.x / camera.zoom / size) * size;
96
+ const startY = Math.floor(-camera.y / camera.zoom / size) * size;
97
+ const endX = startX + this.canvas.width / camera.zoom + size;
98
+ const endY = startY + this.canvas.height / camera.zoom + size;
99
+ this.ctx.strokeStyle = color;
100
+ this.ctx.globalAlpha = alpha;
101
+ this.ctx.lineWidth = 1 / camera.zoom;
102
+ this.ctx.beginPath();
103
+ for (let x = startX; x < endX; x += size) {
104
+ const screenX = x * camera.zoom + camera.x;
105
+ this.ctx.moveTo(screenX, 0);
106
+ this.ctx.lineTo(screenX, this.canvas.height);
107
+ }
108
+ for (let y = startY; y < endY; y += size) {
109
+ const screenY = y * camera.zoom + camera.y;
110
+ this.ctx.moveTo(0, screenY);
111
+ this.ctx.lineTo(this.canvas.width, screenY);
112
+ }
113
+ this.ctx.stroke();
114
+ this.ctx.restore();
115
+ }
116
+ renderElement(element) {
117
+ this.ctx.save();
118
+ this.ctx.globalAlpha = element.opacity;
119
+ if (element.rotation !== 0) {
120
+ const cx = element.x + element.w / 2;
121
+ const cy = element.y + element.h / 2;
122
+ this.ctx.translate(cx, cy);
123
+ this.ctx.rotate(element.rotation);
124
+ this.ctx.translate(-cx, -cy);
125
+ }
126
+ switch (element.type) {
127
+ case "freedraw":
128
+ this.renderFreedraw(element);
129
+ break;
130
+ case "rectangle":
131
+ this.renderRectangle(element);
132
+ break;
133
+ case "ellipse":
134
+ this.renderEllipse(element);
135
+ break;
136
+ case "arrow":
137
+ this.renderArrow(element);
138
+ break;
139
+ case "text":
140
+ this.renderText(element);
141
+ break;
142
+ case "image":
143
+ this.renderImage(element);
144
+ break;
145
+ case "node":
146
+ this.renderNode(element);
147
+ break;
148
+ }
149
+ this.ctx.restore();
150
+ }
151
+ renderFreedraw(element) {
152
+ if (element.points.length === 0) return;
153
+ const options = {
154
+ size: element.style?.size ?? element.strokeWidth * 2,
155
+ thinning: element.style?.thinning ?? 0.5,
156
+ smoothing: element.style?.smoothing ?? 0.5,
157
+ streamline: element.style?.streamline ?? 0.5,
158
+ taperStart: element.style?.taperStart ?? 0,
159
+ taperEnd: element.style?.taperEnd ?? 0,
160
+ simulatePressure: true
161
+ };
162
+ const points = element.points.map((p) => [p[0], p[1], p[2] ?? 0.5]);
163
+ const stroke = getStroke(points, options);
164
+ this.ctx.fillStyle = element.stroke;
165
+ this.ctx.beginPath();
166
+ this.ctx.moveTo(stroke[0][0] + element.x, stroke[0][1] + element.y);
167
+ for (let i = 1; i < stroke.length; i++) {
168
+ this.ctx.lineTo(stroke[i][0] + element.x, stroke[i][1] + element.y);
169
+ }
170
+ this.ctx.closePath();
171
+ this.ctx.fill();
172
+ }
173
+ renderRectangle(element) {
174
+ this.ctx.fillStyle = element.fill;
175
+ this.ctx.strokeStyle = element.stroke;
176
+ this.ctx.lineWidth = element.strokeWidth;
177
+ if (element.fill !== "transparent") {
178
+ this.ctx.fillRect(element.x, element.y, element.w, element.h);
179
+ }
180
+ if (element.strokeWidth > 0) {
181
+ this.ctx.strokeRect(element.x, element.y, element.w, element.h);
182
+ }
183
+ }
184
+ renderEllipse(element) {
185
+ this.ctx.fillStyle = element.fill;
186
+ this.ctx.strokeStyle = element.stroke;
187
+ this.ctx.lineWidth = element.strokeWidth;
188
+ const cx = element.x + element.w / 2;
189
+ const cy = element.y + element.h / 2;
190
+ const rx = element.w / 2;
191
+ const ry = element.h / 2;
192
+ this.ctx.beginPath();
193
+ this.ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
194
+ if (element.fill !== "transparent") {
195
+ this.ctx.fill();
196
+ }
197
+ if (element.strokeWidth > 0) {
198
+ this.ctx.stroke();
199
+ }
200
+ }
201
+ renderArrow(element) {
202
+ if (element.points.length < 2) return;
203
+ this.ctx.strokeStyle = element.stroke;
204
+ this.ctx.fillStyle = element.stroke;
205
+ this.ctx.lineWidth = element.strokeWidth;
206
+ this.ctx.beginPath();
207
+ this.ctx.moveTo(
208
+ element.points[0][0] + element.x,
209
+ element.points[0][1] + element.y
210
+ );
211
+ for (let i = 1; i < element.points.length; i++) {
212
+ this.ctx.lineTo(
213
+ element.points[i][0] + element.x,
214
+ element.points[i][1] + element.y
215
+ );
216
+ }
217
+ this.ctx.stroke();
218
+ if (element.head === "triangle") {
219
+ const last = element.points[element.points.length - 1];
220
+ const prev = element.points[element.points.length - 2];
221
+ const angle = Math.atan2(last[1] - prev[1], last[0] - prev[0]);
222
+ const headLen = 10;
223
+ this.ctx.beginPath();
224
+ this.ctx.moveTo(last[0] + element.x, last[1] + element.y);
225
+ this.ctx.lineTo(
226
+ last[0] + element.x - headLen * Math.cos(angle - Math.PI / 6),
227
+ last[1] + element.y - headLen * Math.sin(angle - Math.PI / 6)
228
+ );
229
+ this.ctx.lineTo(
230
+ last[0] + element.x - headLen * Math.cos(angle + Math.PI / 6),
231
+ last[1] + element.y - headLen * Math.sin(angle + Math.PI / 6)
232
+ );
233
+ this.ctx.closePath();
234
+ this.ctx.fill();
235
+ }
236
+ }
237
+ renderText(element) {
238
+ this.ctx.fillStyle = element.fill;
239
+ this.ctx.strokeStyle = element.stroke;
240
+ this.ctx.font = `${element.fontSize}px ${element.fontFamily}`;
241
+ this.ctx.textAlign = element.align;
242
+ const x = element.x + (element.align === "center" ? element.w / 2 : 0);
243
+ const y = element.y + element.fontSize;
244
+ if (element.fill !== "transparent") {
245
+ this.ctx.fillText(element.text, x, y);
246
+ }
247
+ if (element.strokeWidth > 0) {
248
+ this.ctx.lineWidth = element.strokeWidth;
249
+ this.ctx.strokeText(element.text, x, y);
250
+ }
251
+ }
252
+ renderImage(element) {
253
+ const cached = this.imageCache.get(element.src);
254
+ if (cached) {
255
+ this.ctx.drawImage(cached, element.x, element.y, element.w, element.h);
256
+ } else {
257
+ this.loadImage(element.src);
258
+ this.ctx.fillStyle = "#f3f4f6";
259
+ this.ctx.fillRect(element.x, element.y, element.w, element.h);
260
+ }
261
+ }
262
+ renderNode(element) {
263
+ const radius = 8;
264
+ this.ctx.fillStyle = element.fill;
265
+ this.ctx.strokeStyle = element.stroke;
266
+ this.ctx.lineWidth = element.strokeWidth;
267
+ this.ctx.beginPath();
268
+ this.ctx.roundRect(element.x, element.y, element.w, element.h, radius);
269
+ if (element.fill !== "transparent") {
270
+ this.ctx.fill();
271
+ }
272
+ if (element.strokeWidth > 0) {
273
+ this.ctx.stroke();
274
+ }
275
+ if (element.preview?.title) {
276
+ this.ctx.fillStyle = element.stroke;
277
+ this.ctx.font = "14px sans-serif";
278
+ this.ctx.textAlign = "left";
279
+ this.ctx.fillText(element.preview.title, element.x + 12, element.y + 24);
280
+ }
281
+ }
282
+ renderSelectionBox(element, camera) {
283
+ const strokeWidth = 2 / camera.zoom;
284
+ const dashPattern = 4 / camera.zoom;
285
+ const handleSize = 6 / camera.zoom;
286
+ const padding = 2 / camera.zoom;
287
+ const rotateHandleDistance = 20 / camera.zoom;
288
+ const rotateHandleRadius = 4 / camera.zoom;
289
+ this.ctx.strokeStyle = "#3b82f6";
290
+ this.ctx.lineWidth = strokeWidth;
291
+ this.ctx.setLineDash([dashPattern, dashPattern]);
292
+ this.ctx.strokeRect(
293
+ element.x - padding,
294
+ element.y - padding,
295
+ element.w + padding * 2,
296
+ element.h + padding * 2
297
+ );
298
+ this.ctx.setLineDash([]);
299
+ this.ctx.fillStyle = "#ffffff";
300
+ this.ctx.strokeStyle = "#3b82f6";
301
+ this.ctx.lineWidth = 1 / camera.zoom;
302
+ const { x, y, w, h } = element;
303
+ const handles = [
304
+ { x, y },
305
+ { x: x + w, y },
306
+ { x, y: y + h },
307
+ { x: x + w, y: y + h },
308
+ { x: x + w / 2, y },
309
+ { x: x + w / 2, y: y + h },
310
+ { x: x + w, y: y + h / 2 },
311
+ { x, y: y + h / 2 }
312
+ ];
313
+ for (const handle of handles) {
314
+ this.ctx.fillRect(
315
+ handle.x - handleSize / 2,
316
+ handle.y - handleSize / 2,
317
+ handleSize,
318
+ handleSize
319
+ );
320
+ this.ctx.strokeRect(
321
+ handle.x - handleSize / 2,
322
+ handle.y - handleSize / 2,
323
+ handleSize,
324
+ handleSize
325
+ );
326
+ }
327
+ this.ctx.beginPath();
328
+ this.ctx.arc(
329
+ x + w / 2,
330
+ y - rotateHandleDistance,
331
+ rotateHandleRadius,
332
+ 0,
333
+ Math.PI * 2
334
+ );
335
+ this.ctx.fill();
336
+ this.ctx.stroke();
337
+ this.ctx.beginPath();
338
+ this.ctx.moveTo(x + w / 2, y);
339
+ this.ctx.lineTo(x + w / 2, y - rotateHandleDistance + rotateHandleRadius);
340
+ this.ctx.stroke();
341
+ }
342
+ renderHoverHighlight(element) {
343
+ this.ctx.strokeStyle = "#93c5fd";
344
+ this.ctx.lineWidth = 1;
345
+ this.ctx.strokeRect(
346
+ element.x - 1,
347
+ element.y - 1,
348
+ element.w + 2,
349
+ element.h + 2
350
+ );
351
+ }
352
+ renderMarquee(marquee, camera) {
353
+ const minX = Math.min(marquee.start.x, marquee.current.x);
354
+ const minY = Math.min(marquee.start.y, marquee.current.y);
355
+ const maxX = Math.max(marquee.start.x, marquee.current.x);
356
+ const maxY = Math.max(marquee.start.y, marquee.current.y);
357
+ const width = maxX - minX;
358
+ const height = maxY - minY;
359
+ this.ctx.save();
360
+ this.ctx.translate(camera.x, camera.y);
361
+ this.ctx.scale(camera.zoom, camera.zoom);
362
+ this.ctx.fillStyle = "rgba(59, 130, 246, 0.1)";
363
+ this.ctx.fillRect(minX, minY, width, height);
364
+ this.ctx.strokeStyle = "#3b82f6";
365
+ this.ctx.lineWidth = 1.5 / camera.zoom;
366
+ this.ctx.setLineDash([5 / camera.zoom, 5 / camera.zoom]);
367
+ this.ctx.strokeRect(minX, minY, width, height);
368
+ this.ctx.setLineDash([]);
369
+ this.ctx.restore();
370
+ }
371
+ async loadImage(src) {
372
+ if (this.imageCache.has(src)) return;
373
+ try {
374
+ if (this.getImageBitmap) {
375
+ const bitmap = await this.getImageBitmap(src);
376
+ this.imageCache.set(src, bitmap);
377
+ } else {
378
+ const img = new Image();
379
+ img.crossOrigin = "anonymous";
380
+ await new Promise((resolve, reject) => {
381
+ img.onload = resolve;
382
+ img.onerror = reject;
383
+ img.src = src;
384
+ });
385
+ this.imageCache.set(src, img);
386
+ }
387
+ } catch (error) {
388
+ console.error("Failed to load image:", src, error);
389
+ }
390
+ }
391
+ };
392
+
393
+ // src/history.ts
394
+ var History = class {
395
+ constructor(maxSize = 50) {
396
+ this.past = [];
397
+ this.future = [];
398
+ this.maxSize = maxSize;
399
+ }
400
+ push(scene) {
401
+ this.past.push({
402
+ scene: JSON.parse(JSON.stringify(scene)),
403
+ timestamp: Date.now()
404
+ });
405
+ if (this.past.length > this.maxSize) {
406
+ this.past.shift();
407
+ }
408
+ this.future = [];
409
+ }
410
+ undo(currentScene) {
411
+ if (this.past.length === 0) return null;
412
+ this.future.push({
413
+ scene: JSON.parse(JSON.stringify(currentScene)),
414
+ timestamp: Date.now()
415
+ });
416
+ const state = this.past.pop();
417
+ return state.scene;
418
+ }
419
+ redo(currentScene) {
420
+ if (this.future.length === 0) return null;
421
+ this.past.push({
422
+ scene: JSON.parse(JSON.stringify(currentScene)),
423
+ timestamp: Date.now()
424
+ });
425
+ const state = this.future.pop();
426
+ return state.scene;
427
+ }
428
+ canUndo() {
429
+ return this.past.length > 0;
430
+ }
431
+ canRedo() {
432
+ return this.future.length > 0;
433
+ }
434
+ clear() {
435
+ this.past = [];
436
+ this.future = [];
437
+ }
438
+ };
439
+
440
+ // src/tools/base-tool.ts
441
+ var BaseTool = class {
442
+ constructor() {
443
+ this.isActive = false;
444
+ }
445
+ activate(opts) {
446
+ this.isActive = true;
447
+ }
448
+ deactivate() {
449
+ this.isActive = false;
450
+ }
451
+ handlePointerDown(e, context, selectedIds) {
452
+ }
453
+ handlePointerMove(e, context) {
454
+ }
455
+ handlePointerUp(e, context) {
456
+ }
457
+ handleKeyDown(e, context) {
458
+ }
459
+ };
460
+
461
+ // src/geometry.ts
462
+ function getOBBCorners(element) {
463
+ const { x, y, w, h, rotation } = element;
464
+ if (rotation === 0) {
465
+ return [
466
+ { x, y },
467
+ { x: x + w, y },
468
+ { x: x + w, y: y + h },
469
+ { x, y: y + h }
470
+ ];
471
+ }
472
+ const center = { x: x + w / 2, y: y + h / 2 };
473
+ const corners = [
474
+ { x, y },
475
+ { x: x + w, y },
476
+ { x: x + w, y: y + h },
477
+ { x, y: y + h }
478
+ ];
479
+ return corners.map((corner) => rotatePoint(corner, center, rotation));
480
+ }
481
+ function isPointInOBB(point, element) {
482
+ const { x, y, w, h, rotation } = element;
483
+ if (rotation === 0) {
484
+ return point.x >= x && point.x <= x + w && point.y >= y && point.y <= y + h;
485
+ }
486
+ const center = { x: x + w / 2, y: y + h / 2 };
487
+ const rotated = rotatePoint(point, center, -rotation);
488
+ return rotated.x >= x && rotated.x <= x + w && rotated.y >= y && rotated.y <= y + h;
489
+ }
490
+ function getOBBBounds(element) {
491
+ if (element.rotation === 0) {
492
+ return {
493
+ x: element.x,
494
+ y: element.y,
495
+ w: element.w,
496
+ h: element.h
497
+ };
498
+ }
499
+ const corners = getOBBCorners(element);
500
+ const xs = corners.map((c) => c.x);
501
+ const ys = corners.map((c) => c.y);
502
+ const minX = Math.min(...xs);
503
+ const minY = Math.min(...ys);
504
+ const maxX = Math.max(...xs);
505
+ const maxY = Math.max(...ys);
506
+ return {
507
+ x: minX,
508
+ y: minY,
509
+ w: maxX - minX,
510
+ h: maxY - minY
511
+ };
512
+ }
513
+
514
+ // src/tools/select-tool.ts
515
+ var SelectTool = class extends BaseTool {
516
+ constructor() {
517
+ super(...arguments);
518
+ this.name = "select";
519
+ this.dragStart = null;
520
+ this.dragElementId = null;
521
+ this.dragHandle = null;
522
+ this.initialBounds = null;
523
+ // Marquee selection state
524
+ this.marqueeStart = null;
525
+ this.marqueeCurrent = null;
526
+ this.isMarqueeActive = false;
527
+ // Double-click detection for text editing
528
+ this.lastClickTime = 0;
529
+ this.lastClickPoint = null;
530
+ this.lastClickedElementId = null;
531
+ }
532
+ handlePointerDown(e, context, selectedIds) {
533
+ const canvas = e.target;
534
+ const scene = context.getScene();
535
+ const point = screenToCanvas(
536
+ { x: e.clientX, y: e.clientY },
537
+ scene.camera,
538
+ canvas
539
+ );
540
+ for (const id of selectedIds) {
541
+ const element = scene.elements.find((el) => el.id === id);
542
+ if (!element) continue;
543
+ const handle = this.getHandleAtPoint(element, point, scene.camera.zoom);
544
+ if (handle) {
545
+ context.recordHistory();
546
+ this.dragStart = point;
547
+ this.dragElementId = element.id;
548
+ this.dragHandle = handle;
549
+ this.initialBounds = {
550
+ x: element.x,
551
+ y: element.y,
552
+ w: element.w,
553
+ h: element.h
554
+ };
555
+ return;
556
+ }
557
+ }
558
+ for (let i = scene.elements.length - 1; i >= 0; i--) {
559
+ const element = scene.elements[i];
560
+ if (isPointInOBB(point, element)) {
561
+ const now = Date.now();
562
+ const isDoubleClick = this.lastClickedElementId === element.id && now - this.lastClickTime < 300 && this.lastClickPoint && Math.abs(point.x - this.lastClickPoint.x) < 5 && Math.abs(point.y - this.lastClickPoint.y) < 5;
563
+ if (isDoubleClick && element.type === "text") {
564
+ const drawfn = context.getDrawfn();
565
+ if (drawfn.editTextElement) {
566
+ drawfn.editTextElement(element.id);
567
+ }
568
+ return;
569
+ }
570
+ this.lastClickTime = now;
571
+ this.lastClickPoint = point;
572
+ this.lastClickedElementId = element.id;
573
+ context.recordHistory();
574
+ this.dragStart = point;
575
+ this.dragElementId = element.id;
576
+ this.dragHandle = null;
577
+ if (!selectedIds.has(element.id)) {
578
+ if (!e.shiftKey) {
579
+ selectedIds.clear();
580
+ }
581
+ selectedIds.add(element.id);
582
+ }
583
+ return;
584
+ }
585
+ }
586
+ if (!e.shiftKey) {
587
+ selectedIds.clear();
588
+ }
589
+ this.marqueeStart = point;
590
+ this.marqueeCurrent = point;
591
+ this.isMarqueeActive = true;
592
+ }
593
+ handlePointerMove(e, context) {
594
+ const canvas = e.target;
595
+ const scene = context.getScene();
596
+ const point = screenToCanvas(
597
+ { x: e.clientX, y: e.clientY },
598
+ scene.camera,
599
+ canvas
600
+ );
601
+ if (this.isMarqueeActive && this.marqueeStart) {
602
+ this.marqueeCurrent = point;
603
+ return;
604
+ }
605
+ if (!this.dragStart || !this.dragElementId) return;
606
+ if (this.dragHandle && this.initialBounds) {
607
+ this.handleResize(point, this.dragHandle, context);
608
+ } else {
609
+ const dx = point.x - this.dragStart.x;
610
+ const dy = point.y - this.dragStart.y;
611
+ const element = scene.elements.find((el) => el.id === this.dragElementId);
612
+ if (element) {
613
+ context.updateElement(this.dragElementId, {
614
+ x: element.x + dx,
615
+ y: element.y + dy
616
+ });
617
+ }
618
+ this.dragStart = point;
619
+ }
620
+ }
621
+ handlePointerUp(e, context) {
622
+ if (this.isMarqueeActive && this.marqueeStart && this.marqueeCurrent) {
623
+ const selection = context.getSelection();
624
+ const elementsInMarquee = this.getElementsInMarquee(context);
625
+ if (e.shiftKey) {
626
+ elementsInMarquee.forEach((id) => selection.add(id));
627
+ } else {
628
+ selection.clear();
629
+ elementsInMarquee.forEach((id) => selection.add(id));
630
+ }
631
+ this.marqueeStart = null;
632
+ this.marqueeCurrent = null;
633
+ this.isMarqueeActive = false;
634
+ return;
635
+ }
636
+ this.dragStart = null;
637
+ this.dragElementId = null;
638
+ this.dragHandle = null;
639
+ this.initialBounds = null;
640
+ }
641
+ getMarquee() {
642
+ if (this.isMarqueeActive && this.marqueeStart && this.marqueeCurrent) {
643
+ return { start: this.marqueeStart, current: this.marqueeCurrent };
644
+ }
645
+ return null;
646
+ }
647
+ getElementsInMarquee(context) {
648
+ if (!this.marqueeStart || !this.marqueeCurrent) return [];
649
+ const scene = context.getScene();
650
+ const minX = Math.min(this.marqueeStart.x, this.marqueeCurrent.x);
651
+ const minY = Math.min(this.marqueeStart.y, this.marqueeCurrent.y);
652
+ const maxX = Math.max(this.marqueeStart.x, this.marqueeCurrent.x);
653
+ const maxY = Math.max(this.marqueeStart.y, this.marqueeCurrent.y);
654
+ const elementsInBounds = [];
655
+ for (const element of scene.elements) {
656
+ const centerX = element.x + element.w / 2;
657
+ const centerY = element.y + element.h / 2;
658
+ if (centerX >= minX && centerX <= maxX && centerY >= minY && centerY <= maxY) {
659
+ elementsInBounds.push(element.id);
660
+ }
661
+ }
662
+ return elementsInBounds;
663
+ }
664
+ getHandleAtPoint(element, point, zoom) {
665
+ const handleSize = 8 / zoom;
666
+ const { x, y, w, h, rotation } = element;
667
+ const localHandles = [
668
+ { type: "nw", x, y },
669
+ { type: "ne", x: x + w, y },
670
+ { type: "sw", x, y: y + h },
671
+ { type: "se", x: x + w, y: y + h },
672
+ { type: "n", x: x + w / 2, y },
673
+ { type: "s", x: x + w / 2, y: y + h },
674
+ { type: "e", x: x + w, y: y + h / 2 },
675
+ { type: "w", x, y: y + h / 2 },
676
+ { type: "rotate", x: x + w / 2, y: y - 20 / zoom }
677
+ ];
678
+ const handles = rotation !== 0 ? localHandles.map((handle) => {
679
+ const center = { x: x + w / 2, y: y + h / 2 };
680
+ const rotated = this.rotatePoint(
681
+ { x: handle.x, y: handle.y },
682
+ center,
683
+ rotation
684
+ );
685
+ return { type: handle.type, x: rotated.x, y: rotated.y };
686
+ }) : localHandles;
687
+ for (const handle of handles) {
688
+ const dist = Math.sqrt(
689
+ Math.pow(point.x - handle.x, 2) + Math.pow(point.y - handle.y, 2)
690
+ );
691
+ if (dist < handleSize) {
692
+ return handle.type;
693
+ }
694
+ }
695
+ return null;
696
+ }
697
+ rotatePoint(point, center, angle) {
698
+ const cos = Math.cos(angle);
699
+ const sin = Math.sin(angle);
700
+ const dx = point.x - center.x;
701
+ const dy = point.y - center.y;
702
+ return {
703
+ x: center.x + dx * cos - dy * sin,
704
+ y: center.y + dx * sin + dy * cos
705
+ };
706
+ }
707
+ handleResize(point, handle, context) {
708
+ if (!this.dragElementId || !this.initialBounds || !this.dragStart) return;
709
+ const dx = point.x - this.dragStart.x;
710
+ const dy = point.y - this.dragStart.y;
711
+ let updates = {};
712
+ switch (handle) {
713
+ case "nw":
714
+ updates = {
715
+ x: this.initialBounds.x + dx,
716
+ y: this.initialBounds.y + dy,
717
+ w: Math.max(10, this.initialBounds.w - dx),
718
+ h: Math.max(10, this.initialBounds.h - dy)
719
+ };
720
+ break;
721
+ case "ne":
722
+ updates = {
723
+ y: this.initialBounds.y + dy,
724
+ w: Math.max(10, this.initialBounds.w + dx),
725
+ h: Math.max(10, this.initialBounds.h - dy)
726
+ };
727
+ break;
728
+ case "sw":
729
+ updates = {
730
+ x: this.initialBounds.x + dx,
731
+ w: Math.max(10, this.initialBounds.w - dx),
732
+ h: Math.max(10, this.initialBounds.h + dy)
733
+ };
734
+ break;
735
+ case "se":
736
+ updates = {
737
+ w: Math.max(10, this.initialBounds.w + dx),
738
+ h: Math.max(10, this.initialBounds.h + dy)
739
+ };
740
+ break;
741
+ case "n":
742
+ updates = {
743
+ y: this.initialBounds.y + dy,
744
+ h: Math.max(10, this.initialBounds.h - dy)
745
+ };
746
+ break;
747
+ case "s":
748
+ updates = {
749
+ h: Math.max(10, this.initialBounds.h + dy)
750
+ };
751
+ break;
752
+ case "e":
753
+ updates = {
754
+ w: Math.max(10, this.initialBounds.w + dx)
755
+ };
756
+ break;
757
+ case "w":
758
+ updates = {
759
+ x: this.initialBounds.x + dx,
760
+ w: Math.max(10, this.initialBounds.w - dx)
761
+ };
762
+ break;
763
+ case "rotate":
764
+ const cx = this.initialBounds.x + this.initialBounds.w / 2;
765
+ const cy = this.initialBounds.y + this.initialBounds.h / 2;
766
+ const angle = Math.atan2(point.y - cy, point.x - cx);
767
+ updates = { rotation: angle };
768
+ break;
769
+ }
770
+ if (Object.keys(updates).length > 0) {
771
+ context.updateElement(this.dragElementId, updates);
772
+ }
773
+ }
774
+ };
775
+
776
+ // src/tools/pan-tool.ts
777
+ var PanTool = class extends BaseTool {
778
+ constructor() {
779
+ super(...arguments);
780
+ this.name = "pan";
781
+ this.dragStart = null;
782
+ }
783
+ handlePointerDown(e, context) {
784
+ this.dragStart = { x: e.clientX, y: e.clientY };
785
+ }
786
+ handlePointerMove(e, context) {
787
+ if (!this.dragStart) return;
788
+ const dx = e.clientX - this.dragStart.x;
789
+ const dy = e.clientY - this.dragStart.y;
790
+ const camera = context.getCamera();
791
+ context.getDrawfn().setCamera({
792
+ x: camera.x + dx,
793
+ y: camera.y + dy
794
+ });
795
+ this.dragStart = { x: e.clientX, y: e.clientY };
796
+ }
797
+ handlePointerUp(e, context) {
798
+ this.dragStart = null;
799
+ }
800
+ };
801
+
802
+ // src/tools/pen-tool.ts
803
+ var PenTool = class extends BaseTool {
804
+ constructor() {
805
+ super(...arguments);
806
+ this.name = "pen";
807
+ this.currentElement = null;
808
+ this.currentElementId = null;
809
+ this.currentPoints = [];
810
+ }
811
+ handlePointerDown(e, context) {
812
+ const canvas = e.target;
813
+ const scene = context.getScene();
814
+ const point = screenToCanvas(
815
+ { x: e.clientX, y: e.clientY },
816
+ scene.camera,
817
+ canvas
818
+ );
819
+ context.recordHistory();
820
+ this.currentPoints = [[point.x, point.y, e.pressure || 0.5]];
821
+ this.currentElement = {
822
+ id: generateId(),
823
+ type: "freedraw",
824
+ x: point.x,
825
+ y: point.y,
826
+ w: 1,
827
+ h: 1,
828
+ rotation: 0,
829
+ stroke: "#000000",
830
+ fill: "transparent",
831
+ strokeWidth: 2,
832
+ opacity: 1,
833
+ points: [],
834
+ style: {
835
+ size: 4,
836
+ thinning: 0.5,
837
+ smoothing: 0.5,
838
+ streamline: 0.5
839
+ }
840
+ };
841
+ this.currentElementId = context.addElement(this.currentElement);
842
+ }
843
+ handlePointerMove(e, context) {
844
+ if (!this.currentElement || !this.currentElementId) return;
845
+ const canvas = e.target;
846
+ const scene = context.getScene();
847
+ const point = screenToCanvas(
848
+ { x: e.clientX, y: e.clientY },
849
+ scene.camera,
850
+ canvas
851
+ );
852
+ this.currentPoints.push([
853
+ point.x - this.currentElement.x,
854
+ point.y - this.currentElement.y,
855
+ e.pressure || 0.5
856
+ ]);
857
+ const xs = this.currentPoints.map((p) => p[0]);
858
+ const ys = this.currentPoints.map((p) => p[1]);
859
+ const w = Math.max(...xs) - Math.min(...xs);
860
+ const h = Math.max(...ys) - Math.min(...ys);
861
+ context.updateElement(this.currentElementId, {
862
+ points: this.currentPoints,
863
+ w,
864
+ h
865
+ });
866
+ }
867
+ handlePointerUp(e, context) {
868
+ this.currentElement = null;
869
+ this.currentElementId = null;
870
+ this.currentPoints = [];
871
+ }
872
+ };
873
+
874
+ // src/tools/rectangle-tool.ts
875
+ var RectangleTool = class extends BaseTool {
876
+ constructor() {
877
+ super(...arguments);
878
+ this.name = "rectangle";
879
+ this.startPoint = null;
880
+ this.currentElementId = null;
881
+ }
882
+ handlePointerDown(e, context) {
883
+ const canvas = e.target;
884
+ const scene = context.getScene();
885
+ this.startPoint = screenToCanvas(
886
+ { x: e.clientX, y: e.clientY },
887
+ scene.camera,
888
+ canvas
889
+ );
890
+ context.recordHistory();
891
+ const element = {
892
+ id: generateId(),
893
+ type: "rectangle",
894
+ x: this.startPoint.x,
895
+ y: this.startPoint.y,
896
+ w: 0,
897
+ h: 0,
898
+ rotation: 0,
899
+ stroke: "#000000",
900
+ fill: "transparent",
901
+ strokeWidth: 2,
902
+ opacity: 1
903
+ };
904
+ this.currentElementId = context.addElement(element);
905
+ }
906
+ handlePointerMove(e, context) {
907
+ if (!this.startPoint || !this.currentElementId) return;
908
+ const canvas = e.target;
909
+ const scene = context.getScene();
910
+ const point = screenToCanvas(
911
+ { x: e.clientX, y: e.clientY },
912
+ scene.camera,
913
+ canvas
914
+ );
915
+ let w = point.x - this.startPoint.x;
916
+ let h = point.y - this.startPoint.y;
917
+ if (e.shiftKey) {
918
+ const size = Math.max(Math.abs(w), Math.abs(h));
919
+ w = w >= 0 ? size : -size;
920
+ h = h >= 0 ? size : -size;
921
+ }
922
+ const updates = {};
923
+ if (w < 0) {
924
+ updates.x = this.startPoint.x + w;
925
+ updates.w = -w;
926
+ } else {
927
+ updates.x = this.startPoint.x;
928
+ updates.w = w;
929
+ }
930
+ if (h < 0) {
931
+ updates.y = this.startPoint.y + h;
932
+ updates.h = -h;
933
+ } else {
934
+ updates.y = this.startPoint.y;
935
+ updates.h = h;
936
+ }
937
+ context.updateElement(this.currentElementId, updates);
938
+ }
939
+ handlePointerUp(e, context) {
940
+ if (this.currentElementId) {
941
+ const scene = context.getScene();
942
+ const element = scene.elements.find(
943
+ (el) => el.id === this.currentElementId
944
+ );
945
+ if (element && (element.w < 1 || element.h < 1)) {
946
+ context.removeElement(this.currentElementId);
947
+ }
948
+ }
949
+ this.startPoint = null;
950
+ this.currentElementId = null;
951
+ }
952
+ };
953
+
954
+ // src/tools/ellipse-tool.ts
955
+ var EllipseTool = class extends BaseTool {
956
+ constructor() {
957
+ super(...arguments);
958
+ this.name = "ellipse";
959
+ this.startPoint = null;
960
+ this.currentElementId = null;
961
+ }
962
+ handlePointerDown(e, context) {
963
+ const canvas = e.target;
964
+ const scene = context.getScene();
965
+ this.startPoint = screenToCanvas(
966
+ { x: e.clientX, y: e.clientY },
967
+ scene.camera,
968
+ canvas
969
+ );
970
+ context.recordHistory();
971
+ const element = {
972
+ id: generateId(),
973
+ type: "ellipse",
974
+ x: this.startPoint.x,
975
+ y: this.startPoint.y,
976
+ w: 0,
977
+ h: 0,
978
+ rotation: 0,
979
+ stroke: "#000000",
980
+ fill: "transparent",
981
+ strokeWidth: 2,
982
+ opacity: 1
983
+ };
984
+ this.currentElementId = context.addElement(element);
985
+ }
986
+ handlePointerMove(e, context) {
987
+ if (!this.startPoint || !this.currentElementId) return;
988
+ const canvas = e.target;
989
+ const scene = context.getScene();
990
+ const point = screenToCanvas(
991
+ { x: e.clientX, y: e.clientY },
992
+ scene.camera,
993
+ canvas
994
+ );
995
+ let w = point.x - this.startPoint.x;
996
+ let h = point.y - this.startPoint.y;
997
+ if (e.shiftKey) {
998
+ const size = Math.max(Math.abs(w), Math.abs(h));
999
+ w = w >= 0 ? size : -size;
1000
+ h = h >= 0 ? size : -size;
1001
+ }
1002
+ const updates = {};
1003
+ if (w < 0) {
1004
+ updates.x = this.startPoint.x + w;
1005
+ updates.w = -w;
1006
+ } else {
1007
+ updates.x = this.startPoint.x;
1008
+ updates.w = w;
1009
+ }
1010
+ if (h < 0) {
1011
+ updates.y = this.startPoint.y + h;
1012
+ updates.h = -h;
1013
+ } else {
1014
+ updates.y = this.startPoint.y;
1015
+ updates.h = h;
1016
+ }
1017
+ context.updateElement(this.currentElementId, updates);
1018
+ }
1019
+ handlePointerUp(e, context) {
1020
+ if (this.currentElementId) {
1021
+ const scene = context.getScene();
1022
+ const element = scene.elements.find(
1023
+ (el) => el.id === this.currentElementId
1024
+ );
1025
+ if (element && (element.w < 1 || element.h < 1)) {
1026
+ context.removeElement(this.currentElementId);
1027
+ }
1028
+ }
1029
+ this.startPoint = null;
1030
+ this.currentElementId = null;
1031
+ }
1032
+ };
1033
+
1034
+ // src/tools/arrow-tool.ts
1035
+ var ArrowTool = class extends BaseTool {
1036
+ constructor() {
1037
+ super(...arguments);
1038
+ this.name = "arrow";
1039
+ this.points = [];
1040
+ this.currentElementId = null;
1041
+ }
1042
+ handlePointerDown(e, context) {
1043
+ const canvas = e.target;
1044
+ const scene = context.getScene();
1045
+ const point = screenToCanvas(
1046
+ { x: e.clientX, y: e.clientY },
1047
+ scene.camera,
1048
+ canvas
1049
+ );
1050
+ context.recordHistory();
1051
+ this.points = [[point.x, point.y]];
1052
+ const element = {
1053
+ id: generateId(),
1054
+ type: "arrow",
1055
+ x: point.x,
1056
+ y: point.y,
1057
+ w: 0,
1058
+ h: 0,
1059
+ rotation: 0,
1060
+ stroke: "#000000",
1061
+ fill: "transparent",
1062
+ strokeWidth: 2,
1063
+ opacity: 1,
1064
+ points: [[0, 0]],
1065
+ head: "triangle"
1066
+ };
1067
+ this.currentElementId = context.addElement(element);
1068
+ }
1069
+ handlePointerMove(e, context) {
1070
+ if (!this.currentElementId || this.points.length === 0) return;
1071
+ const canvas = e.target;
1072
+ const scene = context.getScene();
1073
+ const point = screenToCanvas(
1074
+ { x: e.clientX, y: e.clientY },
1075
+ scene.camera,
1076
+ canvas
1077
+ );
1078
+ const startX = this.points[0][0];
1079
+ const startY = this.points[0][1];
1080
+ let arrowPoints = [
1081
+ [0, 0],
1082
+ [point.x - startX, point.y - startY]
1083
+ ];
1084
+ const minX = Math.min(0, point.x - startX);
1085
+ const minY = Math.min(0, point.y - startY);
1086
+ const maxX = Math.max(0, point.x - startX);
1087
+ const maxY = Math.max(0, point.y - startY);
1088
+ arrowPoints = arrowPoints.map(([x, y]) => [x - minX, y - minY]);
1089
+ context.updateElement(this.currentElementId, {
1090
+ x: startX + minX,
1091
+ y: startY + minY,
1092
+ w: maxX - minX,
1093
+ h: maxY - minY,
1094
+ points: arrowPoints
1095
+ });
1096
+ }
1097
+ handlePointerUp(e, context) {
1098
+ if (this.currentElementId) {
1099
+ const scene = context.getScene();
1100
+ const element = scene.elements.find(
1101
+ (el) => el.id === this.currentElementId
1102
+ );
1103
+ if (element && (element.w < 1 || element.h < 1)) {
1104
+ context.removeElement(this.currentElementId);
1105
+ }
1106
+ }
1107
+ this.points = [];
1108
+ this.currentElementId = null;
1109
+ }
1110
+ };
1111
+
1112
+ // src/tools/text-tool.ts
1113
+ var TextTool = class extends BaseTool {
1114
+ constructor(overlayRoot) {
1115
+ super();
1116
+ this.name = "text";
1117
+ this.overlayRoot = null;
1118
+ this.activeTextarea = null;
1119
+ this.activeElementId = null;
1120
+ this.activeContext = null;
1121
+ this.overlayRoot = overlayRoot || document.body;
1122
+ }
1123
+ handlePointerDown(e, context) {
1124
+ if (this.activeTextarea && this.activeContext) {
1125
+ this.commitText(this.activeContext);
1126
+ }
1127
+ const canvas = e.target;
1128
+ const scene = context.getScene();
1129
+ const point = screenToCanvas(
1130
+ { x: e.clientX, y: e.clientY },
1131
+ scene.camera,
1132
+ canvas
1133
+ );
1134
+ context.recordHistory();
1135
+ const element = {
1136
+ id: generateId(),
1137
+ type: "text",
1138
+ x: point.x,
1139
+ y: point.y,
1140
+ w: 200,
1141
+ h: 40,
1142
+ rotation: 0,
1143
+ stroke: "#000000",
1144
+ fill: "#000000",
1145
+ strokeWidth: 0,
1146
+ opacity: 1,
1147
+ text: "",
1148
+ fontFamily: "sans-serif",
1149
+ fontSize: 16,
1150
+ align: "left"
1151
+ };
1152
+ this.activeElementId = context.addElement(element);
1153
+ this.activeContext = context;
1154
+ this.showTextInput(canvas, context, element);
1155
+ }
1156
+ editElement(id, canvas, scene) {
1157
+ const element = scene.elements.find((el) => el.id === id);
1158
+ if (!element || element.type !== "text") return;
1159
+ this.activeElementId = id;
1160
+ const textarea = document.createElement("textarea");
1161
+ const rect = canvas.getBoundingClientRect();
1162
+ const screenX = element.x * scene.camera.zoom + scene.camera.x + rect.left;
1163
+ const screenY = element.y * scene.camera.zoom + scene.camera.y + rect.top;
1164
+ Object.assign(textarea.style, {
1165
+ position: "fixed",
1166
+ left: `${screenX}px`,
1167
+ top: `${screenY}px`,
1168
+ width: `${element.w * scene.camera.zoom}px`,
1169
+ minHeight: `${element.h * scene.camera.zoom}px`,
1170
+ font: `${element.fontSize * scene.camera.zoom}px ${element.fontFamily}`,
1171
+ color: element.fill,
1172
+ background: "transparent",
1173
+ border: "2px solid #3b82f6",
1174
+ outline: "none",
1175
+ resize: "none",
1176
+ padding: "4px",
1177
+ zIndex: "10000"
1178
+ });
1179
+ textarea.value = element.text;
1180
+ textarea.focus();
1181
+ textarea.select();
1182
+ this.activeTextarea = textarea;
1183
+ if (this.overlayRoot) {
1184
+ this.overlayRoot.appendChild(textarea);
1185
+ }
1186
+ const handleBlur = () => {
1187
+ if (this.activeTextarea && this.activeElementId) {
1188
+ element.text = this.activeTextarea.value;
1189
+ this.activeTextarea.remove();
1190
+ this.activeTextarea = null;
1191
+ this.activeElementId = null;
1192
+ }
1193
+ };
1194
+ textarea.addEventListener("blur", handleBlur);
1195
+ }
1196
+ showTextInput(canvas, context, element) {
1197
+ if (!this.overlayRoot) return;
1198
+ const scene = context.getScene();
1199
+ const textarea = document.createElement("textarea");
1200
+ const rect = canvas.getBoundingClientRect();
1201
+ const screenX = element.x * scene.camera.zoom + scene.camera.x + rect.left;
1202
+ const screenY = element.y * scene.camera.zoom + scene.camera.y + rect.top;
1203
+ Object.assign(textarea.style, {
1204
+ position: "fixed",
1205
+ left: `${screenX}px`,
1206
+ top: `${screenY}px`,
1207
+ width: `${element.w * scene.camera.zoom}px`,
1208
+ minHeight: `${element.h * scene.camera.zoom}px`,
1209
+ font: `${element.fontSize * scene.camera.zoom}px ${element.fontFamily}`,
1210
+ color: element.fill,
1211
+ background: "transparent",
1212
+ border: "2px solid #3b82f6",
1213
+ outline: "none",
1214
+ resize: "none",
1215
+ padding: "4px",
1216
+ zIndex: "10000"
1217
+ });
1218
+ textarea.value = element.text;
1219
+ textarea.focus();
1220
+ this.activeTextarea = textarea;
1221
+ this.overlayRoot.appendChild(textarea);
1222
+ const handleClickOutside = (e) => {
1223
+ if (e.target !== textarea) {
1224
+ this.commitText(context);
1225
+ document.removeEventListener("click", handleClickOutside);
1226
+ }
1227
+ };
1228
+ textarea.addEventListener("blur", () => {
1229
+ this.commitText(context);
1230
+ document.removeEventListener("click", handleClickOutside);
1231
+ });
1232
+ setTimeout(() => {
1233
+ document.addEventListener("click", handleClickOutside);
1234
+ }, 0);
1235
+ }
1236
+ commitText(context) {
1237
+ if (!this.activeTextarea || !this.activeElementId) return;
1238
+ const text = this.activeTextarea.value;
1239
+ if (text.trim() === "") {
1240
+ context.removeElement(this.activeElementId);
1241
+ } else {
1242
+ context.updateElement(this.activeElementId, { text });
1243
+ }
1244
+ this.activeTextarea.remove();
1245
+ this.activeTextarea = null;
1246
+ this.activeElementId = null;
1247
+ this.activeContext = null;
1248
+ }
1249
+ deactivate() {
1250
+ if (this.activeTextarea) {
1251
+ this.activeTextarea.remove();
1252
+ this.activeTextarea = null;
1253
+ }
1254
+ this.activeElementId = null;
1255
+ this.activeContext = null;
1256
+ super.deactivate();
1257
+ }
1258
+ };
1259
+
1260
+ // src/tools/image-tool.ts
1261
+ var ImageTool = class extends BaseTool {
1262
+ constructor() {
1263
+ super(...arguments);
1264
+ this.name = "image";
1265
+ this.pendingImage = null;
1266
+ }
1267
+ activate(opts) {
1268
+ super.activate(opts);
1269
+ if (opts?.src) {
1270
+ this.pendingImage = opts;
1271
+ }
1272
+ }
1273
+ handlePointerDown(e, context) {
1274
+ if (!this.pendingImage) return;
1275
+ const canvas = e.target;
1276
+ const scene = context.getScene();
1277
+ const point = screenToCanvas(
1278
+ { x: e.clientX, y: e.clientY },
1279
+ scene.camera,
1280
+ canvas
1281
+ );
1282
+ context.recordHistory();
1283
+ const w = this.pendingImage.naturalW || 200;
1284
+ const h = this.pendingImage.naturalH || 150;
1285
+ const element = {
1286
+ id: generateId(),
1287
+ type: "image",
1288
+ x: point.x - w / 2,
1289
+ y: point.y - h / 2,
1290
+ w,
1291
+ h,
1292
+ rotation: 0,
1293
+ stroke: "#000000",
1294
+ fill: "transparent",
1295
+ strokeWidth: 0,
1296
+ opacity: 1,
1297
+ src: this.pendingImage.src,
1298
+ naturalW: this.pendingImage.naturalW,
1299
+ naturalH: this.pendingImage.naturalH
1300
+ };
1301
+ context.addElement(element);
1302
+ this.pendingImage = null;
1303
+ }
1304
+ };
1305
+
1306
+ // src/tools/node-tool.ts
1307
+ var NodeTool = class extends BaseTool {
1308
+ constructor() {
1309
+ super(...arguments);
1310
+ this.name = "node";
1311
+ this.pendingNode = null;
1312
+ }
1313
+ activate(opts) {
1314
+ super.activate(opts);
1315
+ if (opts?.nodeId) {
1316
+ this.pendingNode = opts;
1317
+ }
1318
+ }
1319
+ handlePointerDown(e, context) {
1320
+ if (!this.pendingNode) return;
1321
+ const canvas = e.target;
1322
+ const scene = context.getScene();
1323
+ const point = screenToCanvas(
1324
+ { x: e.clientX, y: e.clientY },
1325
+ scene.camera,
1326
+ canvas
1327
+ );
1328
+ context.recordHistory();
1329
+ const element = {
1330
+ id: generateId(),
1331
+ type: "node",
1332
+ x: point.x - 120,
1333
+ y: point.y - 60,
1334
+ w: 240,
1335
+ h: 120,
1336
+ rotation: 0,
1337
+ stroke: "#2563eb",
1338
+ fill: "#eff6ff",
1339
+ strokeWidth: 1,
1340
+ opacity: 1,
1341
+ nodeId: this.pendingNode.nodeId,
1342
+ nodeType: this.pendingNode.nodeType,
1343
+ variant: this.pendingNode.variant || "bookmark",
1344
+ preview: this.pendingNode.preview
1345
+ };
1346
+ context.addElement(element);
1347
+ this.pendingNode = null;
1348
+ }
1349
+ };
1350
+
1351
+ // src/tools/tool-manager.ts
1352
+ var ToolManager = class {
1353
+ constructor(drawfn, overlayRoot) {
1354
+ this.tools = /* @__PURE__ */ new Map();
1355
+ this.currentTool = "select";
1356
+ this.drawfn = drawfn;
1357
+ this.tools.set("select", new SelectTool());
1358
+ this.tools.set("pan", new PanTool());
1359
+ this.tools.set("pen", new PenTool());
1360
+ this.tools.set("rectangle", new RectangleTool());
1361
+ this.tools.set("ellipse", new EllipseTool());
1362
+ this.tools.set("arrow", new ArrowTool());
1363
+ this.tools.set("text", new TextTool(overlayRoot));
1364
+ this.tools.set("image", new ImageTool());
1365
+ this.tools.set("node", new NodeTool());
1366
+ }
1367
+ setTool(tool, opts) {
1368
+ const currentTool = this.tools.get(this.currentTool);
1369
+ currentTool?.deactivate();
1370
+ this.currentTool = tool;
1371
+ const newTool = this.tools.get(tool);
1372
+ newTool?.activate(opts);
1373
+ }
1374
+ getCurrentTool() {
1375
+ return this.currentTool;
1376
+ }
1377
+ handlePointerDown(e, context, selectedIds) {
1378
+ const tool = this.tools.get(this.currentTool);
1379
+ tool?.handlePointerDown(e, context, selectedIds);
1380
+ }
1381
+ handlePointerMove(e, context) {
1382
+ const tool = this.tools.get(this.currentTool);
1383
+ tool?.handlePointerMove(e, context);
1384
+ }
1385
+ handlePointerUp(e, context) {
1386
+ const tool = this.tools.get(this.currentTool);
1387
+ tool?.handlePointerUp(e, context);
1388
+ }
1389
+ handleKeyDown(e, context) {
1390
+ const tool = this.tools.get(this.currentTool);
1391
+ tool?.handleKeyDown(e, context);
1392
+ }
1393
+ getTool(name) {
1394
+ return this.tools.get(name);
1395
+ }
1396
+ getMarquee() {
1397
+ if (this.currentTool === "select") {
1398
+ const selectTool = this.tools.get("select");
1399
+ return selectTool?.getMarquee?.() || null;
1400
+ }
1401
+ return null;
1402
+ }
1403
+ };
1404
+ var Drawfn = class {
1405
+ constructor(options) {
1406
+ this.listeners = /* @__PURE__ */ new Map();
1407
+ this.rafId = null;
1408
+ this.needsRender = false;
1409
+ this.selectedIds = /* @__PURE__ */ new Set();
1410
+ this.hoveredId = null;
1411
+ // Space-to-pan state
1412
+ this.isSpacePressed = false;
1413
+ this.toolBeforeSpace = null;
1414
+ this.handlePointerDown = (e) => {
1415
+ this.emit("pointerDown", e);
1416
+ const context = this.createToolContext();
1417
+ this.toolManager.handlePointerDown(e, context, this.selectedIds);
1418
+ this.scheduleRender();
1419
+ };
1420
+ this.handlePointerMove = (e) => {
1421
+ this.emit("pointerMove", e);
1422
+ const context = this.createToolContext();
1423
+ this.toolManager.handlePointerMove(e, context);
1424
+ this.scheduleRender();
1425
+ };
1426
+ this.handlePointerUp = (e) => {
1427
+ this.emit("pointerUp", e);
1428
+ const context = this.createToolContext();
1429
+ this.toolManager.handlePointerUp(e, context);
1430
+ this.scheduleRender();
1431
+ };
1432
+ this.handleWheel = (e) => {
1433
+ e.preventDefault();
1434
+ const delta = e.deltaY * -1e-3;
1435
+ const oldZoom = this.scene.camera.zoom;
1436
+ const newZoom = Math.max(0.1, Math.min(5, oldZoom + delta));
1437
+ const point = screenToCanvas(
1438
+ { x: e.clientX, y: e.clientY },
1439
+ this.scene.camera,
1440
+ this.canvas
1441
+ );
1442
+ this.scene.camera.zoom = newZoom;
1443
+ const zoomRatio = newZoom / oldZoom;
1444
+ this.scene.camera.x = point.x - (point.x - this.scene.camera.x) * zoomRatio;
1445
+ this.scene.camera.y = point.y - (point.y - this.scene.camera.y) * zoomRatio;
1446
+ this.emit("cameraChanged", this.scene.camera);
1447
+ this.scheduleRender();
1448
+ };
1449
+ this.handleKeyDown = (e) => {
1450
+ if (e.key === " " && !this.isSpacePressed && e.target === this.canvas) {
1451
+ e.preventDefault();
1452
+ this.isSpacePressed = true;
1453
+ this.toolBeforeSpace = this.getTool();
1454
+ this.setTool("pan");
1455
+ this.canvas.style.cursor = "grab";
1456
+ return;
1457
+ }
1458
+ if (e.key === "Delete" || e.key === "Backspace") {
1459
+ this.selectedIds.forEach((id) => this.remove(id));
1460
+ } else if ((e.metaKey || e.ctrlKey) && e.key === "z") {
1461
+ e.preventDefault();
1462
+ if (e.shiftKey) {
1463
+ this.redo();
1464
+ } else {
1465
+ this.undo();
1466
+ }
1467
+ } else if ((e.metaKey || e.ctrlKey) && e.key === "y") {
1468
+ e.preventDefault();
1469
+ this.redo();
1470
+ } else {
1471
+ const context = this.createToolContext();
1472
+ this.toolManager.handleKeyDown(e, context);
1473
+ }
1474
+ };
1475
+ this.handleKeyUp = (e) => {
1476
+ if (e.key === " " && this.isSpacePressed) {
1477
+ this.isSpacePressed = false;
1478
+ if (this.toolBeforeSpace) {
1479
+ this.setTool(this.toolBeforeSpace);
1480
+ this.toolBeforeSpace = null;
1481
+ }
1482
+ this.canvas.style.cursor = "default";
1483
+ }
1484
+ };
1485
+ this.canvas = options.canvas;
1486
+ this.renderer = new Renderer(this.canvas, options.getImageBitmap);
1487
+ this.history = new History();
1488
+ this.toolManager = new ToolManager(this, options.overlayRoot);
1489
+ this.scene = this.createEmptyScene();
1490
+ this.onNodeClick = options.onNodeClick;
1491
+ this.getNodePreview = options.getNodePreview;
1492
+ this.setupEventListeners();
1493
+ this.scheduleRender();
1494
+ }
1495
+ createEmptyScene() {
1496
+ return {
1497
+ version: 1,
1498
+ id: generateId(),
1499
+ elements: [],
1500
+ camera: { x: 0, y: 0, zoom: 1 },
1501
+ background: {
1502
+ color: "#ffffff",
1503
+ grid: { size: 16, color: "#e5e7eb", alpha: 0.6 }
1504
+ }
1505
+ };
1506
+ }
1507
+ load(scene) {
1508
+ this.scene = JSON.parse(JSON.stringify(scene));
1509
+ this.selectedIds.clear();
1510
+ this.history.clear();
1511
+ this.scheduleRender();
1512
+ }
1513
+ loadScene(sceneData) {
1514
+ try {
1515
+ if (!sceneData || typeof sceneData !== "object") {
1516
+ console.error("Invalid scene data: not an object");
1517
+ return false;
1518
+ }
1519
+ const version = sceneData.version || "1.0.0";
1520
+ if (!this.isVersionCompatible(version)) {
1521
+ console.warn(
1522
+ `Scene version ${version} may not be fully compatible with current version`
1523
+ );
1524
+ }
1525
+ const elements = Array.isArray(sceneData.elements) ? sceneData.elements.map((el) => this.sanitizeElement(el)).filter((el) => el !== null) : [];
1526
+ const loadedScene = {
1527
+ version: this.scene.version,
1528
+ id: sceneData.id || generateId(),
1529
+ elements,
1530
+ camera: this.sanitizeCamera(sceneData.camera),
1531
+ background: this.sanitizeBackground(sceneData.background)
1532
+ };
1533
+ this.scene = loadedScene;
1534
+ this.selectedIds.clear();
1535
+ this.history.clear();
1536
+ this.scheduleRender();
1537
+ return true;
1538
+ } catch (error) {
1539
+ console.error("Failed to load scene:", error);
1540
+ return false;
1541
+ }
1542
+ }
1543
+ isVersionCompatible(version) {
1544
+ const [major] = version.split(".").map(Number);
1545
+ const currentVersion = String(this.scene.version);
1546
+ const [currentMajor] = currentVersion.split(".").map(Number);
1547
+ return major === currentMajor;
1548
+ }
1549
+ sanitizeElement(element) {
1550
+ if (!element || typeof element !== "object") return null;
1551
+ if (!element.type || !element.id) return null;
1552
+ const validTypes = [
1553
+ "freedraw",
1554
+ "rectangle",
1555
+ "ellipse",
1556
+ "arrow",
1557
+ "text",
1558
+ "image",
1559
+ "node"
1560
+ ];
1561
+ if (!validTypes.includes(element.type)) {
1562
+ console.warn(`Unknown element type: ${element.type}`);
1563
+ return null;
1564
+ }
1565
+ return {
1566
+ ...element,
1567
+ id: element.id || generateId(),
1568
+ x: typeof element.x === "number" ? element.x : 0,
1569
+ y: typeof element.y === "number" ? element.y : 0,
1570
+ w: typeof element.w === "number" ? element.w : 100,
1571
+ h: typeof element.h === "number" ? element.h : 100,
1572
+ rotation: typeof element.rotation === "number" ? element.rotation : 0,
1573
+ stroke: typeof element.stroke === "string" ? element.stroke : "#000000",
1574
+ fill: typeof element.fill === "string" ? element.fill : "transparent",
1575
+ strokeWidth: typeof element.strokeWidth === "number" ? element.strokeWidth : 2,
1576
+ opacity: typeof element.opacity === "number" ? element.opacity : 1
1577
+ };
1578
+ }
1579
+ sanitizeCamera(camera) {
1580
+ if (!camera || typeof camera !== "object") {
1581
+ return { x: 0, y: 0, zoom: 1 };
1582
+ }
1583
+ return {
1584
+ x: typeof camera.x === "number" ? camera.x : 0,
1585
+ y: typeof camera.y === "number" ? camera.y : 0,
1586
+ zoom: typeof camera.zoom === "number" ? Math.max(0.1, Math.min(5, camera.zoom)) : 1
1587
+ };
1588
+ }
1589
+ sanitizeBackground(background) {
1590
+ const defaultBg = {
1591
+ color: "#ffffff",
1592
+ grid: { size: 16, color: "#e5e7eb", alpha: 0.6 }
1593
+ };
1594
+ if (!background || typeof background !== "object") {
1595
+ return defaultBg;
1596
+ }
1597
+ return {
1598
+ color: typeof background.color === "string" ? background.color : defaultBg.color,
1599
+ grid: background.grid && typeof background.grid === "object" ? {
1600
+ size: typeof background.grid.size === "number" ? background.grid.size : 16,
1601
+ color: typeof background.grid.color === "string" ? background.grid.color : "#e5e7eb",
1602
+ alpha: typeof background.grid.alpha === "number" ? background.grid.alpha : 0.6
1603
+ } : defaultBg.grid
1604
+ };
1605
+ }
1606
+ getScene() {
1607
+ return JSON.parse(JSON.stringify(this.scene));
1608
+ }
1609
+ clear() {
1610
+ this.scene = this.createEmptyScene();
1611
+ this.selectedIds.clear();
1612
+ this.history.clear();
1613
+ this.scheduleRender();
1614
+ }
1615
+ add(element) {
1616
+ const id = element.id || generateId();
1617
+ const el = { ...element, id };
1618
+ this.recordHistory();
1619
+ this.scene.elements.push(el);
1620
+ this.emit("elementAdded", el);
1621
+ this.scheduleRender();
1622
+ return id;
1623
+ }
1624
+ update(id, patch) {
1625
+ const index = this.scene.elements.findIndex((el) => el.id === id);
1626
+ if (index === -1) return;
1627
+ this.recordHistory();
1628
+ this.scene.elements[index] = {
1629
+ ...this.scene.elements[index],
1630
+ ...patch
1631
+ };
1632
+ this.emit("elementUpdated", this.scene.elements[index]);
1633
+ this.scheduleRender();
1634
+ }
1635
+ remove(id) {
1636
+ const index = this.scene.elements.findIndex((el) => el.id === id);
1637
+ if (index === -1) return;
1638
+ this.recordHistory();
1639
+ this.scene.elements.splice(index, 1);
1640
+ this.selectedIds.delete(id);
1641
+ this.emit("elementRemoved", id);
1642
+ this.scheduleRender();
1643
+ }
1644
+ bringToFront(id) {
1645
+ const index = this.scene.elements.findIndex((el) => el.id === id);
1646
+ if (index === -1) return;
1647
+ this.recordHistory();
1648
+ const [element] = this.scene.elements.splice(index, 1);
1649
+ this.scene.elements.push(element);
1650
+ this.scheduleRender();
1651
+ }
1652
+ sendToBack(id) {
1653
+ const index = this.scene.elements.findIndex((el) => el.id === id);
1654
+ if (index === -1) return;
1655
+ this.recordHistory();
1656
+ const [element] = this.scene.elements.splice(index, 1);
1657
+ this.scene.elements.unshift(element);
1658
+ this.scheduleRender();
1659
+ }
1660
+ setTool(tool, opts) {
1661
+ this.toolManager.setTool(tool, opts);
1662
+ }
1663
+ getTool() {
1664
+ return this.toolManager.getCurrentTool();
1665
+ }
1666
+ setCamera(camera) {
1667
+ this.scene.camera = { ...this.scene.camera, ...camera };
1668
+ this.emit("cameraChanged", this.scene.camera);
1669
+ this.scheduleRender();
1670
+ }
1671
+ getCamera() {
1672
+ return { ...this.scene.camera };
1673
+ }
1674
+ screenToCanvas(point) {
1675
+ return screenToCanvas(point, this.scene.camera, this.canvas);
1676
+ }
1677
+ undo() {
1678
+ const prevScene = this.history.undo(this.scene);
1679
+ if (prevScene) {
1680
+ this.scene = prevScene;
1681
+ this.selectedIds.clear();
1682
+ this.emitHistoryChanged();
1683
+ this.scheduleRender();
1684
+ }
1685
+ }
1686
+ redo() {
1687
+ const nextScene = this.history.redo(this.scene);
1688
+ if (nextScene) {
1689
+ this.scene = nextScene;
1690
+ this.selectedIds.clear();
1691
+ this.emitHistoryChanged();
1692
+ this.scheduleRender();
1693
+ }
1694
+ }
1695
+ editTextElement(id) {
1696
+ const element = this.scene.elements.find((el) => el.id === id);
1697
+ if (!element || element.type !== "text") return;
1698
+ this.setTool("text");
1699
+ const textTool = this.toolManager.getTool("text");
1700
+ if (textTool && textTool.editElement) {
1701
+ textTool.editElement(id, this.canvas, this.scene);
1702
+ }
1703
+ }
1704
+ async exportPNG(opts) {
1705
+ const padding = opts?.padding ?? 20;
1706
+ const scale = opts?.scale ?? 2;
1707
+ const transparent = opts?.transparent ?? false;
1708
+ const selectedOnly = opts?.selectedOnly ?? false;
1709
+ const fitContent = opts?.fitContent ?? false;
1710
+ const elementsToExport = selectedOnly ? this.scene.elements.filter((el) => this.selectedIds.has(el.id)) : this.scene.elements;
1711
+ let canvasWidth = this.canvas.width;
1712
+ let canvasHeight = this.canvas.height;
1713
+ let offsetX = 0;
1714
+ let offsetY = 0;
1715
+ if (fitContent && elementsToExport.length > 0) {
1716
+ const bounds = this.getContentBounds(elementsToExport);
1717
+ canvasWidth = (bounds.w + padding * 2) * scale;
1718
+ canvasHeight = (bounds.h + padding * 2) * scale;
1719
+ offsetX = -(bounds.x - padding);
1720
+ offsetY = -(bounds.y - padding);
1721
+ }
1722
+ const tempCanvas = document.createElement("canvas");
1723
+ tempCanvas.width = canvasWidth * (fitContent ? 1 : scale);
1724
+ tempCanvas.height = canvasHeight * (fitContent ? 1 : scale);
1725
+ const ctx = tempCanvas.getContext("2d");
1726
+ ctx.scale(scale, scale);
1727
+ if (fitContent) {
1728
+ ctx.translate(offsetX, offsetY);
1729
+ }
1730
+ if (!transparent) {
1731
+ ctx.fillStyle = this.scene.background.color;
1732
+ ctx.fillRect(
1733
+ fitContent ? -offsetX : 0,
1734
+ fitContent ? -offsetY : 0,
1735
+ fitContent ? canvasWidth / scale : this.canvas.width,
1736
+ fitContent ? canvasHeight / scale : this.canvas.height
1737
+ );
1738
+ }
1739
+ const exportScene = {
1740
+ ...this.scene,
1741
+ elements: elementsToExport
1742
+ };
1743
+ const tempRenderer = new Renderer(tempCanvas);
1744
+ tempRenderer.render(exportScene);
1745
+ return new Promise((resolve, reject) => {
1746
+ tempCanvas.toBlob((blob) => {
1747
+ if (blob) resolve(blob);
1748
+ else reject(new Error("Failed to export PNG"));
1749
+ }, "image/png");
1750
+ });
1751
+ }
1752
+ getContentBounds(elements) {
1753
+ if (elements.length === 0) {
1754
+ return { x: 0, y: 0, w: 800, h: 600 };
1755
+ }
1756
+ let minX = Infinity, minY = Infinity;
1757
+ let maxX = -Infinity, maxY = -Infinity;
1758
+ for (const element of elements) {
1759
+ const bounds = element.rotation !== 0 ? getOBBBounds(element) : getElementBounds(element);
1760
+ minX = Math.min(minX, bounds.x);
1761
+ minY = Math.min(minY, bounds.y);
1762
+ maxX = Math.max(maxX, bounds.x + bounds.w);
1763
+ maxY = Math.max(maxY, bounds.y + bounds.h);
1764
+ }
1765
+ return {
1766
+ x: minX,
1767
+ y: minY,
1768
+ w: maxX - minX,
1769
+ h: maxY - minY
1770
+ };
1771
+ }
1772
+ async exportSVG(opts) {
1773
+ opts?.padding ?? 20;
1774
+ const scale = opts?.scale ?? 1;
1775
+ let svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${this.canvas.width * scale}" height="${this.canvas.height * scale}" viewBox="0 0 ${this.canvas.width} ${this.canvas.height}">`;
1776
+ svg += `<rect width="100%" height="100%" fill="${this.scene.background.color}"/>`;
1777
+ for (const element of this.scene.elements) {
1778
+ svg += this.elementToSVG(element);
1779
+ }
1780
+ svg += "</svg>";
1781
+ return svg;
1782
+ }
1783
+ elementToSVG(element) {
1784
+ let transform = "";
1785
+ if (element.rotation !== 0) {
1786
+ const cx = element.x + element.w / 2;
1787
+ const cy = element.y + element.h / 2;
1788
+ transform = ` transform="rotate(${element.rotation * 180 / Math.PI} ${cx} ${cy})"`;
1789
+ }
1790
+ const commonAttrs = `opacity="${element.opacity}" stroke="${element.stroke}" stroke-width="${element.strokeWidth}" fill="${element.fill}"${transform}`;
1791
+ switch (element.type) {
1792
+ case "rectangle":
1793
+ return `<rect x="${element.x}" y="${element.y}" width="${element.w}" height="${element.h}" ${commonAttrs}/>`;
1794
+ case "ellipse":
1795
+ const cx = element.x + element.w / 2;
1796
+ const cy = element.y + element.h / 2;
1797
+ return `<ellipse cx="${cx}" cy="${cy}" rx="${element.w / 2}" ry="${element.h / 2}" ${commonAttrs}/>`;
1798
+ case "text":
1799
+ const textEl = element;
1800
+ return `<text x="${element.x}" y="${element.y + textEl.fontSize}" font-family="${textEl.fontFamily}" font-size="${textEl.fontSize}" ${commonAttrs}>${this.escapeXml(textEl.text)}</text>`;
1801
+ case "freedraw": {
1802
+ const freedraw = element;
1803
+ if (freedraw.points.length === 0) return "";
1804
+ const points = freedraw.points.map((p) => [
1805
+ p[0],
1806
+ p[1],
1807
+ p[2] ?? 0.5
1808
+ ]);
1809
+ const stroke = getStroke(points, {
1810
+ size: freedraw.style?.size ?? element.strokeWidth * 2,
1811
+ thinning: freedraw.style?.thinning ?? 0.5,
1812
+ smoothing: freedraw.style?.smoothing ?? 0.5,
1813
+ streamline: freedraw.style?.streamline ?? 0.5,
1814
+ simulatePressure: true
1815
+ });
1816
+ let pathData = `M ${stroke[0][0] + element.x} ${stroke[0][1] + element.y}`;
1817
+ for (let i = 1; i < stroke.length; i++) {
1818
+ pathData += ` L ${stroke[i][0] + element.x} ${stroke[i][1] + element.y}`;
1819
+ }
1820
+ pathData += " Z";
1821
+ return `<path d="${pathData}" fill="${element.stroke}" opacity="${element.opacity}"${transform}/>`;
1822
+ }
1823
+ case "arrow": {
1824
+ const arrow = element;
1825
+ if (arrow.points.length < 2) return "";
1826
+ let pathData = `M ${arrow.points[0][0] + element.x} ${arrow.points[0][1] + element.y}`;
1827
+ for (let i = 1; i < arrow.points.length; i++) {
1828
+ pathData += ` L ${arrow.points[i][0] + element.x} ${arrow.points[i][1] + element.y}`;
1829
+ }
1830
+ let svg = `<path d="${pathData}" stroke="${element.stroke}" stroke-width="${element.strokeWidth}" fill="none" opacity="${element.opacity}"${transform}/>`;
1831
+ if (arrow.head === "triangle") {
1832
+ const last = arrow.points[arrow.points.length - 1];
1833
+ const prev = arrow.points[arrow.points.length - 2];
1834
+ const angle = Math.atan2(last[1] - prev[1], last[0] - prev[0]);
1835
+ const headLen = 10;
1836
+ const tip = [last[0] + element.x, last[1] + element.y];
1837
+ const left = [
1838
+ tip[0] - headLen * Math.cos(angle - Math.PI / 6),
1839
+ tip[1] - headLen * Math.sin(angle - Math.PI / 6)
1840
+ ];
1841
+ const right = [
1842
+ tip[0] - headLen * Math.cos(angle + Math.PI / 6),
1843
+ tip[1] - headLen * Math.sin(angle + Math.PI / 6)
1844
+ ];
1845
+ svg += `<polygon points="${tip[0]},${tip[1]} ${left[0]},${left[1]} ${right[0]},${right[1]}" fill="${element.stroke}" opacity="${element.opacity}"${transform}/>`;
1846
+ }
1847
+ return svg;
1848
+ }
1849
+ case "image": {
1850
+ const img = element;
1851
+ return `<image x="${element.x}" y="${element.y}" width="${element.w}" height="${element.h}" href="${this.escapeXml(img.src)}" opacity="${element.opacity}"${transform}/>`;
1852
+ }
1853
+ case "node": {
1854
+ const node = element;
1855
+ let svg = `<rect x="${element.x}" y="${element.y}" width="${element.w}" height="${element.h}" rx="8" ${commonAttrs}/>`;
1856
+ if (node.preview?.title) {
1857
+ svg += `<text x="${element.x + 12}" y="${element.y + 24}" font-family="sans-serif" font-size="14" fill="${element.stroke}">${this.escapeXml(node.preview.title)}</text>`;
1858
+ }
1859
+ return svg;
1860
+ }
1861
+ default:
1862
+ return "";
1863
+ }
1864
+ }
1865
+ escapeXml(text) {
1866
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1867
+ }
1868
+ on(event, handler) {
1869
+ if (!this.listeners.has(event)) {
1870
+ this.listeners.set(event, /* @__PURE__ */ new Set());
1871
+ }
1872
+ this.listeners.get(event).add(handler);
1873
+ return () => {
1874
+ this.listeners.get(event)?.delete(handler);
1875
+ };
1876
+ }
1877
+ emit(event, ...args) {
1878
+ this.listeners.get(event)?.forEach((handler) => handler(...args));
1879
+ }
1880
+ emitHistoryChanged() {
1881
+ this.emit("historyChanged", {
1882
+ canUndo: this.history.canUndo(),
1883
+ canRedo: this.history.canRedo()
1884
+ });
1885
+ }
1886
+ recordHistory() {
1887
+ this.history.push(this.scene);
1888
+ this.emitHistoryChanged();
1889
+ }
1890
+ createToolContext() {
1891
+ return {
1892
+ addElement: (element) => this.add(element),
1893
+ updateElement: (id, patch) => this.update(id, patch),
1894
+ removeElement: (id) => this.remove(id),
1895
+ recordHistory: () => this.recordHistory(),
1896
+ getScene: () => this.getScene(),
1897
+ getCamera: () => this.getCamera(),
1898
+ getDrawfn: () => this,
1899
+ getSelection: () => this.selectedIds
1900
+ };
1901
+ }
1902
+ setupEventListeners() {
1903
+ this.canvas.addEventListener("pointerdown", this.handlePointerDown);
1904
+ this.canvas.addEventListener("pointermove", this.handlePointerMove);
1905
+ this.canvas.addEventListener("pointerup", this.handlePointerUp);
1906
+ this.canvas.addEventListener("wheel", this.handleWheel);
1907
+ window.addEventListener("keydown", this.handleKeyDown);
1908
+ window.addEventListener("keyup", this.handleKeyUp);
1909
+ }
1910
+ scheduleRender() {
1911
+ this.needsRender = true;
1912
+ if (this.rafId !== null) return;
1913
+ this.rafId = requestAnimationFrame(() => {
1914
+ if (this.needsRender) {
1915
+ const marquee = this.toolManager.getMarquee();
1916
+ this.renderer.render(this.scene, {
1917
+ selectedIds: this.selectedIds,
1918
+ hoveredId: this.hoveredId,
1919
+ marquee
1920
+ });
1921
+ this.needsRender = false;
1922
+ }
1923
+ this.rafId = null;
1924
+ });
1925
+ }
1926
+ destroy() {
1927
+ if (this.rafId !== null) {
1928
+ cancelAnimationFrame(this.rafId);
1929
+ }
1930
+ this.canvas.removeEventListener("pointerdown", this.handlePointerDown);
1931
+ this.canvas.removeEventListener("pointermove", this.handlePointerMove);
1932
+ this.canvas.removeEventListener("pointerup", this.handlePointerUp);
1933
+ this.canvas.removeEventListener("wheel", this.handleWheel);
1934
+ window.removeEventListener("keydown", this.handleKeyDown);
1935
+ window.removeEventListener("keyup", this.handleKeyUp);
1936
+ this.listeners.clear();
1937
+ }
1938
+ getSelectedIds() {
1939
+ return Array.from(this.selectedIds);
1940
+ }
1941
+ setSelection(ids) {
1942
+ this.selectedIds = new Set(ids);
1943
+ this.emit("selectionChanged", ids);
1944
+ this.scheduleRender();
1945
+ }
1946
+ getElements() {
1947
+ return [...this.scene.elements];
1948
+ }
1949
+ getElementById(id) {
1950
+ return this.scene.elements.find((el) => el.id === id);
1951
+ }
1952
+ };
1953
+
1954
+ export { Drawfn, canvasToScreen, generateId, screenToCanvas };
1955
+ //# sourceMappingURL=index.js.map
1956
+ //# sourceMappingURL=index.js.map