@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.cjs CHANGED
@@ -20,50 +20,30 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
- AddElementCommand: () => AddElementCommand,
24
23
  ArrowTool: () => ArrowTool,
25
24
  AutoSave: () => AutoSave,
26
- Background: () => Background,
27
- BatchCommand: () => BatchCommand,
28
25
  Camera: () => Camera,
29
- CreateLayerCommand: () => CreateLayerCommand,
30
- DEFAULT_FONT_SIZE_PRESETS: () => DEFAULT_FONT_SIZE_PRESETS,
31
26
  DEFAULT_NOTE_FONT_SIZE: () => DEFAULT_NOTE_FONT_SIZE,
32
- DoubleTapDetector: () => DoubleTapDetector,
33
- ElementRenderer: () => ElementRenderer,
34
27
  ElementStore: () => ElementStore,
35
28
  EraserTool: () => EraserTool,
36
- EventBus: () => EventBus,
37
29
  HandTool: () => HandTool,
38
- HistoryRecorder: () => HistoryRecorder,
39
30
  HistoryStack: () => HistoryStack,
40
31
  ImageTool: () => ImageTool,
41
- InputFilter: () => InputFilter,
42
- InputHandler: () => InputHandler,
43
32
  LayerManager: () => LayerManager,
44
33
  MeasureTool: () => MeasureTool,
45
- NoteEditor: () => NoteEditor,
46
34
  NoteTool: () => NoteTool,
47
- NoteToolbar: () => NoteToolbar,
48
35
  PencilTool: () => PencilTool,
49
- Quadtree: () => Quadtree,
50
- RemoveElementCommand: () => RemoveElementCommand,
51
- RemoveLayerCommand: () => RemoveLayerCommand,
52
36
  SelectTool: () => SelectTool,
53
37
  ShapeTool: () => ShapeTool,
54
38
  TemplateTool: () => TemplateTool,
55
39
  TextTool: () => TextTool,
56
40
  ToolManager: () => ToolManager,
57
- UpdateElementCommand: () => UpdateElementCommand,
58
- UpdateLayerCommand: () => UpdateLayerCommand,
59
41
  VERSION: () => VERSION,
60
42
  Viewport: () => Viewport,
61
43
  boundsIntersect: () => boundsIntersect,
62
- clearStaleBindings: () => clearStaleBindings,
63
44
  createArrow: () => createArrow,
64
45
  createGrid: () => createGrid,
65
46
  createHtmlElement: () => createHtmlElement,
66
- createId: () => createId,
67
47
  createImage: () => createImage,
68
48
  createNote: () => createNote,
69
49
  createShape: () => createShape,
@@ -72,29 +52,20 @@ __export(index_exports, {
72
52
  createText: () => createText,
73
53
  drawHexPath: () => drawHexPath,
74
54
  exportImage: () => exportImage,
75
- exportState: () => exportState,
76
- findBindTarget: () => findBindTarget,
77
- findBoundArrows: () => findBoundArrows,
78
55
  getActiveFormats: () => getActiveFormats,
79
56
  getArrowBounds: () => getArrowBounds,
80
57
  getArrowControlPoint: () => getArrowControlPoint,
81
58
  getArrowMidpoint: () => getArrowMidpoint,
82
59
  getArrowTangentAngle: () => getArrowTangentAngle,
83
60
  getBendFromPoint: () => getBendFromPoint,
84
- getEdgeIntersection: () => getEdgeIntersection,
85
61
  getElementBounds: () => getElementBounds,
86
- getElementCenter: () => getElementCenter,
87
62
  getElementsBoundingBox: () => getElementsBoundingBox,
88
63
  getHexCellsInCone: () => getHexCellsInCone,
89
64
  getHexCellsInLine: () => getHexCellsInLine,
90
65
  getHexCellsInRadius: () => getHexCellsInRadius,
91
66
  getHexCellsInSquare: () => getHexCellsInSquare,
92
67
  getHexDistance: () => getHexDistance,
93
- isBindable: () => isBindable,
94
68
  isNearBezier: () => isNearBezier,
95
- isNoteContentEmpty: () => isNoteContentEmpty,
96
- parseState: () => parseState,
97
- sanitizeNoteHtml: () => sanitizeNoteHtml,
98
69
  setFontSize: () => setFontSize,
99
70
  smartSnap: () => smartSnap,
100
71
  snapPoint: () => snapPoint,
@@ -102,188 +73,41 @@ __export(index_exports, {
102
73
  toggleBold: () => toggleBold,
103
74
  toggleItalic: () => toggleItalic,
104
75
  toggleStrikethrough: () => toggleStrikethrough,
105
- toggleUnderline: () => toggleUnderline,
106
- unbindArrow: () => unbindArrow,
107
- updateBoundArrow: () => updateBoundArrow
76
+ toggleUnderline: () => toggleUnderline
108
77
  });
109
78
  module.exports = __toCommonJS(index_exports);
110
79
 
111
- // src/core/event-bus.ts
112
- var EventBus = class {
113
- listeners = /* @__PURE__ */ new Map();
114
- on(event, listener) {
115
- const existing = this.listeners.get(event);
116
- if (existing) {
117
- existing.add(listener);
118
- } else {
119
- const set = /* @__PURE__ */ new Set([listener]);
120
- this.listeners.set(event, set);
121
- }
122
- return () => this.off(event, listener);
123
- }
124
- off(event, listener) {
125
- this.listeners.get(event)?.delete(listener);
126
- }
127
- emit(event, data) {
128
- this.listeners.get(event)?.forEach((listener) => {
129
- try {
130
- listener(data);
131
- } catch (err) {
132
- console.error(`[fieldnotes] listener error for "${String(event)}"`, err);
133
- }
134
- });
135
- }
136
- clear() {
137
- this.listeners.clear();
138
- }
139
- };
140
-
141
- // src/core/quadtree.ts
142
- var MAX_ITEMS = 8;
143
- var MAX_DEPTH = 8;
144
- function intersects(a, b) {
145
- 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;
80
+ // src/core/snap.ts
81
+ function snapPoint(point, gridSize) {
82
+ return {
83
+ x: Math.round(point.x / gridSize) * gridSize || 0,
84
+ y: Math.round(point.y / gridSize) * gridSize || 0
85
+ };
146
86
  }
147
- var QuadNode = class _QuadNode {
148
- constructor(bounds, depth) {
149
- this.bounds = bounds;
150
- this.depth = depth;
151
- }
152
- items = [];
153
- children = null;
154
- insert(entry) {
155
- if (this.children) {
156
- const idx = this.getChildIndex(entry.bounds);
157
- if (idx !== -1) {
158
- const child = this.children[idx];
159
- if (child) child.insert(entry);
160
- return;
161
- }
162
- this.items.push(entry);
163
- return;
164
- }
165
- this.items.push(entry);
166
- if (this.items.length > MAX_ITEMS && this.depth < MAX_DEPTH) {
167
- this.split();
168
- }
169
- }
170
- remove(id) {
171
- const idx = this.items.findIndex((e) => e.id === id);
172
- if (idx !== -1) {
173
- this.items.splice(idx, 1);
174
- return true;
175
- }
176
- if (this.children) {
177
- for (const child of this.children) {
178
- if (child.remove(id)) {
179
- this.collapseIfEmpty();
180
- return true;
181
- }
182
- }
183
- }
184
- return false;
185
- }
186
- query(rect, result) {
187
- if (!intersects(this.bounds, rect)) return;
188
- for (const item of this.items) {
189
- if (intersects(item.bounds, rect)) {
190
- result.push(item.id);
191
- }
192
- }
193
- if (this.children) {
194
- for (const child of this.children) {
195
- child.query(rect, result);
196
- }
197
- }
198
- }
199
- getChildIndex(itemBounds) {
200
- const midX = this.bounds.x + this.bounds.w / 2;
201
- const midY = this.bounds.y + this.bounds.h / 2;
202
- const left = itemBounds.x >= this.bounds.x && itemBounds.x + itemBounds.w <= midX;
203
- const right = itemBounds.x >= midX && itemBounds.x + itemBounds.w <= this.bounds.x + this.bounds.w;
204
- const top = itemBounds.y >= this.bounds.y && itemBounds.y + itemBounds.h <= midY;
205
- const bottom = itemBounds.y >= midY && itemBounds.y + itemBounds.h <= this.bounds.y + this.bounds.h;
206
- if (left && top) return 0;
207
- if (right && top) return 1;
208
- if (left && bottom) return 2;
209
- if (right && bottom) return 3;
210
- return -1;
211
- }
212
- split() {
213
- const { x, y, w, h } = this.bounds;
214
- const halfW = w / 2;
215
- const halfH = h / 2;
216
- const d = this.depth + 1;
217
- this.children = [
218
- new _QuadNode({ x, y, w: halfW, h: halfH }, d),
219
- new _QuadNode({ x: x + halfW, y, w: halfW, h: halfH }, d),
220
- new _QuadNode({ x, y: y + halfH, w: halfW, h: halfH }, d),
221
- new _QuadNode({ x: x + halfW, y: y + halfH, w: halfW, h: halfH }, d)
222
- ];
223
- const remaining = [];
224
- for (const item of this.items) {
225
- const idx = this.getChildIndex(item.bounds);
226
- if (idx !== -1) {
227
- const target = this.children[idx];
228
- if (target) target.insert(item);
229
- } else {
230
- remaining.push(item);
231
- }
232
- }
233
- this.items = remaining;
234
- }
235
- collapseIfEmpty() {
236
- if (!this.children) return;
237
- let totalItems = this.items.length;
238
- for (const child of this.children) {
239
- if (child.children) return;
240
- totalItems += child.items.length;
241
- }
242
- if (totalItems <= MAX_ITEMS) {
243
- for (const child of this.children) {
244
- this.items.push(...child.items);
245
- }
246
- this.children = null;
247
- }
248
- }
249
- };
250
- var Quadtree = class {
251
- root;
252
- _size = 0;
253
- worldBounds;
254
- constructor(worldBounds) {
255
- this.worldBounds = worldBounds;
256
- this.root = new QuadNode(worldBounds, 0);
257
- }
258
- get size() {
259
- return this._size;
260
- }
261
- insert(id, bounds) {
262
- this.root.insert({ id, bounds });
263
- this._size++;
264
- }
265
- remove(id) {
266
- if (this.root.remove(id)) {
267
- this._size--;
268
- }
269
- }
270
- update(id, newBounds) {
271
- this.remove(id);
272
- this.insert(id, newBounds);
273
- }
274
- query(rect) {
275
- const result = [];
276
- this.root.query(rect, result);
277
- return result;
278
- }
279
- queryPoint(point) {
280
- return this.query({ x: point.x, y: point.y, w: 0, h: 0 });
87
+ function snapToHexCenter(point, cellSize, orientation) {
88
+ if (orientation === "pointy") {
89
+ const hexW = Math.sqrt(3) * cellSize;
90
+ const rowH = 1.5 * cellSize;
91
+ const row = Math.round(point.y / rowH);
92
+ const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
93
+ const col = Math.round((point.x - offsetX) / hexW);
94
+ return { x: col * hexW + offsetX || 0, y: row * rowH || 0 };
95
+ } else {
96
+ const hexH = Math.sqrt(3) * cellSize;
97
+ const colW = 1.5 * cellSize;
98
+ const col = Math.round(point.x / colW);
99
+ const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
100
+ const row = Math.round((point.y - offsetY) / hexH);
101
+ return { x: col * colW || 0, y: row * hexH + offsetY || 0 };
281
102
  }
282
- clear() {
283
- this.root = new QuadNode(this.worldBounds, 0);
284
- this._size = 0;
103
+ }
104
+ function smartSnap(point, ctx) {
105
+ if (!ctx.snapToGrid || !ctx.gridSize) return point;
106
+ if (ctx.gridType === "hex" && ctx.hexOrientation) {
107
+ return snapToHexCenter(point, ctx.gridSize, ctx.hexOrientation);
285
108
  }
286
- };
109
+ return snapPoint(point, ctx.gridSize);
110
+ }
287
111
 
288
112
  // src/elements/note-sanitizer.ts
289
113
  var BOLD_TAGS = /* @__PURE__ */ new Set(["b", "strong"]);
@@ -540,38 +364,6 @@ function migrateElement(obj) {
540
364
  }
541
365
  }
542
366
 
543
- // src/core/snap.ts
544
- function snapPoint(point, gridSize) {
545
- return {
546
- x: Math.round(point.x / gridSize) * gridSize || 0,
547
- y: Math.round(point.y / gridSize) * gridSize || 0
548
- };
549
- }
550
- function snapToHexCenter(point, cellSize, orientation) {
551
- if (orientation === "pointy") {
552
- const hexW = Math.sqrt(3) * cellSize;
553
- const rowH = 1.5 * cellSize;
554
- const row = Math.round(point.y / rowH);
555
- const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
556
- const col = Math.round((point.x - offsetX) / hexW);
557
- return { x: col * hexW + offsetX || 0, y: row * rowH || 0 };
558
- } else {
559
- const hexH = Math.sqrt(3) * cellSize;
560
- const colW = 1.5 * cellSize;
561
- const col = Math.round(point.x / colW);
562
- const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
563
- const row = Math.round((point.y - offsetY) / hexH);
564
- return { x: col * colW || 0, y: row * hexH + offsetY || 0 };
565
- }
566
- }
567
- function smartSnap(point, ctx) {
568
- if (!ctx.snapToGrid || !ctx.gridSize) return point;
569
- if (ctx.gridType === "hex" && ctx.hexOrientation) {
570
- return snapToHexCenter(point, ctx.gridSize, ctx.hexOrientation);
571
- }
572
- return snapPoint(point, ctx.gridSize);
573
- }
574
-
575
367
  // src/core/auto-save.ts
576
368
  var DEFAULT_KEY = "fieldnotes-autosave";
577
369
  var DEFAULT_DEBOUNCE_MS = 1e3;
@@ -735,143 +527,6 @@ var Camera = class {
735
527
  }
736
528
  };
737
529
 
738
- // src/canvas/background.ts
739
- var MIN_PATTERN_SPACING = 16;
740
- var DEFAULTS = {
741
- pattern: "dots",
742
- spacing: 24,
743
- color: "#d0d0d0",
744
- dotRadius: 1,
745
- lineWidth: 0.5
746
- };
747
- var Background = class {
748
- pattern;
749
- spacing;
750
- color;
751
- dotRadius;
752
- lineWidth;
753
- cachedCanvas = null;
754
- cachedCtx = null;
755
- lastZoom = -1;
756
- lastOffsetX = -Infinity;
757
- lastOffsetY = -Infinity;
758
- lastWidth = 0;
759
- lastHeight = 0;
760
- constructor(options = {}) {
761
- this.pattern = options.pattern ?? DEFAULTS.pattern;
762
- this.spacing = options.spacing ?? DEFAULTS.spacing;
763
- this.color = options.color ?? DEFAULTS.color;
764
- this.dotRadius = options.dotRadius ?? DEFAULTS.dotRadius;
765
- this.lineWidth = options.lineWidth ?? DEFAULTS.lineWidth;
766
- }
767
- render(ctx, camera) {
768
- const { width, height } = ctx.canvas;
769
- const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
770
- const cssWidth = width / dpr;
771
- const cssHeight = height / dpr;
772
- ctx.save();
773
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
774
- ctx.clearRect(0, 0, cssWidth, cssHeight);
775
- if (this.pattern === "none") {
776
- ctx.restore();
777
- return;
778
- }
779
- const spacing = this.adaptSpacing(this.spacing, camera.zoom);
780
- const keyZoom = camera.zoom;
781
- const keyX = Math.floor(camera.position.x % spacing);
782
- const keyY = Math.floor(camera.position.y % spacing);
783
- if (this.cachedCanvas !== null && keyZoom === this.lastZoom && keyX === this.lastOffsetX && keyY === this.lastOffsetY && cssWidth === this.lastWidth && cssHeight === this.lastHeight) {
784
- ctx.drawImage(this.cachedCanvas, 0, 0);
785
- ctx.restore();
786
- return;
787
- }
788
- this.ensureCachedCanvas(cssWidth, cssHeight, dpr);
789
- if (this.cachedCtx === null) {
790
- if (this.pattern === "dots") {
791
- this.renderDots(ctx, camera, cssWidth, cssHeight);
792
- } else if (this.pattern === "grid") {
793
- this.renderGrid(ctx, camera, cssWidth, cssHeight);
794
- }
795
- ctx.restore();
796
- return;
797
- }
798
- const offCtx = this.cachedCtx;
799
- offCtx.clearRect(0, 0, cssWidth, cssHeight);
800
- if (this.pattern === "dots") {
801
- this.renderDots(offCtx, camera, cssWidth, cssHeight);
802
- } else if (this.pattern === "grid") {
803
- this.renderGrid(offCtx, camera, cssWidth, cssHeight);
804
- }
805
- this.lastZoom = keyZoom;
806
- this.lastOffsetX = keyX;
807
- this.lastOffsetY = keyY;
808
- this.lastWidth = cssWidth;
809
- this.lastHeight = cssHeight;
810
- ctx.drawImage(this.cachedCanvas, 0, 0);
811
- ctx.restore();
812
- }
813
- ensureCachedCanvas(cssWidth, cssHeight, dpr) {
814
- if (this.cachedCanvas !== null && this.lastWidth === cssWidth && this.lastHeight === cssHeight) {
815
- return;
816
- }
817
- const physWidth = Math.round(cssWidth * dpr);
818
- const physHeight = Math.round(cssHeight * dpr);
819
- if (typeof OffscreenCanvas !== "undefined") {
820
- this.cachedCanvas = new OffscreenCanvas(physWidth, physHeight);
821
- } else if (typeof document !== "undefined") {
822
- const el = document.createElement("canvas");
823
- el.width = physWidth;
824
- el.height = physHeight;
825
- this.cachedCanvas = el;
826
- } else {
827
- this.cachedCanvas = null;
828
- this.cachedCtx = null;
829
- return;
830
- }
831
- const offCtx = this.cachedCanvas.getContext("2d");
832
- if (offCtx !== null) {
833
- offCtx.scale(dpr, dpr);
834
- }
835
- this.cachedCtx = offCtx;
836
- this.lastZoom = -1;
837
- }
838
- adaptSpacing(baseSpacing, zoom) {
839
- let spacing = baseSpacing * zoom;
840
- while (spacing < MIN_PATTERN_SPACING) {
841
- spacing *= 2;
842
- }
843
- return spacing;
844
- }
845
- renderDots(ctx, camera, width, height) {
846
- const spacing = this.adaptSpacing(this.spacing, camera.zoom);
847
- const offsetX = camera.position.x % spacing;
848
- const offsetY = camera.position.y % spacing;
849
- const radius = this.dotRadius * Math.min(camera.zoom, 2);
850
- ctx.fillStyle = this.color;
851
- ctx.beginPath();
852
- for (let x = offsetX; x < width; x += spacing) {
853
- for (let y = offsetY; y < height; y += spacing) {
854
- ctx.moveTo(x + radius, y);
855
- ctx.arc(x, y, radius, 0, Math.PI * 2);
856
- }
857
- }
858
- ctx.fill();
859
- }
860
- renderGrid(ctx, camera, width, height) {
861
- const spacing = this.adaptSpacing(this.spacing, camera.zoom);
862
- const offsetX = camera.position.x % spacing;
863
- const offsetY = camera.position.y % spacing;
864
- const lineW = this.lineWidth * Math.min(camera.zoom, 2);
865
- ctx.fillStyle = this.color;
866
- for (let x = offsetX; x < width; x += spacing) {
867
- ctx.fillRect(x, 0, lineW, height);
868
- }
869
- for (let y = offsetY; y < height; y += spacing) {
870
- ctx.fillRect(0, y, width, lineW);
871
- }
872
- }
873
- };
874
-
875
530
  // src/canvas/input-filter.ts
876
531
  var InputFilter = class _InputFilter {
877
532
  activePenId = null;
@@ -933,62 +588,334 @@ function createId(prefix) {
933
588
  return `${prefix}_${Date.now().toString(36)}_${(counter++).toString(36)}`;
934
589
  }
935
590
 
936
- // src/canvas/keyboard-actions.ts
937
- var KeyboardActions = class {
938
- constructor(deps) {
939
- this.deps = deps;
940
- }
941
- clipboard = [];
942
- pasteCount = 0;
943
- nudgeTimer = null;
944
- nudgeTxId = null;
945
- dispose() {
946
- this.flushPendingNudge();
591
+ // src/core/geometry.ts
592
+ function distSqToSegment(p, a, b) {
593
+ const abx = b.x - a.x;
594
+ const aby = b.y - a.y;
595
+ const apx = p.x - a.x;
596
+ const apy = p.y - a.y;
597
+ const lenSq = abx * abx + aby * aby;
598
+ if (lenSq === 0) {
599
+ return apx * apx + apy * apy;
947
600
  }
948
- selectTool() {
949
- const tm = this.deps.getToolManager();
950
- const ctx = this.deps.getToolContext();
951
- if (!tm || !ctx) return null;
952
- const tool = tm.activeTool;
953
- if (tool?.name !== "select") return null;
954
- return { tool, ctx };
601
+ const t = Math.max(0, Math.min(1, (apx * abx + apy * aby) / lenSq));
602
+ const dx = p.x - (a.x + t * abx);
603
+ const dy = p.y - (a.y + t * aby);
604
+ return dx * dx + dy * dy;
605
+ }
606
+
607
+ // src/elements/arrow-geometry.ts
608
+ function getArrowControlPoint(from, to, bend) {
609
+ const midX = (from.x + to.x) / 2;
610
+ const midY = (from.y + to.y) / 2;
611
+ if (bend === 0) return { x: midX, y: midY };
612
+ const dx = to.x - from.x;
613
+ const dy = to.y - from.y;
614
+ const len = Math.sqrt(dx * dx + dy * dy);
615
+ if (len === 0) return { x: midX, y: midY };
616
+ const perpX = -dy / len;
617
+ const perpY = dx / len;
618
+ return {
619
+ x: midX + perpX * bend,
620
+ y: midY + perpY * bend
621
+ };
622
+ }
623
+ function getArrowMidpoint(from, to, bend) {
624
+ const cp = getArrowControlPoint(from, to, bend);
625
+ return {
626
+ x: 0.25 * from.x + 0.5 * cp.x + 0.25 * to.x,
627
+ y: 0.25 * from.y + 0.5 * cp.y + 0.25 * to.y
628
+ };
629
+ }
630
+ function getBendFromPoint(from, to, dragPoint) {
631
+ const midX = (from.x + to.x) / 2;
632
+ const midY = (from.y + to.y) / 2;
633
+ const dx = to.x - from.x;
634
+ const dy = to.y - from.y;
635
+ const len = Math.sqrt(dx * dx + dy * dy);
636
+ if (len === 0) return 0;
637
+ const perpX = -dy / len;
638
+ const perpY = dx / len;
639
+ return (dragPoint.x - midX) * perpX + (dragPoint.y - midY) * perpY;
640
+ }
641
+ function getArrowTangentAngle(from, to, bend, t) {
642
+ const cp = getArrowControlPoint(from, to, bend);
643
+ const tangentX = 2 * (1 - t) * (cp.x - from.x) + 2 * t * (to.x - cp.x);
644
+ const tangentY = 2 * (1 - t) * (cp.y - from.y) + 2 * t * (to.y - cp.y);
645
+ return Math.atan2(tangentY, tangentX);
646
+ }
647
+ function isNearBezier(point, from, to, bend, threshold) {
648
+ if (bend === 0) return isNearLine(point, from, to, threshold);
649
+ const cp = getArrowControlPoint(from, to, bend);
650
+ const segments = 20;
651
+ for (let i = 0; i < segments; i++) {
652
+ const t0 = i / segments;
653
+ const t1 = (i + 1) / segments;
654
+ const a = bezierPoint(from, cp, to, t0);
655
+ const b = bezierPoint(from, cp, to, t1);
656
+ if (isNearLine(point, a, b, threshold)) return true;
955
657
  }
956
- nudge(dx, dy, byCell) {
957
- if (this.deps.isToolActive()) return false;
958
- const sel = this.selectTool();
959
- if (!sel) return false;
960
- if (sel.tool.selectedIds.length === 0) return false;
961
- const step = byCell ? sel.ctx.gridSize ?? 10 : 1;
962
- if (this.nudgeTimer === null) {
963
- const recorder = this.deps.getHistoryRecorder();
964
- recorder?.begin();
965
- this.nudgeTxId = recorder?.currentTransactionId ?? null;
966
- } else {
967
- clearTimeout(this.nudgeTimer);
968
- }
969
- const moved = sel.tool.nudgeSelection(dx * step, dy * step, sel.ctx);
970
- this.nudgeTimer = setTimeout(() => this.flushPendingNudge(), 400);
971
- return moved;
658
+ return false;
659
+ }
660
+ function getArrowBounds(from, to, bend) {
661
+ if (bend === 0) {
662
+ const minX2 = Math.min(from.x, to.x);
663
+ const minY2 = Math.min(from.y, to.y);
664
+ return {
665
+ x: minX2,
666
+ y: minY2,
667
+ w: Math.abs(to.x - from.x),
668
+ h: Math.abs(to.y - from.y)
669
+ };
972
670
  }
973
- flushPendingNudge() {
974
- if (this.nudgeTimer === null) return;
975
- clearTimeout(this.nudgeTimer);
976
- this.nudgeTimer = null;
977
- const recorder = this.deps.getHistoryRecorder();
978
- if (this.nudgeTxId === null || recorder?.currentTransactionId === this.nudgeTxId) {
979
- recorder?.commit();
980
- }
981
- this.nudgeTxId = null;
671
+ const cp = getArrowControlPoint(from, to, bend);
672
+ const steps = 20;
673
+ let minX = Math.min(from.x, to.x);
674
+ let minY = Math.min(from.y, to.y);
675
+ let maxX = Math.max(from.x, to.x);
676
+ let maxY = Math.max(from.y, to.y);
677
+ for (let i = 1; i < steps; i++) {
678
+ const t = i / steps;
679
+ const p = bezierPoint(from, cp, to, t);
680
+ if (p.x < minX) minX = p.x;
681
+ if (p.y < minY) minY = p.y;
682
+ if (p.x > maxX) maxX = p.x;
683
+ if (p.y > maxY) maxY = p.y;
982
684
  }
983
- deleteSelected() {
984
- if (this.deps.isToolActive()) return;
985
- this.flushPendingNudge();
986
- const sel = this.selectTool();
987
- if (!sel) return;
988
- const ids = sel.tool.selectedIds;
989
- if (ids.length === 0) return;
990
- const recorder = this.deps.getHistoryRecorder();
991
- recorder?.begin();
685
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
686
+ }
687
+ function bezierPoint(from, cp, to, t) {
688
+ const mt = 1 - t;
689
+ return {
690
+ x: mt * mt * from.x + 2 * mt * t * cp.x + t * t * to.x,
691
+ y: mt * mt * from.y + 2 * mt * t * cp.y + t * t * to.y
692
+ };
693
+ }
694
+ function isNearLine(point, a, b, threshold) {
695
+ return distSqToSegment(point, a, b) <= threshold * threshold;
696
+ }
697
+
698
+ // src/elements/element-bounds.ts
699
+ var strokeBoundsCache = /* @__PURE__ */ new WeakMap();
700
+ function getElementBounds(element) {
701
+ if (element.type === "grid") return null;
702
+ if ("size" in element) {
703
+ return {
704
+ x: element.position.x,
705
+ y: element.position.y,
706
+ w: element.size.w,
707
+ h: element.size.h
708
+ };
709
+ }
710
+ if (element.type === "stroke") {
711
+ if (element.points.length === 0) return null;
712
+ const cached = strokeBoundsCache.get(element);
713
+ if (cached) return cached;
714
+ let minX = Infinity;
715
+ let minY = Infinity;
716
+ let maxX = -Infinity;
717
+ let maxY = -Infinity;
718
+ for (const p of element.points) {
719
+ const px = p.x + element.position.x;
720
+ const py = p.y + element.position.y;
721
+ if (px < minX) minX = px;
722
+ if (py < minY) minY = py;
723
+ if (px > maxX) maxX = px;
724
+ if (py > maxY) maxY = py;
725
+ }
726
+ const bounds = { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
727
+ strokeBoundsCache.set(element, bounds);
728
+ return bounds;
729
+ }
730
+ if (element.type === "arrow") {
731
+ return getArrowBoundsAnalytical(element.from, element.to, element.bend);
732
+ }
733
+ if (element.type === "template") {
734
+ return getTemplateBounds(element);
735
+ }
736
+ return null;
737
+ }
738
+ function getArrowBoundsAnalytical(from, to, bend) {
739
+ if (bend === 0) {
740
+ const minX2 = Math.min(from.x, to.x);
741
+ const minY2 = Math.min(from.y, to.y);
742
+ return {
743
+ x: minX2,
744
+ y: minY2,
745
+ w: Math.abs(to.x - from.x),
746
+ h: Math.abs(to.y - from.y)
747
+ };
748
+ }
749
+ const cp = getArrowControlPoint(from, to, bend);
750
+ let minX = Math.min(from.x, to.x);
751
+ let maxX = Math.max(from.x, to.x);
752
+ let minY = Math.min(from.y, to.y);
753
+ let maxY = Math.max(from.y, to.y);
754
+ const tx = from.x - 2 * cp.x + to.x;
755
+ if (tx !== 0) {
756
+ const t = (from.x - cp.x) / tx;
757
+ if (t > 0 && t < 1) {
758
+ const mt = 1 - t;
759
+ const x = mt * mt * from.x + 2 * mt * t * cp.x + t * t * to.x;
760
+ if (x < minX) minX = x;
761
+ if (x > maxX) maxX = x;
762
+ }
763
+ }
764
+ const ty = from.y - 2 * cp.y + to.y;
765
+ if (ty !== 0) {
766
+ const t = (from.y - cp.y) / ty;
767
+ if (t > 0 && t < 1) {
768
+ const mt = 1 - t;
769
+ const y = mt * mt * from.y + 2 * mt * t * cp.y + t * t * to.y;
770
+ if (y < minY) minY = y;
771
+ if (y > maxY) maxY = y;
772
+ }
773
+ }
774
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
775
+ }
776
+ function getTemplateBounds(el) {
777
+ const { x: cx, y: cy } = el.position;
778
+ const r = el.radius;
779
+ switch (el.templateShape) {
780
+ case "circle":
781
+ return { x: cx - r, y: cy - r, w: 2 * r, h: 2 * r };
782
+ case "square":
783
+ return { x: cx - r / 2, y: cy - r / 2, w: r, h: r };
784
+ case "cone": {
785
+ const halfAngle = Math.atan(0.5);
786
+ const tipX = cx;
787
+ const tipY = cy;
788
+ const leftX = cx + r * Math.cos(el.angle - halfAngle);
789
+ const leftY = cy + r * Math.sin(el.angle - halfAngle);
790
+ const rightX = cx + r * Math.cos(el.angle + halfAngle);
791
+ const rightY = cy + r * Math.sin(el.angle + halfAngle);
792
+ const farX = cx + r * Math.cos(el.angle);
793
+ const farY = cy + r * Math.sin(el.angle);
794
+ const xs = [tipX, leftX, rightX, farX];
795
+ const ys = [tipY, leftY, rightY, farY];
796
+ let minX = Infinity;
797
+ let minY = Infinity;
798
+ let maxX = -Infinity;
799
+ let maxY = -Infinity;
800
+ for (let i = 0; i < xs.length; i++) {
801
+ const px = xs[i];
802
+ const py = ys[i];
803
+ if (px !== void 0 && px < minX) minX = px;
804
+ if (px !== void 0 && px > maxX) maxX = px;
805
+ if (py !== void 0 && py < minY) minY = py;
806
+ if (py !== void 0 && py > maxY) maxY = py;
807
+ }
808
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
809
+ }
810
+ case "line": {
811
+ const halfW = r / 12;
812
+ const cos = Math.cos(el.angle);
813
+ const sin = Math.sin(el.angle);
814
+ const perpX = -sin * halfW;
815
+ const perpY = cos * halfW;
816
+ const x0 = cx + perpX;
817
+ const y0 = cy + perpY;
818
+ const x1 = cx + r * cos + perpX;
819
+ const y1 = cy + r * sin + perpY;
820
+ const x2 = cx + r * cos - perpX;
821
+ const y2 = cy + r * sin - perpY;
822
+ const x3 = cx - perpX;
823
+ const y3 = cy - perpY;
824
+ const minX = Math.min(x0, x1, x2, x3);
825
+ const minY = Math.min(y0, y1, y2, y3);
826
+ const maxX = Math.max(x0, x1, x2, x3);
827
+ const maxY = Math.max(y0, y1, y2, y3);
828
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
829
+ }
830
+ }
831
+ }
832
+ function transferStrokeBounds(prev, next) {
833
+ if (prev.type !== "stroke" || next.type !== "stroke") return;
834
+ if (prev.points !== next.points) return;
835
+ if (prev.position.x !== next.position.x || prev.position.y !== next.position.y) return;
836
+ const bounds = strokeBoundsCache.get(prev);
837
+ if (bounds) strokeBoundsCache.set(next, bounds);
838
+ }
839
+ function boundsIntersect(a, b) {
840
+ 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;
841
+ }
842
+
843
+ // src/elements/bounds.ts
844
+ function getElementsBoundingBox(elements) {
845
+ let minX = Infinity;
846
+ let minY = Infinity;
847
+ let maxX = -Infinity;
848
+ let maxY = -Infinity;
849
+ let found = false;
850
+ for (const el of elements) {
851
+ const b = getElementBounds(el);
852
+ if (!b) continue;
853
+ found = true;
854
+ if (b.x < minX) minX = b.x;
855
+ if (b.y < minY) minY = b.y;
856
+ if (b.x + b.w > maxX) maxX = b.x + b.w;
857
+ if (b.y + b.h > maxY) maxY = b.y + b.h;
858
+ }
859
+ if (!found) return null;
860
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
861
+ }
862
+
863
+ // src/canvas/keyboard-actions.ts
864
+ var KeyboardActions = class {
865
+ constructor(deps) {
866
+ this.deps = deps;
867
+ }
868
+ clipboard = [];
869
+ pasteCount = 0;
870
+ nudgeTimer = null;
871
+ nudgeTxId = null;
872
+ dispose() {
873
+ this.flushPendingNudge();
874
+ }
875
+ selectTool() {
876
+ const tm = this.deps.getToolManager();
877
+ const ctx = this.deps.getToolContext();
878
+ if (!tm || !ctx) return null;
879
+ const tool = tm.activeTool;
880
+ if (tool?.name !== "select") return null;
881
+ return { tool, ctx };
882
+ }
883
+ nudge(dx, dy, byCell) {
884
+ if (this.deps.isToolActive()) return false;
885
+ const sel = this.selectTool();
886
+ if (!sel) return false;
887
+ if (sel.tool.selectedIds.length === 0) return false;
888
+ const step = byCell ? sel.ctx.gridSize ?? 10 : 1;
889
+ if (this.nudgeTimer === null) {
890
+ const recorder = this.deps.getHistoryRecorder();
891
+ recorder?.begin();
892
+ this.nudgeTxId = recorder?.currentTransactionId ?? null;
893
+ } else {
894
+ clearTimeout(this.nudgeTimer);
895
+ }
896
+ const moved = sel.tool.nudgeSelection(dx * step, dy * step, sel.ctx);
897
+ this.nudgeTimer = setTimeout(() => this.flushPendingNudge(), 400);
898
+ return moved;
899
+ }
900
+ flushPendingNudge() {
901
+ if (this.nudgeTimer === null) return;
902
+ clearTimeout(this.nudgeTimer);
903
+ this.nudgeTimer = null;
904
+ const recorder = this.deps.getHistoryRecorder();
905
+ if (this.nudgeTxId === null || recorder?.currentTransactionId === this.nudgeTxId) {
906
+ recorder?.commit();
907
+ }
908
+ this.nudgeTxId = null;
909
+ }
910
+ deleteSelected() {
911
+ if (this.deps.isToolActive()) return;
912
+ this.flushPendingNudge();
913
+ const sel = this.selectTool();
914
+ if (!sel) return;
915
+ const ids = sel.tool.selectedIds;
916
+ if (ids.length === 0) return;
917
+ const recorder = this.deps.getHistoryRecorder();
918
+ recorder?.begin();
992
919
  for (const id of ids) {
993
920
  sel.ctx.store.remove(id);
994
921
  }
@@ -1038,8 +965,18 @@ var KeyboardActions = class {
1038
965
  if (this.clipboard.length === 0) return;
1039
966
  const sel = this.selectTool();
1040
967
  if (!sel) return;
968
+ const cursor = this.deps.getLastPointerWorld?.() ?? null;
969
+ if (cursor) {
970
+ const bbox = getElementsBoundingBox(this.clipboard);
971
+ if (bbox) {
972
+ const centerX = bbox.x + bbox.w / 2;
973
+ const centerY = bbox.y + bbox.h / 2;
974
+ this.insertClones(this.clipboard, { x: cursor.x - centerX, y: cursor.y - centerY }, sel);
975
+ return;
976
+ }
977
+ }
1041
978
  this.pasteCount++;
1042
- this.insertClones(this.clipboard, this.pasteCount * 20, sel);
979
+ this.insertClones(this.clipboard, { x: this.pasteCount * 20, y: this.pasteCount * 20 }, sel);
1043
980
  }
1044
981
  duplicate() {
1045
982
  if (this.deps.isToolActive()) return;
@@ -1052,7 +989,7 @@ var KeyboardActions = class {
1052
989
  if (el) source.push(el);
1053
990
  }
1054
991
  if (source.length === 0) return;
1055
- this.insertClones(source, 20, sel);
992
+ this.insertClones(source, { x: 20, y: 20 }, sel);
1056
993
  }
1057
994
  deselect() {
1058
995
  if (this.deps.isToolActive()) return;
@@ -1123,11 +1060,11 @@ var KeyboardActions = class {
1123
1060
  const newId = idMap.get(el.id);
1124
1061
  if (!newId) continue;
1125
1062
  clone.id = newId;
1126
- clone.position = { x: clone.position.x + offset, y: clone.position.y + offset };
1063
+ clone.position = { x: clone.position.x + offset.x, y: clone.position.y + offset.y };
1127
1064
  if (clone.type === "arrow") {
1128
1065
  const arrow = clone;
1129
- arrow.from = { x: arrow.from.x + offset, y: arrow.from.y + offset };
1130
- arrow.to = { x: arrow.to.x + offset, y: arrow.to.y + offset };
1066
+ arrow.from = { x: arrow.from.x + offset.x, y: arrow.from.y + offset.y };
1067
+ arrow.to = { x: arrow.to.x + offset.x, y: arrow.to.y + offset.y };
1131
1068
  delete arrow.cachedControlPoint;
1132
1069
  if (arrow.fromBinding) {
1133
1070
  const newTarget = idMap.get(arrow.fromBinding.elementId);
@@ -1173,6 +1110,9 @@ var DEFAULT_BINDINGS = [
1173
1110
  ["z-front", ["mod+]"]],
1174
1111
  ["z-back", ["mod+["]],
1175
1112
  ["zoom-fit", ["shift+1"]],
1113
+ ["zoom-in", ["mod+="]],
1114
+ ["zoom-out", ["mod+-"]],
1115
+ ["zoom-reset", ["mod+0"]],
1176
1116
  ["nudge-left", ["arrowleft"]],
1177
1117
  ["nudge-right", ["arrowright"]],
1178
1118
  ["nudge-up", ["arrowup"]],
@@ -1309,6 +1249,7 @@ var ShortcutMap = class {
1309
1249
 
1310
1250
  // src/canvas/input-handler.ts
1311
1251
  var ZOOM_SENSITIVITY = 1e-3;
1252
+ var ZOOM_STEP = 1.2;
1312
1253
  var MIDDLE_BUTTON = 1;
1313
1254
  var NUDGE_DELTAS = {
1314
1255
  "nudge-left": [-1, 0],
@@ -1330,7 +1271,8 @@ var InputHandler = class {
1330
1271
  getHistoryRecorder: () => this.historyRecorder,
1331
1272
  getHistoryStack: () => this.historyStack,
1332
1273
  isToolActive: () => this.isToolActive,
1333
- fitToContent: options.fitToContent
1274
+ fitToContent: options.fitToContent,
1275
+ getLastPointerWorld: () => this.lastPointerWorld()
1334
1276
  });
1335
1277
  this.shortcutMap = new ShortcutMap(options.shortcuts?.bindings);
1336
1278
  this.scope = options.shortcuts?.scope ?? "focus";
@@ -1386,11 +1328,21 @@ var InputHandler = class {
1386
1328
  this.element.addEventListener("pointerdown", this.onPointerDown, opts);
1387
1329
  this.element.addEventListener("pointermove", this.onPointerMove, opts);
1388
1330
  this.element.addEventListener("pointerup", this.onPointerUp, opts);
1389
- this.element.addEventListener("pointerleave", this.onPointerUp, opts);
1331
+ this.element.addEventListener("pointerleave", this.onPointerLeave, opts);
1390
1332
  this.element.addEventListener("pointercancel", this.onPointerUp, opts);
1391
1333
  window.addEventListener("keydown", this.onKeyDown, opts);
1392
1334
  window.addEventListener("keyup", this.onKeyUp, opts);
1393
1335
  }
1336
+ viewportCenter() {
1337
+ const rect = this.element.getBoundingClientRect();
1338
+ return { x: rect.width / 2, y: rect.height / 2 };
1339
+ }
1340
+ zoomByFactor(factor) {
1341
+ this.camera.zoomAt(this.camera.zoom * factor, this.viewportCenter());
1342
+ }
1343
+ zoomToLevel(level) {
1344
+ this.camera.zoomAt(level, this.viewportCenter());
1345
+ }
1394
1346
  onWheel = (e) => {
1395
1347
  e.preventDefault();
1396
1348
  const rect = this.element.getBoundingClientRect();
@@ -1559,6 +1511,18 @@ var InputHandler = class {
1559
1511
  e.preventDefault();
1560
1512
  this.actions.zoomToFit();
1561
1513
  return;
1514
+ case "zoom-in":
1515
+ e.preventDefault();
1516
+ this.zoomByFactor(ZOOM_STEP);
1517
+ return;
1518
+ case "zoom-out":
1519
+ e.preventDefault();
1520
+ this.zoomByFactor(1 / ZOOM_STEP);
1521
+ return;
1522
+ case "zoom-reset":
1523
+ e.preventDefault();
1524
+ this.zoomToLevel(1);
1525
+ return;
1562
1526
  case "nudge-left":
1563
1527
  case "nudge-right":
1564
1528
  case "nudge-up":
@@ -1616,6 +1580,16 @@ var InputHandler = class {
1616
1580
  midpoint(a, b) {
1617
1581
  return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
1618
1582
  }
1583
+ lastPointerWorld() {
1584
+ const e = this.lastPointerEvent;
1585
+ if (!e) return null;
1586
+ const rect = this.element.getBoundingClientRect();
1587
+ return this.camera.screenToWorld({ x: e.clientX - rect.left, y: e.clientY - rect.top });
1588
+ }
1589
+ onPointerLeave = (e) => {
1590
+ this.lastPointerEvent = null;
1591
+ this.onPointerUp(e);
1592
+ };
1619
1593
  toPointerState(e) {
1620
1594
  const rect = this.element.getBoundingClientRect();
1621
1595
  return {
@@ -1667,299 +1641,319 @@ var InputHandler = class {
1667
1641
  }
1668
1642
  };
1669
1643
 
1670
- // src/canvas/double-tap-detector.ts
1671
- var DEFAULT_TIMEOUT = 300;
1672
- var DEFAULT_MAX_DISTANCE = 20;
1673
- var DoubleTapDetector = class {
1674
- timeout;
1675
- maxDistance;
1676
- lastTapTime = 0;
1677
- lastTapX = 0;
1678
- lastTapY = 0;
1679
- hasPendingTap = false;
1680
- constructor(options) {
1681
- this.timeout = options?.timeout ?? DEFAULT_TIMEOUT;
1682
- this.maxDistance = options?.maxDistance ?? DEFAULT_MAX_DISTANCE;
1644
+ // src/canvas/background.ts
1645
+ var MIN_PATTERN_SPACING = 16;
1646
+ var DEFAULTS = {
1647
+ pattern: "dots",
1648
+ spacing: 24,
1649
+ color: "#d0d0d0",
1650
+ dotRadius: 1,
1651
+ lineWidth: 0.5
1652
+ };
1653
+ var Background = class {
1654
+ pattern;
1655
+ spacing;
1656
+ color;
1657
+ dotRadius;
1658
+ lineWidth;
1659
+ cachedCanvas = null;
1660
+ cachedCtx = null;
1661
+ lastZoom = -1;
1662
+ lastOffsetX = -Infinity;
1663
+ lastOffsetY = -Infinity;
1664
+ lastWidth = 0;
1665
+ lastHeight = 0;
1666
+ constructor(options = {}) {
1667
+ this.pattern = options.pattern ?? DEFAULTS.pattern;
1668
+ this.spacing = options.spacing ?? DEFAULTS.spacing;
1669
+ this.color = options.color ?? DEFAULTS.color;
1670
+ this.dotRadius = options.dotRadius ?? DEFAULTS.dotRadius;
1671
+ this.lineWidth = options.lineWidth ?? DEFAULTS.lineWidth;
1683
1672
  }
1684
- feed(e) {
1685
- const now = Date.now();
1686
- const x = e.clientX;
1687
- const y = e.clientY;
1688
- if (this.hasPendingTap) {
1689
- const elapsed = now - this.lastTapTime;
1690
- const dx = x - this.lastTapX;
1691
- const dy = y - this.lastTapY;
1692
- const dist = Math.sqrt(dx * dx + dy * dy);
1693
- if (elapsed <= this.timeout && dist <= this.maxDistance) {
1694
- this.reset();
1695
- return true;
1673
+ render(ctx, camera) {
1674
+ const { width, height } = ctx.canvas;
1675
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
1676
+ const cssWidth = width / dpr;
1677
+ const cssHeight = height / dpr;
1678
+ ctx.save();
1679
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
1680
+ ctx.clearRect(0, 0, cssWidth, cssHeight);
1681
+ if (this.pattern === "none") {
1682
+ ctx.restore();
1683
+ return;
1684
+ }
1685
+ const spacing = this.adaptSpacing(this.spacing, camera.zoom);
1686
+ const keyZoom = camera.zoom;
1687
+ const keyX = Math.floor(camera.position.x % spacing);
1688
+ const keyY = Math.floor(camera.position.y % spacing);
1689
+ if (this.cachedCanvas !== null && keyZoom === this.lastZoom && keyX === this.lastOffsetX && keyY === this.lastOffsetY && cssWidth === this.lastWidth && cssHeight === this.lastHeight) {
1690
+ ctx.drawImage(this.cachedCanvas, 0, 0);
1691
+ ctx.restore();
1692
+ return;
1693
+ }
1694
+ this.ensureCachedCanvas(cssWidth, cssHeight, dpr);
1695
+ if (this.cachedCtx === null) {
1696
+ if (this.pattern === "dots") {
1697
+ this.renderDots(ctx, camera, cssWidth, cssHeight);
1698
+ } else if (this.pattern === "grid") {
1699
+ this.renderGrid(ctx, camera, cssWidth, cssHeight);
1696
1700
  }
1701
+ ctx.restore();
1702
+ return;
1697
1703
  }
1698
- this.lastTapTime = now;
1699
- this.lastTapX = x;
1700
- this.lastTapY = y;
1701
- this.hasPendingTap = true;
1702
- return false;
1704
+ const offCtx = this.cachedCtx;
1705
+ offCtx.clearRect(0, 0, cssWidth, cssHeight);
1706
+ if (this.pattern === "dots") {
1707
+ this.renderDots(offCtx, camera, cssWidth, cssHeight);
1708
+ } else if (this.pattern === "grid") {
1709
+ this.renderGrid(offCtx, camera, cssWidth, cssHeight);
1710
+ }
1711
+ this.lastZoom = keyZoom;
1712
+ this.lastOffsetX = keyX;
1713
+ this.lastOffsetY = keyY;
1714
+ this.lastWidth = cssWidth;
1715
+ this.lastHeight = cssHeight;
1716
+ ctx.drawImage(this.cachedCanvas, 0, 0);
1717
+ ctx.restore();
1703
1718
  }
1704
- reset() {
1705
- this.hasPendingTap = false;
1706
- this.lastTapTime = 0;
1707
- this.lastTapX = 0;
1708
- this.lastTapY = 0;
1719
+ ensureCachedCanvas(cssWidth, cssHeight, dpr) {
1720
+ if (this.cachedCanvas !== null && this.lastWidth === cssWidth && this.lastHeight === cssHeight) {
1721
+ return;
1722
+ }
1723
+ const physWidth = Math.round(cssWidth * dpr);
1724
+ const physHeight = Math.round(cssHeight * dpr);
1725
+ if (typeof OffscreenCanvas !== "undefined") {
1726
+ this.cachedCanvas = new OffscreenCanvas(physWidth, physHeight);
1727
+ } else if (typeof document !== "undefined") {
1728
+ const el = document.createElement("canvas");
1729
+ el.width = physWidth;
1730
+ el.height = physHeight;
1731
+ this.cachedCanvas = el;
1732
+ } else {
1733
+ this.cachedCanvas = null;
1734
+ this.cachedCtx = null;
1735
+ return;
1736
+ }
1737
+ const offCtx = this.cachedCanvas.getContext("2d");
1738
+ if (offCtx !== null) {
1739
+ offCtx.scale(dpr, dpr);
1740
+ }
1741
+ this.cachedCtx = offCtx;
1742
+ this.lastZoom = -1;
1743
+ }
1744
+ adaptSpacing(baseSpacing, zoom) {
1745
+ let spacing = baseSpacing * zoom;
1746
+ while (spacing < MIN_PATTERN_SPACING) {
1747
+ spacing *= 2;
1748
+ }
1749
+ return spacing;
1750
+ }
1751
+ renderDots(ctx, camera, width, height) {
1752
+ const spacing = this.adaptSpacing(this.spacing, camera.zoom);
1753
+ const offsetX = camera.position.x % spacing;
1754
+ const offsetY = camera.position.y % spacing;
1755
+ const radius = this.dotRadius * Math.min(camera.zoom, 2);
1756
+ ctx.fillStyle = this.color;
1757
+ ctx.beginPath();
1758
+ for (let x = offsetX; x < width; x += spacing) {
1759
+ for (let y = offsetY; y < height; y += spacing) {
1760
+ ctx.moveTo(x + radius, y);
1761
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
1762
+ }
1763
+ }
1764
+ ctx.fill();
1765
+ }
1766
+ renderGrid(ctx, camera, width, height) {
1767
+ const spacing = this.adaptSpacing(this.spacing, camera.zoom);
1768
+ const offsetX = camera.position.x % spacing;
1769
+ const offsetY = camera.position.y % spacing;
1770
+ const lineW = this.lineWidth * Math.min(camera.zoom, 2);
1771
+ ctx.fillStyle = this.color;
1772
+ for (let x = offsetX; x < width; x += spacing) {
1773
+ ctx.fillRect(x, 0, lineW, height);
1774
+ }
1775
+ for (let y = offsetY; y < height; y += spacing) {
1776
+ ctx.fillRect(0, y, width, lineW);
1777
+ }
1709
1778
  }
1710
1779
  };
1711
1780
 
1712
- // src/core/geometry.ts
1713
- function distSqToSegment(p, a, b) {
1714
- const abx = b.x - a.x;
1715
- const aby = b.y - a.y;
1716
- const apx = p.x - a.x;
1717
- const apy = p.y - a.y;
1718
- const lenSq = abx * abx + aby * aby;
1719
- if (lenSq === 0) {
1720
- return apx * apx + apy * apy;
1781
+ // src/core/event-bus.ts
1782
+ var EventBus = class {
1783
+ listeners = /* @__PURE__ */ new Map();
1784
+ on(event, listener) {
1785
+ const existing = this.listeners.get(event);
1786
+ if (existing) {
1787
+ existing.add(listener);
1788
+ } else {
1789
+ const set = /* @__PURE__ */ new Set([listener]);
1790
+ this.listeners.set(event, set);
1791
+ }
1792
+ return () => this.off(event, listener);
1721
1793
  }
1722
- const t = Math.max(0, Math.min(1, (apx * abx + apy * aby) / lenSq));
1723
- const dx = p.x - (a.x + t * abx);
1724
- const dy = p.y - (a.y + t * aby);
1725
- return dx * dx + dy * dy;
1726
- }
1794
+ off(event, listener) {
1795
+ this.listeners.get(event)?.delete(listener);
1796
+ }
1797
+ emit(event, data) {
1798
+ this.listeners.get(event)?.forEach((listener) => {
1799
+ try {
1800
+ listener(data);
1801
+ } catch (err) {
1802
+ console.error(`[fieldnotes] listener error for "${String(event)}"`, err);
1803
+ }
1804
+ });
1805
+ }
1806
+ clear() {
1807
+ this.listeners.clear();
1808
+ }
1809
+ };
1727
1810
 
1728
- // src/elements/arrow-geometry.ts
1729
- function getArrowControlPoint(from, to, bend) {
1730
- const midX = (from.x + to.x) / 2;
1731
- const midY = (from.y + to.y) / 2;
1732
- if (bend === 0) return { x: midX, y: midY };
1733
- const dx = to.x - from.x;
1734
- const dy = to.y - from.y;
1735
- const len = Math.sqrt(dx * dx + dy * dy);
1736
- if (len === 0) return { x: midX, y: midY };
1737
- const perpX = -dy / len;
1738
- const perpY = dx / len;
1739
- return {
1740
- x: midX + perpX * bend,
1741
- y: midY + perpY * bend
1742
- };
1743
- }
1744
- function getArrowMidpoint(from, to, bend) {
1745
- const cp = getArrowControlPoint(from, to, bend);
1746
- return {
1747
- x: 0.25 * from.x + 0.5 * cp.x + 0.25 * to.x,
1748
- y: 0.25 * from.y + 0.5 * cp.y + 0.25 * to.y
1749
- };
1750
- }
1751
- function getBendFromPoint(from, to, dragPoint) {
1752
- const midX = (from.x + to.x) / 2;
1753
- const midY = (from.y + to.y) / 2;
1754
- const dx = to.x - from.x;
1755
- const dy = to.y - from.y;
1756
- const len = Math.sqrt(dx * dx + dy * dy);
1757
- if (len === 0) return 0;
1758
- const perpX = -dy / len;
1759
- const perpY = dx / len;
1760
- return (dragPoint.x - midX) * perpX + (dragPoint.y - midY) * perpY;
1761
- }
1762
- function getArrowTangentAngle(from, to, bend, t) {
1763
- const cp = getArrowControlPoint(from, to, bend);
1764
- const tangentX = 2 * (1 - t) * (cp.x - from.x) + 2 * t * (to.x - cp.x);
1765
- const tangentY = 2 * (1 - t) * (cp.y - from.y) + 2 * t * (to.y - cp.y);
1766
- return Math.atan2(tangentY, tangentX);
1811
+ // src/core/quadtree.ts
1812
+ var MAX_ITEMS = 8;
1813
+ var MAX_DEPTH = 8;
1814
+ function intersects(a, b) {
1815
+ 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;
1767
1816
  }
1768
- function isNearBezier(point, from, to, bend, threshold) {
1769
- if (bend === 0) return isNearLine(point, from, to, threshold);
1770
- const cp = getArrowControlPoint(from, to, bend);
1771
- const segments = 20;
1772
- for (let i = 0; i < segments; i++) {
1773
- const t0 = i / segments;
1774
- const t1 = (i + 1) / segments;
1775
- const a = bezierPoint(from, cp, to, t0);
1776
- const b = bezierPoint(from, cp, to, t1);
1777
- if (isNearLine(point, a, b, threshold)) return true;
1817
+ var QuadNode = class _QuadNode {
1818
+ constructor(bounds, depth) {
1819
+ this.bounds = bounds;
1820
+ this.depth = depth;
1778
1821
  }
1779
- return false;
1780
- }
1781
- function getArrowBounds(from, to, bend) {
1782
- if (bend === 0) {
1783
- const minX2 = Math.min(from.x, to.x);
1784
- const minY2 = Math.min(from.y, to.y);
1785
- return {
1786
- x: minX2,
1787
- y: minY2,
1788
- w: Math.abs(to.x - from.x),
1789
- h: Math.abs(to.y - from.y)
1790
- };
1822
+ items = [];
1823
+ children = null;
1824
+ insert(entry) {
1825
+ if (this.children) {
1826
+ const idx = this.getChildIndex(entry.bounds);
1827
+ if (idx !== -1) {
1828
+ const child = this.children[idx];
1829
+ if (child) child.insert(entry);
1830
+ return;
1831
+ }
1832
+ this.items.push(entry);
1833
+ return;
1834
+ }
1835
+ this.items.push(entry);
1836
+ if (this.items.length > MAX_ITEMS && this.depth < MAX_DEPTH) {
1837
+ this.split();
1838
+ }
1791
1839
  }
1792
- const cp = getArrowControlPoint(from, to, bend);
1793
- const steps = 20;
1794
- let minX = Math.min(from.x, to.x);
1795
- let minY = Math.min(from.y, to.y);
1796
- let maxX = Math.max(from.x, to.x);
1797
- let maxY = Math.max(from.y, to.y);
1798
- for (let i = 1; i < steps; i++) {
1799
- const t = i / steps;
1800
- const p = bezierPoint(from, cp, to, t);
1801
- if (p.x < minX) minX = p.x;
1802
- if (p.y < minY) minY = p.y;
1803
- if (p.x > maxX) maxX = p.x;
1804
- if (p.y > maxY) maxY = p.y;
1840
+ remove(id) {
1841
+ const idx = this.items.findIndex((e) => e.id === id);
1842
+ if (idx !== -1) {
1843
+ this.items.splice(idx, 1);
1844
+ return true;
1845
+ }
1846
+ if (this.children) {
1847
+ for (const child of this.children) {
1848
+ if (child.remove(id)) {
1849
+ this.collapseIfEmpty();
1850
+ return true;
1851
+ }
1852
+ }
1853
+ }
1854
+ return false;
1805
1855
  }
1806
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1807
- }
1808
- function bezierPoint(from, cp, to, t) {
1809
- const mt = 1 - t;
1810
- return {
1811
- x: mt * mt * from.x + 2 * mt * t * cp.x + t * t * to.x,
1812
- y: mt * mt * from.y + 2 * mt * t * cp.y + t * t * to.y
1813
- };
1814
- }
1815
- function isNearLine(point, a, b, threshold) {
1816
- return distSqToSegment(point, a, b) <= threshold * threshold;
1817
- }
1818
-
1819
- // src/elements/element-bounds.ts
1820
- var strokeBoundsCache = /* @__PURE__ */ new WeakMap();
1821
- function getElementBounds(element) {
1822
- if (element.type === "grid") return null;
1823
- if ("size" in element) {
1824
- return {
1825
- x: element.position.x,
1826
- y: element.position.y,
1827
- w: element.size.w,
1828
- h: element.size.h
1829
- };
1856
+ query(rect, result) {
1857
+ if (!intersects(this.bounds, rect)) return;
1858
+ for (const item of this.items) {
1859
+ if (intersects(item.bounds, rect)) {
1860
+ result.push(item.id);
1861
+ }
1862
+ }
1863
+ if (this.children) {
1864
+ for (const child of this.children) {
1865
+ child.query(rect, result);
1866
+ }
1867
+ }
1830
1868
  }
1831
- if (element.type === "stroke") {
1832
- if (element.points.length === 0) return null;
1833
- const cached = strokeBoundsCache.get(element);
1834
- if (cached) return cached;
1835
- let minX = Infinity;
1836
- let minY = Infinity;
1837
- let maxX = -Infinity;
1838
- let maxY = -Infinity;
1839
- for (const p of element.points) {
1840
- const px = p.x + element.position.x;
1841
- const py = p.y + element.position.y;
1842
- if (px < minX) minX = px;
1843
- if (py < minY) minY = py;
1844
- if (px > maxX) maxX = px;
1845
- if (py > maxY) maxY = py;
1869
+ getChildIndex(itemBounds) {
1870
+ const midX = this.bounds.x + this.bounds.w / 2;
1871
+ const midY = this.bounds.y + this.bounds.h / 2;
1872
+ const left = itemBounds.x >= this.bounds.x && itemBounds.x + itemBounds.w <= midX;
1873
+ const right = itemBounds.x >= midX && itemBounds.x + itemBounds.w <= this.bounds.x + this.bounds.w;
1874
+ const top = itemBounds.y >= this.bounds.y && itemBounds.y + itemBounds.h <= midY;
1875
+ const bottom = itemBounds.y >= midY && itemBounds.y + itemBounds.h <= this.bounds.y + this.bounds.h;
1876
+ if (left && top) return 0;
1877
+ if (right && top) return 1;
1878
+ if (left && bottom) return 2;
1879
+ if (right && bottom) return 3;
1880
+ return -1;
1881
+ }
1882
+ split() {
1883
+ const { x, y, w, h } = this.bounds;
1884
+ const halfW = w / 2;
1885
+ const halfH = h / 2;
1886
+ const d = this.depth + 1;
1887
+ this.children = [
1888
+ new _QuadNode({ x, y, w: halfW, h: halfH }, d),
1889
+ new _QuadNode({ x: x + halfW, y, w: halfW, h: halfH }, d),
1890
+ new _QuadNode({ x, y: y + halfH, w: halfW, h: halfH }, d),
1891
+ new _QuadNode({ x: x + halfW, y: y + halfH, w: halfW, h: halfH }, d)
1892
+ ];
1893
+ const remaining = [];
1894
+ for (const item of this.items) {
1895
+ const idx = this.getChildIndex(item.bounds);
1896
+ if (idx !== -1) {
1897
+ const target = this.children[idx];
1898
+ if (target) target.insert(item);
1899
+ } else {
1900
+ remaining.push(item);
1901
+ }
1846
1902
  }
1847
- const bounds = { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1848
- strokeBoundsCache.set(element, bounds);
1849
- return bounds;
1903
+ this.items = remaining;
1850
1904
  }
1851
- if (element.type === "arrow") {
1852
- return getArrowBoundsAnalytical(element.from, element.to, element.bend);
1905
+ collapseIfEmpty() {
1906
+ if (!this.children) return;
1907
+ let totalItems = this.items.length;
1908
+ for (const child of this.children) {
1909
+ if (child.children) return;
1910
+ totalItems += child.items.length;
1911
+ }
1912
+ if (totalItems <= MAX_ITEMS) {
1913
+ for (const child of this.children) {
1914
+ this.items.push(...child.items);
1915
+ }
1916
+ this.children = null;
1917
+ }
1853
1918
  }
1854
- if (element.type === "template") {
1855
- return getTemplateBounds(element);
1919
+ };
1920
+ var Quadtree = class {
1921
+ root;
1922
+ _size = 0;
1923
+ worldBounds;
1924
+ constructor(worldBounds) {
1925
+ this.worldBounds = worldBounds;
1926
+ this.root = new QuadNode(worldBounds, 0);
1856
1927
  }
1857
- return null;
1858
- }
1859
- function getArrowBoundsAnalytical(from, to, bend) {
1860
- if (bend === 0) {
1861
- const minX2 = Math.min(from.x, to.x);
1862
- const minY2 = Math.min(from.y, to.y);
1863
- return {
1864
- x: minX2,
1865
- y: minY2,
1866
- w: Math.abs(to.x - from.x),
1867
- h: Math.abs(to.y - from.y)
1868
- };
1928
+ get size() {
1929
+ return this._size;
1869
1930
  }
1870
- const cp = getArrowControlPoint(from, to, bend);
1871
- let minX = Math.min(from.x, to.x);
1872
- let maxX = Math.max(from.x, to.x);
1873
- let minY = Math.min(from.y, to.y);
1874
- let maxY = Math.max(from.y, to.y);
1875
- const tx = from.x - 2 * cp.x + to.x;
1876
- if (tx !== 0) {
1877
- const t = (from.x - cp.x) / tx;
1878
- if (t > 0 && t < 1) {
1879
- const mt = 1 - t;
1880
- const x = mt * mt * from.x + 2 * mt * t * cp.x + t * t * to.x;
1881
- if (x < minX) minX = x;
1882
- if (x > maxX) maxX = x;
1883
- }
1931
+ insert(id, bounds) {
1932
+ this.root.insert({ id, bounds });
1933
+ this._size++;
1884
1934
  }
1885
- const ty = from.y - 2 * cp.y + to.y;
1886
- if (ty !== 0) {
1887
- const t = (from.y - cp.y) / ty;
1888
- if (t > 0 && t < 1) {
1889
- const mt = 1 - t;
1890
- const y = mt * mt * from.y + 2 * mt * t * cp.y + t * t * to.y;
1891
- if (y < minY) minY = y;
1892
- if (y > maxY) maxY = y;
1935
+ remove(id) {
1936
+ if (this.root.remove(id)) {
1937
+ this._size--;
1893
1938
  }
1894
1939
  }
1895
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1896
- }
1897
- function getTemplateBounds(el) {
1898
- const { x: cx, y: cy } = el.position;
1899
- const r = el.radius;
1900
- switch (el.templateShape) {
1901
- case "circle":
1902
- return { x: cx - r, y: cy - r, w: 2 * r, h: 2 * r };
1903
- case "square":
1904
- return { x: cx - r / 2, y: cy - r / 2, w: r, h: r };
1905
- case "cone": {
1906
- const halfAngle = Math.atan(0.5);
1907
- const tipX = cx;
1908
- const tipY = cy;
1909
- const leftX = cx + r * Math.cos(el.angle - halfAngle);
1910
- const leftY = cy + r * Math.sin(el.angle - halfAngle);
1911
- const rightX = cx + r * Math.cos(el.angle + halfAngle);
1912
- const rightY = cy + r * Math.sin(el.angle + halfAngle);
1913
- const farX = cx + r * Math.cos(el.angle);
1914
- const farY = cy + r * Math.sin(el.angle);
1915
- const xs = [tipX, leftX, rightX, farX];
1916
- const ys = [tipY, leftY, rightY, farY];
1917
- let minX = Infinity;
1918
- let minY = Infinity;
1919
- let maxX = -Infinity;
1920
- let maxY = -Infinity;
1921
- for (let i = 0; i < xs.length; i++) {
1922
- const px = xs[i];
1923
- const py = ys[i];
1924
- if (px !== void 0 && px < minX) minX = px;
1925
- if (px !== void 0 && px > maxX) maxX = px;
1926
- if (py !== void 0 && py < minY) minY = py;
1927
- if (py !== void 0 && py > maxY) maxY = py;
1928
- }
1929
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1930
- }
1931
- case "line": {
1932
- const halfW = r / 12;
1933
- const cos = Math.cos(el.angle);
1934
- const sin = Math.sin(el.angle);
1935
- const perpX = -sin * halfW;
1936
- const perpY = cos * halfW;
1937
- const x0 = cx + perpX;
1938
- const y0 = cy + perpY;
1939
- const x1 = cx + r * cos + perpX;
1940
- const y1 = cy + r * sin + perpY;
1941
- const x2 = cx + r * cos - perpX;
1942
- const y2 = cy + r * sin - perpY;
1943
- const x3 = cx - perpX;
1944
- const y3 = cy - perpY;
1945
- const minX = Math.min(x0, x1, x2, x3);
1946
- const minY = Math.min(y0, y1, y2, y3);
1947
- const maxX = Math.max(x0, x1, x2, x3);
1948
- const maxY = Math.max(y0, y1, y2, y3);
1949
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1950
- }
1940
+ update(id, newBounds) {
1941
+ this.remove(id);
1942
+ this.insert(id, newBounds);
1951
1943
  }
1952
- }
1953
- function transferStrokeBounds(prev, next) {
1954
- if (prev.type !== "stroke" || next.type !== "stroke") return;
1955
- if (prev.points !== next.points) return;
1956
- if (prev.position.x !== next.position.x || prev.position.y !== next.position.y) return;
1957
- const bounds = strokeBoundsCache.get(prev);
1958
- if (bounds) strokeBoundsCache.set(next, bounds);
1959
- }
1960
- function boundsIntersect(a, b) {
1961
- 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;
1962
- }
1944
+ query(rect) {
1945
+ const result = [];
1946
+ this.root.query(rect, result);
1947
+ return result;
1948
+ }
1949
+ queryPoint(point) {
1950
+ return this.query({ x: point.x, y: point.y, w: 0, h: 0 });
1951
+ }
1952
+ clear() {
1953
+ this.root = new QuadNode(this.worldBounds, 0);
1954
+ this._size = 0;
1955
+ }
1956
+ };
1963
1957
 
1964
1958
  // src/elements/stroke-smoothing.ts
1965
1959
  var MIN_PRESSURE_SCALE = 0.2;
@@ -2354,51 +2348,6 @@ function updateBoundArrow(arrow, store) {
2354
2348
  }
2355
2349
  return Object.keys(updates).length > 0 ? updates : null;
2356
2350
  }
2357
- function clearStaleBindings(arrow, store) {
2358
- const updates = {};
2359
- let hasUpdates = false;
2360
- if (arrow.fromBinding && !store.getById(arrow.fromBinding.elementId)) {
2361
- updates.fromBinding = void 0;
2362
- hasUpdates = true;
2363
- }
2364
- if (arrow.toBinding && !store.getById(arrow.toBinding.elementId)) {
2365
- updates.toBinding = void 0;
2366
- hasUpdates = true;
2367
- }
2368
- return hasUpdates ? updates : null;
2369
- }
2370
- function unbindArrow(arrow, store) {
2371
- const updates = {};
2372
- if (arrow.fromBinding) {
2373
- const el = store.getById(arrow.fromBinding.elementId);
2374
- const bounds = el ? getElementBounds(el) : null;
2375
- if (bounds) {
2376
- const angle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 0);
2377
- const rayTarget = {
2378
- x: arrow.from.x + Math.cos(angle) * 1e3,
2379
- y: arrow.from.y + Math.sin(angle) * 1e3
2380
- };
2381
- const edge = getEdgeIntersection(bounds, rayTarget);
2382
- updates.from = edge;
2383
- updates.position = edge;
2384
- }
2385
- updates.fromBinding = void 0;
2386
- }
2387
- if (arrow.toBinding) {
2388
- const el = store.getById(arrow.toBinding.elementId);
2389
- const bounds = el ? getElementBounds(el) : null;
2390
- if (bounds) {
2391
- const angle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 1);
2392
- const rayTarget = {
2393
- x: arrow.to.x - Math.cos(angle) * 1e3,
2394
- y: arrow.to.y - Math.sin(angle) * 1e3
2395
- };
2396
- updates.to = getEdgeIntersection(bounds, rayTarget);
2397
- }
2398
- updates.toBinding = void 0;
2399
- }
2400
- return updates;
2401
- }
2402
2351
 
2403
2352
  // src/elements/grid-renderer.ts
2404
2353
  function getSquareGridLines(bounds, cellSize) {
@@ -3626,6 +3575,8 @@ var NoteEditor = class {
3626
3575
  inputHandler = null;
3627
3576
  pendingEditId = null;
3628
3577
  onStopCallback = null;
3578
+ beginHistory = null;
3579
+ commitHistory = null;
3629
3580
  toolbar;
3630
3581
  placeholder;
3631
3582
  constructor(options) {
@@ -3641,6 +3592,10 @@ var NoteEditor = class {
3641
3592
  setOnStop(callback) {
3642
3593
  this.onStopCallback = callback;
3643
3594
  }
3595
+ setHistoryHooks(begin, commit) {
3596
+ this.beginHistory = begin;
3597
+ this.commitHistory = commit;
3598
+ }
3644
3599
  startEditing(node, elementId, store) {
3645
3600
  if (this.editingId === elementId) return;
3646
3601
  if (this.editingId) {
@@ -3672,18 +3627,21 @@ var NoteEditor = class {
3672
3627
  this.editingNode.removeAttribute("data-fn-empty");
3673
3628
  const text = sanitizeNoteHtml(this.editingNode.innerHTML);
3674
3629
  const current = store.getById(this.editingId);
3675
- if (current && (current.type === "note" || current.type === "text") && current.text !== text) {
3676
- store.update(this.editingId, { text });
3677
- }
3630
+ const textChanged = !!current && (current.type === "note" || current.type === "text") && current.text !== text;
3678
3631
  this.editingNode.contentEditable = "false";
3679
3632
  Object.assign(this.editingNode.style, {
3680
3633
  userSelect: "none",
3681
3634
  cursor: "default"
3682
3635
  });
3683
3636
  this.toolbar?.hide();
3637
+ this.beginHistory?.();
3638
+ if (textChanged) {
3639
+ store.update(this.editingId, { text });
3640
+ }
3684
3641
  if (this.editingId && this.onStopCallback) {
3685
3642
  this.onStopCallback(this.editingId);
3686
3643
  }
3644
+ this.commitHistory?.();
3687
3645
  this.editingId = null;
3688
3646
  this.editingNode = null;
3689
3647
  this.blurHandler = null;
@@ -3756,26 +3714,6 @@ var NoteEditor = class {
3756
3714
  }
3757
3715
  };
3758
3716
 
3759
- // src/elements/bounds.ts
3760
- function getElementsBoundingBox(elements) {
3761
- let minX = Infinity;
3762
- let minY = Infinity;
3763
- let maxX = -Infinity;
3764
- let maxY = -Infinity;
3765
- let found = false;
3766
- for (const el of elements) {
3767
- const b = getElementBounds(el);
3768
- if (!b) continue;
3769
- found = true;
3770
- if (b.x < minX) minX = b.x;
3771
- if (b.y < minY) minY = b.y;
3772
- if (b.x + b.w > maxX) maxX = b.x + b.w;
3773
- if (b.y + b.h > maxY) maxY = b.y + b.h;
3774
- }
3775
- if (!found) return null;
3776
- return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
3777
- }
3778
-
3779
3717
  // src/tools/tool-manager.ts
3780
3718
  var ToolManager = class {
3781
3719
  tools = /* @__PURE__ */ new Map();
@@ -4591,6 +4529,48 @@ var InteractMode = class {
4591
4529
  };
4592
4530
  };
4593
4531
 
4532
+ // src/canvas/double-tap-detector.ts
4533
+ var DEFAULT_TIMEOUT = 300;
4534
+ var DEFAULT_MAX_DISTANCE = 20;
4535
+ var DoubleTapDetector = class {
4536
+ timeout;
4537
+ maxDistance;
4538
+ lastTapTime = 0;
4539
+ lastTapX = 0;
4540
+ lastTapY = 0;
4541
+ hasPendingTap = false;
4542
+ constructor(options) {
4543
+ this.timeout = options?.timeout ?? DEFAULT_TIMEOUT;
4544
+ this.maxDistance = options?.maxDistance ?? DEFAULT_MAX_DISTANCE;
4545
+ }
4546
+ feed(e) {
4547
+ const now = Date.now();
4548
+ const x = e.clientX;
4549
+ const y = e.clientY;
4550
+ if (this.hasPendingTap) {
4551
+ const elapsed = now - this.lastTapTime;
4552
+ const dx = x - this.lastTapX;
4553
+ const dy = y - this.lastTapY;
4554
+ const dist = Math.sqrt(dx * dx + dy * dy);
4555
+ if (elapsed <= this.timeout && dist <= this.maxDistance) {
4556
+ this.reset();
4557
+ return true;
4558
+ }
4559
+ }
4560
+ this.lastTapTime = now;
4561
+ this.lastTapX = x;
4562
+ this.lastTapY = y;
4563
+ this.hasPendingTap = true;
4564
+ return false;
4565
+ }
4566
+ reset() {
4567
+ this.hasPendingTap = false;
4568
+ this.lastTapTime = 0;
4569
+ this.lastTapX = 0;
4570
+ this.lastTapY = 0;
4571
+ }
4572
+ };
4573
+
4594
4574
  // src/canvas/dom-node-manager.ts
4595
4575
  var DomNodeManager = class {
4596
4576
  domNodes = /* @__PURE__ */ new Map();
@@ -5288,6 +5268,10 @@ var Viewport = class {
5288
5268
  placeholder: options.placeholder
5289
5269
  });
5290
5270
  this.noteEditor.setOnStop((id) => this.onTextEditStop(id));
5271
+ this.noteEditor.setHistoryHooks(
5272
+ () => this.historyRecorder.begin(),
5273
+ () => this.historyRecorder.commit()
5274
+ );
5291
5275
  this.onHtmlElementMount = options.onHtmlElementMount;
5292
5276
  this.dropHandler = options.onDrop;
5293
5277
  this.history = new HistoryStack();
@@ -5304,6 +5288,7 @@ var Viewport = class {
5304
5288
  requestRender: () => this.requestRender(),
5305
5289
  switchTool: (name) => this.toolManager.setTool(name, this.toolContext),
5306
5290
  editElement: (id) => this.startEditingElement(id),
5291
+ fitNoteHeight: (id) => this.fitNoteHeight(id),
5307
5292
  setCursor: (cursor) => {
5308
5293
  this.wrapper.style.cursor = cursor;
5309
5294
  },
@@ -5638,31 +5623,38 @@ var Viewport = class {
5638
5623
  this.noteEditor.startEditing(node, id, this.store);
5639
5624
  }
5640
5625
  }
5626
+ fitNoteHeight(elementId) {
5627
+ const element = this.store.getById(elementId);
5628
+ if (!element || element.type !== "note") return;
5629
+ if (isNoteContentEmpty(element.text)) return;
5630
+ const node = this.domNodeManager.getNode(elementId);
5631
+ if (!node) return;
5632
+ const measured = node.scrollHeight;
5633
+ if (measured > element.size.h) {
5634
+ this.store.update(elementId, { size: { w: element.size.w, h: measured } });
5635
+ }
5636
+ }
5641
5637
  onTextEditStop(elementId) {
5642
5638
  const element = this.store.getById(elementId);
5643
5639
  if (!element) return;
5644
5640
  if (element.type === "note") {
5645
5641
  if (isNoteContentEmpty(element.text)) {
5646
- this.historyRecorder.begin();
5647
5642
  this.store.remove(elementId);
5648
- this.historyRecorder.commit();
5643
+ return;
5649
5644
  }
5645
+ this.fitNoteHeight(elementId);
5650
5646
  return;
5651
5647
  }
5652
5648
  if (element.type !== "text") return;
5653
5649
  if (!element.text || element.text.trim() === "") {
5654
- this.historyRecorder.begin();
5655
5650
  this.store.remove(elementId);
5656
- this.historyRecorder.commit();
5657
5651
  return;
5658
5652
  }
5659
5653
  const node = this.domNodeManager.getNode(elementId);
5660
5654
  if (node && "size" in element) {
5661
- const measuredHeight = node.scrollHeight;
5662
- if (measuredHeight !== element.size.h) {
5663
- this.store.update(elementId, {
5664
- size: { w: element.size.w, h: measuredHeight }
5665
- });
5655
+ const measured = node.scrollHeight;
5656
+ if (measured !== element.size.h) {
5657
+ this.store.update(elementId, { size: { w: element.size.w, h: measured } });
5666
5658
  }
5667
5659
  }
5668
5660
  }
@@ -6379,8 +6371,13 @@ var SelectTool = class {
6379
6371
  }
6380
6372
  this.pendingSingleSelectId = null;
6381
6373
  this.hasDragged = false;
6374
+ const resizedNoteId = this.mode.type === "resizing" ? this.mode.elementId : null;
6382
6375
  this.mode = { type: "idle" };
6383
6376
  ctx.setCursor?.("default");
6377
+ if (resizedNoteId !== null) {
6378
+ const el = ctx.store.getById(resizedNoteId);
6379
+ if (el?.type === "note") ctx.fitNoteHeight?.(resizedNoteId);
6380
+ }
6384
6381
  }
6385
6382
  onHover(state, ctx) {
6386
6383
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
@@ -7592,53 +7589,33 @@ var TemplateTool = class {
7592
7589
  };
7593
7590
 
7594
7591
  // src/index.ts
7595
- var VERSION = "0.24.0";
7592
+ var VERSION = "0.26.0";
7596
7593
  // Annotate the CommonJS export names for ESM import in node:
7597
7594
  0 && (module.exports = {
7598
- AddElementCommand,
7599
7595
  ArrowTool,
7600
7596
  AutoSave,
7601
- Background,
7602
- BatchCommand,
7603
7597
  Camera,
7604
- CreateLayerCommand,
7605
- DEFAULT_FONT_SIZE_PRESETS,
7606
7598
  DEFAULT_NOTE_FONT_SIZE,
7607
- DoubleTapDetector,
7608
- ElementRenderer,
7609
7599
  ElementStore,
7610
7600
  EraserTool,
7611
- EventBus,
7612
7601
  HandTool,
7613
- HistoryRecorder,
7614
7602
  HistoryStack,
7615
7603
  ImageTool,
7616
- InputFilter,
7617
- InputHandler,
7618
7604
  LayerManager,
7619
7605
  MeasureTool,
7620
- NoteEditor,
7621
7606
  NoteTool,
7622
- NoteToolbar,
7623
7607
  PencilTool,
7624
- Quadtree,
7625
- RemoveElementCommand,
7626
- RemoveLayerCommand,
7627
7608
  SelectTool,
7628
7609
  ShapeTool,
7629
7610
  TemplateTool,
7630
7611
  TextTool,
7631
7612
  ToolManager,
7632
- UpdateElementCommand,
7633
- UpdateLayerCommand,
7634
7613
  VERSION,
7635
7614
  Viewport,
7636
7615
  boundsIntersect,
7637
- clearStaleBindings,
7638
7616
  createArrow,
7639
7617
  createGrid,
7640
7618
  createHtmlElement,
7641
- createId,
7642
7619
  createImage,
7643
7620
  createNote,
7644
7621
  createShape,
@@ -7647,29 +7624,20 @@ var VERSION = "0.24.0";
7647
7624
  createText,
7648
7625
  drawHexPath,
7649
7626
  exportImage,
7650
- exportState,
7651
- findBindTarget,
7652
- findBoundArrows,
7653
7627
  getActiveFormats,
7654
7628
  getArrowBounds,
7655
7629
  getArrowControlPoint,
7656
7630
  getArrowMidpoint,
7657
7631
  getArrowTangentAngle,
7658
7632
  getBendFromPoint,
7659
- getEdgeIntersection,
7660
7633
  getElementBounds,
7661
- getElementCenter,
7662
7634
  getElementsBoundingBox,
7663
7635
  getHexCellsInCone,
7664
7636
  getHexCellsInLine,
7665
7637
  getHexCellsInRadius,
7666
7638
  getHexCellsInSquare,
7667
7639
  getHexDistance,
7668
- isBindable,
7669
7640
  isNearBezier,
7670
- isNoteContentEmpty,
7671
- parseState,
7672
- sanitizeNoteHtml,
7673
7641
  setFontSize,
7674
7642
  smartSnap,
7675
7643
  snapPoint,
@@ -7677,8 +7645,6 @@ var VERSION = "0.24.0";
7677
7645
  toggleBold,
7678
7646
  toggleItalic,
7679
7647
  toggleStrikethrough,
7680
- toggleUnderline,
7681
- unbindArrow,
7682
- updateBoundArrow
7648
+ toggleUnderline
7683
7649
  });
7684
7650
  //# sourceMappingURL=index.cjs.map