@fieldnotes/core 0.24.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;
@@ -1597,115 +1252,387 @@ var InputHandler = class {
1597
1252
  const newZoom = this.camera.zoom * scale;
1598
1253
  this.camera.zoomAt(newZoom, center);
1599
1254
  }
1600
- const dx = center.x - this.lastPointer.x;
1601
- const dy = center.y - this.lastPointer.y;
1602
- this.camera.pan(dx, dy);
1603
- this.lastPinchDistance = dist;
1604
- this.lastPinchCenter = center;
1605
- this.lastPointer = { ...center };
1606
- }
1607
- getPinchPoints() {
1608
- const pts = [...this.activePointers.values()];
1609
- return [pts[0] ?? { x: 0, y: 0 }, pts[1] ?? { x: 0, y: 0 }];
1610
- }
1611
- distance(a, b) {
1612
- const dx = a.x - b.x;
1613
- const dy = a.y - b.y;
1614
- return Math.sqrt(dx * dx + dy * dy);
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
+ }
1615
1520
  }
1616
- midpoint(a, b) {
1617
- return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
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;
1618
1536
  }
1619
- toPointerState(e) {
1620
- const rect = this.element.getBoundingClientRect();
1621
- return {
1622
- x: e.clientX - rect.left,
1623
- y: e.clientY - rect.top,
1624
- pressure: e.pressure,
1625
- pointerType: e.pointerType === "touch" || e.pointerType === "pen" ? e.pointerType : "mouse",
1626
- shiftKey: e.shiftKey
1627
- };
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
+ }
1628
1549
  }
1629
- dispatchToolDown(e) {
1630
- if (!this.toolManager || !this.toolContext) return;
1631
- this.actions.flushPendingNudge();
1632
- this.historyRecorder?.begin();
1633
- this.isToolActive = true;
1634
- this.toolManager.handlePointerDown(this.toPointerState(e), this.toolContext);
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;
1635
1562
  }
1636
- dispatchToolMove(e) {
1637
- if (!this.toolManager || !this.toolContext) return;
1638
- this.toolManager.handlePointerMove(this.toPointerState(e), this.toolContext);
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
+ }
1583
+ }
1584
+ this.items = remaining;
1639
1585
  }
1640
- dispatchToolHover(e) {
1641
- if (!this.toolManager?.activeTool || !this.toolContext) return;
1642
- const tool = this.toolManager.activeTool;
1643
- if (tool.onHover) {
1644
- tool.onHover(this.toPointerState(e), this.toolContext);
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;
1645
1598
  }
1646
1599
  }
1647
- dispatchToolUp(e) {
1648
- if (!this.toolManager || !this.toolContext) return;
1649
- this.toolManager.handlePointerUp(this.toPointerState(e), this.toolContext);
1650
- this.historyRecorder?.commit();
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);
1651
1608
  }
1652
- isInScope() {
1653
- if (this.scope === "window") return true;
1654
- const active = document.activeElement;
1655
- return active === this.element || this.element.contains(active);
1609
+ get size() {
1610
+ return this._size;
1656
1611
  }
1657
- focusSelf() {
1658
- if (this.scope !== "focus" || this.isInScope()) return;
1659
- this.element.focus({ preventScroll: true });
1612
+ insert(id, bounds) {
1613
+ this.root.insert({ id, bounds });
1614
+ this._size++;
1660
1615
  }
1661
- cancelToolIfActive(e) {
1662
- if (this.isToolActive) {
1663
- this.dispatchToolUp(e);
1664
- this.isToolActive = false;
1616
+ remove(id) {
1617
+ if (this.root.remove(id)) {
1618
+ this._size--;
1665
1619
  }
1666
- this.deferredDown = null;
1667
1620
  }
1668
- };
1669
-
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;
1621
+ update(id, newBounds) {
1622
+ this.remove(id);
1623
+ this.insert(id, newBounds);
1683
1624
  }
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;
1696
- }
1697
- }
1698
- this.lastTapTime = now;
1699
- this.lastTapX = x;
1700
- this.lastTapY = y;
1701
- this.hasPendingTap = true;
1702
- return false;
1625
+ query(rect) {
1626
+ const result = [];
1627
+ this.root.query(rect, result);
1628
+ return result;
1703
1629
  }
1704
- reset() {
1705
- this.hasPendingTap = false;
1706
- this.lastTapTime = 0;
1707
- this.lastTapX = 0;
1708
- this.lastTapY = 0;
1630
+ queryPoint(point) {
1631
+ return this.query({ x: point.x, y: point.y, w: 0, h: 0 });
1632
+ }
1633
+ clear() {
1634
+ this.root = new QuadNode(this.worldBounds, 0);
1635
+ this._size = 0;
1709
1636
  }
1710
1637
  };
1711
1638
 
@@ -2354,51 +2281,6 @@ function updateBoundArrow(arrow, store) {
2354
2281
  }
2355
2282
  return Object.keys(updates).length > 0 ? updates : null;
2356
2283
  }
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
2284
 
2403
2285
  // src/elements/grid-renderer.ts
2404
2286
  function getSquareGridLines(bounds, cellSize) {
@@ -4591,6 +4473,48 @@ var InteractMode = class {
4591
4473
  };
4592
4474
  };
4593
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
+
4594
4518
  // src/canvas/dom-node-manager.ts
4595
4519
  var DomNodeManager = class {
4596
4520
  domNodes = /* @__PURE__ */ new Map();
@@ -7592,53 +7516,33 @@ var TemplateTool = class {
7592
7516
  };
7593
7517
 
7594
7518
  // src/index.ts
7595
- var VERSION = "0.24.0";
7519
+ var VERSION = "0.25.0";
7596
7520
  // Annotate the CommonJS export names for ESM import in node:
7597
7521
  0 && (module.exports = {
7598
- AddElementCommand,
7599
7522
  ArrowTool,
7600
7523
  AutoSave,
7601
- Background,
7602
- BatchCommand,
7603
7524
  Camera,
7604
- CreateLayerCommand,
7605
- DEFAULT_FONT_SIZE_PRESETS,
7606
7525
  DEFAULT_NOTE_FONT_SIZE,
7607
- DoubleTapDetector,
7608
- ElementRenderer,
7609
7526
  ElementStore,
7610
7527
  EraserTool,
7611
- EventBus,
7612
7528
  HandTool,
7613
- HistoryRecorder,
7614
7529
  HistoryStack,
7615
7530
  ImageTool,
7616
- InputFilter,
7617
- InputHandler,
7618
7531
  LayerManager,
7619
7532
  MeasureTool,
7620
- NoteEditor,
7621
7533
  NoteTool,
7622
- NoteToolbar,
7623
7534
  PencilTool,
7624
- Quadtree,
7625
- RemoveElementCommand,
7626
- RemoveLayerCommand,
7627
7535
  SelectTool,
7628
7536
  ShapeTool,
7629
7537
  TemplateTool,
7630
7538
  TextTool,
7631
7539
  ToolManager,
7632
- UpdateElementCommand,
7633
- UpdateLayerCommand,
7634
7540
  VERSION,
7635
7541
  Viewport,
7636
7542
  boundsIntersect,
7637
- clearStaleBindings,
7638
7543
  createArrow,
7639
7544
  createGrid,
7640
7545
  createHtmlElement,
7641
- createId,
7642
7546
  createImage,
7643
7547
  createNote,
7644
7548
  createShape,
@@ -7647,29 +7551,20 @@ var VERSION = "0.24.0";
7647
7551
  createText,
7648
7552
  drawHexPath,
7649
7553
  exportImage,
7650
- exportState,
7651
- findBindTarget,
7652
- findBoundArrows,
7653
7554
  getActiveFormats,
7654
7555
  getArrowBounds,
7655
7556
  getArrowControlPoint,
7656
7557
  getArrowMidpoint,
7657
7558
  getArrowTangentAngle,
7658
7559
  getBendFromPoint,
7659
- getEdgeIntersection,
7660
7560
  getElementBounds,
7661
- getElementCenter,
7662
7561
  getElementsBoundingBox,
7663
7562
  getHexCellsInCone,
7664
7563
  getHexCellsInLine,
7665
7564
  getHexCellsInRadius,
7666
7565
  getHexCellsInSquare,
7667
7566
  getHexDistance,
7668
- isBindable,
7669
7567
  isNearBezier,
7670
- isNoteContentEmpty,
7671
- parseState,
7672
- sanitizeNoteHtml,
7673
7568
  setFontSize,
7674
7569
  smartSnap,
7675
7570
  snapPoint,
@@ -7677,8 +7572,6 @@ var VERSION = "0.24.0";
7677
7572
  toggleBold,
7678
7573
  toggleItalic,
7679
7574
  toggleStrikethrough,
7680
- toggleUnderline,
7681
- unbindArrow,
7682
- updateBoundArrow
7575
+ toggleUnderline
7683
7576
  });
7684
7577
  //# sourceMappingURL=index.cjs.map