@fieldnotes/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,2247 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AddElementCommand: () => AddElementCommand,
24
+ ArrowTool: () => ArrowTool,
25
+ Background: () => Background,
26
+ BatchCommand: () => BatchCommand,
27
+ Camera: () => Camera,
28
+ ElementRenderer: () => ElementRenderer,
29
+ ElementStore: () => ElementStore,
30
+ EraserTool: () => EraserTool,
31
+ EventBus: () => EventBus,
32
+ HandTool: () => HandTool,
33
+ HistoryRecorder: () => HistoryRecorder,
34
+ HistoryStack: () => HistoryStack,
35
+ ImageTool: () => ImageTool,
36
+ InputHandler: () => InputHandler,
37
+ NoteEditor: () => NoteEditor,
38
+ NoteTool: () => NoteTool,
39
+ PencilTool: () => PencilTool,
40
+ RemoveElementCommand: () => RemoveElementCommand,
41
+ SelectTool: () => SelectTool,
42
+ ToolManager: () => ToolManager,
43
+ UpdateElementCommand: () => UpdateElementCommand,
44
+ VERSION: () => VERSION,
45
+ Viewport: () => Viewport,
46
+ createArrow: () => createArrow,
47
+ createHtmlElement: () => createHtmlElement,
48
+ createId: () => createId,
49
+ createImage: () => createImage,
50
+ createNote: () => createNote,
51
+ createStroke: () => createStroke,
52
+ exportState: () => exportState,
53
+ getArrowBounds: () => getArrowBounds,
54
+ getArrowControlPoint: () => getArrowControlPoint,
55
+ getArrowMidpoint: () => getArrowMidpoint,
56
+ getArrowTangentAngle: () => getArrowTangentAngle,
57
+ getBendFromPoint: () => getBendFromPoint,
58
+ isNearBezier: () => isNearBezier,
59
+ parseState: () => parseState
60
+ });
61
+ module.exports = __toCommonJS(index_exports);
62
+
63
+ // src/core/event-bus.ts
64
+ var EventBus = class {
65
+ listeners = /* @__PURE__ */ new Map();
66
+ on(event, listener) {
67
+ const existing = this.listeners.get(event);
68
+ if (existing) {
69
+ existing.add(listener);
70
+ } else {
71
+ const set = /* @__PURE__ */ new Set([listener]);
72
+ this.listeners.set(event, set);
73
+ }
74
+ return () => this.off(event, listener);
75
+ }
76
+ off(event, listener) {
77
+ this.listeners.get(event)?.delete(listener);
78
+ }
79
+ emit(event, data) {
80
+ this.listeners.get(event)?.forEach((listener) => listener(data));
81
+ }
82
+ clear() {
83
+ this.listeners.clear();
84
+ }
85
+ };
86
+
87
+ // src/core/state-serializer.ts
88
+ var CURRENT_VERSION = 1;
89
+ function exportState(elements, camera) {
90
+ return {
91
+ version: CURRENT_VERSION,
92
+ camera: {
93
+ position: { ...camera.position },
94
+ zoom: camera.zoom
95
+ },
96
+ elements: elements.map((el) => structuredClone(el))
97
+ };
98
+ }
99
+ function parseState(json) {
100
+ const data = JSON.parse(json);
101
+ validateState(data);
102
+ return data;
103
+ }
104
+ function validateState(data) {
105
+ if (!data || typeof data !== "object") {
106
+ throw new Error("Invalid state: expected an object");
107
+ }
108
+ const obj = data;
109
+ if (typeof obj["version"] !== "number") {
110
+ throw new Error("Invalid state: missing or invalid version");
111
+ }
112
+ if (!obj["camera"] || typeof obj["camera"] !== "object") {
113
+ throw new Error("Invalid state: missing camera");
114
+ }
115
+ const cam = obj["camera"];
116
+ if (!cam["position"] || typeof cam["position"] !== "object") {
117
+ throw new Error("Invalid state: missing camera.position");
118
+ }
119
+ const pos = cam["position"];
120
+ if (typeof pos["x"] !== "number" || typeof pos["y"] !== "number") {
121
+ throw new Error("Invalid state: camera.position must have x and y numbers");
122
+ }
123
+ if (typeof cam["zoom"] !== "number") {
124
+ throw new Error("Invalid state: missing camera.zoom");
125
+ }
126
+ if (!Array.isArray(obj["elements"])) {
127
+ throw new Error("Invalid state: elements must be an array");
128
+ }
129
+ for (const el of obj["elements"]) {
130
+ validateElement(el);
131
+ migrateElement(el);
132
+ }
133
+ }
134
+ var VALID_TYPES = /* @__PURE__ */ new Set(["stroke", "note", "arrow", "image", "html"]);
135
+ function validateElement(el) {
136
+ if (!el || typeof el !== "object") {
137
+ throw new Error("Invalid element: expected an object");
138
+ }
139
+ const obj = el;
140
+ if (typeof obj["id"] !== "string") {
141
+ throw new Error("Invalid element: missing id");
142
+ }
143
+ if (typeof obj["type"] !== "string" || !VALID_TYPES.has(obj["type"])) {
144
+ throw new Error(`Invalid element: unknown type "${String(obj["type"])}"`);
145
+ }
146
+ if (typeof obj["zIndex"] !== "number") {
147
+ throw new Error("Invalid element: missing zIndex");
148
+ }
149
+ }
150
+ function migrateElement(obj) {
151
+ if (obj["type"] === "arrow" && typeof obj["bend"] !== "number") {
152
+ obj["bend"] = 0;
153
+ }
154
+ }
155
+
156
+ // src/canvas/camera.ts
157
+ var DEFAULT_MIN_ZOOM = 0.1;
158
+ var DEFAULT_MAX_ZOOM = 10;
159
+ var Camera = class {
160
+ x = 0;
161
+ y = 0;
162
+ z = 1;
163
+ minZoom;
164
+ maxZoom;
165
+ changeListeners = /* @__PURE__ */ new Set();
166
+ constructor(options = {}) {
167
+ this.minZoom = options.minZoom ?? DEFAULT_MIN_ZOOM;
168
+ this.maxZoom = options.maxZoom ?? DEFAULT_MAX_ZOOM;
169
+ }
170
+ get position() {
171
+ return { x: this.x, y: this.y };
172
+ }
173
+ get zoom() {
174
+ return this.z;
175
+ }
176
+ pan(dx, dy) {
177
+ this.x += dx;
178
+ this.y += dy;
179
+ this.notifyChange();
180
+ }
181
+ moveTo(x, y) {
182
+ this.x = x;
183
+ this.y = y;
184
+ this.notifyChange();
185
+ }
186
+ setZoom(level) {
187
+ this.z = Math.min(this.maxZoom, Math.max(this.minZoom, level));
188
+ this.notifyChange();
189
+ }
190
+ zoomAt(level, screenPoint) {
191
+ const before = this.screenToWorld(screenPoint);
192
+ this.z = Math.min(this.maxZoom, Math.max(this.minZoom, level));
193
+ const after = this.screenToWorld(screenPoint);
194
+ this.x += (after.x - before.x) * this.z;
195
+ this.y += (after.y - before.y) * this.z;
196
+ this.notifyChange();
197
+ }
198
+ screenToWorld(screen) {
199
+ return {
200
+ x: (screen.x - this.x) / this.z,
201
+ y: (screen.y - this.y) / this.z
202
+ };
203
+ }
204
+ worldToScreen(world) {
205
+ return {
206
+ x: world.x * this.z + this.x,
207
+ y: world.y * this.z + this.y
208
+ };
209
+ }
210
+ toCSSTransform() {
211
+ return `translate3d(${this.x}px, ${this.y}px, 0) scale(${this.z})`;
212
+ }
213
+ onChange(listener) {
214
+ this.changeListeners.add(listener);
215
+ return () => this.changeListeners.delete(listener);
216
+ }
217
+ notifyChange() {
218
+ this.changeListeners.forEach((fn) => fn());
219
+ }
220
+ };
221
+
222
+ // src/canvas/background.ts
223
+ var DEFAULTS = {
224
+ pattern: "dots",
225
+ spacing: 24,
226
+ color: "#d0d0d0",
227
+ dotRadius: 1,
228
+ lineWidth: 0.5
229
+ };
230
+ var Background = class {
231
+ pattern;
232
+ spacing;
233
+ color;
234
+ dotRadius;
235
+ lineWidth;
236
+ constructor(options = {}) {
237
+ this.pattern = options.pattern ?? DEFAULTS.pattern;
238
+ this.spacing = options.spacing ?? DEFAULTS.spacing;
239
+ this.color = options.color ?? DEFAULTS.color;
240
+ this.dotRadius = options.dotRadius ?? DEFAULTS.dotRadius;
241
+ this.lineWidth = options.lineWidth ?? DEFAULTS.lineWidth;
242
+ }
243
+ render(ctx, camera) {
244
+ const { width, height } = ctx.canvas;
245
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
246
+ const cssWidth = width / dpr;
247
+ const cssHeight = height / dpr;
248
+ ctx.save();
249
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
250
+ ctx.clearRect(0, 0, cssWidth, cssHeight);
251
+ if (this.pattern === "dots") {
252
+ this.renderDots(ctx, camera, cssWidth, cssHeight);
253
+ } else if (this.pattern === "grid") {
254
+ this.renderGrid(ctx, camera, cssWidth, cssHeight);
255
+ }
256
+ ctx.restore();
257
+ }
258
+ renderDots(ctx, camera, width, height) {
259
+ const spacing = this.spacing * camera.zoom;
260
+ const offsetX = camera.position.x % spacing;
261
+ const offsetY = camera.position.y % spacing;
262
+ const radius = this.dotRadius * Math.min(camera.zoom, 2);
263
+ ctx.fillStyle = this.color;
264
+ ctx.beginPath();
265
+ for (let x = offsetX; x < width; x += spacing) {
266
+ for (let y = offsetY; y < height; y += spacing) {
267
+ ctx.moveTo(x + radius, y);
268
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
269
+ }
270
+ }
271
+ ctx.fill();
272
+ }
273
+ renderGrid(ctx, camera, width, height) {
274
+ const spacing = this.spacing * camera.zoom;
275
+ const offsetX = camera.position.x % spacing;
276
+ const offsetY = camera.position.y % spacing;
277
+ const lineW = this.lineWidth * Math.min(camera.zoom, 2);
278
+ ctx.fillStyle = this.color;
279
+ for (let x = offsetX; x < width; x += spacing) {
280
+ ctx.fillRect(x, 0, lineW, height);
281
+ }
282
+ for (let y = offsetY; y < height; y += spacing) {
283
+ ctx.fillRect(0, y, width, lineW);
284
+ }
285
+ }
286
+ };
287
+
288
+ // src/canvas/input-handler.ts
289
+ var ZOOM_SENSITIVITY = 1e-3;
290
+ var MIDDLE_BUTTON = 1;
291
+ var InputHandler = class {
292
+ constructor(element, camera, options = {}) {
293
+ this.element = element;
294
+ this.camera = camera;
295
+ this.toolManager = options.toolManager ?? null;
296
+ this.toolContext = options.toolContext ?? null;
297
+ this.historyRecorder = options.historyRecorder ?? null;
298
+ this.historyStack = options.historyStack ?? null;
299
+ this.element.style.touchAction = "none";
300
+ this.bind();
301
+ }
302
+ isPanning = false;
303
+ lastPointer = { x: 0, y: 0 };
304
+ spaceHeld = false;
305
+ activePointers = /* @__PURE__ */ new Map();
306
+ lastPinchDistance = 0;
307
+ lastPinchCenter = { x: 0, y: 0 };
308
+ toolManager;
309
+ toolContext;
310
+ historyRecorder;
311
+ historyStack;
312
+ isToolActive = false;
313
+ abortController = new AbortController();
314
+ setToolManager(toolManager, toolContext) {
315
+ this.toolManager = toolManager;
316
+ this.toolContext = toolContext;
317
+ }
318
+ destroy() {
319
+ this.abortController.abort();
320
+ }
321
+ bind() {
322
+ const opts = { signal: this.abortController.signal };
323
+ this.element.addEventListener("wheel", this.onWheel, { ...opts, passive: false });
324
+ this.element.addEventListener("pointerdown", this.onPointerDown, opts);
325
+ this.element.addEventListener("pointermove", this.onPointerMove, opts);
326
+ this.element.addEventListener("pointerup", this.onPointerUp, opts);
327
+ this.element.addEventListener("pointerleave", this.onPointerUp, opts);
328
+ this.element.addEventListener("pointercancel", this.onPointerUp, opts);
329
+ window.addEventListener("keydown", this.onKeyDown, opts);
330
+ window.addEventListener("keyup", this.onKeyUp, opts);
331
+ }
332
+ onWheel = (e) => {
333
+ e.preventDefault();
334
+ const zoomFactor = 1 - e.deltaY * ZOOM_SENSITIVITY;
335
+ const newZoom = this.camera.zoom * zoomFactor;
336
+ this.camera.zoomAt(newZoom, { x: e.clientX, y: e.clientY });
337
+ };
338
+ isInteractiveHtmlContent(e) {
339
+ const target = e.target;
340
+ if (!target) return false;
341
+ const node = target.closest("[data-element-id]");
342
+ if (!node) return false;
343
+ const elementId = node.dataset["elementId"];
344
+ if (!elementId) return false;
345
+ const store = this.toolContext?.store;
346
+ if (!store) return false;
347
+ const element = store.getById(elementId);
348
+ if (!element || element.type !== "html") return false;
349
+ return true;
350
+ }
351
+ onPointerDown = (e) => {
352
+ this.activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
353
+ if (this.isInteractiveHtmlContent(e)) {
354
+ return;
355
+ }
356
+ this.element.setPointerCapture?.(e.pointerId);
357
+ if (this.activePointers.size === 2) {
358
+ this.startPinch();
359
+ this.cancelToolIfActive(e);
360
+ return;
361
+ }
362
+ if (e.button === MIDDLE_BUTTON || e.button === 0 && this.spaceHeld) {
363
+ this.isPanning = true;
364
+ this.lastPointer = { x: e.clientX, y: e.clientY };
365
+ return;
366
+ }
367
+ if (this.activePointers.size === 1 && e.button === 0) {
368
+ this.dispatchToolDown(e);
369
+ }
370
+ };
371
+ onPointerMove = (e) => {
372
+ if (this.activePointers.has(e.pointerId)) {
373
+ this.activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
374
+ }
375
+ if (this.activePointers.size === 2) {
376
+ this.handlePinchMove();
377
+ return;
378
+ }
379
+ if (this.isPanning) {
380
+ const dx = e.clientX - this.lastPointer.x;
381
+ const dy = e.clientY - this.lastPointer.y;
382
+ this.lastPointer = { x: e.clientX, y: e.clientY };
383
+ this.camera.pan(dx, dy);
384
+ return;
385
+ }
386
+ if (this.isToolActive) {
387
+ this.dispatchToolMove(e);
388
+ } else if (this.activePointers.size === 0) {
389
+ this.dispatchToolHover(e);
390
+ }
391
+ };
392
+ onPointerUp = (e) => {
393
+ this.activePointers.delete(e.pointerId);
394
+ if (this.activePointers.size < 2) {
395
+ this.lastPinchDistance = 0;
396
+ }
397
+ if (this.isPanning && this.activePointers.size === 0) {
398
+ this.isPanning = false;
399
+ }
400
+ if (this.isToolActive) {
401
+ this.dispatchToolUp(e);
402
+ this.isToolActive = false;
403
+ }
404
+ };
405
+ onKeyDown = (e) => {
406
+ if (e.target?.isContentEditable) return;
407
+ if (e.key === " ") {
408
+ this.spaceHeld = true;
409
+ }
410
+ if (e.key === "Delete" || e.key === "Backspace") {
411
+ this.deleteSelected();
412
+ }
413
+ if ((e.ctrlKey || e.metaKey) && e.key === "z" && !e.shiftKey) {
414
+ e.preventDefault();
415
+ this.handleUndo();
416
+ }
417
+ if ((e.ctrlKey || e.metaKey) && (e.key === "y" || e.key === "z" && e.shiftKey)) {
418
+ e.preventDefault();
419
+ this.handleRedo();
420
+ }
421
+ };
422
+ onKeyUp = (e) => {
423
+ if (e.key === " ") {
424
+ this.spaceHeld = false;
425
+ }
426
+ };
427
+ startPinch() {
428
+ this.isPanning = true;
429
+ const [a, b] = this.getPinchPoints();
430
+ this.lastPinchDistance = this.distance(a, b);
431
+ this.lastPinchCenter = this.midpoint(a, b);
432
+ this.lastPointer = { ...this.lastPinchCenter };
433
+ }
434
+ handlePinchMove() {
435
+ const [a, b] = this.getPinchPoints();
436
+ const dist = this.distance(a, b);
437
+ const center = this.midpoint(a, b);
438
+ if (this.lastPinchDistance > 0) {
439
+ const scale = dist / this.lastPinchDistance;
440
+ const newZoom = this.camera.zoom * scale;
441
+ this.camera.zoomAt(newZoom, center);
442
+ }
443
+ const dx = center.x - this.lastPointer.x;
444
+ const dy = center.y - this.lastPointer.y;
445
+ this.camera.pan(dx, dy);
446
+ this.lastPinchDistance = dist;
447
+ this.lastPinchCenter = center;
448
+ this.lastPointer = { ...center };
449
+ }
450
+ getPinchPoints() {
451
+ const pts = [...this.activePointers.values()];
452
+ return [pts[0] ?? { x: 0, y: 0 }, pts[1] ?? { x: 0, y: 0 }];
453
+ }
454
+ distance(a, b) {
455
+ const dx = a.x - b.x;
456
+ const dy = a.y - b.y;
457
+ return Math.sqrt(dx * dx + dy * dy);
458
+ }
459
+ midpoint(a, b) {
460
+ return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
461
+ }
462
+ toPointerState(e) {
463
+ return { x: e.clientX, y: e.clientY, pressure: e.pressure };
464
+ }
465
+ dispatchToolDown(e) {
466
+ if (!this.toolManager || !this.toolContext) return;
467
+ this.historyRecorder?.begin();
468
+ this.isToolActive = true;
469
+ this.toolManager.handlePointerDown(this.toPointerState(e), this.toolContext);
470
+ }
471
+ dispatchToolMove(e) {
472
+ if (!this.toolManager || !this.toolContext) return;
473
+ this.toolManager.handlePointerMove(this.toPointerState(e), this.toolContext);
474
+ }
475
+ dispatchToolHover(e) {
476
+ if (!this.toolManager?.activeTool || !this.toolContext) return;
477
+ const tool = this.toolManager.activeTool;
478
+ if (tool.onHover) {
479
+ tool.onHover(this.toPointerState(e), this.toolContext);
480
+ }
481
+ }
482
+ dispatchToolUp(e) {
483
+ if (!this.toolManager || !this.toolContext) return;
484
+ this.toolManager.handlePointerUp(this.toPointerState(e), this.toolContext);
485
+ this.historyRecorder?.commit();
486
+ }
487
+ deleteSelected() {
488
+ if (!this.toolManager || !this.toolContext) return;
489
+ const tool = this.toolManager.activeTool;
490
+ if (tool?.name !== "select") return;
491
+ const selectTool = tool;
492
+ const ids = selectTool.selectedIds;
493
+ if (ids.length === 0) return;
494
+ this.historyRecorder?.begin();
495
+ for (const id of ids) {
496
+ this.toolContext.store.remove(id);
497
+ }
498
+ this.historyRecorder?.commit();
499
+ this.toolContext.requestRender();
500
+ }
501
+ handleUndo() {
502
+ if (!this.historyStack || !this.toolContext) return;
503
+ this.historyRecorder?.pause();
504
+ this.historyStack.undo(this.toolContext.store);
505
+ this.historyRecorder?.resume();
506
+ this.toolContext.requestRender();
507
+ }
508
+ handleRedo() {
509
+ if (!this.historyStack || !this.toolContext) return;
510
+ this.historyRecorder?.pause();
511
+ this.historyStack.redo(this.toolContext.store);
512
+ this.historyRecorder?.resume();
513
+ this.toolContext.requestRender();
514
+ }
515
+ cancelToolIfActive(e) {
516
+ if (this.isToolActive) {
517
+ this.dispatchToolUp(e);
518
+ this.isToolActive = false;
519
+ }
520
+ }
521
+ };
522
+
523
+ // src/elements/element-store.ts
524
+ var ElementStore = class {
525
+ elements = /* @__PURE__ */ new Map();
526
+ bus = new EventBus();
527
+ get count() {
528
+ return this.elements.size;
529
+ }
530
+ getAll() {
531
+ return [...this.elements.values()].sort((a, b) => a.zIndex - b.zIndex);
532
+ }
533
+ getById(id) {
534
+ return this.elements.get(id);
535
+ }
536
+ getElementsByType(type) {
537
+ return this.getAll().filter(
538
+ (el) => el.type === type
539
+ );
540
+ }
541
+ add(element) {
542
+ this.elements.set(element.id, element);
543
+ this.bus.emit("add", element);
544
+ }
545
+ update(id, partial) {
546
+ const existing = this.elements.get(id);
547
+ if (!existing) return;
548
+ const updated = { ...existing, ...partial, id: existing.id, type: existing.type };
549
+ this.elements.set(id, updated);
550
+ this.bus.emit("update", { previous: existing, current: updated });
551
+ }
552
+ remove(id) {
553
+ const element = this.elements.get(id);
554
+ if (!element) return;
555
+ this.elements.delete(id);
556
+ this.bus.emit("remove", element);
557
+ }
558
+ clear() {
559
+ this.elements.clear();
560
+ this.bus.emit("clear", null);
561
+ }
562
+ snapshot() {
563
+ return this.getAll().map((el) => ({ ...el }));
564
+ }
565
+ loadSnapshot(elements) {
566
+ this.elements.clear();
567
+ for (const el of elements) {
568
+ this.elements.set(el.id, el);
569
+ }
570
+ }
571
+ on(event, listener) {
572
+ return this.bus.on(event, listener);
573
+ }
574
+ };
575
+
576
+ // src/elements/arrow-geometry.ts
577
+ function getArrowControlPoint(from, to, bend) {
578
+ const midX = (from.x + to.x) / 2;
579
+ const midY = (from.y + to.y) / 2;
580
+ if (bend === 0) return { x: midX, y: midY };
581
+ const dx = to.x - from.x;
582
+ const dy = to.y - from.y;
583
+ const len = Math.sqrt(dx * dx + dy * dy);
584
+ if (len === 0) return { x: midX, y: midY };
585
+ const perpX = -dy / len;
586
+ const perpY = dx / len;
587
+ return {
588
+ x: midX + perpX * bend,
589
+ y: midY + perpY * bend
590
+ };
591
+ }
592
+ function getArrowMidpoint(from, to, bend) {
593
+ const cp = getArrowControlPoint(from, to, bend);
594
+ return {
595
+ x: 0.25 * from.x + 0.5 * cp.x + 0.25 * to.x,
596
+ y: 0.25 * from.y + 0.5 * cp.y + 0.25 * to.y
597
+ };
598
+ }
599
+ function getBendFromPoint(from, to, dragPoint) {
600
+ const midX = (from.x + to.x) / 2;
601
+ const midY = (from.y + to.y) / 2;
602
+ const dx = to.x - from.x;
603
+ const dy = to.y - from.y;
604
+ const len = Math.sqrt(dx * dx + dy * dy);
605
+ if (len === 0) return 0;
606
+ const perpX = -dy / len;
607
+ const perpY = dx / len;
608
+ return (dragPoint.x - midX) * perpX + (dragPoint.y - midY) * perpY;
609
+ }
610
+ function getArrowTangentAngle(from, to, bend, t) {
611
+ const cp = getArrowControlPoint(from, to, bend);
612
+ const tangentX = 2 * (1 - t) * (cp.x - from.x) + 2 * t * (to.x - cp.x);
613
+ const tangentY = 2 * (1 - t) * (cp.y - from.y) + 2 * t * (to.y - cp.y);
614
+ return Math.atan2(tangentY, tangentX);
615
+ }
616
+ function isNearBezier(point, from, to, bend, threshold) {
617
+ if (bend === 0) return isNearLine(point, from, to, threshold);
618
+ const cp = getArrowControlPoint(from, to, bend);
619
+ const segments = 20;
620
+ for (let i = 0; i < segments; i++) {
621
+ const t0 = i / segments;
622
+ const t1 = (i + 1) / segments;
623
+ const a = bezierPoint(from, cp, to, t0);
624
+ const b = bezierPoint(from, cp, to, t1);
625
+ if (isNearLine(point, a, b, threshold)) return true;
626
+ }
627
+ return false;
628
+ }
629
+ function getArrowBounds(from, to, bend) {
630
+ if (bend === 0) {
631
+ const minX2 = Math.min(from.x, to.x);
632
+ const minY2 = Math.min(from.y, to.y);
633
+ return {
634
+ x: minX2,
635
+ y: minY2,
636
+ w: Math.abs(to.x - from.x),
637
+ h: Math.abs(to.y - from.y)
638
+ };
639
+ }
640
+ const cp = getArrowControlPoint(from, to, bend);
641
+ const steps = 20;
642
+ let minX = Math.min(from.x, to.x);
643
+ let minY = Math.min(from.y, to.y);
644
+ let maxX = Math.max(from.x, to.x);
645
+ let maxY = Math.max(from.y, to.y);
646
+ for (let i = 1; i < steps; i++) {
647
+ const t = i / steps;
648
+ const p = bezierPoint(from, cp, to, t);
649
+ if (p.x < minX) minX = p.x;
650
+ if (p.y < minY) minY = p.y;
651
+ if (p.x > maxX) maxX = p.x;
652
+ if (p.y > maxY) maxY = p.y;
653
+ }
654
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
655
+ }
656
+ function bezierPoint(from, cp, to, t) {
657
+ const mt = 1 - t;
658
+ return {
659
+ x: mt * mt * from.x + 2 * mt * t * cp.x + t * t * to.x,
660
+ y: mt * mt * from.y + 2 * mt * t * cp.y + t * t * to.y
661
+ };
662
+ }
663
+ function isNearLine(point, a, b, threshold) {
664
+ const dx = b.x - a.x;
665
+ const dy = b.y - a.y;
666
+ const lenSq = dx * dx + dy * dy;
667
+ if (lenSq === 0) {
668
+ return Math.hypot(point.x - a.x, point.y - a.y) <= threshold;
669
+ }
670
+ const t = Math.max(0, Math.min(1, ((point.x - a.x) * dx + (point.y - a.y) * dy) / lenSq));
671
+ const projX = a.x + t * dx;
672
+ const projY = a.y + t * dy;
673
+ return Math.hypot(point.x - projX, point.y - projY) <= threshold;
674
+ }
675
+
676
+ // src/elements/element-renderer.ts
677
+ var DOM_ELEMENT_TYPES = /* @__PURE__ */ new Set(["note", "image", "html"]);
678
+ var ARROWHEAD_LENGTH = 12;
679
+ var ARROWHEAD_ANGLE = Math.PI / 6;
680
+ var ElementRenderer = class {
681
+ isDomElement(element) {
682
+ return DOM_ELEMENT_TYPES.has(element.type);
683
+ }
684
+ renderCanvasElement(ctx, element) {
685
+ switch (element.type) {
686
+ case "stroke":
687
+ this.renderStroke(ctx, element);
688
+ break;
689
+ case "arrow":
690
+ this.renderArrow(ctx, element);
691
+ break;
692
+ }
693
+ }
694
+ renderStroke(ctx, stroke) {
695
+ if (stroke.points.length < 2) return;
696
+ ctx.save();
697
+ ctx.translate(stroke.position.x, stroke.position.y);
698
+ ctx.strokeStyle = stroke.color;
699
+ ctx.lineWidth = stroke.width;
700
+ ctx.lineCap = "round";
701
+ ctx.lineJoin = "round";
702
+ ctx.globalAlpha = stroke.opacity;
703
+ ctx.beginPath();
704
+ const first = stroke.points[0];
705
+ if (first) {
706
+ ctx.moveTo(first.x, first.y);
707
+ }
708
+ for (let i = 1; i < stroke.points.length; i++) {
709
+ const pt = stroke.points[i];
710
+ if (pt) {
711
+ ctx.lineTo(pt.x, pt.y);
712
+ }
713
+ }
714
+ ctx.stroke();
715
+ ctx.restore();
716
+ }
717
+ renderArrow(ctx, arrow) {
718
+ ctx.save();
719
+ ctx.strokeStyle = arrow.color;
720
+ ctx.lineWidth = arrow.width;
721
+ ctx.lineCap = "round";
722
+ ctx.beginPath();
723
+ ctx.moveTo(arrow.from.x, arrow.from.y);
724
+ if (arrow.bend !== 0) {
725
+ const cp = getArrowControlPoint(arrow.from, arrow.to, arrow.bend);
726
+ ctx.quadraticCurveTo(cp.x, cp.y, arrow.to.x, arrow.to.y);
727
+ } else {
728
+ ctx.lineTo(arrow.to.x, arrow.to.y);
729
+ }
730
+ ctx.stroke();
731
+ this.renderArrowhead(ctx, arrow);
732
+ ctx.restore();
733
+ }
734
+ renderArrowhead(ctx, arrow) {
735
+ const angle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 1);
736
+ ctx.beginPath();
737
+ ctx.moveTo(arrow.to.x, arrow.to.y);
738
+ ctx.lineTo(
739
+ arrow.to.x - ARROWHEAD_LENGTH * Math.cos(angle - ARROWHEAD_ANGLE),
740
+ arrow.to.y - ARROWHEAD_LENGTH * Math.sin(angle - ARROWHEAD_ANGLE)
741
+ );
742
+ ctx.lineTo(
743
+ arrow.to.x - ARROWHEAD_LENGTH * Math.cos(angle + ARROWHEAD_ANGLE),
744
+ arrow.to.y - ARROWHEAD_LENGTH * Math.sin(angle + ARROWHEAD_ANGLE)
745
+ );
746
+ ctx.closePath();
747
+ ctx.fillStyle = arrow.color;
748
+ ctx.fill();
749
+ }
750
+ };
751
+
752
+ // src/elements/note-editor.ts
753
+ var NoteEditor = class {
754
+ editingId = null;
755
+ editingNode = null;
756
+ blurHandler = null;
757
+ keyHandler = null;
758
+ pointerHandler = null;
759
+ pendingEditId = null;
760
+ get isEditing() {
761
+ return this.editingId !== null;
762
+ }
763
+ get editingElementId() {
764
+ return this.editingId;
765
+ }
766
+ startEditing(node, elementId, store) {
767
+ if (this.editingId === elementId) return;
768
+ if (this.editingId) {
769
+ this.stopEditing(store);
770
+ }
771
+ this.pendingEditId = elementId;
772
+ requestAnimationFrame(() => {
773
+ if (this.pendingEditId !== elementId) return;
774
+ this.pendingEditId = null;
775
+ this.activateEditing(node, elementId, store);
776
+ });
777
+ }
778
+ stopEditing(store) {
779
+ this.pendingEditId = null;
780
+ if (!this.editingId || !this.editingNode) return;
781
+ const text = this.editingNode.textContent ?? "";
782
+ store.update(this.editingId, { text });
783
+ this.editingNode.contentEditable = "false";
784
+ Object.assign(this.editingNode.style, {
785
+ userSelect: "none",
786
+ cursor: "default"
787
+ });
788
+ if (this.blurHandler) {
789
+ this.editingNode.removeEventListener("blur", this.blurHandler);
790
+ }
791
+ if (this.keyHandler) {
792
+ this.editingNode.removeEventListener("keydown", this.keyHandler);
793
+ }
794
+ if (this.pointerHandler) {
795
+ this.editingNode.removeEventListener("pointerdown", this.pointerHandler);
796
+ }
797
+ this.editingId = null;
798
+ this.editingNode = null;
799
+ this.blurHandler = null;
800
+ this.keyHandler = null;
801
+ this.pointerHandler = null;
802
+ }
803
+ destroy(store) {
804
+ this.pendingEditId = null;
805
+ if (this.isEditing) {
806
+ this.stopEditing(store);
807
+ }
808
+ }
809
+ activateEditing(node, elementId, store) {
810
+ this.editingId = elementId;
811
+ this.editingNode = node;
812
+ node.contentEditable = "true";
813
+ Object.assign(node.style, {
814
+ userSelect: "text",
815
+ cursor: "text",
816
+ outline: "none"
817
+ });
818
+ node.focus();
819
+ const selection = window.getSelection?.();
820
+ if (selection) {
821
+ const range = document.createRange();
822
+ range.selectNodeContents(node);
823
+ range.collapse(false);
824
+ selection.removeAllRanges();
825
+ selection.addRange(range);
826
+ }
827
+ this.blurHandler = () => this.stopEditing(store);
828
+ this.keyHandler = (e) => {
829
+ if (e.key === "Escape") {
830
+ node.blur();
831
+ }
832
+ e.stopPropagation();
833
+ };
834
+ this.pointerHandler = (e) => {
835
+ e.stopPropagation();
836
+ };
837
+ node.addEventListener("blur", this.blurHandler);
838
+ node.addEventListener("keydown", this.keyHandler);
839
+ node.addEventListener("pointerdown", this.pointerHandler);
840
+ }
841
+ };
842
+
843
+ // src/tools/tool-manager.ts
844
+ var ToolManager = class {
845
+ tools = /* @__PURE__ */ new Map();
846
+ current = null;
847
+ changeListeners = /* @__PURE__ */ new Set();
848
+ get activeTool() {
849
+ return this.current;
850
+ }
851
+ get toolNames() {
852
+ return [...this.tools.keys()];
853
+ }
854
+ register(tool) {
855
+ this.tools.set(tool.name, tool);
856
+ }
857
+ setTool(name, ctx) {
858
+ const tool = this.tools.get(name);
859
+ if (!tool) return;
860
+ this.current?.onDeactivate?.(ctx);
861
+ this.current = tool;
862
+ this.current.onActivate?.(ctx);
863
+ this.changeListeners.forEach((fn) => fn(name));
864
+ }
865
+ handlePointerDown(state, ctx) {
866
+ this.current?.onPointerDown(state, ctx);
867
+ }
868
+ handlePointerMove(state, ctx) {
869
+ this.current?.onPointerMove(state, ctx);
870
+ }
871
+ handlePointerUp(state, ctx) {
872
+ this.current?.onPointerUp(state, ctx);
873
+ }
874
+ onChange(listener) {
875
+ this.changeListeners.add(listener);
876
+ return () => this.changeListeners.delete(listener);
877
+ }
878
+ };
879
+
880
+ // src/history/history-stack.ts
881
+ var DEFAULT_MAX_SIZE = 100;
882
+ var HistoryStack = class {
883
+ undoStack = [];
884
+ redoStack = [];
885
+ maxSize;
886
+ changeListeners = /* @__PURE__ */ new Set();
887
+ constructor(options = {}) {
888
+ this.maxSize = options.maxSize ?? DEFAULT_MAX_SIZE;
889
+ }
890
+ get canUndo() {
891
+ return this.undoStack.length > 0;
892
+ }
893
+ get canRedo() {
894
+ return this.redoStack.length > 0;
895
+ }
896
+ get undoCount() {
897
+ return this.undoStack.length;
898
+ }
899
+ get redoCount() {
900
+ return this.redoStack.length;
901
+ }
902
+ push(command) {
903
+ this.undoStack.push(command);
904
+ this.redoStack = [];
905
+ if (this.undoStack.length > this.maxSize) {
906
+ this.undoStack.shift();
907
+ }
908
+ this.notifyChange();
909
+ }
910
+ undo(store) {
911
+ const command = this.undoStack.pop();
912
+ if (!command) return false;
913
+ command.undo(store);
914
+ this.redoStack.push(command);
915
+ this.notifyChange();
916
+ return true;
917
+ }
918
+ redo(store) {
919
+ const command = this.redoStack.pop();
920
+ if (!command) return false;
921
+ command.execute(store);
922
+ this.undoStack.push(command);
923
+ this.notifyChange();
924
+ return true;
925
+ }
926
+ clear() {
927
+ this.undoStack = [];
928
+ this.redoStack = [];
929
+ this.notifyChange();
930
+ }
931
+ onChange(listener) {
932
+ this.changeListeners.add(listener);
933
+ return () => this.changeListeners.delete(listener);
934
+ }
935
+ notifyChange() {
936
+ this.changeListeners.forEach((fn) => fn());
937
+ }
938
+ };
939
+
940
+ // src/history/commands.ts
941
+ var AddElementCommand = class {
942
+ element;
943
+ constructor(element) {
944
+ this.element = { ...element };
945
+ }
946
+ execute(store) {
947
+ store.add(this.element);
948
+ }
949
+ undo(store) {
950
+ store.remove(this.element.id);
951
+ }
952
+ };
953
+ var RemoveElementCommand = class {
954
+ element;
955
+ constructor(element) {
956
+ this.element = { ...element };
957
+ }
958
+ execute(store) {
959
+ store.remove(this.element.id);
960
+ }
961
+ undo(store) {
962
+ store.add(this.element);
963
+ }
964
+ };
965
+ var UpdateElementCommand = class {
966
+ constructor(id, previous, current) {
967
+ this.id = id;
968
+ this.previous = previous;
969
+ this.current = current;
970
+ }
971
+ execute(store) {
972
+ store.update(this.id, { ...this.current });
973
+ }
974
+ undo(store) {
975
+ store.update(this.id, { ...this.previous });
976
+ }
977
+ };
978
+ var BatchCommand = class {
979
+ commands;
980
+ constructor(commands) {
981
+ this.commands = [...commands];
982
+ }
983
+ execute(store) {
984
+ for (const cmd of this.commands) {
985
+ cmd.execute(store);
986
+ }
987
+ }
988
+ undo(store) {
989
+ for (let i = this.commands.length - 1; i >= 0; i--) {
990
+ this.commands[i]?.undo(store);
991
+ }
992
+ }
993
+ };
994
+
995
+ // src/history/history-recorder.ts
996
+ var HistoryRecorder = class {
997
+ constructor(store, stack) {
998
+ this.store = store;
999
+ this.stack = stack;
1000
+ this.unsubscribers = [
1001
+ store.on("add", (el) => this.onAdd(el)),
1002
+ store.on("remove", (el) => this.onRemove(el)),
1003
+ store.on("update", ({ previous, current }) => this.onUpdate(previous, current))
1004
+ ];
1005
+ }
1006
+ recording = true;
1007
+ transaction = null;
1008
+ updateSnapshots = /* @__PURE__ */ new Map();
1009
+ unsubscribers;
1010
+ pause() {
1011
+ this.recording = false;
1012
+ }
1013
+ resume() {
1014
+ this.recording = true;
1015
+ }
1016
+ begin() {
1017
+ this.transaction = [];
1018
+ this.updateSnapshots.clear();
1019
+ }
1020
+ commit() {
1021
+ if (!this.transaction) return;
1022
+ const finalCommands = this.flushUpdateSnapshots();
1023
+ const all = [...this.transaction, ...finalCommands];
1024
+ this.transaction = null;
1025
+ this.updateSnapshots.clear();
1026
+ if (all.length === 0) return;
1027
+ const first = all[0];
1028
+ this.stack.push(all.length === 1 && first ? first : new BatchCommand(all));
1029
+ }
1030
+ rollback() {
1031
+ this.transaction = null;
1032
+ this.updateSnapshots.clear();
1033
+ }
1034
+ destroy() {
1035
+ this.unsubscribers.forEach((fn) => fn());
1036
+ }
1037
+ record(command) {
1038
+ if (this.transaction) {
1039
+ this.transaction.push(command);
1040
+ } else {
1041
+ this.stack.push(command);
1042
+ }
1043
+ }
1044
+ onAdd(element) {
1045
+ if (!this.recording) return;
1046
+ this.record(new AddElementCommand(element));
1047
+ }
1048
+ onRemove(element) {
1049
+ if (!this.recording) return;
1050
+ if (this.transaction && this.updateSnapshots.has(element.id)) {
1051
+ this.updateSnapshots.delete(element.id);
1052
+ }
1053
+ this.record(new RemoveElementCommand(element));
1054
+ }
1055
+ onUpdate(previous, current) {
1056
+ if (!this.recording) return;
1057
+ if (this.transaction) {
1058
+ if (!this.updateSnapshots.has(current.id)) {
1059
+ this.updateSnapshots.set(current.id, { ...previous });
1060
+ }
1061
+ } else {
1062
+ this.stack.push(new UpdateElementCommand(current.id, previous, current));
1063
+ }
1064
+ }
1065
+ flushUpdateSnapshots() {
1066
+ const commands = [];
1067
+ for (const [id, previous] of this.updateSnapshots) {
1068
+ const current = this.store.getById(id);
1069
+ if (current) {
1070
+ commands.push(new UpdateElementCommand(id, previous, current));
1071
+ }
1072
+ }
1073
+ return commands;
1074
+ }
1075
+ };
1076
+
1077
+ // src/elements/create-id.ts
1078
+ var counter = 0;
1079
+ function createId(prefix) {
1080
+ return `${prefix}_${Date.now().toString(36)}_${(counter++).toString(36)}`;
1081
+ }
1082
+
1083
+ // src/elements/element-factory.ts
1084
+ function createStroke(input) {
1085
+ return {
1086
+ id: createId("stroke"),
1087
+ type: "stroke",
1088
+ position: input.position ?? { x: 0, y: 0 },
1089
+ zIndex: input.zIndex ?? 0,
1090
+ locked: input.locked ?? false,
1091
+ points: input.points,
1092
+ color: input.color ?? "#000000",
1093
+ width: input.width ?? 2,
1094
+ opacity: input.opacity ?? 1
1095
+ };
1096
+ }
1097
+ function createNote(input) {
1098
+ return {
1099
+ id: createId("note"),
1100
+ type: "note",
1101
+ position: input.position,
1102
+ zIndex: input.zIndex ?? 0,
1103
+ locked: input.locked ?? false,
1104
+ size: input.size ?? { w: 200, h: 100 },
1105
+ text: input.text ?? "",
1106
+ backgroundColor: input.backgroundColor ?? "#ffeb3b"
1107
+ };
1108
+ }
1109
+ function createArrow(input) {
1110
+ return {
1111
+ id: createId("arrow"),
1112
+ type: "arrow",
1113
+ position: input.position ?? { x: 0, y: 0 },
1114
+ zIndex: input.zIndex ?? 0,
1115
+ locked: input.locked ?? false,
1116
+ from: input.from,
1117
+ to: input.to,
1118
+ bend: input.bend ?? 0,
1119
+ color: input.color ?? "#000000",
1120
+ width: input.width ?? 2
1121
+ };
1122
+ }
1123
+ function createImage(input) {
1124
+ return {
1125
+ id: createId("image"),
1126
+ type: "image",
1127
+ position: input.position,
1128
+ zIndex: input.zIndex ?? 0,
1129
+ locked: input.locked ?? false,
1130
+ size: input.size,
1131
+ src: input.src
1132
+ };
1133
+ }
1134
+ function createHtmlElement(input) {
1135
+ return {
1136
+ id: createId("html"),
1137
+ type: "html",
1138
+ position: input.position,
1139
+ zIndex: input.zIndex ?? 0,
1140
+ locked: input.locked ?? false,
1141
+ size: input.size
1142
+ };
1143
+ }
1144
+
1145
+ // src/canvas/viewport.ts
1146
+ var Viewport = class {
1147
+ constructor(container, options = {}) {
1148
+ this.container = container;
1149
+ this.camera = new Camera(options.camera);
1150
+ this.background = new Background(options.background);
1151
+ this.store = new ElementStore();
1152
+ this.toolManager = new ToolManager();
1153
+ this.renderer = new ElementRenderer();
1154
+ this.noteEditor = new NoteEditor();
1155
+ this.history = new HistoryStack();
1156
+ this.historyRecorder = new HistoryRecorder(this.store, this.history);
1157
+ this.wrapper = this.createWrapper();
1158
+ this.canvasEl = this.createCanvas();
1159
+ this.domLayer = this.createDomLayer();
1160
+ this.wrapper.appendChild(this.canvasEl);
1161
+ this.wrapper.appendChild(this.domLayer);
1162
+ this.container.appendChild(this.wrapper);
1163
+ this.toolContext = {
1164
+ camera: this.camera,
1165
+ store: this.store,
1166
+ requestRender: () => this.requestRender(),
1167
+ switchTool: (name) => this.toolManager.setTool(name, this.toolContext),
1168
+ editElement: (id) => this.startEditingNote(id),
1169
+ setCursor: (cursor) => {
1170
+ this.wrapper.style.cursor = cursor;
1171
+ }
1172
+ };
1173
+ this.inputHandler = new InputHandler(this.wrapper, this.camera, {
1174
+ toolManager: this.toolManager,
1175
+ toolContext: this.toolContext,
1176
+ historyRecorder: this.historyRecorder,
1177
+ historyStack: this.history
1178
+ });
1179
+ this.unsubCamera = this.camera.onChange(() => {
1180
+ this.applyCameraTransform();
1181
+ this.requestRender();
1182
+ });
1183
+ this.unsubStore = [
1184
+ this.store.on("add", () => this.requestRender()),
1185
+ this.store.on("remove", (el) => this.removeDomNode(el.id)),
1186
+ this.store.on("update", () => this.requestRender()),
1187
+ this.store.on("clear", () => this.clearDomNodes())
1188
+ ];
1189
+ this.wrapper.addEventListener("dblclick", this.onDblClick);
1190
+ this.wrapper.addEventListener("dragover", this.onDragOver);
1191
+ this.wrapper.addEventListener("drop", this.onDrop);
1192
+ this.observeResize();
1193
+ this.syncCanvasSize();
1194
+ this.startRenderLoop();
1195
+ }
1196
+ camera;
1197
+ store;
1198
+ toolManager;
1199
+ history;
1200
+ domLayer;
1201
+ canvasEl;
1202
+ wrapper;
1203
+ unsubCamera;
1204
+ unsubStore;
1205
+ inputHandler;
1206
+ background;
1207
+ renderer;
1208
+ noteEditor;
1209
+ historyRecorder;
1210
+ toolContext;
1211
+ resizeObserver = null;
1212
+ animFrameId = 0;
1213
+ needsRender = true;
1214
+ domNodes = /* @__PURE__ */ new Map();
1215
+ htmlContent = /* @__PURE__ */ new Map();
1216
+ get ctx() {
1217
+ return this.canvasEl.getContext("2d");
1218
+ }
1219
+ requestRender() {
1220
+ this.needsRender = true;
1221
+ }
1222
+ exportState() {
1223
+ return exportState(this.store.snapshot(), this.camera);
1224
+ }
1225
+ exportJSON() {
1226
+ return JSON.stringify(this.exportState());
1227
+ }
1228
+ loadState(state) {
1229
+ this.historyRecorder.pause();
1230
+ this.noteEditor.destroy(this.store);
1231
+ this.clearDomNodes();
1232
+ this.store.loadSnapshot(state.elements);
1233
+ this.history.clear();
1234
+ this.historyRecorder.resume();
1235
+ this.camera.moveTo(state.camera.position.x, state.camera.position.y);
1236
+ this.camera.setZoom(state.camera.zoom);
1237
+ }
1238
+ loadJSON(json) {
1239
+ this.loadState(parseState(json));
1240
+ }
1241
+ undo() {
1242
+ this.historyRecorder.pause();
1243
+ const result = this.history.undo(this.store);
1244
+ this.historyRecorder.resume();
1245
+ if (result) this.requestRender();
1246
+ return result;
1247
+ }
1248
+ redo() {
1249
+ this.historyRecorder.pause();
1250
+ const result = this.history.redo(this.store);
1251
+ this.historyRecorder.resume();
1252
+ if (result) this.requestRender();
1253
+ return result;
1254
+ }
1255
+ addImage(src, position, size = { w: 300, h: 200 }) {
1256
+ const image = createImage({ position, size, src });
1257
+ this.historyRecorder.begin();
1258
+ this.store.add(image);
1259
+ this.historyRecorder.commit();
1260
+ this.requestRender();
1261
+ }
1262
+ addHtmlElement(dom, position, size = { w: 200, h: 150 }) {
1263
+ const el = createHtmlElement({ position, size });
1264
+ this.htmlContent.set(el.id, dom);
1265
+ this.historyRecorder.begin();
1266
+ this.store.add(el);
1267
+ this.historyRecorder.commit();
1268
+ this.requestRender();
1269
+ return el.id;
1270
+ }
1271
+ destroy() {
1272
+ cancelAnimationFrame(this.animFrameId);
1273
+ this.noteEditor.destroy(this.store);
1274
+ this.historyRecorder.destroy();
1275
+ this.wrapper.removeEventListener("dblclick", this.onDblClick);
1276
+ this.wrapper.removeEventListener("dragover", this.onDragOver);
1277
+ this.wrapper.removeEventListener("drop", this.onDrop);
1278
+ this.inputHandler.destroy();
1279
+ this.unsubCamera();
1280
+ this.unsubStore.forEach((fn) => fn());
1281
+ this.resizeObserver?.disconnect();
1282
+ this.resizeObserver = null;
1283
+ this.wrapper.remove();
1284
+ }
1285
+ startRenderLoop() {
1286
+ const loop = () => {
1287
+ if (this.needsRender) {
1288
+ this.render();
1289
+ this.needsRender = false;
1290
+ }
1291
+ this.animFrameId = requestAnimationFrame(loop);
1292
+ };
1293
+ this.animFrameId = requestAnimationFrame(loop);
1294
+ }
1295
+ render() {
1296
+ const ctx = this.ctx;
1297
+ if (!ctx) return;
1298
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
1299
+ ctx.save();
1300
+ ctx.scale(dpr, dpr);
1301
+ this.background.render(ctx, this.camera);
1302
+ ctx.save();
1303
+ ctx.translate(this.camera.position.x, this.camera.position.y);
1304
+ ctx.scale(this.camera.zoom, this.camera.zoom);
1305
+ for (const element of this.store.getAll()) {
1306
+ if (this.renderer.isDomElement(element)) {
1307
+ this.syncDomNode(element);
1308
+ } else {
1309
+ this.renderer.renderCanvasElement(ctx, element);
1310
+ }
1311
+ }
1312
+ const activeTool = this.toolManager.activeTool;
1313
+ if (activeTool?.renderOverlay) {
1314
+ activeTool.renderOverlay(ctx);
1315
+ }
1316
+ ctx.restore();
1317
+ ctx.restore();
1318
+ }
1319
+ startEditingNote(id) {
1320
+ const element = this.store.getById(id);
1321
+ if (!element || element.type !== "note") return;
1322
+ this.render();
1323
+ const node = this.domNodes.get(id);
1324
+ if (node) {
1325
+ this.noteEditor.startEditing(node, id, this.store);
1326
+ }
1327
+ }
1328
+ onDblClick = (e) => {
1329
+ const el = document.elementFromPoint(e.clientX, e.clientY);
1330
+ if (!el) return;
1331
+ const nodeEl = el.closest("[data-element-id]");
1332
+ if (!nodeEl) return;
1333
+ const elementId = nodeEl.dataset["elementId"];
1334
+ if (elementId) this.startEditingNote(elementId);
1335
+ };
1336
+ onDragOver = (e) => {
1337
+ e.preventDefault();
1338
+ };
1339
+ onDrop = (e) => {
1340
+ e.preventDefault();
1341
+ const files = e.dataTransfer?.files;
1342
+ if (!files) return;
1343
+ const rect = this.wrapper.getBoundingClientRect();
1344
+ for (const file of files) {
1345
+ if (!file.type.startsWith("image/")) continue;
1346
+ const reader = new FileReader();
1347
+ reader.onload = () => {
1348
+ const src = reader.result;
1349
+ if (typeof src !== "string") return;
1350
+ const screenPos = { x: e.clientX - rect.left, y: e.clientY - rect.top };
1351
+ const worldPos = this.camera.screenToWorld(screenPos);
1352
+ this.addImage(src, worldPos);
1353
+ };
1354
+ reader.readAsDataURL(file);
1355
+ }
1356
+ };
1357
+ syncDomNode(element) {
1358
+ let node = this.domNodes.get(element.id);
1359
+ if (!node) {
1360
+ node = document.createElement("div");
1361
+ node.dataset["elementId"] = element.id;
1362
+ Object.assign(node.style, {
1363
+ position: "absolute",
1364
+ pointerEvents: "auto"
1365
+ });
1366
+ this.domLayer.appendChild(node);
1367
+ this.domNodes.set(element.id, node);
1368
+ }
1369
+ const size = "size" in element ? element.size : null;
1370
+ Object.assign(node.style, {
1371
+ left: `${element.position.x}px`,
1372
+ top: `${element.position.y}px`,
1373
+ width: size ? `${size.w}px` : "auto",
1374
+ height: size ? `${size.h}px` : "auto"
1375
+ });
1376
+ this.renderDomContent(node, element);
1377
+ }
1378
+ renderDomContent(node, element) {
1379
+ if (element.type === "note") {
1380
+ if (!node.dataset["initialized"]) {
1381
+ node.dataset["initialized"] = "true";
1382
+ Object.assign(node.style, {
1383
+ backgroundColor: element.backgroundColor,
1384
+ padding: "8px",
1385
+ borderRadius: "4px",
1386
+ boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
1387
+ fontSize: "14px",
1388
+ overflow: "hidden",
1389
+ cursor: "default",
1390
+ userSelect: "none",
1391
+ wordWrap: "break-word"
1392
+ });
1393
+ node.textContent = element.text || "";
1394
+ node.addEventListener("dblclick", (e) => {
1395
+ e.stopPropagation();
1396
+ const id = node.dataset["elementId"];
1397
+ if (id) this.startEditingNote(id);
1398
+ });
1399
+ }
1400
+ if (!this.noteEditor.isEditing || this.noteEditor.editingElementId !== element.id) {
1401
+ if (node.textContent !== element.text) {
1402
+ node.textContent = element.text || "";
1403
+ }
1404
+ node.style.backgroundColor = element.backgroundColor;
1405
+ }
1406
+ }
1407
+ if (element.type === "image") {
1408
+ if (!node.dataset["initialized"]) {
1409
+ node.dataset["initialized"] = "true";
1410
+ const img = document.createElement("img");
1411
+ img.src = element.src;
1412
+ Object.assign(img.style, {
1413
+ width: "100%",
1414
+ height: "100%",
1415
+ objectFit: "contain",
1416
+ pointerEvents: "none"
1417
+ });
1418
+ img.draggable = false;
1419
+ node.appendChild(img);
1420
+ } else {
1421
+ const img = node.querySelector("img");
1422
+ if (img && img.src !== element.src) {
1423
+ img.src = element.src;
1424
+ }
1425
+ }
1426
+ }
1427
+ if (element.type === "html" && !node.dataset["initialized"]) {
1428
+ const content = this.htmlContent.get(element.id);
1429
+ if (content) {
1430
+ node.dataset["initialized"] = "true";
1431
+ Object.assign(node.style, {
1432
+ overflow: "hidden"
1433
+ });
1434
+ node.appendChild(content);
1435
+ }
1436
+ }
1437
+ }
1438
+ removeDomNode(id) {
1439
+ this.htmlContent.delete(id);
1440
+ const node = this.domNodes.get(id);
1441
+ if (node) {
1442
+ node.remove();
1443
+ this.domNodes.delete(id);
1444
+ }
1445
+ this.requestRender();
1446
+ }
1447
+ clearDomNodes() {
1448
+ this.domNodes.forEach((node) => node.remove());
1449
+ this.domNodes.clear();
1450
+ this.htmlContent.clear();
1451
+ this.requestRender();
1452
+ }
1453
+ createWrapper() {
1454
+ const el = document.createElement("div");
1455
+ Object.assign(el.style, {
1456
+ position: "relative",
1457
+ width: "100%",
1458
+ height: "100%",
1459
+ overflow: "hidden"
1460
+ });
1461
+ return el;
1462
+ }
1463
+ createCanvas() {
1464
+ const el = document.createElement("canvas");
1465
+ Object.assign(el.style, {
1466
+ position: "absolute",
1467
+ top: "0",
1468
+ left: "0",
1469
+ width: "100%",
1470
+ height: "100%"
1471
+ });
1472
+ return el;
1473
+ }
1474
+ createDomLayer() {
1475
+ const el = document.createElement("div");
1476
+ Object.assign(el.style, {
1477
+ position: "absolute",
1478
+ top: "0",
1479
+ left: "0",
1480
+ width: "100%",
1481
+ height: "100%",
1482
+ pointerEvents: "none",
1483
+ transformOrigin: "0 0"
1484
+ });
1485
+ return el;
1486
+ }
1487
+ applyCameraTransform() {
1488
+ this.domLayer.style.transform = this.camera.toCSSTransform();
1489
+ }
1490
+ syncCanvasSize() {
1491
+ const rect = this.container.getBoundingClientRect();
1492
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
1493
+ this.canvasEl.width = rect.width * dpr;
1494
+ this.canvasEl.height = rect.height * dpr;
1495
+ this.requestRender();
1496
+ }
1497
+ observeResize() {
1498
+ if (typeof ResizeObserver === "undefined") return;
1499
+ this.resizeObserver = new ResizeObserver(() => this.syncCanvasSize());
1500
+ this.resizeObserver.observe(this.container);
1501
+ }
1502
+ };
1503
+
1504
+ // src/tools/hand-tool.ts
1505
+ var HandTool = class {
1506
+ name = "hand";
1507
+ panning = false;
1508
+ lastScreen = { x: 0, y: 0 };
1509
+ onActivate(ctx) {
1510
+ ctx.setCursor?.("grab");
1511
+ }
1512
+ onDeactivate(ctx) {
1513
+ ctx.setCursor?.("default");
1514
+ }
1515
+ onPointerDown(state, ctx) {
1516
+ this.panning = true;
1517
+ this.lastScreen = { x: state.x, y: state.y };
1518
+ ctx.setCursor?.("grabbing");
1519
+ }
1520
+ onPointerMove(state, ctx) {
1521
+ if (!this.panning) return;
1522
+ const dx = state.x - this.lastScreen.x;
1523
+ const dy = state.y - this.lastScreen.y;
1524
+ this.lastScreen = { x: state.x, y: state.y };
1525
+ ctx.camera.pan(dx, dy);
1526
+ }
1527
+ onPointerUp(_state, ctx) {
1528
+ this.panning = false;
1529
+ ctx.setCursor?.("grab");
1530
+ }
1531
+ };
1532
+
1533
+ // src/tools/pencil-tool.ts
1534
+ var MIN_POINTS_FOR_STROKE = 2;
1535
+ var PencilTool = class {
1536
+ name = "pencil";
1537
+ drawing = false;
1538
+ points = [];
1539
+ color;
1540
+ width;
1541
+ constructor(options = {}) {
1542
+ this.color = options.color ?? "#000000";
1543
+ this.width = options.width ?? 2;
1544
+ }
1545
+ onActivate(ctx) {
1546
+ ctx.setCursor?.("crosshair");
1547
+ }
1548
+ onDeactivate(ctx) {
1549
+ ctx.setCursor?.("default");
1550
+ }
1551
+ setOptions(options) {
1552
+ if (options.color !== void 0) this.color = options.color;
1553
+ if (options.width !== void 0) this.width = options.width;
1554
+ }
1555
+ onPointerDown(state, ctx) {
1556
+ this.drawing = true;
1557
+ const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
1558
+ this.points = [world];
1559
+ }
1560
+ onPointerMove(state, ctx) {
1561
+ if (!this.drawing) return;
1562
+ const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
1563
+ this.points.push(world);
1564
+ ctx.requestRender();
1565
+ }
1566
+ onPointerUp(_state, ctx) {
1567
+ if (!this.drawing) return;
1568
+ this.drawing = false;
1569
+ if (this.points.length < MIN_POINTS_FOR_STROKE) {
1570
+ this.points = [];
1571
+ return;
1572
+ }
1573
+ const stroke = createStroke({
1574
+ points: this.points,
1575
+ color: this.color,
1576
+ width: this.width
1577
+ });
1578
+ ctx.store.add(stroke);
1579
+ this.points = [];
1580
+ ctx.requestRender();
1581
+ }
1582
+ renderOverlay(ctx) {
1583
+ if (!this.drawing || this.points.length < 2) return;
1584
+ ctx.save();
1585
+ ctx.strokeStyle = this.color;
1586
+ ctx.lineWidth = this.width;
1587
+ ctx.lineCap = "round";
1588
+ ctx.lineJoin = "round";
1589
+ ctx.globalAlpha = 0.8;
1590
+ ctx.beginPath();
1591
+ const first = this.points[0];
1592
+ if (!first) return;
1593
+ ctx.moveTo(first.x, first.y);
1594
+ for (let i = 1; i < this.points.length; i++) {
1595
+ const p = this.points[i];
1596
+ if (p) ctx.lineTo(p.x, p.y);
1597
+ }
1598
+ ctx.stroke();
1599
+ ctx.restore();
1600
+ }
1601
+ };
1602
+
1603
+ // src/tools/eraser-tool.ts
1604
+ var DEFAULT_RADIUS = 20;
1605
+ function makeEraserCursor(radius) {
1606
+ const size = radius * 2;
1607
+ const svg = `<svg xmlns='http://www.w3.org/2000/svg' width='${size}' height='${size}'><circle cx='${radius}' cy='${radius}' r='${radius - 1}' fill='none' stroke='%23666' stroke-width='1.5'/></svg>`;
1608
+ return `url("data:image/svg+xml,${svg}") ${radius} ${radius}, crosshair`;
1609
+ }
1610
+ var EraserTool = class {
1611
+ name = "eraser";
1612
+ erasing = false;
1613
+ radius;
1614
+ cursor;
1615
+ constructor(options = {}) {
1616
+ this.radius = options.radius ?? DEFAULT_RADIUS;
1617
+ this.cursor = makeEraserCursor(this.radius);
1618
+ }
1619
+ onActivate(ctx) {
1620
+ ctx.setCursor?.(this.cursor);
1621
+ }
1622
+ onDeactivate(ctx) {
1623
+ ctx.setCursor?.("default");
1624
+ }
1625
+ onPointerDown(state, ctx) {
1626
+ this.erasing = true;
1627
+ this.eraseAt(state, ctx);
1628
+ }
1629
+ onPointerMove(state, ctx) {
1630
+ if (!this.erasing) return;
1631
+ this.eraseAt(state, ctx);
1632
+ }
1633
+ onPointerUp(_state, _ctx) {
1634
+ this.erasing = false;
1635
+ }
1636
+ eraseAt(state, ctx) {
1637
+ const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
1638
+ const strokes = ctx.store.getElementsByType("stroke");
1639
+ let erased = false;
1640
+ for (const stroke of strokes) {
1641
+ if (this.strokeIntersects(stroke, world)) {
1642
+ ctx.store.remove(stroke.id);
1643
+ erased = true;
1644
+ }
1645
+ }
1646
+ if (erased) ctx.requestRender();
1647
+ }
1648
+ strokeIntersects(stroke, point) {
1649
+ const radiusSq = this.radius * this.radius;
1650
+ return stroke.points.some((p) => {
1651
+ const dx = p.x + stroke.position.x - point.x;
1652
+ const dy = p.y + stroke.position.y - point.y;
1653
+ return dx * dx + dy * dy <= radiusSq;
1654
+ });
1655
+ }
1656
+ };
1657
+
1658
+ // src/tools/arrow-handles.ts
1659
+ var HANDLE_RADIUS = 5;
1660
+ var HANDLE_HIT_PADDING = 4;
1661
+ var ARROW_HANDLE_CURSORS = {
1662
+ start: "crosshair",
1663
+ end: "crosshair",
1664
+ mid: "grab"
1665
+ };
1666
+ function getArrowHandleCursor(handle, active) {
1667
+ if (handle === "mid" && active) return "grabbing";
1668
+ return ARROW_HANDLE_CURSORS[handle];
1669
+ }
1670
+ function getArrowHandlePositions(arrow) {
1671
+ const mid = getArrowMidpoint(arrow.from, arrow.to, arrow.bend);
1672
+ return [
1673
+ ["start", arrow.from],
1674
+ ["mid", mid],
1675
+ ["end", arrow.to]
1676
+ ];
1677
+ }
1678
+ function hitTestArrowHandles(world, selectedIds, ctx) {
1679
+ if (selectedIds.length === 0) return null;
1680
+ const zoom = ctx.camera.zoom;
1681
+ const hitRadius = (HANDLE_RADIUS + HANDLE_HIT_PADDING) / zoom;
1682
+ for (const id of selectedIds) {
1683
+ const el = ctx.store.getById(id);
1684
+ if (!el || el.type !== "arrow") continue;
1685
+ const handles = getArrowHandlePositions(el);
1686
+ for (const [handle, pos] of handles) {
1687
+ const dx = world.x - pos.x;
1688
+ const dy = world.y - pos.y;
1689
+ if (dx * dx + dy * dy <= hitRadius * hitRadius) {
1690
+ return { elementId: id, handle };
1691
+ }
1692
+ }
1693
+ }
1694
+ return null;
1695
+ }
1696
+ function applyArrowHandleDrag(handle, elementId, world, ctx) {
1697
+ const el = ctx.store.getById(elementId);
1698
+ if (!el || el.type !== "arrow") return;
1699
+ switch (handle) {
1700
+ case "start":
1701
+ ctx.store.update(elementId, {
1702
+ from: { x: world.x, y: world.y },
1703
+ position: { x: world.x, y: world.y }
1704
+ });
1705
+ break;
1706
+ case "end":
1707
+ ctx.store.update(elementId, {
1708
+ to: { x: world.x, y: world.y }
1709
+ });
1710
+ break;
1711
+ case "mid": {
1712
+ const bend = getBendFromPoint(el.from, el.to, world);
1713
+ ctx.store.update(elementId, { bend });
1714
+ break;
1715
+ }
1716
+ }
1717
+ ctx.requestRender();
1718
+ }
1719
+ function renderArrowHandles(canvasCtx, arrow, zoom) {
1720
+ const radius = HANDLE_RADIUS / zoom;
1721
+ const handles = getArrowHandlePositions(arrow);
1722
+ canvasCtx.setLineDash([]);
1723
+ canvasCtx.lineWidth = 1.5 / zoom;
1724
+ for (const [handle, pos] of handles) {
1725
+ canvasCtx.fillStyle = handle === "mid" ? "#2196F3" : "#ffffff";
1726
+ canvasCtx.strokeStyle = "#2196F3";
1727
+ canvasCtx.beginPath();
1728
+ canvasCtx.arc(pos.x, pos.y, radius, 0, Math.PI * 2);
1729
+ canvasCtx.fill();
1730
+ canvasCtx.stroke();
1731
+ }
1732
+ }
1733
+
1734
+ // src/tools/select-tool.ts
1735
+ var HANDLE_SIZE = 8;
1736
+ var HANDLE_HIT_PADDING2 = 4;
1737
+ var SELECTION_PAD = 4;
1738
+ var MIN_ELEMENT_SIZE = 20;
1739
+ var HANDLE_CURSORS = {
1740
+ nw: "nwse-resize",
1741
+ se: "nwse-resize",
1742
+ ne: "nesw-resize",
1743
+ sw: "nesw-resize"
1744
+ };
1745
+ var SelectTool = class {
1746
+ name = "select";
1747
+ _selectedIds = [];
1748
+ mode = { type: "idle" };
1749
+ lastWorld = { x: 0, y: 0 };
1750
+ currentWorld = { x: 0, y: 0 };
1751
+ ctx = null;
1752
+ get selectedIds() {
1753
+ return [...this._selectedIds];
1754
+ }
1755
+ get isMarqueeActive() {
1756
+ return this.mode.type === "marquee";
1757
+ }
1758
+ onActivate(ctx) {
1759
+ this.ctx = ctx;
1760
+ }
1761
+ onDeactivate(ctx) {
1762
+ this._selectedIds = [];
1763
+ this.mode = { type: "idle" };
1764
+ ctx.setCursor?.("default");
1765
+ }
1766
+ onPointerDown(state, ctx) {
1767
+ this.ctx = ctx;
1768
+ const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
1769
+ this.lastWorld = world;
1770
+ this.currentWorld = world;
1771
+ const arrowHit = hitTestArrowHandles(world, this._selectedIds, ctx);
1772
+ if (arrowHit) {
1773
+ this.mode = {
1774
+ type: "arrow-handle",
1775
+ elementId: arrowHit.elementId,
1776
+ handle: arrowHit.handle
1777
+ };
1778
+ ctx.requestRender();
1779
+ return;
1780
+ }
1781
+ const resizeHit = this.hitTestResizeHandle(world, ctx);
1782
+ if (resizeHit) {
1783
+ const el = ctx.store.getById(resizeHit.elementId);
1784
+ if (el) {
1785
+ this.mode = {
1786
+ type: "resizing",
1787
+ elementId: resizeHit.elementId,
1788
+ handle: resizeHit.handle
1789
+ };
1790
+ ctx.requestRender();
1791
+ return;
1792
+ }
1793
+ }
1794
+ const hit = this.hitTest(world, ctx);
1795
+ if (hit) {
1796
+ const alreadySelected = this._selectedIds.includes(hit.id);
1797
+ if (!alreadySelected) {
1798
+ this._selectedIds = [hit.id];
1799
+ }
1800
+ this.mode = hit.locked ? { type: "idle" } : { type: "dragging" };
1801
+ } else {
1802
+ this._selectedIds = [];
1803
+ this.mode = { type: "marquee", start: world };
1804
+ }
1805
+ ctx.requestRender();
1806
+ }
1807
+ onPointerMove(state, ctx) {
1808
+ const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
1809
+ this.currentWorld = world;
1810
+ if (this.mode.type === "arrow-handle") {
1811
+ ctx.setCursor?.(getArrowHandleCursor(this.mode.handle, true));
1812
+ applyArrowHandleDrag(this.mode.handle, this.mode.elementId, world, ctx);
1813
+ return;
1814
+ }
1815
+ if (this.mode.type === "resizing") {
1816
+ ctx.setCursor?.(HANDLE_CURSORS[this.mode.handle]);
1817
+ this.handleResize(world, ctx);
1818
+ return;
1819
+ }
1820
+ if (this.mode.type === "dragging" && this._selectedIds.length > 0) {
1821
+ ctx.setCursor?.("move");
1822
+ const dx = world.x - this.lastWorld.x;
1823
+ const dy = world.y - this.lastWorld.y;
1824
+ this.lastWorld = world;
1825
+ for (const id of this._selectedIds) {
1826
+ const el = ctx.store.getById(id);
1827
+ if (!el || el.locked) continue;
1828
+ if (el.type === "arrow") {
1829
+ ctx.store.update(id, {
1830
+ position: { x: el.position.x + dx, y: el.position.y + dy },
1831
+ from: { x: el.from.x + dx, y: el.from.y + dy },
1832
+ to: { x: el.to.x + dx, y: el.to.y + dy }
1833
+ });
1834
+ } else {
1835
+ ctx.store.update(id, {
1836
+ position: { x: el.position.x + dx, y: el.position.y + dy }
1837
+ });
1838
+ }
1839
+ }
1840
+ ctx.requestRender();
1841
+ return;
1842
+ }
1843
+ if (this.mode.type === "marquee") {
1844
+ ctx.setCursor?.("crosshair");
1845
+ ctx.requestRender();
1846
+ return;
1847
+ }
1848
+ this.updateHoverCursor(world, ctx);
1849
+ }
1850
+ onPointerUp(_state, ctx) {
1851
+ if (this.mode.type === "marquee") {
1852
+ const rect = this.getMarqueeRect();
1853
+ if (rect) {
1854
+ this._selectedIds = this.findElementsInRect(rect, ctx);
1855
+ }
1856
+ ctx.requestRender();
1857
+ }
1858
+ this.mode = { type: "idle" };
1859
+ ctx.setCursor?.("default");
1860
+ }
1861
+ onHover(state, ctx) {
1862
+ const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
1863
+ this.updateHoverCursor(world, ctx);
1864
+ }
1865
+ renderOverlay(canvasCtx) {
1866
+ this.renderMarquee(canvasCtx);
1867
+ this.renderSelectionBoxes(canvasCtx);
1868
+ }
1869
+ updateHoverCursor(world, ctx) {
1870
+ const arrowHit = hitTestArrowHandles(world, this._selectedIds, ctx);
1871
+ if (arrowHit) {
1872
+ ctx.setCursor?.(getArrowHandleCursor(arrowHit.handle, false));
1873
+ return;
1874
+ }
1875
+ const resizeHit = this.hitTestResizeHandle(world, ctx);
1876
+ if (resizeHit) {
1877
+ ctx.setCursor?.(HANDLE_CURSORS[resizeHit.handle]);
1878
+ return;
1879
+ }
1880
+ const hit = this.hitTest(world, ctx);
1881
+ ctx.setCursor?.(hit ? "move" : "default");
1882
+ }
1883
+ handleResize(world, ctx) {
1884
+ if (this.mode.type !== "resizing") return;
1885
+ const el = ctx.store.getById(this.mode.elementId);
1886
+ if (!el || !("size" in el)) return;
1887
+ const { handle } = this.mode;
1888
+ const dx = world.x - this.lastWorld.x;
1889
+ const dy = world.y - this.lastWorld.y;
1890
+ this.lastWorld = world;
1891
+ let { x, y, w, h } = { x: el.position.x, y: el.position.y, w: el.size.w, h: el.size.h };
1892
+ switch (handle) {
1893
+ case "se":
1894
+ w += dx;
1895
+ h += dy;
1896
+ break;
1897
+ case "sw":
1898
+ x += dx;
1899
+ w -= dx;
1900
+ h += dy;
1901
+ break;
1902
+ case "ne":
1903
+ y += dy;
1904
+ w += dx;
1905
+ h -= dy;
1906
+ break;
1907
+ case "nw":
1908
+ x += dx;
1909
+ y += dy;
1910
+ w -= dx;
1911
+ h -= dy;
1912
+ break;
1913
+ }
1914
+ if (w < MIN_ELEMENT_SIZE) {
1915
+ if (handle === "nw" || handle === "sw") x = el.position.x + el.size.w - MIN_ELEMENT_SIZE;
1916
+ w = MIN_ELEMENT_SIZE;
1917
+ }
1918
+ if (h < MIN_ELEMENT_SIZE) {
1919
+ if (handle === "nw" || handle === "ne") y = el.position.y + el.size.h - MIN_ELEMENT_SIZE;
1920
+ h = MIN_ELEMENT_SIZE;
1921
+ }
1922
+ ctx.store.update(this.mode.elementId, {
1923
+ position: { x, y },
1924
+ size: { w, h }
1925
+ });
1926
+ ctx.requestRender();
1927
+ }
1928
+ hitTestResizeHandle(world, ctx) {
1929
+ if (this._selectedIds.length === 0) return null;
1930
+ const zoom = ctx.camera.zoom;
1931
+ const handleHalf = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / zoom;
1932
+ for (const id of this._selectedIds) {
1933
+ const el = ctx.store.getById(id);
1934
+ if (!el || !("size" in el)) continue;
1935
+ const bounds = this.getElementBounds(el);
1936
+ if (!bounds) continue;
1937
+ const corners = this.getHandlePositions(bounds);
1938
+ for (const [handle, pos] of corners) {
1939
+ if (Math.abs(world.x - pos.x) <= handleHalf && Math.abs(world.y - pos.y) <= handleHalf) {
1940
+ return { elementId: id, handle };
1941
+ }
1942
+ }
1943
+ }
1944
+ return null;
1945
+ }
1946
+ getHandlePositions(bounds) {
1947
+ return [
1948
+ ["nw", { x: bounds.x, y: bounds.y }],
1949
+ ["ne", { x: bounds.x + bounds.w, y: bounds.y }],
1950
+ ["sw", { x: bounds.x, y: bounds.y + bounds.h }],
1951
+ ["se", { x: bounds.x + bounds.w, y: bounds.y + bounds.h }]
1952
+ ];
1953
+ }
1954
+ renderMarquee(canvasCtx) {
1955
+ if (this.mode.type !== "marquee") return;
1956
+ const rect = this.getMarqueeRect();
1957
+ if (!rect) return;
1958
+ canvasCtx.save();
1959
+ canvasCtx.strokeStyle = "#2196F3";
1960
+ canvasCtx.fillStyle = "rgba(33, 150, 243, 0.08)";
1961
+ canvasCtx.lineWidth = 1;
1962
+ canvasCtx.setLineDash([4, 4]);
1963
+ canvasCtx.strokeRect(rect.x, rect.y, rect.w, rect.h);
1964
+ canvasCtx.fillRect(rect.x, rect.y, rect.w, rect.h);
1965
+ canvasCtx.restore();
1966
+ }
1967
+ renderSelectionBoxes(canvasCtx) {
1968
+ if (this._selectedIds.length === 0 || !this.ctx) return;
1969
+ const zoom = this.ctx.camera.zoom;
1970
+ const handleWorldSize = HANDLE_SIZE / zoom;
1971
+ canvasCtx.save();
1972
+ canvasCtx.strokeStyle = "#2196F3";
1973
+ canvasCtx.lineWidth = 1.5 / zoom;
1974
+ canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
1975
+ for (const id of this._selectedIds) {
1976
+ const el = this.ctx.store.getById(id);
1977
+ if (!el) continue;
1978
+ if (el.type === "arrow") {
1979
+ renderArrowHandles(canvasCtx, el, zoom);
1980
+ continue;
1981
+ }
1982
+ const bounds = this.getElementBounds(el);
1983
+ if (!bounds) continue;
1984
+ const pad = SELECTION_PAD / zoom;
1985
+ canvasCtx.strokeRect(bounds.x - pad, bounds.y - pad, bounds.w + pad * 2, bounds.h + pad * 2);
1986
+ if ("size" in el) {
1987
+ canvasCtx.setLineDash([]);
1988
+ canvasCtx.fillStyle = "#ffffff";
1989
+ const corners = this.getHandlePositions(bounds);
1990
+ for (const [, pos] of corners) {
1991
+ canvasCtx.fillRect(
1992
+ pos.x - handleWorldSize / 2,
1993
+ pos.y - handleWorldSize / 2,
1994
+ handleWorldSize,
1995
+ handleWorldSize
1996
+ );
1997
+ canvasCtx.strokeRect(
1998
+ pos.x - handleWorldSize / 2,
1999
+ pos.y - handleWorldSize / 2,
2000
+ handleWorldSize,
2001
+ handleWorldSize
2002
+ );
2003
+ }
2004
+ canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
2005
+ }
2006
+ }
2007
+ canvasCtx.restore();
2008
+ }
2009
+ getMarqueeRect() {
2010
+ if (this.mode.type !== "marquee") return null;
2011
+ const { start } = this.mode;
2012
+ const end = this.currentWorld;
2013
+ const x = Math.min(start.x, end.x);
2014
+ const y = Math.min(start.y, end.y);
2015
+ const w = Math.abs(end.x - start.x);
2016
+ const h = Math.abs(end.y - start.y);
2017
+ if (w === 0 && h === 0) return null;
2018
+ return { x, y, w, h };
2019
+ }
2020
+ findElementsInRect(marquee, ctx) {
2021
+ const ids = [];
2022
+ for (const el of ctx.store.getAll()) {
2023
+ const bounds = this.getElementBounds(el);
2024
+ if (bounds && this.rectsOverlap(marquee, bounds)) {
2025
+ ids.push(el.id);
2026
+ }
2027
+ }
2028
+ return ids;
2029
+ }
2030
+ rectsOverlap(a, b) {
2031
+ return a.x <= b.x + b.w && a.x + a.w >= b.x && a.y <= b.y + b.h && a.y + a.h >= b.y;
2032
+ }
2033
+ getElementBounds(el) {
2034
+ if ("size" in el) {
2035
+ return { x: el.position.x, y: el.position.y, w: el.size.w, h: el.size.h };
2036
+ }
2037
+ if (el.type === "stroke" && el.points.length > 0) {
2038
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
2039
+ for (const p of el.points) {
2040
+ const px = p.x + el.position.x;
2041
+ const py = p.y + el.position.y;
2042
+ if (px < minX) minX = px;
2043
+ if (py < minY) minY = py;
2044
+ if (px > maxX) maxX = px;
2045
+ if (py > maxY) maxY = py;
2046
+ }
2047
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
2048
+ }
2049
+ if (el.type === "arrow") {
2050
+ return getArrowBounds(el.from, el.to, el.bend);
2051
+ }
2052
+ return null;
2053
+ }
2054
+ hitTest(world, ctx) {
2055
+ const elements = ctx.store.getAll().reverse();
2056
+ for (const el of elements) {
2057
+ if (this.isInsideBounds(world, el)) return el;
2058
+ }
2059
+ return null;
2060
+ }
2061
+ isInsideBounds(point, el) {
2062
+ if ("size" in el) {
2063
+ const s = el.size;
2064
+ return point.x >= el.position.x && point.x <= el.position.x + s.w && point.y >= el.position.y && point.y <= el.position.y + s.h;
2065
+ }
2066
+ if (el.type === "stroke") {
2067
+ const HIT_RADIUS = 10;
2068
+ return el.points.some((p) => {
2069
+ const dx = p.x + el.position.x - point.x;
2070
+ const dy = p.y + el.position.y - point.y;
2071
+ return dx * dx + dy * dy <= HIT_RADIUS * HIT_RADIUS;
2072
+ });
2073
+ }
2074
+ if (el.type === "arrow") {
2075
+ return isNearBezier(point, el.from, el.to, el.bend, 10);
2076
+ }
2077
+ return false;
2078
+ }
2079
+ };
2080
+
2081
+ // src/tools/arrow-tool.ts
2082
+ var ArrowTool = class {
2083
+ name = "arrow";
2084
+ drawing = false;
2085
+ start = { x: 0, y: 0 };
2086
+ end = { x: 0, y: 0 };
2087
+ color;
2088
+ width;
2089
+ constructor(options = {}) {
2090
+ this.color = options.color ?? "#000000";
2091
+ this.width = options.width ?? 2;
2092
+ }
2093
+ onPointerDown(state, ctx) {
2094
+ this.drawing = true;
2095
+ this.start = ctx.camera.screenToWorld({ x: state.x, y: state.y });
2096
+ this.end = { ...this.start };
2097
+ }
2098
+ onPointerMove(state, ctx) {
2099
+ if (!this.drawing) return;
2100
+ this.end = ctx.camera.screenToWorld({ x: state.x, y: state.y });
2101
+ ctx.requestRender();
2102
+ }
2103
+ onPointerUp(_state, ctx) {
2104
+ if (!this.drawing) return;
2105
+ this.drawing = false;
2106
+ if (this.start.x === this.end.x && this.start.y === this.end.y) return;
2107
+ const arrow = createArrow({
2108
+ from: this.start,
2109
+ to: this.end,
2110
+ color: this.color,
2111
+ width: this.width
2112
+ });
2113
+ ctx.store.add(arrow);
2114
+ ctx.requestRender();
2115
+ }
2116
+ renderOverlay(ctx) {
2117
+ if (!this.drawing) return;
2118
+ if (this.start.x === this.end.x && this.start.y === this.end.y) return;
2119
+ ctx.save();
2120
+ ctx.strokeStyle = this.color;
2121
+ ctx.lineWidth = this.width;
2122
+ ctx.lineCap = "round";
2123
+ ctx.globalAlpha = 0.6;
2124
+ ctx.beginPath();
2125
+ ctx.moveTo(this.start.x, this.start.y);
2126
+ ctx.lineTo(this.end.x, this.end.y);
2127
+ ctx.stroke();
2128
+ const angle = Math.atan2(this.end.y - this.start.y, this.end.x - this.start.x);
2129
+ const headLen = 12;
2130
+ const headAngle = Math.PI / 6;
2131
+ ctx.fillStyle = this.color;
2132
+ ctx.beginPath();
2133
+ ctx.moveTo(this.end.x, this.end.y);
2134
+ ctx.lineTo(
2135
+ this.end.x - headLen * Math.cos(angle - headAngle),
2136
+ this.end.y - headLen * Math.sin(angle - headAngle)
2137
+ );
2138
+ ctx.lineTo(
2139
+ this.end.x - headLen * Math.cos(angle + headAngle),
2140
+ this.end.y - headLen * Math.sin(angle + headAngle)
2141
+ );
2142
+ ctx.closePath();
2143
+ ctx.fill();
2144
+ ctx.restore();
2145
+ }
2146
+ };
2147
+
2148
+ // src/tools/note-tool.ts
2149
+ var NoteTool = class {
2150
+ name = "note";
2151
+ backgroundColor;
2152
+ size;
2153
+ constructor(options = {}) {
2154
+ this.backgroundColor = options.backgroundColor ?? "#ffeb3b";
2155
+ this.size = options.size ?? { w: 200, h: 100 };
2156
+ }
2157
+ onPointerDown(_state, _ctx) {
2158
+ }
2159
+ onPointerMove(_state, _ctx) {
2160
+ }
2161
+ onPointerUp(state, ctx) {
2162
+ const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
2163
+ const note = createNote({
2164
+ position: world,
2165
+ size: { ...this.size },
2166
+ backgroundColor: this.backgroundColor
2167
+ });
2168
+ ctx.store.add(note);
2169
+ ctx.requestRender();
2170
+ ctx.switchTool?.("select");
2171
+ ctx.editElement?.(note.id);
2172
+ }
2173
+ };
2174
+
2175
+ // src/tools/image-tool.ts
2176
+ var ImageTool = class {
2177
+ name = "image";
2178
+ size;
2179
+ src = null;
2180
+ constructor(options = {}) {
2181
+ this.size = options.size ?? { w: 300, h: 200 };
2182
+ }
2183
+ setSrc(src) {
2184
+ this.src = src;
2185
+ }
2186
+ onPointerDown(_state, _ctx) {
2187
+ }
2188
+ onPointerMove(_state, _ctx) {
2189
+ }
2190
+ onPointerUp(state, ctx) {
2191
+ if (!this.src) return;
2192
+ const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
2193
+ const image = createImage({
2194
+ position: world,
2195
+ size: { ...this.size },
2196
+ src: this.src
2197
+ });
2198
+ ctx.store.add(image);
2199
+ ctx.requestRender();
2200
+ this.src = null;
2201
+ ctx.switchTool?.("select");
2202
+ }
2203
+ };
2204
+
2205
+ // src/index.ts
2206
+ var VERSION = "0.1.0";
2207
+ // Annotate the CommonJS export names for ESM import in node:
2208
+ 0 && (module.exports = {
2209
+ AddElementCommand,
2210
+ ArrowTool,
2211
+ Background,
2212
+ BatchCommand,
2213
+ Camera,
2214
+ ElementRenderer,
2215
+ ElementStore,
2216
+ EraserTool,
2217
+ EventBus,
2218
+ HandTool,
2219
+ HistoryRecorder,
2220
+ HistoryStack,
2221
+ ImageTool,
2222
+ InputHandler,
2223
+ NoteEditor,
2224
+ NoteTool,
2225
+ PencilTool,
2226
+ RemoveElementCommand,
2227
+ SelectTool,
2228
+ ToolManager,
2229
+ UpdateElementCommand,
2230
+ VERSION,
2231
+ Viewport,
2232
+ createArrow,
2233
+ createHtmlElement,
2234
+ createId,
2235
+ createImage,
2236
+ createNote,
2237
+ createStroke,
2238
+ exportState,
2239
+ getArrowBounds,
2240
+ getArrowControlPoint,
2241
+ getArrowMidpoint,
2242
+ getArrowTangentAngle,
2243
+ getBendFromPoint,
2244
+ isNearBezier,
2245
+ parseState
2246
+ });
2247
+ //# sourceMappingURL=index.cjs.map