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