@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.js CHANGED
@@ -1,179 +1,34 @@
1
- // src/core/event-bus.ts
2
- var EventBus = class {
3
- listeners = /* @__PURE__ */ new Map();
4
- on(event, listener) {
5
- const existing = this.listeners.get(event);
6
- if (existing) {
7
- existing.add(listener);
8
- } else {
9
- const set = /* @__PURE__ */ new Set([listener]);
10
- this.listeners.set(event, set);
11
- }
12
- return () => this.off(event, listener);
13
- }
14
- off(event, listener) {
15
- this.listeners.get(event)?.delete(listener);
16
- }
17
- emit(event, data) {
18
- this.listeners.get(event)?.forEach((listener) => {
19
- try {
20
- listener(data);
21
- } catch (err) {
22
- console.error(`[fieldnotes] listener error for "${String(event)}"`, err);
23
- }
24
- });
25
- }
26
- clear() {
27
- this.listeners.clear();
28
- }
29
- };
30
-
31
- // src/core/quadtree.ts
32
- var MAX_ITEMS = 8;
33
- var MAX_DEPTH = 8;
34
- function intersects(a, b) {
35
- return a.x <= b.x + b.w && a.x + a.w >= b.x && a.y <= b.y + b.h && a.y + a.h >= b.y;
1
+ // src/core/snap.ts
2
+ function snapPoint(point, gridSize) {
3
+ return {
4
+ x: Math.round(point.x / gridSize) * gridSize || 0,
5
+ y: Math.round(point.y / gridSize) * gridSize || 0
6
+ };
36
7
  }
37
- var QuadNode = class _QuadNode {
38
- constructor(bounds, depth) {
39
- this.bounds = bounds;
40
- this.depth = depth;
41
- }
42
- items = [];
43
- children = null;
44
- insert(entry) {
45
- if (this.children) {
46
- const idx = this.getChildIndex(entry.bounds);
47
- if (idx !== -1) {
48
- const child = this.children[idx];
49
- if (child) child.insert(entry);
50
- return;
51
- }
52
- this.items.push(entry);
53
- return;
54
- }
55
- this.items.push(entry);
56
- if (this.items.length > MAX_ITEMS && this.depth < MAX_DEPTH) {
57
- this.split();
58
- }
59
- }
60
- remove(id) {
61
- const idx = this.items.findIndex((e) => e.id === id);
62
- if (idx !== -1) {
63
- this.items.splice(idx, 1);
64
- return true;
65
- }
66
- if (this.children) {
67
- for (const child of this.children) {
68
- if (child.remove(id)) {
69
- this.collapseIfEmpty();
70
- return true;
71
- }
72
- }
73
- }
74
- return false;
75
- }
76
- query(rect, result) {
77
- if (!intersects(this.bounds, rect)) return;
78
- for (const item of this.items) {
79
- if (intersects(item.bounds, rect)) {
80
- result.push(item.id);
81
- }
82
- }
83
- if (this.children) {
84
- for (const child of this.children) {
85
- child.query(rect, result);
86
- }
87
- }
88
- }
89
- getChildIndex(itemBounds) {
90
- const midX = this.bounds.x + this.bounds.w / 2;
91
- const midY = this.bounds.y + this.bounds.h / 2;
92
- const left = itemBounds.x >= this.bounds.x && itemBounds.x + itemBounds.w <= midX;
93
- const right = itemBounds.x >= midX && itemBounds.x + itemBounds.w <= this.bounds.x + this.bounds.w;
94
- const top = itemBounds.y >= this.bounds.y && itemBounds.y + itemBounds.h <= midY;
95
- const bottom = itemBounds.y >= midY && itemBounds.y + itemBounds.h <= this.bounds.y + this.bounds.h;
96
- if (left && top) return 0;
97
- if (right && top) return 1;
98
- if (left && bottom) return 2;
99
- if (right && bottom) return 3;
100
- return -1;
101
- }
102
- split() {
103
- const { x, y, w, h } = this.bounds;
104
- const halfW = w / 2;
105
- const halfH = h / 2;
106
- const d = this.depth + 1;
107
- this.children = [
108
- new _QuadNode({ x, y, w: halfW, h: halfH }, d),
109
- new _QuadNode({ x: x + halfW, y, w: halfW, h: halfH }, d),
110
- new _QuadNode({ x, y: y + halfH, w: halfW, h: halfH }, d),
111
- new _QuadNode({ x: x + halfW, y: y + halfH, w: halfW, h: halfH }, d)
112
- ];
113
- const remaining = [];
114
- for (const item of this.items) {
115
- const idx = this.getChildIndex(item.bounds);
116
- if (idx !== -1) {
117
- const target = this.children[idx];
118
- if (target) target.insert(item);
119
- } else {
120
- remaining.push(item);
121
- }
122
- }
123
- this.items = remaining;
124
- }
125
- collapseIfEmpty() {
126
- if (!this.children) return;
127
- let totalItems = this.items.length;
128
- for (const child of this.children) {
129
- if (child.children) return;
130
- totalItems += child.items.length;
131
- }
132
- if (totalItems <= MAX_ITEMS) {
133
- for (const child of this.children) {
134
- this.items.push(...child.items);
135
- }
136
- this.children = null;
137
- }
138
- }
139
- };
140
- var Quadtree = class {
141
- root;
142
- _size = 0;
143
- worldBounds;
144
- constructor(worldBounds) {
145
- this.worldBounds = worldBounds;
146
- this.root = new QuadNode(worldBounds, 0);
147
- }
148
- get size() {
149
- return this._size;
150
- }
151
- insert(id, bounds) {
152
- this.root.insert({ id, bounds });
153
- this._size++;
154
- }
155
- remove(id) {
156
- if (this.root.remove(id)) {
157
- this._size--;
158
- }
159
- }
160
- update(id, newBounds) {
161
- this.remove(id);
162
- this.insert(id, newBounds);
163
- }
164
- query(rect) {
165
- const result = [];
166
- this.root.query(rect, result);
167
- return result;
168
- }
169
- queryPoint(point) {
170
- return this.query({ x: point.x, y: point.y, w: 0, h: 0 });
8
+ function snapToHexCenter(point, cellSize, orientation) {
9
+ if (orientation === "pointy") {
10
+ const hexW = Math.sqrt(3) * cellSize;
11
+ const rowH = 1.5 * cellSize;
12
+ const row = Math.round(point.y / rowH);
13
+ const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
14
+ const col = Math.round((point.x - offsetX) / hexW);
15
+ return { x: col * hexW + offsetX || 0, y: row * rowH || 0 };
16
+ } else {
17
+ const hexH = Math.sqrt(3) * cellSize;
18
+ const colW = 1.5 * cellSize;
19
+ const col = Math.round(point.x / colW);
20
+ const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
21
+ const row = Math.round((point.y - offsetY) / hexH);
22
+ return { x: col * colW || 0, y: row * hexH + offsetY || 0 };
171
23
  }
172
- clear() {
173
- this.root = new QuadNode(this.worldBounds, 0);
174
- this._size = 0;
24
+ }
25
+ function smartSnap(point, ctx) {
26
+ if (!ctx.snapToGrid || !ctx.gridSize) return point;
27
+ if (ctx.gridType === "hex" && ctx.hexOrientation) {
28
+ return snapToHexCenter(point, ctx.gridSize, ctx.hexOrientation);
175
29
  }
176
- };
30
+ return snapPoint(point, ctx.gridSize);
31
+ }
177
32
 
178
33
  // src/elements/note-sanitizer.ts
179
34
  var BOLD_TAGS = /* @__PURE__ */ new Set(["b", "strong"]);
@@ -430,38 +285,6 @@ function migrateElement(obj) {
430
285
  }
431
286
  }
432
287
 
433
- // src/core/snap.ts
434
- function snapPoint(point, gridSize) {
435
- return {
436
- x: Math.round(point.x / gridSize) * gridSize || 0,
437
- y: Math.round(point.y / gridSize) * gridSize || 0
438
- };
439
- }
440
- function snapToHexCenter(point, cellSize, orientation) {
441
- if (orientation === "pointy") {
442
- const hexW = Math.sqrt(3) * cellSize;
443
- const rowH = 1.5 * cellSize;
444
- const row = Math.round(point.y / rowH);
445
- const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
446
- const col = Math.round((point.x - offsetX) / hexW);
447
- return { x: col * hexW + offsetX || 0, y: row * rowH || 0 };
448
- } else {
449
- const hexH = Math.sqrt(3) * cellSize;
450
- const colW = 1.5 * cellSize;
451
- const col = Math.round(point.x / colW);
452
- const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
453
- const row = Math.round((point.y - offsetY) / hexH);
454
- return { x: col * colW || 0, y: row * hexH + offsetY || 0 };
455
- }
456
- }
457
- function smartSnap(point, ctx) {
458
- if (!ctx.snapToGrid || !ctx.gridSize) return point;
459
- if (ctx.gridType === "hex" && ctx.hexOrientation) {
460
- return snapToHexCenter(point, ctx.gridSize, ctx.hexOrientation);
461
- }
462
- return snapPoint(point, ctx.gridSize);
463
- }
464
-
465
288
  // src/core/auto-save.ts
466
289
  var DEFAULT_KEY = "fieldnotes-autosave";
467
290
  var DEFAULT_DEBOUNCE_MS = 1e3;
@@ -625,143 +448,6 @@ var Camera = class {
625
448
  }
626
449
  };
627
450
 
628
- // src/canvas/background.ts
629
- var MIN_PATTERN_SPACING = 16;
630
- var DEFAULTS = {
631
- pattern: "dots",
632
- spacing: 24,
633
- color: "#d0d0d0",
634
- dotRadius: 1,
635
- lineWidth: 0.5
636
- };
637
- var Background = class {
638
- pattern;
639
- spacing;
640
- color;
641
- dotRadius;
642
- lineWidth;
643
- cachedCanvas = null;
644
- cachedCtx = null;
645
- lastZoom = -1;
646
- lastOffsetX = -Infinity;
647
- lastOffsetY = -Infinity;
648
- lastWidth = 0;
649
- lastHeight = 0;
650
- constructor(options = {}) {
651
- this.pattern = options.pattern ?? DEFAULTS.pattern;
652
- this.spacing = options.spacing ?? DEFAULTS.spacing;
653
- this.color = options.color ?? DEFAULTS.color;
654
- this.dotRadius = options.dotRadius ?? DEFAULTS.dotRadius;
655
- this.lineWidth = options.lineWidth ?? DEFAULTS.lineWidth;
656
- }
657
- render(ctx, camera) {
658
- const { width, height } = ctx.canvas;
659
- const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
660
- const cssWidth = width / dpr;
661
- const cssHeight = height / dpr;
662
- ctx.save();
663
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
664
- ctx.clearRect(0, 0, cssWidth, cssHeight);
665
- if (this.pattern === "none") {
666
- ctx.restore();
667
- return;
668
- }
669
- const spacing = this.adaptSpacing(this.spacing, camera.zoom);
670
- const keyZoom = camera.zoom;
671
- const keyX = Math.floor(camera.position.x % spacing);
672
- const keyY = Math.floor(camera.position.y % spacing);
673
- if (this.cachedCanvas !== null && keyZoom === this.lastZoom && keyX === this.lastOffsetX && keyY === this.lastOffsetY && cssWidth === this.lastWidth && cssHeight === this.lastHeight) {
674
- ctx.drawImage(this.cachedCanvas, 0, 0);
675
- ctx.restore();
676
- return;
677
- }
678
- this.ensureCachedCanvas(cssWidth, cssHeight, dpr);
679
- if (this.cachedCtx === null) {
680
- if (this.pattern === "dots") {
681
- this.renderDots(ctx, camera, cssWidth, cssHeight);
682
- } else if (this.pattern === "grid") {
683
- this.renderGrid(ctx, camera, cssWidth, cssHeight);
684
- }
685
- ctx.restore();
686
- return;
687
- }
688
- const offCtx = this.cachedCtx;
689
- offCtx.clearRect(0, 0, cssWidth, cssHeight);
690
- if (this.pattern === "dots") {
691
- this.renderDots(offCtx, camera, cssWidth, cssHeight);
692
- } else if (this.pattern === "grid") {
693
- this.renderGrid(offCtx, camera, cssWidth, cssHeight);
694
- }
695
- this.lastZoom = keyZoom;
696
- this.lastOffsetX = keyX;
697
- this.lastOffsetY = keyY;
698
- this.lastWidth = cssWidth;
699
- this.lastHeight = cssHeight;
700
- ctx.drawImage(this.cachedCanvas, 0, 0);
701
- ctx.restore();
702
- }
703
- ensureCachedCanvas(cssWidth, cssHeight, dpr) {
704
- if (this.cachedCanvas !== null && this.lastWidth === cssWidth && this.lastHeight === cssHeight) {
705
- return;
706
- }
707
- const physWidth = Math.round(cssWidth * dpr);
708
- const physHeight = Math.round(cssHeight * dpr);
709
- if (typeof OffscreenCanvas !== "undefined") {
710
- this.cachedCanvas = new OffscreenCanvas(physWidth, physHeight);
711
- } else if (typeof document !== "undefined") {
712
- const el = document.createElement("canvas");
713
- el.width = physWidth;
714
- el.height = physHeight;
715
- this.cachedCanvas = el;
716
- } else {
717
- this.cachedCanvas = null;
718
- this.cachedCtx = null;
719
- return;
720
- }
721
- const offCtx = this.cachedCanvas.getContext("2d");
722
- if (offCtx !== null) {
723
- offCtx.scale(dpr, dpr);
724
- }
725
- this.cachedCtx = offCtx;
726
- this.lastZoom = -1;
727
- }
728
- adaptSpacing(baseSpacing, zoom) {
729
- let spacing = baseSpacing * zoom;
730
- while (spacing < MIN_PATTERN_SPACING) {
731
- spacing *= 2;
732
- }
733
- return spacing;
734
- }
735
- renderDots(ctx, camera, width, height) {
736
- const spacing = this.adaptSpacing(this.spacing, camera.zoom);
737
- const offsetX = camera.position.x % spacing;
738
- const offsetY = camera.position.y % spacing;
739
- const radius = this.dotRadius * Math.min(camera.zoom, 2);
740
- ctx.fillStyle = this.color;
741
- ctx.beginPath();
742
- for (let x = offsetX; x < width; x += spacing) {
743
- for (let y = offsetY; y < height; y += spacing) {
744
- ctx.moveTo(x + radius, y);
745
- ctx.arc(x, y, radius, 0, Math.PI * 2);
746
- }
747
- }
748
- ctx.fill();
749
- }
750
- renderGrid(ctx, camera, width, height) {
751
- const spacing = this.adaptSpacing(this.spacing, camera.zoom);
752
- const offsetX = camera.position.x % spacing;
753
- const offsetY = camera.position.y % spacing;
754
- const lineW = this.lineWidth * Math.min(camera.zoom, 2);
755
- ctx.fillStyle = this.color;
756
- for (let x = offsetX; x < width; x += spacing) {
757
- ctx.fillRect(x, 0, lineW, height);
758
- }
759
- for (let y = offsetY; y < height; y += spacing) {
760
- ctx.fillRect(0, y, width, lineW);
761
- }
762
- }
763
- };
764
-
765
451
  // src/canvas/input-filter.ts
766
452
  var InputFilter = class _InputFilter {
767
453
  activePenId = null;
@@ -1487,115 +1173,387 @@ var InputHandler = class {
1487
1173
  const newZoom = this.camera.zoom * scale;
1488
1174
  this.camera.zoomAt(newZoom, center);
1489
1175
  }
1490
- const dx = center.x - this.lastPointer.x;
1491
- const dy = center.y - this.lastPointer.y;
1492
- this.camera.pan(dx, dy);
1493
- this.lastPinchDistance = dist;
1494
- this.lastPinchCenter = center;
1495
- this.lastPointer = { ...center };
1496
- }
1497
- getPinchPoints() {
1498
- const pts = [...this.activePointers.values()];
1499
- return [pts[0] ?? { x: 0, y: 0 }, pts[1] ?? { x: 0, y: 0 }];
1500
- }
1501
- distance(a, b) {
1502
- const dx = a.x - b.x;
1503
- const dy = a.y - b.y;
1504
- return Math.sqrt(dx * dx + dy * dy);
1176
+ const dx = center.x - this.lastPointer.x;
1177
+ const dy = center.y - this.lastPointer.y;
1178
+ this.camera.pan(dx, dy);
1179
+ this.lastPinchDistance = dist;
1180
+ this.lastPinchCenter = center;
1181
+ this.lastPointer = { ...center };
1182
+ }
1183
+ getPinchPoints() {
1184
+ const pts = [...this.activePointers.values()];
1185
+ return [pts[0] ?? { x: 0, y: 0 }, pts[1] ?? { x: 0, y: 0 }];
1186
+ }
1187
+ distance(a, b) {
1188
+ const dx = a.x - b.x;
1189
+ const dy = a.y - b.y;
1190
+ return Math.sqrt(dx * dx + dy * dy);
1191
+ }
1192
+ midpoint(a, b) {
1193
+ return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
1194
+ }
1195
+ toPointerState(e) {
1196
+ const rect = this.element.getBoundingClientRect();
1197
+ return {
1198
+ x: e.clientX - rect.left,
1199
+ y: e.clientY - rect.top,
1200
+ pressure: e.pressure,
1201
+ pointerType: e.pointerType === "touch" || e.pointerType === "pen" ? e.pointerType : "mouse",
1202
+ shiftKey: e.shiftKey
1203
+ };
1204
+ }
1205
+ dispatchToolDown(e) {
1206
+ if (!this.toolManager || !this.toolContext) return;
1207
+ this.actions.flushPendingNudge();
1208
+ this.historyRecorder?.begin();
1209
+ this.isToolActive = true;
1210
+ this.toolManager.handlePointerDown(this.toPointerState(e), this.toolContext);
1211
+ }
1212
+ dispatchToolMove(e) {
1213
+ if (!this.toolManager || !this.toolContext) return;
1214
+ this.toolManager.handlePointerMove(this.toPointerState(e), this.toolContext);
1215
+ }
1216
+ dispatchToolHover(e) {
1217
+ if (!this.toolManager?.activeTool || !this.toolContext) return;
1218
+ const tool = this.toolManager.activeTool;
1219
+ if (tool.onHover) {
1220
+ tool.onHover(this.toPointerState(e), this.toolContext);
1221
+ }
1222
+ }
1223
+ dispatchToolUp(e) {
1224
+ if (!this.toolManager || !this.toolContext) return;
1225
+ this.toolManager.handlePointerUp(this.toPointerState(e), this.toolContext);
1226
+ this.historyRecorder?.commit();
1227
+ }
1228
+ isInScope() {
1229
+ if (this.scope === "window") return true;
1230
+ const active = document.activeElement;
1231
+ return active === this.element || this.element.contains(active);
1232
+ }
1233
+ focusSelf() {
1234
+ if (this.scope !== "focus" || this.isInScope()) return;
1235
+ this.element.focus({ preventScroll: true });
1236
+ }
1237
+ cancelToolIfActive(e) {
1238
+ if (this.isToolActive) {
1239
+ this.dispatchToolUp(e);
1240
+ this.isToolActive = false;
1241
+ }
1242
+ this.deferredDown = null;
1243
+ }
1244
+ };
1245
+
1246
+ // src/canvas/background.ts
1247
+ var MIN_PATTERN_SPACING = 16;
1248
+ var DEFAULTS = {
1249
+ pattern: "dots",
1250
+ spacing: 24,
1251
+ color: "#d0d0d0",
1252
+ dotRadius: 1,
1253
+ lineWidth: 0.5
1254
+ };
1255
+ var Background = class {
1256
+ pattern;
1257
+ spacing;
1258
+ color;
1259
+ dotRadius;
1260
+ lineWidth;
1261
+ cachedCanvas = null;
1262
+ cachedCtx = null;
1263
+ lastZoom = -1;
1264
+ lastOffsetX = -Infinity;
1265
+ lastOffsetY = -Infinity;
1266
+ lastWidth = 0;
1267
+ lastHeight = 0;
1268
+ constructor(options = {}) {
1269
+ this.pattern = options.pattern ?? DEFAULTS.pattern;
1270
+ this.spacing = options.spacing ?? DEFAULTS.spacing;
1271
+ this.color = options.color ?? DEFAULTS.color;
1272
+ this.dotRadius = options.dotRadius ?? DEFAULTS.dotRadius;
1273
+ this.lineWidth = options.lineWidth ?? DEFAULTS.lineWidth;
1274
+ }
1275
+ render(ctx, camera) {
1276
+ const { width, height } = ctx.canvas;
1277
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
1278
+ const cssWidth = width / dpr;
1279
+ const cssHeight = height / dpr;
1280
+ ctx.save();
1281
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
1282
+ ctx.clearRect(0, 0, cssWidth, cssHeight);
1283
+ if (this.pattern === "none") {
1284
+ ctx.restore();
1285
+ return;
1286
+ }
1287
+ const spacing = this.adaptSpacing(this.spacing, camera.zoom);
1288
+ const keyZoom = camera.zoom;
1289
+ const keyX = Math.floor(camera.position.x % spacing);
1290
+ const keyY = Math.floor(camera.position.y % spacing);
1291
+ if (this.cachedCanvas !== null && keyZoom === this.lastZoom && keyX === this.lastOffsetX && keyY === this.lastOffsetY && cssWidth === this.lastWidth && cssHeight === this.lastHeight) {
1292
+ ctx.drawImage(this.cachedCanvas, 0, 0);
1293
+ ctx.restore();
1294
+ return;
1295
+ }
1296
+ this.ensureCachedCanvas(cssWidth, cssHeight, dpr);
1297
+ if (this.cachedCtx === null) {
1298
+ if (this.pattern === "dots") {
1299
+ this.renderDots(ctx, camera, cssWidth, cssHeight);
1300
+ } else if (this.pattern === "grid") {
1301
+ this.renderGrid(ctx, camera, cssWidth, cssHeight);
1302
+ }
1303
+ ctx.restore();
1304
+ return;
1305
+ }
1306
+ const offCtx = this.cachedCtx;
1307
+ offCtx.clearRect(0, 0, cssWidth, cssHeight);
1308
+ if (this.pattern === "dots") {
1309
+ this.renderDots(offCtx, camera, cssWidth, cssHeight);
1310
+ } else if (this.pattern === "grid") {
1311
+ this.renderGrid(offCtx, camera, cssWidth, cssHeight);
1312
+ }
1313
+ this.lastZoom = keyZoom;
1314
+ this.lastOffsetX = keyX;
1315
+ this.lastOffsetY = keyY;
1316
+ this.lastWidth = cssWidth;
1317
+ this.lastHeight = cssHeight;
1318
+ ctx.drawImage(this.cachedCanvas, 0, 0);
1319
+ ctx.restore();
1320
+ }
1321
+ ensureCachedCanvas(cssWidth, cssHeight, dpr) {
1322
+ if (this.cachedCanvas !== null && this.lastWidth === cssWidth && this.lastHeight === cssHeight) {
1323
+ return;
1324
+ }
1325
+ const physWidth = Math.round(cssWidth * dpr);
1326
+ const physHeight = Math.round(cssHeight * dpr);
1327
+ if (typeof OffscreenCanvas !== "undefined") {
1328
+ this.cachedCanvas = new OffscreenCanvas(physWidth, physHeight);
1329
+ } else if (typeof document !== "undefined") {
1330
+ const el = document.createElement("canvas");
1331
+ el.width = physWidth;
1332
+ el.height = physHeight;
1333
+ this.cachedCanvas = el;
1334
+ } else {
1335
+ this.cachedCanvas = null;
1336
+ this.cachedCtx = null;
1337
+ return;
1338
+ }
1339
+ const offCtx = this.cachedCanvas.getContext("2d");
1340
+ if (offCtx !== null) {
1341
+ offCtx.scale(dpr, dpr);
1342
+ }
1343
+ this.cachedCtx = offCtx;
1344
+ this.lastZoom = -1;
1345
+ }
1346
+ adaptSpacing(baseSpacing, zoom) {
1347
+ let spacing = baseSpacing * zoom;
1348
+ while (spacing < MIN_PATTERN_SPACING) {
1349
+ spacing *= 2;
1350
+ }
1351
+ return spacing;
1352
+ }
1353
+ renderDots(ctx, camera, width, height) {
1354
+ const spacing = this.adaptSpacing(this.spacing, camera.zoom);
1355
+ const offsetX = camera.position.x % spacing;
1356
+ const offsetY = camera.position.y % spacing;
1357
+ const radius = this.dotRadius * Math.min(camera.zoom, 2);
1358
+ ctx.fillStyle = this.color;
1359
+ ctx.beginPath();
1360
+ for (let x = offsetX; x < width; x += spacing) {
1361
+ for (let y = offsetY; y < height; y += spacing) {
1362
+ ctx.moveTo(x + radius, y);
1363
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
1364
+ }
1365
+ }
1366
+ ctx.fill();
1367
+ }
1368
+ renderGrid(ctx, camera, width, height) {
1369
+ const spacing = this.adaptSpacing(this.spacing, camera.zoom);
1370
+ const offsetX = camera.position.x % spacing;
1371
+ const offsetY = camera.position.y % spacing;
1372
+ const lineW = this.lineWidth * Math.min(camera.zoom, 2);
1373
+ ctx.fillStyle = this.color;
1374
+ for (let x = offsetX; x < width; x += spacing) {
1375
+ ctx.fillRect(x, 0, lineW, height);
1376
+ }
1377
+ for (let y = offsetY; y < height; y += spacing) {
1378
+ ctx.fillRect(0, y, width, lineW);
1379
+ }
1380
+ }
1381
+ };
1382
+
1383
+ // src/core/event-bus.ts
1384
+ var EventBus = class {
1385
+ listeners = /* @__PURE__ */ new Map();
1386
+ on(event, listener) {
1387
+ const existing = this.listeners.get(event);
1388
+ if (existing) {
1389
+ existing.add(listener);
1390
+ } else {
1391
+ const set = /* @__PURE__ */ new Set([listener]);
1392
+ this.listeners.set(event, set);
1393
+ }
1394
+ return () => this.off(event, listener);
1395
+ }
1396
+ off(event, listener) {
1397
+ this.listeners.get(event)?.delete(listener);
1398
+ }
1399
+ emit(event, data) {
1400
+ this.listeners.get(event)?.forEach((listener) => {
1401
+ try {
1402
+ listener(data);
1403
+ } catch (err) {
1404
+ console.error(`[fieldnotes] listener error for "${String(event)}"`, err);
1405
+ }
1406
+ });
1407
+ }
1408
+ clear() {
1409
+ this.listeners.clear();
1410
+ }
1411
+ };
1412
+
1413
+ // src/core/quadtree.ts
1414
+ var MAX_ITEMS = 8;
1415
+ var MAX_DEPTH = 8;
1416
+ function intersects(a, b) {
1417
+ 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;
1418
+ }
1419
+ var QuadNode = class _QuadNode {
1420
+ constructor(bounds, depth) {
1421
+ this.bounds = bounds;
1422
+ this.depth = depth;
1423
+ }
1424
+ items = [];
1425
+ children = null;
1426
+ insert(entry) {
1427
+ if (this.children) {
1428
+ const idx = this.getChildIndex(entry.bounds);
1429
+ if (idx !== -1) {
1430
+ const child = this.children[idx];
1431
+ if (child) child.insert(entry);
1432
+ return;
1433
+ }
1434
+ this.items.push(entry);
1435
+ return;
1436
+ }
1437
+ this.items.push(entry);
1438
+ if (this.items.length > MAX_ITEMS && this.depth < MAX_DEPTH) {
1439
+ this.split();
1440
+ }
1505
1441
  }
1506
- midpoint(a, b) {
1507
- return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
1442
+ remove(id) {
1443
+ const idx = this.items.findIndex((e) => e.id === id);
1444
+ if (idx !== -1) {
1445
+ this.items.splice(idx, 1);
1446
+ return true;
1447
+ }
1448
+ if (this.children) {
1449
+ for (const child of this.children) {
1450
+ if (child.remove(id)) {
1451
+ this.collapseIfEmpty();
1452
+ return true;
1453
+ }
1454
+ }
1455
+ }
1456
+ return false;
1508
1457
  }
1509
- toPointerState(e) {
1510
- const rect = this.element.getBoundingClientRect();
1511
- return {
1512
- x: e.clientX - rect.left,
1513
- y: e.clientY - rect.top,
1514
- pressure: e.pressure,
1515
- pointerType: e.pointerType === "touch" || e.pointerType === "pen" ? e.pointerType : "mouse",
1516
- shiftKey: e.shiftKey
1517
- };
1458
+ query(rect, result) {
1459
+ if (!intersects(this.bounds, rect)) return;
1460
+ for (const item of this.items) {
1461
+ if (intersects(item.bounds, rect)) {
1462
+ result.push(item.id);
1463
+ }
1464
+ }
1465
+ if (this.children) {
1466
+ for (const child of this.children) {
1467
+ child.query(rect, result);
1468
+ }
1469
+ }
1518
1470
  }
1519
- dispatchToolDown(e) {
1520
- if (!this.toolManager || !this.toolContext) return;
1521
- this.actions.flushPendingNudge();
1522
- this.historyRecorder?.begin();
1523
- this.isToolActive = true;
1524
- this.toolManager.handlePointerDown(this.toPointerState(e), this.toolContext);
1471
+ getChildIndex(itemBounds) {
1472
+ const midX = this.bounds.x + this.bounds.w / 2;
1473
+ const midY = this.bounds.y + this.bounds.h / 2;
1474
+ const left = itemBounds.x >= this.bounds.x && itemBounds.x + itemBounds.w <= midX;
1475
+ const right = itemBounds.x >= midX && itemBounds.x + itemBounds.w <= this.bounds.x + this.bounds.w;
1476
+ const top = itemBounds.y >= this.bounds.y && itemBounds.y + itemBounds.h <= midY;
1477
+ const bottom = itemBounds.y >= midY && itemBounds.y + itemBounds.h <= this.bounds.y + this.bounds.h;
1478
+ if (left && top) return 0;
1479
+ if (right && top) return 1;
1480
+ if (left && bottom) return 2;
1481
+ if (right && bottom) return 3;
1482
+ return -1;
1525
1483
  }
1526
- dispatchToolMove(e) {
1527
- if (!this.toolManager || !this.toolContext) return;
1528
- this.toolManager.handlePointerMove(this.toPointerState(e), this.toolContext);
1484
+ split() {
1485
+ const { x, y, w, h } = this.bounds;
1486
+ const halfW = w / 2;
1487
+ const halfH = h / 2;
1488
+ const d = this.depth + 1;
1489
+ this.children = [
1490
+ new _QuadNode({ x, y, w: halfW, h: halfH }, d),
1491
+ new _QuadNode({ x: x + halfW, y, w: halfW, h: halfH }, d),
1492
+ new _QuadNode({ x, y: y + halfH, w: halfW, h: halfH }, d),
1493
+ new _QuadNode({ x: x + halfW, y: y + halfH, w: halfW, h: halfH }, d)
1494
+ ];
1495
+ const remaining = [];
1496
+ for (const item of this.items) {
1497
+ const idx = this.getChildIndex(item.bounds);
1498
+ if (idx !== -1) {
1499
+ const target = this.children[idx];
1500
+ if (target) target.insert(item);
1501
+ } else {
1502
+ remaining.push(item);
1503
+ }
1504
+ }
1505
+ this.items = remaining;
1529
1506
  }
1530
- dispatchToolHover(e) {
1531
- if (!this.toolManager?.activeTool || !this.toolContext) return;
1532
- const tool = this.toolManager.activeTool;
1533
- if (tool.onHover) {
1534
- tool.onHover(this.toPointerState(e), this.toolContext);
1507
+ collapseIfEmpty() {
1508
+ if (!this.children) return;
1509
+ let totalItems = this.items.length;
1510
+ for (const child of this.children) {
1511
+ if (child.children) return;
1512
+ totalItems += child.items.length;
1513
+ }
1514
+ if (totalItems <= MAX_ITEMS) {
1515
+ for (const child of this.children) {
1516
+ this.items.push(...child.items);
1517
+ }
1518
+ this.children = null;
1535
1519
  }
1536
1520
  }
1537
- dispatchToolUp(e) {
1538
- if (!this.toolManager || !this.toolContext) return;
1539
- this.toolManager.handlePointerUp(this.toPointerState(e), this.toolContext);
1540
- this.historyRecorder?.commit();
1521
+ };
1522
+ var Quadtree = class {
1523
+ root;
1524
+ _size = 0;
1525
+ worldBounds;
1526
+ constructor(worldBounds) {
1527
+ this.worldBounds = worldBounds;
1528
+ this.root = new QuadNode(worldBounds, 0);
1541
1529
  }
1542
- isInScope() {
1543
- if (this.scope === "window") return true;
1544
- const active = document.activeElement;
1545
- return active === this.element || this.element.contains(active);
1530
+ get size() {
1531
+ return this._size;
1546
1532
  }
1547
- focusSelf() {
1548
- if (this.scope !== "focus" || this.isInScope()) return;
1549
- this.element.focus({ preventScroll: true });
1533
+ insert(id, bounds) {
1534
+ this.root.insert({ id, bounds });
1535
+ this._size++;
1550
1536
  }
1551
- cancelToolIfActive(e) {
1552
- if (this.isToolActive) {
1553
- this.dispatchToolUp(e);
1554
- this.isToolActive = false;
1537
+ remove(id) {
1538
+ if (this.root.remove(id)) {
1539
+ this._size--;
1555
1540
  }
1556
- this.deferredDown = null;
1557
1541
  }
1558
- };
1559
-
1560
- // src/canvas/double-tap-detector.ts
1561
- var DEFAULT_TIMEOUT = 300;
1562
- var DEFAULT_MAX_DISTANCE = 20;
1563
- var DoubleTapDetector = class {
1564
- timeout;
1565
- maxDistance;
1566
- lastTapTime = 0;
1567
- lastTapX = 0;
1568
- lastTapY = 0;
1569
- hasPendingTap = false;
1570
- constructor(options) {
1571
- this.timeout = options?.timeout ?? DEFAULT_TIMEOUT;
1572
- this.maxDistance = options?.maxDistance ?? DEFAULT_MAX_DISTANCE;
1542
+ update(id, newBounds) {
1543
+ this.remove(id);
1544
+ this.insert(id, newBounds);
1573
1545
  }
1574
- feed(e) {
1575
- const now = Date.now();
1576
- const x = e.clientX;
1577
- const y = e.clientY;
1578
- if (this.hasPendingTap) {
1579
- const elapsed = now - this.lastTapTime;
1580
- const dx = x - this.lastTapX;
1581
- const dy = y - this.lastTapY;
1582
- const dist = Math.sqrt(dx * dx + dy * dy);
1583
- if (elapsed <= this.timeout && dist <= this.maxDistance) {
1584
- this.reset();
1585
- return true;
1586
- }
1587
- }
1588
- this.lastTapTime = now;
1589
- this.lastTapX = x;
1590
- this.lastTapY = y;
1591
- this.hasPendingTap = true;
1592
- return false;
1546
+ query(rect) {
1547
+ const result = [];
1548
+ this.root.query(rect, result);
1549
+ return result;
1593
1550
  }
1594
- reset() {
1595
- this.hasPendingTap = false;
1596
- this.lastTapTime = 0;
1597
- this.lastTapX = 0;
1598
- this.lastTapY = 0;
1551
+ queryPoint(point) {
1552
+ return this.query({ x: point.x, y: point.y, w: 0, h: 0 });
1553
+ }
1554
+ clear() {
1555
+ this.root = new QuadNode(this.worldBounds, 0);
1556
+ this._size = 0;
1599
1557
  }
1600
1558
  };
1601
1559
 
@@ -2244,51 +2202,6 @@ function updateBoundArrow(arrow, store) {
2244
2202
  }
2245
2203
  return Object.keys(updates).length > 0 ? updates : null;
2246
2204
  }
2247
- function clearStaleBindings(arrow, store) {
2248
- const updates = {};
2249
- let hasUpdates = false;
2250
- if (arrow.fromBinding && !store.getById(arrow.fromBinding.elementId)) {
2251
- updates.fromBinding = void 0;
2252
- hasUpdates = true;
2253
- }
2254
- if (arrow.toBinding && !store.getById(arrow.toBinding.elementId)) {
2255
- updates.toBinding = void 0;
2256
- hasUpdates = true;
2257
- }
2258
- return hasUpdates ? updates : null;
2259
- }
2260
- function unbindArrow(arrow, store) {
2261
- const updates = {};
2262
- if (arrow.fromBinding) {
2263
- const el = store.getById(arrow.fromBinding.elementId);
2264
- const bounds = el ? getElementBounds(el) : null;
2265
- if (bounds) {
2266
- const angle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 0);
2267
- const rayTarget = {
2268
- x: arrow.from.x + Math.cos(angle) * 1e3,
2269
- y: arrow.from.y + Math.sin(angle) * 1e3
2270
- };
2271
- const edge = getEdgeIntersection(bounds, rayTarget);
2272
- updates.from = edge;
2273
- updates.position = edge;
2274
- }
2275
- updates.fromBinding = void 0;
2276
- }
2277
- if (arrow.toBinding) {
2278
- const el = store.getById(arrow.toBinding.elementId);
2279
- const bounds = el ? getElementBounds(el) : null;
2280
- if (bounds) {
2281
- const angle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 1);
2282
- const rayTarget = {
2283
- x: arrow.to.x - Math.cos(angle) * 1e3,
2284
- y: arrow.to.y - Math.sin(angle) * 1e3
2285
- };
2286
- updates.to = getEdgeIntersection(bounds, rayTarget);
2287
- }
2288
- updates.toBinding = void 0;
2289
- }
2290
- return updates;
2291
- }
2292
2205
 
2293
2206
  // src/elements/grid-renderer.ts
2294
2207
  function getSquareGridLines(bounds, cellSize) {
@@ -4481,6 +4394,48 @@ var InteractMode = class {
4481
4394
  };
4482
4395
  };
4483
4396
 
4397
+ // src/canvas/double-tap-detector.ts
4398
+ var DEFAULT_TIMEOUT = 300;
4399
+ var DEFAULT_MAX_DISTANCE = 20;
4400
+ var DoubleTapDetector = class {
4401
+ timeout;
4402
+ maxDistance;
4403
+ lastTapTime = 0;
4404
+ lastTapX = 0;
4405
+ lastTapY = 0;
4406
+ hasPendingTap = false;
4407
+ constructor(options) {
4408
+ this.timeout = options?.timeout ?? DEFAULT_TIMEOUT;
4409
+ this.maxDistance = options?.maxDistance ?? DEFAULT_MAX_DISTANCE;
4410
+ }
4411
+ feed(e) {
4412
+ const now = Date.now();
4413
+ const x = e.clientX;
4414
+ const y = e.clientY;
4415
+ if (this.hasPendingTap) {
4416
+ const elapsed = now - this.lastTapTime;
4417
+ const dx = x - this.lastTapX;
4418
+ const dy = y - this.lastTapY;
4419
+ const dist = Math.sqrt(dx * dx + dy * dy);
4420
+ if (elapsed <= this.timeout && dist <= this.maxDistance) {
4421
+ this.reset();
4422
+ return true;
4423
+ }
4424
+ }
4425
+ this.lastTapTime = now;
4426
+ this.lastTapX = x;
4427
+ this.lastTapY = y;
4428
+ this.hasPendingTap = true;
4429
+ return false;
4430
+ }
4431
+ reset() {
4432
+ this.hasPendingTap = false;
4433
+ this.lastTapTime = 0;
4434
+ this.lastTapX = 0;
4435
+ this.lastTapY = 0;
4436
+ }
4437
+ };
4438
+
4484
4439
  // src/canvas/dom-node-manager.ts
4485
4440
  var DomNodeManager = class {
4486
4441
  domNodes = /* @__PURE__ */ new Map();
@@ -7482,52 +7437,32 @@ var TemplateTool = class {
7482
7437
  };
7483
7438
 
7484
7439
  // src/index.ts
7485
- var VERSION = "0.24.0";
7440
+ var VERSION = "0.25.0";
7486
7441
  export {
7487
- AddElementCommand,
7488
7442
  ArrowTool,
7489
7443
  AutoSave,
7490
- Background,
7491
- BatchCommand,
7492
7444
  Camera,
7493
- CreateLayerCommand,
7494
- DEFAULT_FONT_SIZE_PRESETS,
7495
7445
  DEFAULT_NOTE_FONT_SIZE,
7496
- DoubleTapDetector,
7497
- ElementRenderer,
7498
7446
  ElementStore,
7499
7447
  EraserTool,
7500
- EventBus,
7501
7448
  HandTool,
7502
- HistoryRecorder,
7503
7449
  HistoryStack,
7504
7450
  ImageTool,
7505
- InputFilter,
7506
- InputHandler,
7507
7451
  LayerManager,
7508
7452
  MeasureTool,
7509
- NoteEditor,
7510
7453
  NoteTool,
7511
- NoteToolbar,
7512
7454
  PencilTool,
7513
- Quadtree,
7514
- RemoveElementCommand,
7515
- RemoveLayerCommand,
7516
7455
  SelectTool,
7517
7456
  ShapeTool,
7518
7457
  TemplateTool,
7519
7458
  TextTool,
7520
7459
  ToolManager,
7521
- UpdateElementCommand,
7522
- UpdateLayerCommand,
7523
7460
  VERSION,
7524
7461
  Viewport,
7525
7462
  boundsIntersect,
7526
- clearStaleBindings,
7527
7463
  createArrow,
7528
7464
  createGrid,
7529
7465
  createHtmlElement,
7530
- createId,
7531
7466
  createImage,
7532
7467
  createNote,
7533
7468
  createShape,
@@ -7536,29 +7471,20 @@ export {
7536
7471
  createText,
7537
7472
  drawHexPath,
7538
7473
  exportImage,
7539
- exportState,
7540
- findBindTarget,
7541
- findBoundArrows,
7542
7474
  getActiveFormats,
7543
7475
  getArrowBounds,
7544
7476
  getArrowControlPoint,
7545
7477
  getArrowMidpoint,
7546
7478
  getArrowTangentAngle,
7547
7479
  getBendFromPoint,
7548
- getEdgeIntersection,
7549
7480
  getElementBounds,
7550
- getElementCenter,
7551
7481
  getElementsBoundingBox,
7552
7482
  getHexCellsInCone,
7553
7483
  getHexCellsInLine,
7554
7484
  getHexCellsInRadius,
7555
7485
  getHexCellsInSquare,
7556
7486
  getHexDistance,
7557
- isBindable,
7558
7487
  isNearBezier,
7559
- isNoteContentEmpty,
7560
- parseState,
7561
- sanitizeNoteHtml,
7562
7488
  setFontSize,
7563
7489
  smartSnap,
7564
7490
  snapPoint,
@@ -7566,8 +7492,6 @@ export {
7566
7492
  toggleBold,
7567
7493
  toggleItalic,
7568
7494
  toggleStrikethrough,
7569
- toggleUnderline,
7570
- unbindArrow,
7571
- updateBoundArrow
7495
+ toggleUnderline
7572
7496
  };
7573
7497
  //# sourceMappingURL=index.js.map