@fieldnotes/core 0.23.0 → 0.25.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;
@@ -1033,16 +688,17 @@ var KeyboardActions = class {
1033
688
  this.pasteCount = 0;
1034
689
  }
1035
690
  paste() {
691
+ if (this.deps.isToolActive()) return;
1036
692
  this.flushPendingNudge();
1037
- if (this.clipboard.length === 0 || this.deps.isToolActive()) return;
693
+ if (this.clipboard.length === 0) return;
1038
694
  const sel = this.selectTool();
1039
695
  if (!sel) return;
1040
696
  this.pasteCount++;
1041
697
  this.insertClones(this.clipboard, this.pasteCount * 20, sel);
1042
698
  }
1043
699
  duplicate() {
1044
- this.flushPendingNudge();
1045
700
  if (this.deps.isToolActive()) return;
701
+ this.flushPendingNudge();
1046
702
  const sel = this.selectTool();
1047
703
  if (!sel) return;
1048
704
  const source = [];
@@ -1226,6 +882,11 @@ function parseBinding(binding) {
1226
882
  throw new Error(`Invalid shortcut binding "${binding}": unknown modifier "${part}"`);
1227
883
  }
1228
884
  }
885
+ if (parsed.mod && (parsed.ctrl || parsed.meta)) {
886
+ throw new Error(
887
+ `Invalid shortcut binding "${binding}": "mod" already means Ctrl or Cmd; don't combine it with ctrl/meta`
888
+ );
889
+ }
1229
890
  return parsed;
1230
891
  }
1231
892
  function bindingMatches(p, e, allowShift) {
@@ -1369,6 +1030,10 @@ var InputHandler = class {
1369
1030
  this.inputFilter.reset();
1370
1031
  this.deferredDown = null;
1371
1032
  this.lastPointerEvent = null;
1033
+ if (this.scope === "focus") {
1034
+ this.element.removeAttribute("tabindex");
1035
+ this.element.style.outline = "";
1036
+ }
1372
1037
  }
1373
1038
  bind() {
1374
1039
  const opts = { signal: this.abortController.signal };
@@ -1559,145 +1224,433 @@ var InputHandler = class {
1559
1224
  }
1560
1225
  return;
1561
1226
  }
1562
- default:
1563
- if (action.startsWith("tool:")) {
1564
- if (this.isToolActive) return;
1565
- e.preventDefault();
1566
- this.toolContext?.switchTool?.(action.slice("tool:".length));
1567
- return;
1568
- }
1569
- console.warn(`[fieldnotes] unknown shortcut action "${action}"`);
1227
+ default:
1228
+ if (action.startsWith("tool:")) {
1229
+ if (this.isToolActive) return;
1230
+ e.preventDefault();
1231
+ this.toolContext?.switchTool?.(action.slice("tool:".length));
1232
+ return;
1233
+ }
1234
+ console.warn(`[fieldnotes] unknown shortcut action "${action}"`);
1235
+ }
1236
+ }
1237
+ startPinch() {
1238
+ this.inputFilter.reset();
1239
+ this.deferredDown = null;
1240
+ this.isPanning = true;
1241
+ const [a, b] = this.getPinchPoints();
1242
+ this.lastPinchDistance = this.distance(a, b);
1243
+ this.lastPinchCenter = this.midpoint(a, b);
1244
+ this.lastPointer = { ...this.lastPinchCenter };
1245
+ }
1246
+ handlePinchMove() {
1247
+ const [a, b] = this.getPinchPoints();
1248
+ const dist = this.distance(a, b);
1249
+ const center = this.midpoint(a, b);
1250
+ if (this.lastPinchDistance > 0) {
1251
+ const scale = dist / this.lastPinchDistance;
1252
+ const newZoom = this.camera.zoom * scale;
1253
+ this.camera.zoomAt(newZoom, center);
1254
+ }
1255
+ const dx = center.x - this.lastPointer.x;
1256
+ const dy = center.y - this.lastPointer.y;
1257
+ this.camera.pan(dx, dy);
1258
+ this.lastPinchDistance = dist;
1259
+ this.lastPinchCenter = center;
1260
+ this.lastPointer = { ...center };
1261
+ }
1262
+ getPinchPoints() {
1263
+ const pts = [...this.activePointers.values()];
1264
+ return [pts[0] ?? { x: 0, y: 0 }, pts[1] ?? { x: 0, y: 0 }];
1265
+ }
1266
+ distance(a, b) {
1267
+ const dx = a.x - b.x;
1268
+ const dy = a.y - b.y;
1269
+ return Math.sqrt(dx * dx + dy * dy);
1270
+ }
1271
+ midpoint(a, b) {
1272
+ return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
1273
+ }
1274
+ toPointerState(e) {
1275
+ const rect = this.element.getBoundingClientRect();
1276
+ return {
1277
+ x: e.clientX - rect.left,
1278
+ y: e.clientY - rect.top,
1279
+ pressure: e.pressure,
1280
+ pointerType: e.pointerType === "touch" || e.pointerType === "pen" ? e.pointerType : "mouse",
1281
+ shiftKey: e.shiftKey
1282
+ };
1283
+ }
1284
+ dispatchToolDown(e) {
1285
+ if (!this.toolManager || !this.toolContext) return;
1286
+ this.actions.flushPendingNudge();
1287
+ this.historyRecorder?.begin();
1288
+ this.isToolActive = true;
1289
+ this.toolManager.handlePointerDown(this.toPointerState(e), this.toolContext);
1290
+ }
1291
+ dispatchToolMove(e) {
1292
+ if (!this.toolManager || !this.toolContext) return;
1293
+ this.toolManager.handlePointerMove(this.toPointerState(e), this.toolContext);
1294
+ }
1295
+ dispatchToolHover(e) {
1296
+ if (!this.toolManager?.activeTool || !this.toolContext) return;
1297
+ const tool = this.toolManager.activeTool;
1298
+ if (tool.onHover) {
1299
+ tool.onHover(this.toPointerState(e), this.toolContext);
1300
+ }
1301
+ }
1302
+ dispatchToolUp(e) {
1303
+ if (!this.toolManager || !this.toolContext) return;
1304
+ this.toolManager.handlePointerUp(this.toPointerState(e), this.toolContext);
1305
+ this.historyRecorder?.commit();
1306
+ }
1307
+ isInScope() {
1308
+ if (this.scope === "window") return true;
1309
+ const active = document.activeElement;
1310
+ return active === this.element || this.element.contains(active);
1311
+ }
1312
+ focusSelf() {
1313
+ if (this.scope !== "focus" || this.isInScope()) return;
1314
+ this.element.focus({ preventScroll: true });
1315
+ }
1316
+ cancelToolIfActive(e) {
1317
+ if (this.isToolActive) {
1318
+ this.dispatchToolUp(e);
1319
+ this.isToolActive = false;
1320
+ }
1321
+ this.deferredDown = null;
1322
+ }
1323
+ };
1324
+
1325
+ // src/canvas/background.ts
1326
+ var MIN_PATTERN_SPACING = 16;
1327
+ var DEFAULTS = {
1328
+ pattern: "dots",
1329
+ spacing: 24,
1330
+ color: "#d0d0d0",
1331
+ dotRadius: 1,
1332
+ lineWidth: 0.5
1333
+ };
1334
+ var Background = class {
1335
+ pattern;
1336
+ spacing;
1337
+ color;
1338
+ dotRadius;
1339
+ lineWidth;
1340
+ cachedCanvas = null;
1341
+ cachedCtx = null;
1342
+ lastZoom = -1;
1343
+ lastOffsetX = -Infinity;
1344
+ lastOffsetY = -Infinity;
1345
+ lastWidth = 0;
1346
+ lastHeight = 0;
1347
+ constructor(options = {}) {
1348
+ this.pattern = options.pattern ?? DEFAULTS.pattern;
1349
+ this.spacing = options.spacing ?? DEFAULTS.spacing;
1350
+ this.color = options.color ?? DEFAULTS.color;
1351
+ this.dotRadius = options.dotRadius ?? DEFAULTS.dotRadius;
1352
+ this.lineWidth = options.lineWidth ?? DEFAULTS.lineWidth;
1353
+ }
1354
+ render(ctx, camera) {
1355
+ const { width, height } = ctx.canvas;
1356
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
1357
+ const cssWidth = width / dpr;
1358
+ const cssHeight = height / dpr;
1359
+ ctx.save();
1360
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
1361
+ ctx.clearRect(0, 0, cssWidth, cssHeight);
1362
+ if (this.pattern === "none") {
1363
+ ctx.restore();
1364
+ return;
1365
+ }
1366
+ const spacing = this.adaptSpacing(this.spacing, camera.zoom);
1367
+ const keyZoom = camera.zoom;
1368
+ const keyX = Math.floor(camera.position.x % spacing);
1369
+ const keyY = Math.floor(camera.position.y % spacing);
1370
+ if (this.cachedCanvas !== null && keyZoom === this.lastZoom && keyX === this.lastOffsetX && keyY === this.lastOffsetY && cssWidth === this.lastWidth && cssHeight === this.lastHeight) {
1371
+ ctx.drawImage(this.cachedCanvas, 0, 0);
1372
+ ctx.restore();
1373
+ return;
1374
+ }
1375
+ this.ensureCachedCanvas(cssWidth, cssHeight, dpr);
1376
+ if (this.cachedCtx === null) {
1377
+ if (this.pattern === "dots") {
1378
+ this.renderDots(ctx, camera, cssWidth, cssHeight);
1379
+ } else if (this.pattern === "grid") {
1380
+ this.renderGrid(ctx, camera, cssWidth, cssHeight);
1381
+ }
1382
+ ctx.restore();
1383
+ return;
1384
+ }
1385
+ const offCtx = this.cachedCtx;
1386
+ offCtx.clearRect(0, 0, cssWidth, cssHeight);
1387
+ if (this.pattern === "dots") {
1388
+ this.renderDots(offCtx, camera, cssWidth, cssHeight);
1389
+ } else if (this.pattern === "grid") {
1390
+ this.renderGrid(offCtx, camera, cssWidth, cssHeight);
1391
+ }
1392
+ this.lastZoom = keyZoom;
1393
+ this.lastOffsetX = keyX;
1394
+ this.lastOffsetY = keyY;
1395
+ this.lastWidth = cssWidth;
1396
+ this.lastHeight = cssHeight;
1397
+ ctx.drawImage(this.cachedCanvas, 0, 0);
1398
+ ctx.restore();
1399
+ }
1400
+ ensureCachedCanvas(cssWidth, cssHeight, dpr) {
1401
+ if (this.cachedCanvas !== null && this.lastWidth === cssWidth && this.lastHeight === cssHeight) {
1402
+ return;
1403
+ }
1404
+ const physWidth = Math.round(cssWidth * dpr);
1405
+ const physHeight = Math.round(cssHeight * dpr);
1406
+ if (typeof OffscreenCanvas !== "undefined") {
1407
+ this.cachedCanvas = new OffscreenCanvas(physWidth, physHeight);
1408
+ } else if (typeof document !== "undefined") {
1409
+ const el = document.createElement("canvas");
1410
+ el.width = physWidth;
1411
+ el.height = physHeight;
1412
+ this.cachedCanvas = el;
1413
+ } else {
1414
+ this.cachedCanvas = null;
1415
+ this.cachedCtx = null;
1416
+ return;
1417
+ }
1418
+ const offCtx = this.cachedCanvas.getContext("2d");
1419
+ if (offCtx !== null) {
1420
+ offCtx.scale(dpr, dpr);
1421
+ }
1422
+ this.cachedCtx = offCtx;
1423
+ this.lastZoom = -1;
1424
+ }
1425
+ adaptSpacing(baseSpacing, zoom) {
1426
+ let spacing = baseSpacing * zoom;
1427
+ while (spacing < MIN_PATTERN_SPACING) {
1428
+ spacing *= 2;
1429
+ }
1430
+ return spacing;
1431
+ }
1432
+ renderDots(ctx, camera, width, height) {
1433
+ const spacing = this.adaptSpacing(this.spacing, camera.zoom);
1434
+ const offsetX = camera.position.x % spacing;
1435
+ const offsetY = camera.position.y % spacing;
1436
+ const radius = this.dotRadius * Math.min(camera.zoom, 2);
1437
+ ctx.fillStyle = this.color;
1438
+ ctx.beginPath();
1439
+ for (let x = offsetX; x < width; x += spacing) {
1440
+ for (let y = offsetY; y < height; y += spacing) {
1441
+ ctx.moveTo(x + radius, y);
1442
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
1443
+ }
1444
+ }
1445
+ ctx.fill();
1446
+ }
1447
+ renderGrid(ctx, camera, width, height) {
1448
+ const spacing = this.adaptSpacing(this.spacing, camera.zoom);
1449
+ const offsetX = camera.position.x % spacing;
1450
+ const offsetY = camera.position.y % spacing;
1451
+ const lineW = this.lineWidth * Math.min(camera.zoom, 2);
1452
+ ctx.fillStyle = this.color;
1453
+ for (let x = offsetX; x < width; x += spacing) {
1454
+ ctx.fillRect(x, 0, lineW, height);
1455
+ }
1456
+ for (let y = offsetY; y < height; y += spacing) {
1457
+ ctx.fillRect(0, y, width, lineW);
1458
+ }
1459
+ }
1460
+ };
1461
+
1462
+ // src/core/event-bus.ts
1463
+ var EventBus = class {
1464
+ listeners = /* @__PURE__ */ new Map();
1465
+ on(event, listener) {
1466
+ const existing = this.listeners.get(event);
1467
+ if (existing) {
1468
+ existing.add(listener);
1469
+ } else {
1470
+ const set = /* @__PURE__ */ new Set([listener]);
1471
+ this.listeners.set(event, set);
1472
+ }
1473
+ return () => this.off(event, listener);
1474
+ }
1475
+ off(event, listener) {
1476
+ this.listeners.get(event)?.delete(listener);
1477
+ }
1478
+ emit(event, data) {
1479
+ this.listeners.get(event)?.forEach((listener) => {
1480
+ try {
1481
+ listener(data);
1482
+ } catch (err) {
1483
+ console.error(`[fieldnotes] listener error for "${String(event)}"`, err);
1484
+ }
1485
+ });
1486
+ }
1487
+ clear() {
1488
+ this.listeners.clear();
1489
+ }
1490
+ };
1491
+
1492
+ // src/core/quadtree.ts
1493
+ var MAX_ITEMS = 8;
1494
+ var MAX_DEPTH = 8;
1495
+ function intersects(a, b) {
1496
+ 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;
1497
+ }
1498
+ var QuadNode = class _QuadNode {
1499
+ constructor(bounds, depth) {
1500
+ this.bounds = bounds;
1501
+ this.depth = depth;
1502
+ }
1503
+ items = [];
1504
+ children = null;
1505
+ insert(entry) {
1506
+ if (this.children) {
1507
+ const idx = this.getChildIndex(entry.bounds);
1508
+ if (idx !== -1) {
1509
+ const child = this.children[idx];
1510
+ if (child) child.insert(entry);
1511
+ return;
1512
+ }
1513
+ this.items.push(entry);
1514
+ return;
1515
+ }
1516
+ this.items.push(entry);
1517
+ if (this.items.length > MAX_ITEMS && this.depth < MAX_DEPTH) {
1518
+ this.split();
1519
+ }
1520
+ }
1521
+ remove(id) {
1522
+ const idx = this.items.findIndex((e) => e.id === id);
1523
+ if (idx !== -1) {
1524
+ this.items.splice(idx, 1);
1525
+ return true;
1526
+ }
1527
+ if (this.children) {
1528
+ for (const child of this.children) {
1529
+ if (child.remove(id)) {
1530
+ this.collapseIfEmpty();
1531
+ return true;
1532
+ }
1533
+ }
1534
+ }
1535
+ return false;
1536
+ }
1537
+ query(rect, result) {
1538
+ if (!intersects(this.bounds, rect)) return;
1539
+ for (const item of this.items) {
1540
+ if (intersects(item.bounds, rect)) {
1541
+ result.push(item.id);
1542
+ }
1543
+ }
1544
+ if (this.children) {
1545
+ for (const child of this.children) {
1546
+ child.query(rect, result);
1547
+ }
1548
+ }
1549
+ }
1550
+ getChildIndex(itemBounds) {
1551
+ const midX = this.bounds.x + this.bounds.w / 2;
1552
+ const midY = this.bounds.y + this.bounds.h / 2;
1553
+ const left = itemBounds.x >= this.bounds.x && itemBounds.x + itemBounds.w <= midX;
1554
+ const right = itemBounds.x >= midX && itemBounds.x + itemBounds.w <= this.bounds.x + this.bounds.w;
1555
+ const top = itemBounds.y >= this.bounds.y && itemBounds.y + itemBounds.h <= midY;
1556
+ const bottom = itemBounds.y >= midY && itemBounds.y + itemBounds.h <= this.bounds.y + this.bounds.h;
1557
+ if (left && top) return 0;
1558
+ if (right && top) return 1;
1559
+ if (left && bottom) return 2;
1560
+ if (right && bottom) return 3;
1561
+ return -1;
1562
+ }
1563
+ split() {
1564
+ const { x, y, w, h } = this.bounds;
1565
+ const halfW = w / 2;
1566
+ const halfH = h / 2;
1567
+ const d = this.depth + 1;
1568
+ this.children = [
1569
+ new _QuadNode({ x, y, w: halfW, h: halfH }, d),
1570
+ new _QuadNode({ x: x + halfW, y, w: halfW, h: halfH }, d),
1571
+ new _QuadNode({ x, y: y + halfH, w: halfW, h: halfH }, d),
1572
+ new _QuadNode({ x: x + halfW, y: y + halfH, w: halfW, h: halfH }, d)
1573
+ ];
1574
+ const remaining = [];
1575
+ for (const item of this.items) {
1576
+ const idx = this.getChildIndex(item.bounds);
1577
+ if (idx !== -1) {
1578
+ const target = this.children[idx];
1579
+ if (target) target.insert(item);
1580
+ } else {
1581
+ remaining.push(item);
1582
+ }
1570
1583
  }
1584
+ this.items = remaining;
1571
1585
  }
1572
- startPinch() {
1573
- this.inputFilter.reset();
1574
- this.deferredDown = null;
1575
- this.isPanning = true;
1576
- const [a, b] = this.getPinchPoints();
1577
- this.lastPinchDistance = this.distance(a, b);
1578
- this.lastPinchCenter = this.midpoint(a, b);
1579
- this.lastPointer = { ...this.lastPinchCenter };
1580
- }
1581
- handlePinchMove() {
1582
- const [a, b] = this.getPinchPoints();
1583
- const dist = this.distance(a, b);
1584
- const center = this.midpoint(a, b);
1585
- if (this.lastPinchDistance > 0) {
1586
- const scale = dist / this.lastPinchDistance;
1587
- const newZoom = this.camera.zoom * scale;
1588
- this.camera.zoomAt(newZoom, center);
1586
+ collapseIfEmpty() {
1587
+ if (!this.children) return;
1588
+ let totalItems = this.items.length;
1589
+ for (const child of this.children) {
1590
+ if (child.children) return;
1591
+ totalItems += child.items.length;
1592
+ }
1593
+ if (totalItems <= MAX_ITEMS) {
1594
+ for (const child of this.children) {
1595
+ this.items.push(...child.items);
1596
+ }
1597
+ this.children = null;
1589
1598
  }
1590
- const dx = center.x - this.lastPointer.x;
1591
- const dy = center.y - this.lastPointer.y;
1592
- this.camera.pan(dx, dy);
1593
- this.lastPinchDistance = dist;
1594
- this.lastPinchCenter = center;
1595
- this.lastPointer = { ...center };
1596
- }
1597
- getPinchPoints() {
1598
- const pts = [...this.activePointers.values()];
1599
- return [pts[0] ?? { x: 0, y: 0 }, pts[1] ?? { x: 0, y: 0 }];
1600
- }
1601
- distance(a, b) {
1602
- const dx = a.x - b.x;
1603
- const dy = a.y - b.y;
1604
- return Math.sqrt(dx * dx + dy * dy);
1605
- }
1606
- midpoint(a, b) {
1607
- return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
1608
1599
  }
1609
- toPointerState(e) {
1610
- const rect = this.element.getBoundingClientRect();
1611
- return {
1612
- x: e.clientX - rect.left,
1613
- y: e.clientY - rect.top,
1614
- pressure: e.pressure,
1615
- pointerType: e.pointerType === "touch" || e.pointerType === "pen" ? e.pointerType : "mouse",
1616
- shiftKey: e.shiftKey
1617
- };
1600
+ };
1601
+ var Quadtree = class {
1602
+ root;
1603
+ _size = 0;
1604
+ worldBounds;
1605
+ constructor(worldBounds) {
1606
+ this.worldBounds = worldBounds;
1607
+ this.root = new QuadNode(worldBounds, 0);
1618
1608
  }
1619
- dispatchToolDown(e) {
1620
- if (!this.toolManager || !this.toolContext) return;
1621
- this.actions.flushPendingNudge();
1622
- this.historyRecorder?.begin();
1623
- this.isToolActive = true;
1624
- this.toolManager.handlePointerDown(this.toPointerState(e), this.toolContext);
1609
+ get size() {
1610
+ return this._size;
1625
1611
  }
1626
- dispatchToolMove(e) {
1627
- if (!this.toolManager || !this.toolContext) return;
1628
- this.toolManager.handlePointerMove(this.toPointerState(e), this.toolContext);
1612
+ insert(id, bounds) {
1613
+ this.root.insert({ id, bounds });
1614
+ this._size++;
1629
1615
  }
1630
- dispatchToolHover(e) {
1631
- if (!this.toolManager?.activeTool || !this.toolContext) return;
1632
- const tool = this.toolManager.activeTool;
1633
- if (tool.onHover) {
1634
- tool.onHover(this.toPointerState(e), this.toolContext);
1616
+ remove(id) {
1617
+ if (this.root.remove(id)) {
1618
+ this._size--;
1635
1619
  }
1636
1620
  }
1637
- dispatchToolUp(e) {
1638
- if (!this.toolManager || !this.toolContext) return;
1639
- this.toolManager.handlePointerUp(this.toPointerState(e), this.toolContext);
1640
- this.historyRecorder?.commit();
1621
+ update(id, newBounds) {
1622
+ this.remove(id);
1623
+ this.insert(id, newBounds);
1641
1624
  }
1642
- isInScope() {
1643
- if (this.scope === "window") return true;
1644
- const active = document.activeElement;
1645
- return active === this.element || this.element.contains(active);
1625
+ query(rect) {
1626
+ const result = [];
1627
+ this.root.query(rect, result);
1628
+ return result;
1646
1629
  }
1647
- focusSelf() {
1648
- if (this.scope !== "focus" || this.isInScope()) return;
1649
- this.element.focus({ preventScroll: true });
1630
+ queryPoint(point) {
1631
+ return this.query({ x: point.x, y: point.y, w: 0, h: 0 });
1650
1632
  }
1651
- cancelToolIfActive(e) {
1652
- if (this.isToolActive) {
1653
- this.dispatchToolUp(e);
1654
- this.isToolActive = false;
1655
- }
1656
- this.deferredDown = null;
1633
+ clear() {
1634
+ this.root = new QuadNode(this.worldBounds, 0);
1635
+ this._size = 0;
1657
1636
  }
1658
1637
  };
1659
1638
 
1660
- // src/canvas/double-tap-detector.ts
1661
- var DEFAULT_TIMEOUT = 300;
1662
- var DEFAULT_MAX_DISTANCE = 20;
1663
- var DoubleTapDetector = class {
1664
- timeout;
1665
- maxDistance;
1666
- lastTapTime = 0;
1667
- lastTapX = 0;
1668
- lastTapY = 0;
1669
- hasPendingTap = false;
1670
- constructor(options) {
1671
- this.timeout = options?.timeout ?? DEFAULT_TIMEOUT;
1672
- this.maxDistance = options?.maxDistance ?? DEFAULT_MAX_DISTANCE;
1673
- }
1674
- feed(e) {
1675
- const now = Date.now();
1676
- const x = e.clientX;
1677
- const y = e.clientY;
1678
- if (this.hasPendingTap) {
1679
- const elapsed = now - this.lastTapTime;
1680
- const dx = x - this.lastTapX;
1681
- const dy = y - this.lastTapY;
1682
- const dist = Math.sqrt(dx * dx + dy * dy);
1683
- if (elapsed <= this.timeout && dist <= this.maxDistance) {
1684
- this.reset();
1685
- return true;
1686
- }
1687
- }
1688
- this.lastTapTime = now;
1689
- this.lastTapX = x;
1690
- this.lastTapY = y;
1691
- this.hasPendingTap = true;
1692
- return false;
1693
- }
1694
- reset() {
1695
- this.hasPendingTap = false;
1696
- this.lastTapTime = 0;
1697
- this.lastTapX = 0;
1698
- this.lastTapY = 0;
1639
+ // src/core/geometry.ts
1640
+ function distSqToSegment(p, a, b) {
1641
+ const abx = b.x - a.x;
1642
+ const aby = b.y - a.y;
1643
+ const apx = p.x - a.x;
1644
+ const apy = p.y - a.y;
1645
+ const lenSq = abx * abx + aby * aby;
1646
+ if (lenSq === 0) {
1647
+ return apx * apx + apy * apy;
1699
1648
  }
1700
- };
1649
+ const t = Math.max(0, Math.min(1, (apx * abx + apy * aby) / lenSq));
1650
+ const dx = p.x - (a.x + t * abx);
1651
+ const dy = p.y - (a.y + t * aby);
1652
+ return dx * dx + dy * dy;
1653
+ }
1701
1654
 
1702
1655
  // src/elements/arrow-geometry.ts
1703
1656
  function getArrowControlPoint(from, to, bend) {
@@ -1787,16 +1740,7 @@ function bezierPoint(from, cp, to, t) {
1787
1740
  };
1788
1741
  }
1789
1742
  function isNearLine(point, a, b, threshold) {
1790
- const dx = b.x - a.x;
1791
- const dy = b.y - a.y;
1792
- const lenSq = dx * dx + dy * dy;
1793
- if (lenSq === 0) {
1794
- return Math.hypot(point.x - a.x, point.y - a.y) <= threshold;
1795
- }
1796
- const t = Math.max(0, Math.min(1, ((point.x - a.x) * dx + (point.y - a.y) * dy) / lenSq));
1797
- const projX = a.x + t * dx;
1798
- const projY = a.y + t * dy;
1799
- return Math.hypot(point.x - projX, point.y - projY) <= threshold;
1743
+ return distSqToSegment(point, a, b) <= threshold * threshold;
1800
1744
  }
1801
1745
 
1802
1746
  // src/elements/element-bounds.ts
@@ -2337,51 +2281,6 @@ function updateBoundArrow(arrow, store) {
2337
2281
  }
2338
2282
  return Object.keys(updates).length > 0 ? updates : null;
2339
2283
  }
2340
- function clearStaleBindings(arrow, store) {
2341
- const updates = {};
2342
- let hasUpdates = false;
2343
- if (arrow.fromBinding && !store.getById(arrow.fromBinding.elementId)) {
2344
- updates.fromBinding = void 0;
2345
- hasUpdates = true;
2346
- }
2347
- if (arrow.toBinding && !store.getById(arrow.toBinding.elementId)) {
2348
- updates.toBinding = void 0;
2349
- hasUpdates = true;
2350
- }
2351
- return hasUpdates ? updates : null;
2352
- }
2353
- function unbindArrow(arrow, store) {
2354
- const updates = {};
2355
- if (arrow.fromBinding) {
2356
- const el = store.getById(arrow.fromBinding.elementId);
2357
- const bounds = el ? getElementBounds(el) : null;
2358
- if (bounds) {
2359
- const angle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 0);
2360
- const rayTarget = {
2361
- x: arrow.from.x + Math.cos(angle) * 1e3,
2362
- y: arrow.from.y + Math.sin(angle) * 1e3
2363
- };
2364
- const edge = getEdgeIntersection(bounds, rayTarget);
2365
- updates.from = edge;
2366
- updates.position = edge;
2367
- }
2368
- updates.fromBinding = void 0;
2369
- }
2370
- if (arrow.toBinding) {
2371
- const el = store.getById(arrow.toBinding.elementId);
2372
- const bounds = el ? getElementBounds(el) : null;
2373
- if (bounds) {
2374
- const angle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 1);
2375
- const rayTarget = {
2376
- x: arrow.to.x - Math.cos(angle) * 1e3,
2377
- y: arrow.to.y - Math.sin(angle) * 1e3
2378
- };
2379
- updates.to = getEdgeIntersection(bounds, rayTarget);
2380
- }
2381
- updates.toBinding = void 0;
2382
- }
2383
- return updates;
2384
- }
2385
2284
 
2386
2285
  // src/elements/grid-renderer.ts
2387
2286
  function getSquareGridLines(bounds, cellSize) {
@@ -3227,9 +3126,9 @@ var ElementRenderer = class {
3227
3126
  });
3228
3127
  }
3229
3128
  };
3230
- img.onerror = () => {
3129
+ img.onerror = (event) => {
3231
3130
  this.imageCache.set(src, "failed");
3232
- this.onImageError?.(src);
3131
+ this.onImageError?.(src, event);
3233
3132
  this.onImageLoad?.();
3234
3133
  };
3235
3134
  return null;
@@ -3654,7 +3553,10 @@ var NoteEditor = class {
3654
3553
  this.editingNode.removeAttribute("data-fn-placeholder");
3655
3554
  this.editingNode.removeAttribute("data-fn-empty");
3656
3555
  const text = sanitizeNoteHtml(this.editingNode.innerHTML);
3657
- store.update(this.editingId, { text });
3556
+ const current = store.getById(this.editingId);
3557
+ if (current && (current.type === "note" || current.type === "text") && current.text !== text) {
3558
+ store.update(this.editingId, { text });
3559
+ }
3658
3560
  this.editingNode.contentEditable = "false";
3659
3561
  Object.assign(this.editingNode.style, {
3660
3562
  userSelect: "none",
@@ -4571,6 +4473,48 @@ var InteractMode = class {
4571
4473
  };
4572
4474
  };
4573
4475
 
4476
+ // src/canvas/double-tap-detector.ts
4477
+ var DEFAULT_TIMEOUT = 300;
4478
+ var DEFAULT_MAX_DISTANCE = 20;
4479
+ var DoubleTapDetector = class {
4480
+ timeout;
4481
+ maxDistance;
4482
+ lastTapTime = 0;
4483
+ lastTapX = 0;
4484
+ lastTapY = 0;
4485
+ hasPendingTap = false;
4486
+ constructor(options) {
4487
+ this.timeout = options?.timeout ?? DEFAULT_TIMEOUT;
4488
+ this.maxDistance = options?.maxDistance ?? DEFAULT_MAX_DISTANCE;
4489
+ }
4490
+ feed(e) {
4491
+ const now = Date.now();
4492
+ const x = e.clientX;
4493
+ const y = e.clientY;
4494
+ if (this.hasPendingTap) {
4495
+ const elapsed = now - this.lastTapTime;
4496
+ const dx = x - this.lastTapX;
4497
+ const dy = y - this.lastTapY;
4498
+ const dist = Math.sqrt(dx * dx + dy * dy);
4499
+ if (elapsed <= this.timeout && dist <= this.maxDistance) {
4500
+ this.reset();
4501
+ return true;
4502
+ }
4503
+ }
4504
+ this.lastTapTime = now;
4505
+ this.lastTapX = x;
4506
+ this.lastTapY = y;
4507
+ this.hasPendingTap = true;
4508
+ return false;
4509
+ }
4510
+ reset() {
4511
+ this.hasPendingTap = false;
4512
+ this.lastTapTime = 0;
4513
+ this.lastTapX = 0;
4514
+ this.lastTapY = 0;
4515
+ }
4516
+ };
4517
+
4574
4518
  // src/canvas/dom-node-manager.ts
4575
4519
  var DomNodeManager = class {
4576
4520
  domNodes = /* @__PURE__ */ new Map();
@@ -5251,13 +5195,13 @@ var Viewport = class {
5251
5195
  this.renderLoop.markAllLayersDirty();
5252
5196
  this.requestRender();
5253
5197
  });
5254
- this.renderer.setOnImageError((src) => {
5198
+ this.renderer.setOnImageError((src, cause) => {
5255
5199
  const elementIds = [];
5256
5200
  for (const el of this.store.getAll()) {
5257
5201
  if (el.type === "image" && el.src === src) elementIds.push(el.id);
5258
5202
  }
5259
5203
  if (options.onImageError) {
5260
- options.onImageError({ src, elementIds });
5204
+ options.onImageError({ src, elementIds, cause });
5261
5205
  } else {
5262
5206
  console.warn(`[fieldnotes] image failed to load: ${src}`);
5263
5207
  }
@@ -5476,6 +5420,10 @@ var Viewport = class {
5476
5420
  this.loadState(parseState(json));
5477
5421
  }
5478
5422
  setTool(name) {
5423
+ if (!this.toolManager.getTool(name)) {
5424
+ console.warn(`[fieldnotes] setTool: no tool registered as "${name}"`);
5425
+ return;
5426
+ }
5479
5427
  this.toolManager.setTool(name, this.toolContext);
5480
5428
  }
5481
5429
  get shortcuts() {
@@ -5972,20 +5920,6 @@ var PencilTool = class {
5972
5920
  };
5973
5921
 
5974
5922
  // src/elements/stroke-hit.ts
5975
- function distSqToSegment(p, a, b) {
5976
- const abx = b.x - a.x;
5977
- const aby = b.y - a.y;
5978
- const apx = p.x - a.x;
5979
- const apy = p.y - a.y;
5980
- const lenSq = abx * abx + aby * aby;
5981
- if (lenSq === 0) {
5982
- return apx * apx + apy * apy;
5983
- }
5984
- const t = Math.max(0, Math.min(1, (apx * abx + apy * aby) / lenSq));
5985
- const dx = p.x - (a.x + t * abx);
5986
- const dy = p.y - (a.y + t * aby);
5987
- return dx * dx + dy * dy;
5988
- }
5989
5923
  function hitTestStroke(stroke, point, radius) {
5990
5924
  const bounds = getElementBounds(stroke);
5991
5925
  if (!bounds) return false;
@@ -7582,53 +7516,33 @@ var TemplateTool = class {
7582
7516
  };
7583
7517
 
7584
7518
  // src/index.ts
7585
- var VERSION = "0.23.0";
7519
+ var VERSION = "0.25.0";
7586
7520
  // Annotate the CommonJS export names for ESM import in node:
7587
7521
  0 && (module.exports = {
7588
- AddElementCommand,
7589
7522
  ArrowTool,
7590
7523
  AutoSave,
7591
- Background,
7592
- BatchCommand,
7593
7524
  Camera,
7594
- CreateLayerCommand,
7595
- DEFAULT_FONT_SIZE_PRESETS,
7596
7525
  DEFAULT_NOTE_FONT_SIZE,
7597
- DoubleTapDetector,
7598
- ElementRenderer,
7599
7526
  ElementStore,
7600
7527
  EraserTool,
7601
- EventBus,
7602
7528
  HandTool,
7603
- HistoryRecorder,
7604
7529
  HistoryStack,
7605
7530
  ImageTool,
7606
- InputFilter,
7607
- InputHandler,
7608
7531
  LayerManager,
7609
7532
  MeasureTool,
7610
- NoteEditor,
7611
7533
  NoteTool,
7612
- NoteToolbar,
7613
7534
  PencilTool,
7614
- Quadtree,
7615
- RemoveElementCommand,
7616
- RemoveLayerCommand,
7617
7535
  SelectTool,
7618
7536
  ShapeTool,
7619
7537
  TemplateTool,
7620
7538
  TextTool,
7621
7539
  ToolManager,
7622
- UpdateElementCommand,
7623
- UpdateLayerCommand,
7624
7540
  VERSION,
7625
7541
  Viewport,
7626
7542
  boundsIntersect,
7627
- clearStaleBindings,
7628
7543
  createArrow,
7629
7544
  createGrid,
7630
7545
  createHtmlElement,
7631
- createId,
7632
7546
  createImage,
7633
7547
  createNote,
7634
7548
  createShape,
@@ -7637,29 +7551,20 @@ var VERSION = "0.23.0";
7637
7551
  createText,
7638
7552
  drawHexPath,
7639
7553
  exportImage,
7640
- exportState,
7641
- findBindTarget,
7642
- findBoundArrows,
7643
7554
  getActiveFormats,
7644
7555
  getArrowBounds,
7645
7556
  getArrowControlPoint,
7646
7557
  getArrowMidpoint,
7647
7558
  getArrowTangentAngle,
7648
7559
  getBendFromPoint,
7649
- getEdgeIntersection,
7650
7560
  getElementBounds,
7651
- getElementCenter,
7652
7561
  getElementsBoundingBox,
7653
7562
  getHexCellsInCone,
7654
7563
  getHexCellsInLine,
7655
7564
  getHexCellsInRadius,
7656
7565
  getHexCellsInSquare,
7657
7566
  getHexDistance,
7658
- isBindable,
7659
7567
  isNearBezier,
7660
- isNoteContentEmpty,
7661
- parseState,
7662
- sanitizeNoteHtml,
7663
7568
  setFontSize,
7664
7569
  smartSnap,
7665
7570
  snapPoint,
@@ -7667,8 +7572,6 @@ var VERSION = "0.23.0";
7667
7572
  toggleBold,
7668
7573
  toggleItalic,
7669
7574
  toggleStrikethrough,
7670
- toggleUnderline,
7671
- unbindArrow,
7672
- updateBoundArrow
7575
+ toggleUnderline
7673
7576
  });
7674
7577
  //# sourceMappingURL=index.cjs.map