@fieldnotes/core 0.24.0 → 0.26.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.js CHANGED
@@ -1,179 +1,34 @@
1
- // src/core/event-bus.ts
2
- var EventBus = class {
3
- listeners = /* @__PURE__ */ new Map();
4
- on(event, listener) {
5
- const existing = this.listeners.get(event);
6
- if (existing) {
7
- existing.add(listener);
8
- } else {
9
- const set = /* @__PURE__ */ new Set([listener]);
10
- this.listeners.set(event, set);
11
- }
12
- return () => this.off(event, listener);
13
- }
14
- off(event, listener) {
15
- this.listeners.get(event)?.delete(listener);
16
- }
17
- emit(event, data) {
18
- this.listeners.get(event)?.forEach((listener) => {
19
- try {
20
- listener(data);
21
- } catch (err) {
22
- console.error(`[fieldnotes] listener error for "${String(event)}"`, err);
23
- }
24
- });
25
- }
26
- clear() {
27
- this.listeners.clear();
28
- }
29
- };
30
-
31
- // src/core/quadtree.ts
32
- var MAX_ITEMS = 8;
33
- var MAX_DEPTH = 8;
34
- function intersects(a, b) {
35
- 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;
1
+ // src/core/snap.ts
2
+ function snapPoint(point, gridSize) {
3
+ return {
4
+ x: Math.round(point.x / gridSize) * gridSize || 0,
5
+ y: Math.round(point.y / gridSize) * gridSize || 0
6
+ };
36
7
  }
37
- var QuadNode = class _QuadNode {
38
- constructor(bounds, depth) {
39
- this.bounds = bounds;
40
- this.depth = depth;
41
- }
42
- items = [];
43
- children = null;
44
- insert(entry) {
45
- if (this.children) {
46
- const idx = this.getChildIndex(entry.bounds);
47
- if (idx !== -1) {
48
- const child = this.children[idx];
49
- if (child) child.insert(entry);
50
- return;
51
- }
52
- this.items.push(entry);
53
- return;
54
- }
55
- this.items.push(entry);
56
- if (this.items.length > MAX_ITEMS && this.depth < MAX_DEPTH) {
57
- this.split();
58
- }
59
- }
60
- remove(id) {
61
- const idx = this.items.findIndex((e) => e.id === id);
62
- if (idx !== -1) {
63
- this.items.splice(idx, 1);
64
- return true;
65
- }
66
- if (this.children) {
67
- for (const child of this.children) {
68
- if (child.remove(id)) {
69
- this.collapseIfEmpty();
70
- return true;
71
- }
72
- }
73
- }
74
- return false;
75
- }
76
- query(rect, result) {
77
- if (!intersects(this.bounds, rect)) return;
78
- for (const item of this.items) {
79
- if (intersects(item.bounds, rect)) {
80
- result.push(item.id);
81
- }
82
- }
83
- if (this.children) {
84
- for (const child of this.children) {
85
- child.query(rect, result);
86
- }
87
- }
88
- }
89
- getChildIndex(itemBounds) {
90
- const midX = this.bounds.x + this.bounds.w / 2;
91
- const midY = this.bounds.y + this.bounds.h / 2;
92
- const left = itemBounds.x >= this.bounds.x && itemBounds.x + itemBounds.w <= midX;
93
- const right = itemBounds.x >= midX && itemBounds.x + itemBounds.w <= this.bounds.x + this.bounds.w;
94
- const top = itemBounds.y >= this.bounds.y && itemBounds.y + itemBounds.h <= midY;
95
- const bottom = itemBounds.y >= midY && itemBounds.y + itemBounds.h <= this.bounds.y + this.bounds.h;
96
- if (left && top) return 0;
97
- if (right && top) return 1;
98
- if (left && bottom) return 2;
99
- if (right && bottom) return 3;
100
- return -1;
101
- }
102
- split() {
103
- const { x, y, w, h } = this.bounds;
104
- const halfW = w / 2;
105
- const halfH = h / 2;
106
- const d = this.depth + 1;
107
- this.children = [
108
- new _QuadNode({ x, y, w: halfW, h: halfH }, d),
109
- new _QuadNode({ x: x + halfW, y, w: halfW, h: halfH }, d),
110
- new _QuadNode({ x, y: y + halfH, w: halfW, h: halfH }, d),
111
- new _QuadNode({ x: x + halfW, y: y + halfH, w: halfW, h: halfH }, d)
112
- ];
113
- const remaining = [];
114
- for (const item of this.items) {
115
- const idx = this.getChildIndex(item.bounds);
116
- if (idx !== -1) {
117
- const target = this.children[idx];
118
- if (target) target.insert(item);
119
- } else {
120
- remaining.push(item);
121
- }
122
- }
123
- this.items = remaining;
124
- }
125
- collapseIfEmpty() {
126
- if (!this.children) return;
127
- let totalItems = this.items.length;
128
- for (const child of this.children) {
129
- if (child.children) return;
130
- totalItems += child.items.length;
131
- }
132
- if (totalItems <= MAX_ITEMS) {
133
- for (const child of this.children) {
134
- this.items.push(...child.items);
135
- }
136
- this.children = null;
137
- }
138
- }
139
- };
140
- var Quadtree = class {
141
- root;
142
- _size = 0;
143
- worldBounds;
144
- constructor(worldBounds) {
145
- this.worldBounds = worldBounds;
146
- this.root = new QuadNode(worldBounds, 0);
147
- }
148
- get size() {
149
- return this._size;
150
- }
151
- insert(id, bounds) {
152
- this.root.insert({ id, bounds });
153
- this._size++;
154
- }
155
- remove(id) {
156
- if (this.root.remove(id)) {
157
- this._size--;
158
- }
159
- }
160
- update(id, newBounds) {
161
- this.remove(id);
162
- this.insert(id, newBounds);
163
- }
164
- query(rect) {
165
- const result = [];
166
- this.root.query(rect, result);
167
- return result;
168
- }
169
- queryPoint(point) {
170
- return this.query({ x: point.x, y: point.y, w: 0, h: 0 });
8
+ function snapToHexCenter(point, cellSize, orientation) {
9
+ if (orientation === "pointy") {
10
+ const hexW = Math.sqrt(3) * cellSize;
11
+ const rowH = 1.5 * cellSize;
12
+ const row = Math.round(point.y / rowH);
13
+ const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
14
+ const col = Math.round((point.x - offsetX) / hexW);
15
+ return { x: col * hexW + offsetX || 0, y: row * rowH || 0 };
16
+ } else {
17
+ const hexH = Math.sqrt(3) * cellSize;
18
+ const colW = 1.5 * cellSize;
19
+ const col = Math.round(point.x / colW);
20
+ const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
21
+ const row = Math.round((point.y - offsetY) / hexH);
22
+ return { x: col * colW || 0, y: row * hexH + offsetY || 0 };
171
23
  }
172
- clear() {
173
- this.root = new QuadNode(this.worldBounds, 0);
174
- this._size = 0;
24
+ }
25
+ function smartSnap(point, ctx) {
26
+ if (!ctx.snapToGrid || !ctx.gridSize) return point;
27
+ if (ctx.gridType === "hex" && ctx.hexOrientation) {
28
+ return snapToHexCenter(point, ctx.gridSize, ctx.hexOrientation);
175
29
  }
176
- };
30
+ return snapPoint(point, ctx.gridSize);
31
+ }
177
32
 
178
33
  // src/elements/note-sanitizer.ts
179
34
  var BOLD_TAGS = /* @__PURE__ */ new Set(["b", "strong"]);
@@ -430,38 +285,6 @@ function migrateElement(obj) {
430
285
  }
431
286
  }
432
287
 
433
- // src/core/snap.ts
434
- function snapPoint(point, gridSize) {
435
- return {
436
- x: Math.round(point.x / gridSize) * gridSize || 0,
437
- y: Math.round(point.y / gridSize) * gridSize || 0
438
- };
439
- }
440
- function snapToHexCenter(point, cellSize, orientation) {
441
- if (orientation === "pointy") {
442
- const hexW = Math.sqrt(3) * cellSize;
443
- const rowH = 1.5 * cellSize;
444
- const row = Math.round(point.y / rowH);
445
- const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
446
- const col = Math.round((point.x - offsetX) / hexW);
447
- return { x: col * hexW + offsetX || 0, y: row * rowH || 0 };
448
- } else {
449
- const hexH = Math.sqrt(3) * cellSize;
450
- const colW = 1.5 * cellSize;
451
- const col = Math.round(point.x / colW);
452
- const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
453
- const row = Math.round((point.y - offsetY) / hexH);
454
- return { x: col * colW || 0, y: row * hexH + offsetY || 0 };
455
- }
456
- }
457
- function smartSnap(point, ctx) {
458
- if (!ctx.snapToGrid || !ctx.gridSize) return point;
459
- if (ctx.gridType === "hex" && ctx.hexOrientation) {
460
- return snapToHexCenter(point, ctx.gridSize, ctx.hexOrientation);
461
- }
462
- return snapPoint(point, ctx.gridSize);
463
- }
464
-
465
288
  // src/core/auto-save.ts
466
289
  var DEFAULT_KEY = "fieldnotes-autosave";
467
290
  var DEFAULT_DEBOUNCE_MS = 1e3;
@@ -625,143 +448,6 @@ var Camera = class {
625
448
  }
626
449
  };
627
450
 
628
- // src/canvas/background.ts
629
- var MIN_PATTERN_SPACING = 16;
630
- var DEFAULTS = {
631
- pattern: "dots",
632
- spacing: 24,
633
- color: "#d0d0d0",
634
- dotRadius: 1,
635
- lineWidth: 0.5
636
- };
637
- var Background = class {
638
- pattern;
639
- spacing;
640
- color;
641
- dotRadius;
642
- lineWidth;
643
- cachedCanvas = null;
644
- cachedCtx = null;
645
- lastZoom = -1;
646
- lastOffsetX = -Infinity;
647
- lastOffsetY = -Infinity;
648
- lastWidth = 0;
649
- lastHeight = 0;
650
- constructor(options = {}) {
651
- this.pattern = options.pattern ?? DEFAULTS.pattern;
652
- this.spacing = options.spacing ?? DEFAULTS.spacing;
653
- this.color = options.color ?? DEFAULTS.color;
654
- this.dotRadius = options.dotRadius ?? DEFAULTS.dotRadius;
655
- this.lineWidth = options.lineWidth ?? DEFAULTS.lineWidth;
656
- }
657
- render(ctx, camera) {
658
- const { width, height } = ctx.canvas;
659
- const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
660
- const cssWidth = width / dpr;
661
- const cssHeight = height / dpr;
662
- ctx.save();
663
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
664
- ctx.clearRect(0, 0, cssWidth, cssHeight);
665
- if (this.pattern === "none") {
666
- ctx.restore();
667
- return;
668
- }
669
- const spacing = this.adaptSpacing(this.spacing, camera.zoom);
670
- const keyZoom = camera.zoom;
671
- const keyX = Math.floor(camera.position.x % spacing);
672
- const keyY = Math.floor(camera.position.y % spacing);
673
- if (this.cachedCanvas !== null && keyZoom === this.lastZoom && keyX === this.lastOffsetX && keyY === this.lastOffsetY && cssWidth === this.lastWidth && cssHeight === this.lastHeight) {
674
- ctx.drawImage(this.cachedCanvas, 0, 0);
675
- ctx.restore();
676
- return;
677
- }
678
- this.ensureCachedCanvas(cssWidth, cssHeight, dpr);
679
- if (this.cachedCtx === null) {
680
- if (this.pattern === "dots") {
681
- this.renderDots(ctx, camera, cssWidth, cssHeight);
682
- } else if (this.pattern === "grid") {
683
- this.renderGrid(ctx, camera, cssWidth, cssHeight);
684
- }
685
- ctx.restore();
686
- return;
687
- }
688
- const offCtx = this.cachedCtx;
689
- offCtx.clearRect(0, 0, cssWidth, cssHeight);
690
- if (this.pattern === "dots") {
691
- this.renderDots(offCtx, camera, cssWidth, cssHeight);
692
- } else if (this.pattern === "grid") {
693
- this.renderGrid(offCtx, camera, cssWidth, cssHeight);
694
- }
695
- this.lastZoom = keyZoom;
696
- this.lastOffsetX = keyX;
697
- this.lastOffsetY = keyY;
698
- this.lastWidth = cssWidth;
699
- this.lastHeight = cssHeight;
700
- ctx.drawImage(this.cachedCanvas, 0, 0);
701
- ctx.restore();
702
- }
703
- ensureCachedCanvas(cssWidth, cssHeight, dpr) {
704
- if (this.cachedCanvas !== null && this.lastWidth === cssWidth && this.lastHeight === cssHeight) {
705
- return;
706
- }
707
- const physWidth = Math.round(cssWidth * dpr);
708
- const physHeight = Math.round(cssHeight * dpr);
709
- if (typeof OffscreenCanvas !== "undefined") {
710
- this.cachedCanvas = new OffscreenCanvas(physWidth, physHeight);
711
- } else if (typeof document !== "undefined") {
712
- const el = document.createElement("canvas");
713
- el.width = physWidth;
714
- el.height = physHeight;
715
- this.cachedCanvas = el;
716
- } else {
717
- this.cachedCanvas = null;
718
- this.cachedCtx = null;
719
- return;
720
- }
721
- const offCtx = this.cachedCanvas.getContext("2d");
722
- if (offCtx !== null) {
723
- offCtx.scale(dpr, dpr);
724
- }
725
- this.cachedCtx = offCtx;
726
- this.lastZoom = -1;
727
- }
728
- adaptSpacing(baseSpacing, zoom) {
729
- let spacing = baseSpacing * zoom;
730
- while (spacing < MIN_PATTERN_SPACING) {
731
- spacing *= 2;
732
- }
733
- return spacing;
734
- }
735
- renderDots(ctx, camera, width, height) {
736
- const spacing = this.adaptSpacing(this.spacing, camera.zoom);
737
- const offsetX = camera.position.x % spacing;
738
- const offsetY = camera.position.y % spacing;
739
- const radius = this.dotRadius * Math.min(camera.zoom, 2);
740
- ctx.fillStyle = this.color;
741
- ctx.beginPath();
742
- for (let x = offsetX; x < width; x += spacing) {
743
- for (let y = offsetY; y < height; y += spacing) {
744
- ctx.moveTo(x + radius, y);
745
- ctx.arc(x, y, radius, 0, Math.PI * 2);
746
- }
747
- }
748
- ctx.fill();
749
- }
750
- renderGrid(ctx, camera, width, height) {
751
- const spacing = this.adaptSpacing(this.spacing, camera.zoom);
752
- const offsetX = camera.position.x % spacing;
753
- const offsetY = camera.position.y % spacing;
754
- const lineW = this.lineWidth * Math.min(camera.zoom, 2);
755
- ctx.fillStyle = this.color;
756
- for (let x = offsetX; x < width; x += spacing) {
757
- ctx.fillRect(x, 0, lineW, height);
758
- }
759
- for (let y = offsetY; y < height; y += spacing) {
760
- ctx.fillRect(0, y, width, lineW);
761
- }
762
- }
763
- };
764
-
765
451
  // src/canvas/input-filter.ts
766
452
  var InputFilter = class _InputFilter {
767
453
  activePenId = null;
@@ -823,62 +509,334 @@ function createId(prefix) {
823
509
  return `${prefix}_${Date.now().toString(36)}_${(counter++).toString(36)}`;
824
510
  }
825
511
 
826
- // src/canvas/keyboard-actions.ts
827
- var KeyboardActions = class {
828
- constructor(deps) {
829
- this.deps = deps;
830
- }
831
- clipboard = [];
832
- pasteCount = 0;
833
- nudgeTimer = null;
834
- nudgeTxId = null;
835
- dispose() {
836
- this.flushPendingNudge();
512
+ // src/core/geometry.ts
513
+ function distSqToSegment(p, a, b) {
514
+ const abx = b.x - a.x;
515
+ const aby = b.y - a.y;
516
+ const apx = p.x - a.x;
517
+ const apy = p.y - a.y;
518
+ const lenSq = abx * abx + aby * aby;
519
+ if (lenSq === 0) {
520
+ return apx * apx + apy * apy;
837
521
  }
838
- selectTool() {
839
- const tm = this.deps.getToolManager();
840
- const ctx = this.deps.getToolContext();
841
- if (!tm || !ctx) return null;
842
- const tool = tm.activeTool;
843
- if (tool?.name !== "select") return null;
844
- return { tool, ctx };
522
+ const t = Math.max(0, Math.min(1, (apx * abx + apy * aby) / lenSq));
523
+ const dx = p.x - (a.x + t * abx);
524
+ const dy = p.y - (a.y + t * aby);
525
+ return dx * dx + dy * dy;
526
+ }
527
+
528
+ // src/elements/arrow-geometry.ts
529
+ function getArrowControlPoint(from, to, bend) {
530
+ const midX = (from.x + to.x) / 2;
531
+ const midY = (from.y + to.y) / 2;
532
+ if (bend === 0) return { x: midX, y: midY };
533
+ const dx = to.x - from.x;
534
+ const dy = to.y - from.y;
535
+ const len = Math.sqrt(dx * dx + dy * dy);
536
+ if (len === 0) return { x: midX, y: midY };
537
+ const perpX = -dy / len;
538
+ const perpY = dx / len;
539
+ return {
540
+ x: midX + perpX * bend,
541
+ y: midY + perpY * bend
542
+ };
543
+ }
544
+ function getArrowMidpoint(from, to, bend) {
545
+ const cp = getArrowControlPoint(from, to, bend);
546
+ return {
547
+ x: 0.25 * from.x + 0.5 * cp.x + 0.25 * to.x,
548
+ y: 0.25 * from.y + 0.5 * cp.y + 0.25 * to.y
549
+ };
550
+ }
551
+ function getBendFromPoint(from, to, dragPoint) {
552
+ const midX = (from.x + to.x) / 2;
553
+ const midY = (from.y + to.y) / 2;
554
+ const dx = to.x - from.x;
555
+ const dy = to.y - from.y;
556
+ const len = Math.sqrt(dx * dx + dy * dy);
557
+ if (len === 0) return 0;
558
+ const perpX = -dy / len;
559
+ const perpY = dx / len;
560
+ return (dragPoint.x - midX) * perpX + (dragPoint.y - midY) * perpY;
561
+ }
562
+ function getArrowTangentAngle(from, to, bend, t) {
563
+ const cp = getArrowControlPoint(from, to, bend);
564
+ const tangentX = 2 * (1 - t) * (cp.x - from.x) + 2 * t * (to.x - cp.x);
565
+ const tangentY = 2 * (1 - t) * (cp.y - from.y) + 2 * t * (to.y - cp.y);
566
+ return Math.atan2(tangentY, tangentX);
567
+ }
568
+ function isNearBezier(point, from, to, bend, threshold) {
569
+ if (bend === 0) return isNearLine(point, from, to, threshold);
570
+ const cp = getArrowControlPoint(from, to, bend);
571
+ const segments = 20;
572
+ for (let i = 0; i < segments; i++) {
573
+ const t0 = i / segments;
574
+ const t1 = (i + 1) / segments;
575
+ const a = bezierPoint(from, cp, to, t0);
576
+ const b = bezierPoint(from, cp, to, t1);
577
+ if (isNearLine(point, a, b, threshold)) return true;
845
578
  }
846
- nudge(dx, dy, byCell) {
847
- if (this.deps.isToolActive()) return false;
848
- const sel = this.selectTool();
849
- if (!sel) return false;
850
- if (sel.tool.selectedIds.length === 0) return false;
851
- const step = byCell ? sel.ctx.gridSize ?? 10 : 1;
852
- if (this.nudgeTimer === null) {
853
- const recorder = this.deps.getHistoryRecorder();
854
- recorder?.begin();
855
- this.nudgeTxId = recorder?.currentTransactionId ?? null;
856
- } else {
857
- clearTimeout(this.nudgeTimer);
858
- }
859
- const moved = sel.tool.nudgeSelection(dx * step, dy * step, sel.ctx);
860
- this.nudgeTimer = setTimeout(() => this.flushPendingNudge(), 400);
861
- return moved;
579
+ return false;
580
+ }
581
+ function getArrowBounds(from, to, bend) {
582
+ if (bend === 0) {
583
+ const minX2 = Math.min(from.x, to.x);
584
+ const minY2 = Math.min(from.y, to.y);
585
+ return {
586
+ x: minX2,
587
+ y: minY2,
588
+ w: Math.abs(to.x - from.x),
589
+ h: Math.abs(to.y - from.y)
590
+ };
862
591
  }
863
- flushPendingNudge() {
864
- if (this.nudgeTimer === null) return;
865
- clearTimeout(this.nudgeTimer);
866
- this.nudgeTimer = null;
867
- const recorder = this.deps.getHistoryRecorder();
868
- if (this.nudgeTxId === null || recorder?.currentTransactionId === this.nudgeTxId) {
869
- recorder?.commit();
870
- }
871
- this.nudgeTxId = null;
592
+ const cp = getArrowControlPoint(from, to, bend);
593
+ const steps = 20;
594
+ let minX = Math.min(from.x, to.x);
595
+ let minY = Math.min(from.y, to.y);
596
+ let maxX = Math.max(from.x, to.x);
597
+ let maxY = Math.max(from.y, to.y);
598
+ for (let i = 1; i < steps; i++) {
599
+ const t = i / steps;
600
+ const p = bezierPoint(from, cp, to, t);
601
+ if (p.x < minX) minX = p.x;
602
+ if (p.y < minY) minY = p.y;
603
+ if (p.x > maxX) maxX = p.x;
604
+ if (p.y > maxY) maxY = p.y;
872
605
  }
873
- deleteSelected() {
874
- if (this.deps.isToolActive()) return;
875
- this.flushPendingNudge();
876
- const sel = this.selectTool();
877
- if (!sel) return;
878
- const ids = sel.tool.selectedIds;
879
- if (ids.length === 0) return;
880
- const recorder = this.deps.getHistoryRecorder();
881
- recorder?.begin();
606
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
607
+ }
608
+ function bezierPoint(from, cp, to, t) {
609
+ const mt = 1 - t;
610
+ return {
611
+ x: mt * mt * from.x + 2 * mt * t * cp.x + t * t * to.x,
612
+ y: mt * mt * from.y + 2 * mt * t * cp.y + t * t * to.y
613
+ };
614
+ }
615
+ function isNearLine(point, a, b, threshold) {
616
+ return distSqToSegment(point, a, b) <= threshold * threshold;
617
+ }
618
+
619
+ // src/elements/element-bounds.ts
620
+ var strokeBoundsCache = /* @__PURE__ */ new WeakMap();
621
+ function getElementBounds(element) {
622
+ if (element.type === "grid") return null;
623
+ if ("size" in element) {
624
+ return {
625
+ x: element.position.x,
626
+ y: element.position.y,
627
+ w: element.size.w,
628
+ h: element.size.h
629
+ };
630
+ }
631
+ if (element.type === "stroke") {
632
+ if (element.points.length === 0) return null;
633
+ const cached = strokeBoundsCache.get(element);
634
+ if (cached) return cached;
635
+ let minX = Infinity;
636
+ let minY = Infinity;
637
+ let maxX = -Infinity;
638
+ let maxY = -Infinity;
639
+ for (const p of element.points) {
640
+ const px = p.x + element.position.x;
641
+ const py = p.y + element.position.y;
642
+ if (px < minX) minX = px;
643
+ if (py < minY) minY = py;
644
+ if (px > maxX) maxX = px;
645
+ if (py > maxY) maxY = py;
646
+ }
647
+ const bounds = { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
648
+ strokeBoundsCache.set(element, bounds);
649
+ return bounds;
650
+ }
651
+ if (element.type === "arrow") {
652
+ return getArrowBoundsAnalytical(element.from, element.to, element.bend);
653
+ }
654
+ if (element.type === "template") {
655
+ return getTemplateBounds(element);
656
+ }
657
+ return null;
658
+ }
659
+ function getArrowBoundsAnalytical(from, to, bend) {
660
+ if (bend === 0) {
661
+ const minX2 = Math.min(from.x, to.x);
662
+ const minY2 = Math.min(from.y, to.y);
663
+ return {
664
+ x: minX2,
665
+ y: minY2,
666
+ w: Math.abs(to.x - from.x),
667
+ h: Math.abs(to.y - from.y)
668
+ };
669
+ }
670
+ const cp = getArrowControlPoint(from, to, bend);
671
+ let minX = Math.min(from.x, to.x);
672
+ let maxX = Math.max(from.x, to.x);
673
+ let minY = Math.min(from.y, to.y);
674
+ let maxY = Math.max(from.y, to.y);
675
+ const tx = from.x - 2 * cp.x + to.x;
676
+ if (tx !== 0) {
677
+ const t = (from.x - cp.x) / tx;
678
+ if (t > 0 && t < 1) {
679
+ const mt = 1 - t;
680
+ const x = mt * mt * from.x + 2 * mt * t * cp.x + t * t * to.x;
681
+ if (x < minX) minX = x;
682
+ if (x > maxX) maxX = x;
683
+ }
684
+ }
685
+ const ty = from.y - 2 * cp.y + to.y;
686
+ if (ty !== 0) {
687
+ const t = (from.y - cp.y) / ty;
688
+ if (t > 0 && t < 1) {
689
+ const mt = 1 - t;
690
+ const y = mt * mt * from.y + 2 * mt * t * cp.y + t * t * to.y;
691
+ if (y < minY) minY = y;
692
+ if (y > maxY) maxY = y;
693
+ }
694
+ }
695
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
696
+ }
697
+ function getTemplateBounds(el) {
698
+ const { x: cx, y: cy } = el.position;
699
+ const r = el.radius;
700
+ switch (el.templateShape) {
701
+ case "circle":
702
+ return { x: cx - r, y: cy - r, w: 2 * r, h: 2 * r };
703
+ case "square":
704
+ return { x: cx - r / 2, y: cy - r / 2, w: r, h: r };
705
+ case "cone": {
706
+ const halfAngle = Math.atan(0.5);
707
+ const tipX = cx;
708
+ const tipY = cy;
709
+ const leftX = cx + r * Math.cos(el.angle - halfAngle);
710
+ const leftY = cy + r * Math.sin(el.angle - halfAngle);
711
+ const rightX = cx + r * Math.cos(el.angle + halfAngle);
712
+ const rightY = cy + r * Math.sin(el.angle + halfAngle);
713
+ const farX = cx + r * Math.cos(el.angle);
714
+ const farY = cy + r * Math.sin(el.angle);
715
+ const xs = [tipX, leftX, rightX, farX];
716
+ const ys = [tipY, leftY, rightY, farY];
717
+ let minX = Infinity;
718
+ let minY = Infinity;
719
+ let maxX = -Infinity;
720
+ let maxY = -Infinity;
721
+ for (let i = 0; i < xs.length; i++) {
722
+ const px = xs[i];
723
+ const py = ys[i];
724
+ if (px !== void 0 && px < minX) minX = px;
725
+ if (px !== void 0 && px > maxX) maxX = px;
726
+ if (py !== void 0 && py < minY) minY = py;
727
+ if (py !== void 0 && py > maxY) maxY = py;
728
+ }
729
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
730
+ }
731
+ case "line": {
732
+ const halfW = r / 12;
733
+ const cos = Math.cos(el.angle);
734
+ const sin = Math.sin(el.angle);
735
+ const perpX = -sin * halfW;
736
+ const perpY = cos * halfW;
737
+ const x0 = cx + perpX;
738
+ const y0 = cy + perpY;
739
+ const x1 = cx + r * cos + perpX;
740
+ const y1 = cy + r * sin + perpY;
741
+ const x2 = cx + r * cos - perpX;
742
+ const y2 = cy + r * sin - perpY;
743
+ const x3 = cx - perpX;
744
+ const y3 = cy - perpY;
745
+ const minX = Math.min(x0, x1, x2, x3);
746
+ const minY = Math.min(y0, y1, y2, y3);
747
+ const maxX = Math.max(x0, x1, x2, x3);
748
+ const maxY = Math.max(y0, y1, y2, y3);
749
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
750
+ }
751
+ }
752
+ }
753
+ function transferStrokeBounds(prev, next) {
754
+ if (prev.type !== "stroke" || next.type !== "stroke") return;
755
+ if (prev.points !== next.points) return;
756
+ if (prev.position.x !== next.position.x || prev.position.y !== next.position.y) return;
757
+ const bounds = strokeBoundsCache.get(prev);
758
+ if (bounds) strokeBoundsCache.set(next, bounds);
759
+ }
760
+ function boundsIntersect(a, b) {
761
+ 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;
762
+ }
763
+
764
+ // src/elements/bounds.ts
765
+ function getElementsBoundingBox(elements) {
766
+ let minX = Infinity;
767
+ let minY = Infinity;
768
+ let maxX = -Infinity;
769
+ let maxY = -Infinity;
770
+ let found = false;
771
+ for (const el of elements) {
772
+ const b = getElementBounds(el);
773
+ if (!b) continue;
774
+ found = true;
775
+ if (b.x < minX) minX = b.x;
776
+ if (b.y < minY) minY = b.y;
777
+ if (b.x + b.w > maxX) maxX = b.x + b.w;
778
+ if (b.y + b.h > maxY) maxY = b.y + b.h;
779
+ }
780
+ if (!found) return null;
781
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
782
+ }
783
+
784
+ // src/canvas/keyboard-actions.ts
785
+ var KeyboardActions = class {
786
+ constructor(deps) {
787
+ this.deps = deps;
788
+ }
789
+ clipboard = [];
790
+ pasteCount = 0;
791
+ nudgeTimer = null;
792
+ nudgeTxId = null;
793
+ dispose() {
794
+ this.flushPendingNudge();
795
+ }
796
+ selectTool() {
797
+ const tm = this.deps.getToolManager();
798
+ const ctx = this.deps.getToolContext();
799
+ if (!tm || !ctx) return null;
800
+ const tool = tm.activeTool;
801
+ if (tool?.name !== "select") return null;
802
+ return { tool, ctx };
803
+ }
804
+ nudge(dx, dy, byCell) {
805
+ if (this.deps.isToolActive()) return false;
806
+ const sel = this.selectTool();
807
+ if (!sel) return false;
808
+ if (sel.tool.selectedIds.length === 0) return false;
809
+ const step = byCell ? sel.ctx.gridSize ?? 10 : 1;
810
+ if (this.nudgeTimer === null) {
811
+ const recorder = this.deps.getHistoryRecorder();
812
+ recorder?.begin();
813
+ this.nudgeTxId = recorder?.currentTransactionId ?? null;
814
+ } else {
815
+ clearTimeout(this.nudgeTimer);
816
+ }
817
+ const moved = sel.tool.nudgeSelection(dx * step, dy * step, sel.ctx);
818
+ this.nudgeTimer = setTimeout(() => this.flushPendingNudge(), 400);
819
+ return moved;
820
+ }
821
+ flushPendingNudge() {
822
+ if (this.nudgeTimer === null) return;
823
+ clearTimeout(this.nudgeTimer);
824
+ this.nudgeTimer = null;
825
+ const recorder = this.deps.getHistoryRecorder();
826
+ if (this.nudgeTxId === null || recorder?.currentTransactionId === this.nudgeTxId) {
827
+ recorder?.commit();
828
+ }
829
+ this.nudgeTxId = null;
830
+ }
831
+ deleteSelected() {
832
+ if (this.deps.isToolActive()) return;
833
+ this.flushPendingNudge();
834
+ const sel = this.selectTool();
835
+ if (!sel) return;
836
+ const ids = sel.tool.selectedIds;
837
+ if (ids.length === 0) return;
838
+ const recorder = this.deps.getHistoryRecorder();
839
+ recorder?.begin();
882
840
  for (const id of ids) {
883
841
  sel.ctx.store.remove(id);
884
842
  }
@@ -928,8 +886,18 @@ var KeyboardActions = class {
928
886
  if (this.clipboard.length === 0) return;
929
887
  const sel = this.selectTool();
930
888
  if (!sel) return;
889
+ const cursor = this.deps.getLastPointerWorld?.() ?? null;
890
+ if (cursor) {
891
+ const bbox = getElementsBoundingBox(this.clipboard);
892
+ if (bbox) {
893
+ const centerX = bbox.x + bbox.w / 2;
894
+ const centerY = bbox.y + bbox.h / 2;
895
+ this.insertClones(this.clipboard, { x: cursor.x - centerX, y: cursor.y - centerY }, sel);
896
+ return;
897
+ }
898
+ }
931
899
  this.pasteCount++;
932
- this.insertClones(this.clipboard, this.pasteCount * 20, sel);
900
+ this.insertClones(this.clipboard, { x: this.pasteCount * 20, y: this.pasteCount * 20 }, sel);
933
901
  }
934
902
  duplicate() {
935
903
  if (this.deps.isToolActive()) return;
@@ -942,7 +910,7 @@ var KeyboardActions = class {
942
910
  if (el) source.push(el);
943
911
  }
944
912
  if (source.length === 0) return;
945
- this.insertClones(source, 20, sel);
913
+ this.insertClones(source, { x: 20, y: 20 }, sel);
946
914
  }
947
915
  deselect() {
948
916
  if (this.deps.isToolActive()) return;
@@ -1013,11 +981,11 @@ var KeyboardActions = class {
1013
981
  const newId = idMap.get(el.id);
1014
982
  if (!newId) continue;
1015
983
  clone.id = newId;
1016
- clone.position = { x: clone.position.x + offset, y: clone.position.y + offset };
984
+ clone.position = { x: clone.position.x + offset.x, y: clone.position.y + offset.y };
1017
985
  if (clone.type === "arrow") {
1018
986
  const arrow = clone;
1019
- arrow.from = { x: arrow.from.x + offset, y: arrow.from.y + offset };
1020
- arrow.to = { x: arrow.to.x + offset, y: arrow.to.y + offset };
987
+ arrow.from = { x: arrow.from.x + offset.x, y: arrow.from.y + offset.y };
988
+ arrow.to = { x: arrow.to.x + offset.x, y: arrow.to.y + offset.y };
1021
989
  delete arrow.cachedControlPoint;
1022
990
  if (arrow.fromBinding) {
1023
991
  const newTarget = idMap.get(arrow.fromBinding.elementId);
@@ -1063,6 +1031,9 @@ var DEFAULT_BINDINGS = [
1063
1031
  ["z-front", ["mod+]"]],
1064
1032
  ["z-back", ["mod+["]],
1065
1033
  ["zoom-fit", ["shift+1"]],
1034
+ ["zoom-in", ["mod+="]],
1035
+ ["zoom-out", ["mod+-"]],
1036
+ ["zoom-reset", ["mod+0"]],
1066
1037
  ["nudge-left", ["arrowleft"]],
1067
1038
  ["nudge-right", ["arrowright"]],
1068
1039
  ["nudge-up", ["arrowup"]],
@@ -1199,6 +1170,7 @@ var ShortcutMap = class {
1199
1170
 
1200
1171
  // src/canvas/input-handler.ts
1201
1172
  var ZOOM_SENSITIVITY = 1e-3;
1173
+ var ZOOM_STEP = 1.2;
1202
1174
  var MIDDLE_BUTTON = 1;
1203
1175
  var NUDGE_DELTAS = {
1204
1176
  "nudge-left": [-1, 0],
@@ -1220,7 +1192,8 @@ var InputHandler = class {
1220
1192
  getHistoryRecorder: () => this.historyRecorder,
1221
1193
  getHistoryStack: () => this.historyStack,
1222
1194
  isToolActive: () => this.isToolActive,
1223
- fitToContent: options.fitToContent
1195
+ fitToContent: options.fitToContent,
1196
+ getLastPointerWorld: () => this.lastPointerWorld()
1224
1197
  });
1225
1198
  this.shortcutMap = new ShortcutMap(options.shortcuts?.bindings);
1226
1199
  this.scope = options.shortcuts?.scope ?? "focus";
@@ -1276,11 +1249,21 @@ var InputHandler = class {
1276
1249
  this.element.addEventListener("pointerdown", this.onPointerDown, opts);
1277
1250
  this.element.addEventListener("pointermove", this.onPointerMove, opts);
1278
1251
  this.element.addEventListener("pointerup", this.onPointerUp, opts);
1279
- this.element.addEventListener("pointerleave", this.onPointerUp, opts);
1252
+ this.element.addEventListener("pointerleave", this.onPointerLeave, opts);
1280
1253
  this.element.addEventListener("pointercancel", this.onPointerUp, opts);
1281
1254
  window.addEventListener("keydown", this.onKeyDown, opts);
1282
1255
  window.addEventListener("keyup", this.onKeyUp, opts);
1283
1256
  }
1257
+ viewportCenter() {
1258
+ const rect = this.element.getBoundingClientRect();
1259
+ return { x: rect.width / 2, y: rect.height / 2 };
1260
+ }
1261
+ zoomByFactor(factor) {
1262
+ this.camera.zoomAt(this.camera.zoom * factor, this.viewportCenter());
1263
+ }
1264
+ zoomToLevel(level) {
1265
+ this.camera.zoomAt(level, this.viewportCenter());
1266
+ }
1284
1267
  onWheel = (e) => {
1285
1268
  e.preventDefault();
1286
1269
  const rect = this.element.getBoundingClientRect();
@@ -1449,6 +1432,18 @@ var InputHandler = class {
1449
1432
  e.preventDefault();
1450
1433
  this.actions.zoomToFit();
1451
1434
  return;
1435
+ case "zoom-in":
1436
+ e.preventDefault();
1437
+ this.zoomByFactor(ZOOM_STEP);
1438
+ return;
1439
+ case "zoom-out":
1440
+ e.preventDefault();
1441
+ this.zoomByFactor(1 / ZOOM_STEP);
1442
+ return;
1443
+ case "zoom-reset":
1444
+ e.preventDefault();
1445
+ this.zoomToLevel(1);
1446
+ return;
1452
1447
  case "nudge-left":
1453
1448
  case "nudge-right":
1454
1449
  case "nudge-up":
@@ -1506,6 +1501,16 @@ var InputHandler = class {
1506
1501
  midpoint(a, b) {
1507
1502
  return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
1508
1503
  }
1504
+ lastPointerWorld() {
1505
+ const e = this.lastPointerEvent;
1506
+ if (!e) return null;
1507
+ const rect = this.element.getBoundingClientRect();
1508
+ return this.camera.screenToWorld({ x: e.clientX - rect.left, y: e.clientY - rect.top });
1509
+ }
1510
+ onPointerLeave = (e) => {
1511
+ this.lastPointerEvent = null;
1512
+ this.onPointerUp(e);
1513
+ };
1509
1514
  toPointerState(e) {
1510
1515
  const rect = this.element.getBoundingClientRect();
1511
1516
  return {
@@ -1557,299 +1562,319 @@ var InputHandler = class {
1557
1562
  }
1558
1563
  };
1559
1564
 
1560
- // src/canvas/double-tap-detector.ts
1561
- var DEFAULT_TIMEOUT = 300;
1562
- var DEFAULT_MAX_DISTANCE = 20;
1563
- var DoubleTapDetector = class {
1564
- timeout;
1565
- maxDistance;
1566
- lastTapTime = 0;
1567
- lastTapX = 0;
1568
- lastTapY = 0;
1569
- hasPendingTap = false;
1570
- constructor(options) {
1571
- this.timeout = options?.timeout ?? DEFAULT_TIMEOUT;
1572
- this.maxDistance = options?.maxDistance ?? DEFAULT_MAX_DISTANCE;
1565
+ // src/canvas/background.ts
1566
+ var MIN_PATTERN_SPACING = 16;
1567
+ var DEFAULTS = {
1568
+ pattern: "dots",
1569
+ spacing: 24,
1570
+ color: "#d0d0d0",
1571
+ dotRadius: 1,
1572
+ lineWidth: 0.5
1573
+ };
1574
+ var Background = class {
1575
+ pattern;
1576
+ spacing;
1577
+ color;
1578
+ dotRadius;
1579
+ lineWidth;
1580
+ cachedCanvas = null;
1581
+ cachedCtx = null;
1582
+ lastZoom = -1;
1583
+ lastOffsetX = -Infinity;
1584
+ lastOffsetY = -Infinity;
1585
+ lastWidth = 0;
1586
+ lastHeight = 0;
1587
+ constructor(options = {}) {
1588
+ this.pattern = options.pattern ?? DEFAULTS.pattern;
1589
+ this.spacing = options.spacing ?? DEFAULTS.spacing;
1590
+ this.color = options.color ?? DEFAULTS.color;
1591
+ this.dotRadius = options.dotRadius ?? DEFAULTS.dotRadius;
1592
+ this.lineWidth = options.lineWidth ?? DEFAULTS.lineWidth;
1573
1593
  }
1574
- feed(e) {
1575
- const now = Date.now();
1576
- const x = e.clientX;
1577
- const y = e.clientY;
1578
- if (this.hasPendingTap) {
1579
- const elapsed = now - this.lastTapTime;
1580
- const dx = x - this.lastTapX;
1581
- const dy = y - this.lastTapY;
1582
- const dist = Math.sqrt(dx * dx + dy * dy);
1583
- if (elapsed <= this.timeout && dist <= this.maxDistance) {
1584
- this.reset();
1585
- return true;
1594
+ render(ctx, camera) {
1595
+ const { width, height } = ctx.canvas;
1596
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
1597
+ const cssWidth = width / dpr;
1598
+ const cssHeight = height / dpr;
1599
+ ctx.save();
1600
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
1601
+ ctx.clearRect(0, 0, cssWidth, cssHeight);
1602
+ if (this.pattern === "none") {
1603
+ ctx.restore();
1604
+ return;
1605
+ }
1606
+ const spacing = this.adaptSpacing(this.spacing, camera.zoom);
1607
+ const keyZoom = camera.zoom;
1608
+ const keyX = Math.floor(camera.position.x % spacing);
1609
+ const keyY = Math.floor(camera.position.y % spacing);
1610
+ if (this.cachedCanvas !== null && keyZoom === this.lastZoom && keyX === this.lastOffsetX && keyY === this.lastOffsetY && cssWidth === this.lastWidth && cssHeight === this.lastHeight) {
1611
+ ctx.drawImage(this.cachedCanvas, 0, 0);
1612
+ ctx.restore();
1613
+ return;
1614
+ }
1615
+ this.ensureCachedCanvas(cssWidth, cssHeight, dpr);
1616
+ if (this.cachedCtx === null) {
1617
+ if (this.pattern === "dots") {
1618
+ this.renderDots(ctx, camera, cssWidth, cssHeight);
1619
+ } else if (this.pattern === "grid") {
1620
+ this.renderGrid(ctx, camera, cssWidth, cssHeight);
1586
1621
  }
1622
+ ctx.restore();
1623
+ return;
1587
1624
  }
1588
- this.lastTapTime = now;
1589
- this.lastTapX = x;
1590
- this.lastTapY = y;
1591
- this.hasPendingTap = true;
1592
- return false;
1625
+ const offCtx = this.cachedCtx;
1626
+ offCtx.clearRect(0, 0, cssWidth, cssHeight);
1627
+ if (this.pattern === "dots") {
1628
+ this.renderDots(offCtx, camera, cssWidth, cssHeight);
1629
+ } else if (this.pattern === "grid") {
1630
+ this.renderGrid(offCtx, camera, cssWidth, cssHeight);
1631
+ }
1632
+ this.lastZoom = keyZoom;
1633
+ this.lastOffsetX = keyX;
1634
+ this.lastOffsetY = keyY;
1635
+ this.lastWidth = cssWidth;
1636
+ this.lastHeight = cssHeight;
1637
+ ctx.drawImage(this.cachedCanvas, 0, 0);
1638
+ ctx.restore();
1593
1639
  }
1594
- reset() {
1595
- this.hasPendingTap = false;
1596
- this.lastTapTime = 0;
1597
- this.lastTapX = 0;
1598
- this.lastTapY = 0;
1640
+ ensureCachedCanvas(cssWidth, cssHeight, dpr) {
1641
+ if (this.cachedCanvas !== null && this.lastWidth === cssWidth && this.lastHeight === cssHeight) {
1642
+ return;
1643
+ }
1644
+ const physWidth = Math.round(cssWidth * dpr);
1645
+ const physHeight = Math.round(cssHeight * dpr);
1646
+ if (typeof OffscreenCanvas !== "undefined") {
1647
+ this.cachedCanvas = new OffscreenCanvas(physWidth, physHeight);
1648
+ } else if (typeof document !== "undefined") {
1649
+ const el = document.createElement("canvas");
1650
+ el.width = physWidth;
1651
+ el.height = physHeight;
1652
+ this.cachedCanvas = el;
1653
+ } else {
1654
+ this.cachedCanvas = null;
1655
+ this.cachedCtx = null;
1656
+ return;
1657
+ }
1658
+ const offCtx = this.cachedCanvas.getContext("2d");
1659
+ if (offCtx !== null) {
1660
+ offCtx.scale(dpr, dpr);
1661
+ }
1662
+ this.cachedCtx = offCtx;
1663
+ this.lastZoom = -1;
1664
+ }
1665
+ adaptSpacing(baseSpacing, zoom) {
1666
+ let spacing = baseSpacing * zoom;
1667
+ while (spacing < MIN_PATTERN_SPACING) {
1668
+ spacing *= 2;
1669
+ }
1670
+ return spacing;
1671
+ }
1672
+ renderDots(ctx, camera, width, height) {
1673
+ const spacing = this.adaptSpacing(this.spacing, camera.zoom);
1674
+ const offsetX = camera.position.x % spacing;
1675
+ const offsetY = camera.position.y % spacing;
1676
+ const radius = this.dotRadius * Math.min(camera.zoom, 2);
1677
+ ctx.fillStyle = this.color;
1678
+ ctx.beginPath();
1679
+ for (let x = offsetX; x < width; x += spacing) {
1680
+ for (let y = offsetY; y < height; y += spacing) {
1681
+ ctx.moveTo(x + radius, y);
1682
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
1683
+ }
1684
+ }
1685
+ ctx.fill();
1686
+ }
1687
+ renderGrid(ctx, camera, width, height) {
1688
+ const spacing = this.adaptSpacing(this.spacing, camera.zoom);
1689
+ const offsetX = camera.position.x % spacing;
1690
+ const offsetY = camera.position.y % spacing;
1691
+ const lineW = this.lineWidth * Math.min(camera.zoom, 2);
1692
+ ctx.fillStyle = this.color;
1693
+ for (let x = offsetX; x < width; x += spacing) {
1694
+ ctx.fillRect(x, 0, lineW, height);
1695
+ }
1696
+ for (let y = offsetY; y < height; y += spacing) {
1697
+ ctx.fillRect(0, y, width, lineW);
1698
+ }
1599
1699
  }
1600
1700
  };
1601
1701
 
1602
- // src/core/geometry.ts
1603
- function distSqToSegment(p, a, b) {
1604
- const abx = b.x - a.x;
1605
- const aby = b.y - a.y;
1606
- const apx = p.x - a.x;
1607
- const apy = p.y - a.y;
1608
- const lenSq = abx * abx + aby * aby;
1609
- if (lenSq === 0) {
1610
- return apx * apx + apy * apy;
1702
+ // src/core/event-bus.ts
1703
+ var EventBus = class {
1704
+ listeners = /* @__PURE__ */ new Map();
1705
+ on(event, listener) {
1706
+ const existing = this.listeners.get(event);
1707
+ if (existing) {
1708
+ existing.add(listener);
1709
+ } else {
1710
+ const set = /* @__PURE__ */ new Set([listener]);
1711
+ this.listeners.set(event, set);
1712
+ }
1713
+ return () => this.off(event, listener);
1611
1714
  }
1612
- const t = Math.max(0, Math.min(1, (apx * abx + apy * aby) / lenSq));
1613
- const dx = p.x - (a.x + t * abx);
1614
- const dy = p.y - (a.y + t * aby);
1615
- return dx * dx + dy * dy;
1616
- }
1715
+ off(event, listener) {
1716
+ this.listeners.get(event)?.delete(listener);
1717
+ }
1718
+ emit(event, data) {
1719
+ this.listeners.get(event)?.forEach((listener) => {
1720
+ try {
1721
+ listener(data);
1722
+ } catch (err) {
1723
+ console.error(`[fieldnotes] listener error for "${String(event)}"`, err);
1724
+ }
1725
+ });
1726
+ }
1727
+ clear() {
1728
+ this.listeners.clear();
1729
+ }
1730
+ };
1617
1731
 
1618
- // src/elements/arrow-geometry.ts
1619
- function getArrowControlPoint(from, to, bend) {
1620
- const midX = (from.x + to.x) / 2;
1621
- const midY = (from.y + to.y) / 2;
1622
- if (bend === 0) return { x: midX, y: midY };
1623
- const dx = to.x - from.x;
1624
- const dy = to.y - from.y;
1625
- const len = Math.sqrt(dx * dx + dy * dy);
1626
- if (len === 0) return { x: midX, y: midY };
1627
- const perpX = -dy / len;
1628
- const perpY = dx / len;
1629
- return {
1630
- x: midX + perpX * bend,
1631
- y: midY + perpY * bend
1632
- };
1633
- }
1634
- function getArrowMidpoint(from, to, bend) {
1635
- const cp = getArrowControlPoint(from, to, bend);
1636
- return {
1637
- x: 0.25 * from.x + 0.5 * cp.x + 0.25 * to.x,
1638
- y: 0.25 * from.y + 0.5 * cp.y + 0.25 * to.y
1639
- };
1640
- }
1641
- function getBendFromPoint(from, to, dragPoint) {
1642
- const midX = (from.x + to.x) / 2;
1643
- const midY = (from.y + to.y) / 2;
1644
- const dx = to.x - from.x;
1645
- const dy = to.y - from.y;
1646
- const len = Math.sqrt(dx * dx + dy * dy);
1647
- if (len === 0) return 0;
1648
- const perpX = -dy / len;
1649
- const perpY = dx / len;
1650
- return (dragPoint.x - midX) * perpX + (dragPoint.y - midY) * perpY;
1651
- }
1652
- function getArrowTangentAngle(from, to, bend, t) {
1653
- const cp = getArrowControlPoint(from, to, bend);
1654
- const tangentX = 2 * (1 - t) * (cp.x - from.x) + 2 * t * (to.x - cp.x);
1655
- const tangentY = 2 * (1 - t) * (cp.y - from.y) + 2 * t * (to.y - cp.y);
1656
- return Math.atan2(tangentY, tangentX);
1732
+ // src/core/quadtree.ts
1733
+ var MAX_ITEMS = 8;
1734
+ var MAX_DEPTH = 8;
1735
+ function intersects(a, b) {
1736
+ 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;
1657
1737
  }
1658
- function isNearBezier(point, from, to, bend, threshold) {
1659
- if (bend === 0) return isNearLine(point, from, to, threshold);
1660
- const cp = getArrowControlPoint(from, to, bend);
1661
- const segments = 20;
1662
- for (let i = 0; i < segments; i++) {
1663
- const t0 = i / segments;
1664
- const t1 = (i + 1) / segments;
1665
- const a = bezierPoint(from, cp, to, t0);
1666
- const b = bezierPoint(from, cp, to, t1);
1667
- if (isNearLine(point, a, b, threshold)) return true;
1738
+ var QuadNode = class _QuadNode {
1739
+ constructor(bounds, depth) {
1740
+ this.bounds = bounds;
1741
+ this.depth = depth;
1668
1742
  }
1669
- return false;
1670
- }
1671
- function getArrowBounds(from, to, bend) {
1672
- if (bend === 0) {
1673
- const minX2 = Math.min(from.x, to.x);
1674
- const minY2 = Math.min(from.y, to.y);
1675
- return {
1676
- x: minX2,
1677
- y: minY2,
1678
- w: Math.abs(to.x - from.x),
1679
- h: Math.abs(to.y - from.y)
1680
- };
1743
+ items = [];
1744
+ children = null;
1745
+ insert(entry) {
1746
+ if (this.children) {
1747
+ const idx = this.getChildIndex(entry.bounds);
1748
+ if (idx !== -1) {
1749
+ const child = this.children[idx];
1750
+ if (child) child.insert(entry);
1751
+ return;
1752
+ }
1753
+ this.items.push(entry);
1754
+ return;
1755
+ }
1756
+ this.items.push(entry);
1757
+ if (this.items.length > MAX_ITEMS && this.depth < MAX_DEPTH) {
1758
+ this.split();
1759
+ }
1681
1760
  }
1682
- const cp = getArrowControlPoint(from, to, bend);
1683
- const steps = 20;
1684
- let minX = Math.min(from.x, to.x);
1685
- let minY = Math.min(from.y, to.y);
1686
- let maxX = Math.max(from.x, to.x);
1687
- let maxY = Math.max(from.y, to.y);
1688
- for (let i = 1; i < steps; i++) {
1689
- const t = i / steps;
1690
- const p = bezierPoint(from, cp, to, t);
1691
- if (p.x < minX) minX = p.x;
1692
- if (p.y < minY) minY = p.y;
1693
- if (p.x > maxX) maxX = p.x;
1694
- if (p.y > maxY) maxY = p.y;
1761
+ remove(id) {
1762
+ const idx = this.items.findIndex((e) => e.id === id);
1763
+ if (idx !== -1) {
1764
+ this.items.splice(idx, 1);
1765
+ return true;
1766
+ }
1767
+ if (this.children) {
1768
+ for (const child of this.children) {
1769
+ if (child.remove(id)) {
1770
+ this.collapseIfEmpty();
1771
+ return true;
1772
+ }
1773
+ }
1774
+ }
1775
+ return false;
1695
1776
  }
1696
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1697
- }
1698
- function bezierPoint(from, cp, to, t) {
1699
- const mt = 1 - t;
1700
- return {
1701
- x: mt * mt * from.x + 2 * mt * t * cp.x + t * t * to.x,
1702
- y: mt * mt * from.y + 2 * mt * t * cp.y + t * t * to.y
1703
- };
1704
- }
1705
- function isNearLine(point, a, b, threshold) {
1706
- return distSqToSegment(point, a, b) <= threshold * threshold;
1707
- }
1708
-
1709
- // src/elements/element-bounds.ts
1710
- var strokeBoundsCache = /* @__PURE__ */ new WeakMap();
1711
- function getElementBounds(element) {
1712
- if (element.type === "grid") return null;
1713
- if ("size" in element) {
1714
- return {
1715
- x: element.position.x,
1716
- y: element.position.y,
1717
- w: element.size.w,
1718
- h: element.size.h
1719
- };
1777
+ query(rect, result) {
1778
+ if (!intersects(this.bounds, rect)) return;
1779
+ for (const item of this.items) {
1780
+ if (intersects(item.bounds, rect)) {
1781
+ result.push(item.id);
1782
+ }
1783
+ }
1784
+ if (this.children) {
1785
+ for (const child of this.children) {
1786
+ child.query(rect, result);
1787
+ }
1788
+ }
1720
1789
  }
1721
- if (element.type === "stroke") {
1722
- if (element.points.length === 0) return null;
1723
- const cached = strokeBoundsCache.get(element);
1724
- if (cached) return cached;
1725
- let minX = Infinity;
1726
- let minY = Infinity;
1727
- let maxX = -Infinity;
1728
- let maxY = -Infinity;
1729
- for (const p of element.points) {
1730
- const px = p.x + element.position.x;
1731
- const py = p.y + element.position.y;
1732
- if (px < minX) minX = px;
1733
- if (py < minY) minY = py;
1734
- if (px > maxX) maxX = px;
1735
- if (py > maxY) maxY = py;
1790
+ getChildIndex(itemBounds) {
1791
+ const midX = this.bounds.x + this.bounds.w / 2;
1792
+ const midY = this.bounds.y + this.bounds.h / 2;
1793
+ const left = itemBounds.x >= this.bounds.x && itemBounds.x + itemBounds.w <= midX;
1794
+ const right = itemBounds.x >= midX && itemBounds.x + itemBounds.w <= this.bounds.x + this.bounds.w;
1795
+ const top = itemBounds.y >= this.bounds.y && itemBounds.y + itemBounds.h <= midY;
1796
+ const bottom = itemBounds.y >= midY && itemBounds.y + itemBounds.h <= this.bounds.y + this.bounds.h;
1797
+ if (left && top) return 0;
1798
+ if (right && top) return 1;
1799
+ if (left && bottom) return 2;
1800
+ if (right && bottom) return 3;
1801
+ return -1;
1802
+ }
1803
+ split() {
1804
+ const { x, y, w, h } = this.bounds;
1805
+ const halfW = w / 2;
1806
+ const halfH = h / 2;
1807
+ const d = this.depth + 1;
1808
+ this.children = [
1809
+ new _QuadNode({ x, y, w: halfW, h: halfH }, d),
1810
+ new _QuadNode({ x: x + halfW, y, w: halfW, h: halfH }, d),
1811
+ new _QuadNode({ x, y: y + halfH, w: halfW, h: halfH }, d),
1812
+ new _QuadNode({ x: x + halfW, y: y + halfH, w: halfW, h: halfH }, d)
1813
+ ];
1814
+ const remaining = [];
1815
+ for (const item of this.items) {
1816
+ const idx = this.getChildIndex(item.bounds);
1817
+ if (idx !== -1) {
1818
+ const target = this.children[idx];
1819
+ if (target) target.insert(item);
1820
+ } else {
1821
+ remaining.push(item);
1822
+ }
1736
1823
  }
1737
- const bounds = { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1738
- strokeBoundsCache.set(element, bounds);
1739
- return bounds;
1824
+ this.items = remaining;
1740
1825
  }
1741
- if (element.type === "arrow") {
1742
- return getArrowBoundsAnalytical(element.from, element.to, element.bend);
1826
+ collapseIfEmpty() {
1827
+ if (!this.children) return;
1828
+ let totalItems = this.items.length;
1829
+ for (const child of this.children) {
1830
+ if (child.children) return;
1831
+ totalItems += child.items.length;
1832
+ }
1833
+ if (totalItems <= MAX_ITEMS) {
1834
+ for (const child of this.children) {
1835
+ this.items.push(...child.items);
1836
+ }
1837
+ this.children = null;
1838
+ }
1743
1839
  }
1744
- if (element.type === "template") {
1745
- return getTemplateBounds(element);
1840
+ };
1841
+ var Quadtree = class {
1842
+ root;
1843
+ _size = 0;
1844
+ worldBounds;
1845
+ constructor(worldBounds) {
1846
+ this.worldBounds = worldBounds;
1847
+ this.root = new QuadNode(worldBounds, 0);
1746
1848
  }
1747
- return null;
1748
- }
1749
- function getArrowBoundsAnalytical(from, to, bend) {
1750
- if (bend === 0) {
1751
- const minX2 = Math.min(from.x, to.x);
1752
- const minY2 = Math.min(from.y, to.y);
1753
- return {
1754
- x: minX2,
1755
- y: minY2,
1756
- w: Math.abs(to.x - from.x),
1757
- h: Math.abs(to.y - from.y)
1758
- };
1849
+ get size() {
1850
+ return this._size;
1759
1851
  }
1760
- const cp = getArrowControlPoint(from, to, bend);
1761
- let minX = Math.min(from.x, to.x);
1762
- let maxX = Math.max(from.x, to.x);
1763
- let minY = Math.min(from.y, to.y);
1764
- let maxY = Math.max(from.y, to.y);
1765
- const tx = from.x - 2 * cp.x + to.x;
1766
- if (tx !== 0) {
1767
- const t = (from.x - cp.x) / tx;
1768
- if (t > 0 && t < 1) {
1769
- const mt = 1 - t;
1770
- const x = mt * mt * from.x + 2 * mt * t * cp.x + t * t * to.x;
1771
- if (x < minX) minX = x;
1772
- if (x > maxX) maxX = x;
1773
- }
1852
+ insert(id, bounds) {
1853
+ this.root.insert({ id, bounds });
1854
+ this._size++;
1774
1855
  }
1775
- const ty = from.y - 2 * cp.y + to.y;
1776
- if (ty !== 0) {
1777
- const t = (from.y - cp.y) / ty;
1778
- if (t > 0 && t < 1) {
1779
- const mt = 1 - t;
1780
- const y = mt * mt * from.y + 2 * mt * t * cp.y + t * t * to.y;
1781
- if (y < minY) minY = y;
1782
- if (y > maxY) maxY = y;
1856
+ remove(id) {
1857
+ if (this.root.remove(id)) {
1858
+ this._size--;
1783
1859
  }
1784
1860
  }
1785
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1786
- }
1787
- function getTemplateBounds(el) {
1788
- const { x: cx, y: cy } = el.position;
1789
- const r = el.radius;
1790
- switch (el.templateShape) {
1791
- case "circle":
1792
- return { x: cx - r, y: cy - r, w: 2 * r, h: 2 * r };
1793
- case "square":
1794
- return { x: cx - r / 2, y: cy - r / 2, w: r, h: r };
1795
- case "cone": {
1796
- const halfAngle = Math.atan(0.5);
1797
- const tipX = cx;
1798
- const tipY = cy;
1799
- const leftX = cx + r * Math.cos(el.angle - halfAngle);
1800
- const leftY = cy + r * Math.sin(el.angle - halfAngle);
1801
- const rightX = cx + r * Math.cos(el.angle + halfAngle);
1802
- const rightY = cy + r * Math.sin(el.angle + halfAngle);
1803
- const farX = cx + r * Math.cos(el.angle);
1804
- const farY = cy + r * Math.sin(el.angle);
1805
- const xs = [tipX, leftX, rightX, farX];
1806
- const ys = [tipY, leftY, rightY, farY];
1807
- let minX = Infinity;
1808
- let minY = Infinity;
1809
- let maxX = -Infinity;
1810
- let maxY = -Infinity;
1811
- for (let i = 0; i < xs.length; i++) {
1812
- const px = xs[i];
1813
- const py = ys[i];
1814
- if (px !== void 0 && px < minX) minX = px;
1815
- if (px !== void 0 && px > maxX) maxX = px;
1816
- if (py !== void 0 && py < minY) minY = py;
1817
- if (py !== void 0 && py > maxY) maxY = py;
1818
- }
1819
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1820
- }
1821
- case "line": {
1822
- const halfW = r / 12;
1823
- const cos = Math.cos(el.angle);
1824
- const sin = Math.sin(el.angle);
1825
- const perpX = -sin * halfW;
1826
- const perpY = cos * halfW;
1827
- const x0 = cx + perpX;
1828
- const y0 = cy + perpY;
1829
- const x1 = cx + r * cos + perpX;
1830
- const y1 = cy + r * sin + perpY;
1831
- const x2 = cx + r * cos - perpX;
1832
- const y2 = cy + r * sin - perpY;
1833
- const x3 = cx - perpX;
1834
- const y3 = cy - perpY;
1835
- const minX = Math.min(x0, x1, x2, x3);
1836
- const minY = Math.min(y0, y1, y2, y3);
1837
- const maxX = Math.max(x0, x1, x2, x3);
1838
- const maxY = Math.max(y0, y1, y2, y3);
1839
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1840
- }
1861
+ update(id, newBounds) {
1862
+ this.remove(id);
1863
+ this.insert(id, newBounds);
1841
1864
  }
1842
- }
1843
- function transferStrokeBounds(prev, next) {
1844
- if (prev.type !== "stroke" || next.type !== "stroke") return;
1845
- if (prev.points !== next.points) return;
1846
- if (prev.position.x !== next.position.x || prev.position.y !== next.position.y) return;
1847
- const bounds = strokeBoundsCache.get(prev);
1848
- if (bounds) strokeBoundsCache.set(next, bounds);
1849
- }
1850
- function boundsIntersect(a, b) {
1851
- 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;
1852
- }
1865
+ query(rect) {
1866
+ const result = [];
1867
+ this.root.query(rect, result);
1868
+ return result;
1869
+ }
1870
+ queryPoint(point) {
1871
+ return this.query({ x: point.x, y: point.y, w: 0, h: 0 });
1872
+ }
1873
+ clear() {
1874
+ this.root = new QuadNode(this.worldBounds, 0);
1875
+ this._size = 0;
1876
+ }
1877
+ };
1853
1878
 
1854
1879
  // src/elements/stroke-smoothing.ts
1855
1880
  var MIN_PRESSURE_SCALE = 0.2;
@@ -2244,51 +2269,6 @@ function updateBoundArrow(arrow, store) {
2244
2269
  }
2245
2270
  return Object.keys(updates).length > 0 ? updates : null;
2246
2271
  }
2247
- function clearStaleBindings(arrow, store) {
2248
- const updates = {};
2249
- let hasUpdates = false;
2250
- if (arrow.fromBinding && !store.getById(arrow.fromBinding.elementId)) {
2251
- updates.fromBinding = void 0;
2252
- hasUpdates = true;
2253
- }
2254
- if (arrow.toBinding && !store.getById(arrow.toBinding.elementId)) {
2255
- updates.toBinding = void 0;
2256
- hasUpdates = true;
2257
- }
2258
- return hasUpdates ? updates : null;
2259
- }
2260
- function unbindArrow(arrow, store) {
2261
- const updates = {};
2262
- if (arrow.fromBinding) {
2263
- const el = store.getById(arrow.fromBinding.elementId);
2264
- const bounds = el ? getElementBounds(el) : null;
2265
- if (bounds) {
2266
- const angle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 0);
2267
- const rayTarget = {
2268
- x: arrow.from.x + Math.cos(angle) * 1e3,
2269
- y: arrow.from.y + Math.sin(angle) * 1e3
2270
- };
2271
- const edge = getEdgeIntersection(bounds, rayTarget);
2272
- updates.from = edge;
2273
- updates.position = edge;
2274
- }
2275
- updates.fromBinding = void 0;
2276
- }
2277
- if (arrow.toBinding) {
2278
- const el = store.getById(arrow.toBinding.elementId);
2279
- const bounds = el ? getElementBounds(el) : null;
2280
- if (bounds) {
2281
- const angle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 1);
2282
- const rayTarget = {
2283
- x: arrow.to.x - Math.cos(angle) * 1e3,
2284
- y: arrow.to.y - Math.sin(angle) * 1e3
2285
- };
2286
- updates.to = getEdgeIntersection(bounds, rayTarget);
2287
- }
2288
- updates.toBinding = void 0;
2289
- }
2290
- return updates;
2291
- }
2292
2272
 
2293
2273
  // src/elements/grid-renderer.ts
2294
2274
  function getSquareGridLines(bounds, cellSize) {
@@ -3516,6 +3496,8 @@ var NoteEditor = class {
3516
3496
  inputHandler = null;
3517
3497
  pendingEditId = null;
3518
3498
  onStopCallback = null;
3499
+ beginHistory = null;
3500
+ commitHistory = null;
3519
3501
  toolbar;
3520
3502
  placeholder;
3521
3503
  constructor(options) {
@@ -3531,6 +3513,10 @@ var NoteEditor = class {
3531
3513
  setOnStop(callback) {
3532
3514
  this.onStopCallback = callback;
3533
3515
  }
3516
+ setHistoryHooks(begin, commit) {
3517
+ this.beginHistory = begin;
3518
+ this.commitHistory = commit;
3519
+ }
3534
3520
  startEditing(node, elementId, store) {
3535
3521
  if (this.editingId === elementId) return;
3536
3522
  if (this.editingId) {
@@ -3562,18 +3548,21 @@ var NoteEditor = class {
3562
3548
  this.editingNode.removeAttribute("data-fn-empty");
3563
3549
  const text = sanitizeNoteHtml(this.editingNode.innerHTML);
3564
3550
  const current = store.getById(this.editingId);
3565
- if (current && (current.type === "note" || current.type === "text") && current.text !== text) {
3566
- store.update(this.editingId, { text });
3567
- }
3551
+ const textChanged = !!current && (current.type === "note" || current.type === "text") && current.text !== text;
3568
3552
  this.editingNode.contentEditable = "false";
3569
3553
  Object.assign(this.editingNode.style, {
3570
3554
  userSelect: "none",
3571
3555
  cursor: "default"
3572
3556
  });
3573
3557
  this.toolbar?.hide();
3558
+ this.beginHistory?.();
3559
+ if (textChanged) {
3560
+ store.update(this.editingId, { text });
3561
+ }
3574
3562
  if (this.editingId && this.onStopCallback) {
3575
3563
  this.onStopCallback(this.editingId);
3576
3564
  }
3565
+ this.commitHistory?.();
3577
3566
  this.editingId = null;
3578
3567
  this.editingNode = null;
3579
3568
  this.blurHandler = null;
@@ -3646,26 +3635,6 @@ var NoteEditor = class {
3646
3635
  }
3647
3636
  };
3648
3637
 
3649
- // src/elements/bounds.ts
3650
- function getElementsBoundingBox(elements) {
3651
- let minX = Infinity;
3652
- let minY = Infinity;
3653
- let maxX = -Infinity;
3654
- let maxY = -Infinity;
3655
- let found = false;
3656
- for (const el of elements) {
3657
- const b = getElementBounds(el);
3658
- if (!b) continue;
3659
- found = true;
3660
- if (b.x < minX) minX = b.x;
3661
- if (b.y < minY) minY = b.y;
3662
- if (b.x + b.w > maxX) maxX = b.x + b.w;
3663
- if (b.y + b.h > maxY) maxY = b.y + b.h;
3664
- }
3665
- if (!found) return null;
3666
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
3667
- }
3668
-
3669
3638
  // src/tools/tool-manager.ts
3670
3639
  var ToolManager = class {
3671
3640
  tools = /* @__PURE__ */ new Map();
@@ -4481,6 +4450,48 @@ var InteractMode = class {
4481
4450
  };
4482
4451
  };
4483
4452
 
4453
+ // src/canvas/double-tap-detector.ts
4454
+ var DEFAULT_TIMEOUT = 300;
4455
+ var DEFAULT_MAX_DISTANCE = 20;
4456
+ var DoubleTapDetector = class {
4457
+ timeout;
4458
+ maxDistance;
4459
+ lastTapTime = 0;
4460
+ lastTapX = 0;
4461
+ lastTapY = 0;
4462
+ hasPendingTap = false;
4463
+ constructor(options) {
4464
+ this.timeout = options?.timeout ?? DEFAULT_TIMEOUT;
4465
+ this.maxDistance = options?.maxDistance ?? DEFAULT_MAX_DISTANCE;
4466
+ }
4467
+ feed(e) {
4468
+ const now = Date.now();
4469
+ const x = e.clientX;
4470
+ const y = e.clientY;
4471
+ if (this.hasPendingTap) {
4472
+ const elapsed = now - this.lastTapTime;
4473
+ const dx = x - this.lastTapX;
4474
+ const dy = y - this.lastTapY;
4475
+ const dist = Math.sqrt(dx * dx + dy * dy);
4476
+ if (elapsed <= this.timeout && dist <= this.maxDistance) {
4477
+ this.reset();
4478
+ return true;
4479
+ }
4480
+ }
4481
+ this.lastTapTime = now;
4482
+ this.lastTapX = x;
4483
+ this.lastTapY = y;
4484
+ this.hasPendingTap = true;
4485
+ return false;
4486
+ }
4487
+ reset() {
4488
+ this.hasPendingTap = false;
4489
+ this.lastTapTime = 0;
4490
+ this.lastTapX = 0;
4491
+ this.lastTapY = 0;
4492
+ }
4493
+ };
4494
+
4484
4495
  // src/canvas/dom-node-manager.ts
4485
4496
  var DomNodeManager = class {
4486
4497
  domNodes = /* @__PURE__ */ new Map();
@@ -5178,6 +5189,10 @@ var Viewport = class {
5178
5189
  placeholder: options.placeholder
5179
5190
  });
5180
5191
  this.noteEditor.setOnStop((id) => this.onTextEditStop(id));
5192
+ this.noteEditor.setHistoryHooks(
5193
+ () => this.historyRecorder.begin(),
5194
+ () => this.historyRecorder.commit()
5195
+ );
5181
5196
  this.onHtmlElementMount = options.onHtmlElementMount;
5182
5197
  this.dropHandler = options.onDrop;
5183
5198
  this.history = new HistoryStack();
@@ -5194,6 +5209,7 @@ var Viewport = class {
5194
5209
  requestRender: () => this.requestRender(),
5195
5210
  switchTool: (name) => this.toolManager.setTool(name, this.toolContext),
5196
5211
  editElement: (id) => this.startEditingElement(id),
5212
+ fitNoteHeight: (id) => this.fitNoteHeight(id),
5197
5213
  setCursor: (cursor) => {
5198
5214
  this.wrapper.style.cursor = cursor;
5199
5215
  },
@@ -5528,31 +5544,38 @@ var Viewport = class {
5528
5544
  this.noteEditor.startEditing(node, id, this.store);
5529
5545
  }
5530
5546
  }
5547
+ fitNoteHeight(elementId) {
5548
+ const element = this.store.getById(elementId);
5549
+ if (!element || element.type !== "note") return;
5550
+ if (isNoteContentEmpty(element.text)) return;
5551
+ const node = this.domNodeManager.getNode(elementId);
5552
+ if (!node) return;
5553
+ const measured = node.scrollHeight;
5554
+ if (measured > element.size.h) {
5555
+ this.store.update(elementId, { size: { w: element.size.w, h: measured } });
5556
+ }
5557
+ }
5531
5558
  onTextEditStop(elementId) {
5532
5559
  const element = this.store.getById(elementId);
5533
5560
  if (!element) return;
5534
5561
  if (element.type === "note") {
5535
5562
  if (isNoteContentEmpty(element.text)) {
5536
- this.historyRecorder.begin();
5537
5563
  this.store.remove(elementId);
5538
- this.historyRecorder.commit();
5564
+ return;
5539
5565
  }
5566
+ this.fitNoteHeight(elementId);
5540
5567
  return;
5541
5568
  }
5542
5569
  if (element.type !== "text") return;
5543
5570
  if (!element.text || element.text.trim() === "") {
5544
- this.historyRecorder.begin();
5545
5571
  this.store.remove(elementId);
5546
- this.historyRecorder.commit();
5547
5572
  return;
5548
5573
  }
5549
5574
  const node = this.domNodeManager.getNode(elementId);
5550
5575
  if (node && "size" in element) {
5551
- const measuredHeight = node.scrollHeight;
5552
- if (measuredHeight !== element.size.h) {
5553
- this.store.update(elementId, {
5554
- size: { w: element.size.w, h: measuredHeight }
5555
- });
5576
+ const measured = node.scrollHeight;
5577
+ if (measured !== element.size.h) {
5578
+ this.store.update(elementId, { size: { w: element.size.w, h: measured } });
5556
5579
  }
5557
5580
  }
5558
5581
  }
@@ -6269,8 +6292,13 @@ var SelectTool = class {
6269
6292
  }
6270
6293
  this.pendingSingleSelectId = null;
6271
6294
  this.hasDragged = false;
6295
+ const resizedNoteId = this.mode.type === "resizing" ? this.mode.elementId : null;
6272
6296
  this.mode = { type: "idle" };
6273
6297
  ctx.setCursor?.("default");
6298
+ if (resizedNoteId !== null) {
6299
+ const el = ctx.store.getById(resizedNoteId);
6300
+ if (el?.type === "note") ctx.fitNoteHeight?.(resizedNoteId);
6301
+ }
6274
6302
  }
6275
6303
  onHover(state, ctx) {
6276
6304
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
@@ -7482,52 +7510,32 @@ var TemplateTool = class {
7482
7510
  };
7483
7511
 
7484
7512
  // src/index.ts
7485
- var VERSION = "0.24.0";
7513
+ var VERSION = "0.26.0";
7486
7514
  export {
7487
- AddElementCommand,
7488
7515
  ArrowTool,
7489
7516
  AutoSave,
7490
- Background,
7491
- BatchCommand,
7492
7517
  Camera,
7493
- CreateLayerCommand,
7494
- DEFAULT_FONT_SIZE_PRESETS,
7495
7518
  DEFAULT_NOTE_FONT_SIZE,
7496
- DoubleTapDetector,
7497
- ElementRenderer,
7498
7519
  ElementStore,
7499
7520
  EraserTool,
7500
- EventBus,
7501
7521
  HandTool,
7502
- HistoryRecorder,
7503
7522
  HistoryStack,
7504
7523
  ImageTool,
7505
- InputFilter,
7506
- InputHandler,
7507
7524
  LayerManager,
7508
7525
  MeasureTool,
7509
- NoteEditor,
7510
7526
  NoteTool,
7511
- NoteToolbar,
7512
7527
  PencilTool,
7513
- Quadtree,
7514
- RemoveElementCommand,
7515
- RemoveLayerCommand,
7516
7528
  SelectTool,
7517
7529
  ShapeTool,
7518
7530
  TemplateTool,
7519
7531
  TextTool,
7520
7532
  ToolManager,
7521
- UpdateElementCommand,
7522
- UpdateLayerCommand,
7523
7533
  VERSION,
7524
7534
  Viewport,
7525
7535
  boundsIntersect,
7526
- clearStaleBindings,
7527
7536
  createArrow,
7528
7537
  createGrid,
7529
7538
  createHtmlElement,
7530
- createId,
7531
7539
  createImage,
7532
7540
  createNote,
7533
7541
  createShape,
@@ -7536,29 +7544,20 @@ export {
7536
7544
  createText,
7537
7545
  drawHexPath,
7538
7546
  exportImage,
7539
- exportState,
7540
- findBindTarget,
7541
- findBoundArrows,
7542
7547
  getActiveFormats,
7543
7548
  getArrowBounds,
7544
7549
  getArrowControlPoint,
7545
7550
  getArrowMidpoint,
7546
7551
  getArrowTangentAngle,
7547
7552
  getBendFromPoint,
7548
- getEdgeIntersection,
7549
7553
  getElementBounds,
7550
- getElementCenter,
7551
7554
  getElementsBoundingBox,
7552
7555
  getHexCellsInCone,
7553
7556
  getHexCellsInLine,
7554
7557
  getHexCellsInRadius,
7555
7558
  getHexCellsInSquare,
7556
7559
  getHexDistance,
7557
- isBindable,
7558
7560
  isNearBezier,
7559
- isNoteContentEmpty,
7560
- parseState,
7561
- sanitizeNoteHtml,
7562
7561
  setFontSize,
7563
7562
  smartSnap,
7564
7563
  snapPoint,
@@ -7566,8 +7565,6 @@ export {
7566
7565
  toggleBold,
7567
7566
  toggleItalic,
7568
7567
  toggleStrikethrough,
7569
- toggleUnderline,
7570
- unbindArrow,
7571
- updateBoundArrow
7568
+ toggleUnderline
7572
7569
  };
7573
7570
  //# sourceMappingURL=index.js.map