@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/README.md +6 -0
- package/dist/index.cjs +509 -606
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -369
- package/dist/index.d.ts +39 -369
- package/dist/index.js +508 -574
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,179 +1,34 @@
|
|
|
1
|
-
// src/core/
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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;
|
|
@@ -923,16 +609,17 @@ var KeyboardActions = class {
|
|
|
923
609
|
this.pasteCount = 0;
|
|
924
610
|
}
|
|
925
611
|
paste() {
|
|
612
|
+
if (this.deps.isToolActive()) return;
|
|
926
613
|
this.flushPendingNudge();
|
|
927
|
-
if (this.clipboard.length === 0
|
|
614
|
+
if (this.clipboard.length === 0) return;
|
|
928
615
|
const sel = this.selectTool();
|
|
929
616
|
if (!sel) return;
|
|
930
617
|
this.pasteCount++;
|
|
931
618
|
this.insertClones(this.clipboard, this.pasteCount * 20, sel);
|
|
932
619
|
}
|
|
933
620
|
duplicate() {
|
|
934
|
-
this.flushPendingNudge();
|
|
935
621
|
if (this.deps.isToolActive()) return;
|
|
622
|
+
this.flushPendingNudge();
|
|
936
623
|
const sel = this.selectTool();
|
|
937
624
|
if (!sel) return;
|
|
938
625
|
const source = [];
|
|
@@ -1116,6 +803,11 @@ function parseBinding(binding) {
|
|
|
1116
803
|
throw new Error(`Invalid shortcut binding "${binding}": unknown modifier "${part}"`);
|
|
1117
804
|
}
|
|
1118
805
|
}
|
|
806
|
+
if (parsed.mod && (parsed.ctrl || parsed.meta)) {
|
|
807
|
+
throw new Error(
|
|
808
|
+
`Invalid shortcut binding "${binding}": "mod" already means Ctrl or Cmd; don't combine it with ctrl/meta`
|
|
809
|
+
);
|
|
810
|
+
}
|
|
1119
811
|
return parsed;
|
|
1120
812
|
}
|
|
1121
813
|
function bindingMatches(p, e, allowShift) {
|
|
@@ -1259,6 +951,10 @@ var InputHandler = class {
|
|
|
1259
951
|
this.inputFilter.reset();
|
|
1260
952
|
this.deferredDown = null;
|
|
1261
953
|
this.lastPointerEvent = null;
|
|
954
|
+
if (this.scope === "focus") {
|
|
955
|
+
this.element.removeAttribute("tabindex");
|
|
956
|
+
this.element.style.outline = "";
|
|
957
|
+
}
|
|
1262
958
|
}
|
|
1263
959
|
bind() {
|
|
1264
960
|
const opts = { signal: this.abortController.signal };
|
|
@@ -1449,145 +1145,433 @@ var InputHandler = class {
|
|
|
1449
1145
|
}
|
|
1450
1146
|
return;
|
|
1451
1147
|
}
|
|
1452
|
-
default:
|
|
1453
|
-
if (action.startsWith("tool:")) {
|
|
1454
|
-
if (this.isToolActive) return;
|
|
1455
|
-
e.preventDefault();
|
|
1456
|
-
this.toolContext?.switchTool?.(action.slice("tool:".length));
|
|
1457
|
-
return;
|
|
1458
|
-
}
|
|
1459
|
-
console.warn(`[fieldnotes] unknown shortcut action "${action}"`);
|
|
1148
|
+
default:
|
|
1149
|
+
if (action.startsWith("tool:")) {
|
|
1150
|
+
if (this.isToolActive) return;
|
|
1151
|
+
e.preventDefault();
|
|
1152
|
+
this.toolContext?.switchTool?.(action.slice("tool:".length));
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
console.warn(`[fieldnotes] unknown shortcut action "${action}"`);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
startPinch() {
|
|
1159
|
+
this.inputFilter.reset();
|
|
1160
|
+
this.deferredDown = null;
|
|
1161
|
+
this.isPanning = true;
|
|
1162
|
+
const [a, b] = this.getPinchPoints();
|
|
1163
|
+
this.lastPinchDistance = this.distance(a, b);
|
|
1164
|
+
this.lastPinchCenter = this.midpoint(a, b);
|
|
1165
|
+
this.lastPointer = { ...this.lastPinchCenter };
|
|
1166
|
+
}
|
|
1167
|
+
handlePinchMove() {
|
|
1168
|
+
const [a, b] = this.getPinchPoints();
|
|
1169
|
+
const dist = this.distance(a, b);
|
|
1170
|
+
const center = this.midpoint(a, b);
|
|
1171
|
+
if (this.lastPinchDistance > 0) {
|
|
1172
|
+
const scale = dist / this.lastPinchDistance;
|
|
1173
|
+
const newZoom = this.camera.zoom * scale;
|
|
1174
|
+
this.camera.zoomAt(newZoom, center);
|
|
1175
|
+
}
|
|
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
|
+
}
|
|
1441
|
+
}
|
|
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;
|
|
1457
|
+
}
|
|
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
|
+
}
|
|
1470
|
+
}
|
|
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;
|
|
1483
|
+
}
|
|
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
|
+
}
|
|
1460
1504
|
}
|
|
1505
|
+
this.items = remaining;
|
|
1461
1506
|
}
|
|
1462
|
-
|
|
1463
|
-
this.
|
|
1464
|
-
|
|
1465
|
-
this.
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
const center = this.midpoint(a, b);
|
|
1475
|
-
if (this.lastPinchDistance > 0) {
|
|
1476
|
-
const scale = dist / this.lastPinchDistance;
|
|
1477
|
-
const newZoom = this.camera.zoom * scale;
|
|
1478
|
-
this.camera.zoomAt(newZoom, center);
|
|
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;
|
|
1479
1519
|
}
|
|
1480
|
-
const dx = center.x - this.lastPointer.x;
|
|
1481
|
-
const dy = center.y - this.lastPointer.y;
|
|
1482
|
-
this.camera.pan(dx, dy);
|
|
1483
|
-
this.lastPinchDistance = dist;
|
|
1484
|
-
this.lastPinchCenter = center;
|
|
1485
|
-
this.lastPointer = { ...center };
|
|
1486
|
-
}
|
|
1487
|
-
getPinchPoints() {
|
|
1488
|
-
const pts = [...this.activePointers.values()];
|
|
1489
|
-
return [pts[0] ?? { x: 0, y: 0 }, pts[1] ?? { x: 0, y: 0 }];
|
|
1490
|
-
}
|
|
1491
|
-
distance(a, b) {
|
|
1492
|
-
const dx = a.x - b.x;
|
|
1493
|
-
const dy = a.y - b.y;
|
|
1494
|
-
return Math.sqrt(dx * dx + dy * dy);
|
|
1495
|
-
}
|
|
1496
|
-
midpoint(a, b) {
|
|
1497
|
-
return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
|
|
1498
1520
|
}
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
};
|
|
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);
|
|
1508
1529
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
this.actions.flushPendingNudge();
|
|
1512
|
-
this.historyRecorder?.begin();
|
|
1513
|
-
this.isToolActive = true;
|
|
1514
|
-
this.toolManager.handlePointerDown(this.toPointerState(e), this.toolContext);
|
|
1530
|
+
get size() {
|
|
1531
|
+
return this._size;
|
|
1515
1532
|
}
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
this.
|
|
1533
|
+
insert(id, bounds) {
|
|
1534
|
+
this.root.insert({ id, bounds });
|
|
1535
|
+
this._size++;
|
|
1519
1536
|
}
|
|
1520
|
-
|
|
1521
|
-
if (
|
|
1522
|
-
|
|
1523
|
-
if (tool.onHover) {
|
|
1524
|
-
tool.onHover(this.toPointerState(e), this.toolContext);
|
|
1537
|
+
remove(id) {
|
|
1538
|
+
if (this.root.remove(id)) {
|
|
1539
|
+
this._size--;
|
|
1525
1540
|
}
|
|
1526
1541
|
}
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
this.
|
|
1530
|
-
this.historyRecorder?.commit();
|
|
1542
|
+
update(id, newBounds) {
|
|
1543
|
+
this.remove(id);
|
|
1544
|
+
this.insert(id, newBounds);
|
|
1531
1545
|
}
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
return
|
|
1546
|
+
query(rect) {
|
|
1547
|
+
const result = [];
|
|
1548
|
+
this.root.query(rect, result);
|
|
1549
|
+
return result;
|
|
1536
1550
|
}
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
this.element.focus({ preventScroll: true });
|
|
1551
|
+
queryPoint(point) {
|
|
1552
|
+
return this.query({ x: point.x, y: point.y, w: 0, h: 0 });
|
|
1540
1553
|
}
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
this.isToolActive = false;
|
|
1545
|
-
}
|
|
1546
|
-
this.deferredDown = null;
|
|
1554
|
+
clear() {
|
|
1555
|
+
this.root = new QuadNode(this.worldBounds, 0);
|
|
1556
|
+
this._size = 0;
|
|
1547
1557
|
}
|
|
1548
1558
|
};
|
|
1549
1559
|
|
|
1550
|
-
// src/
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
hasPendingTap = false;
|
|
1560
|
-
constructor(options) {
|
|
1561
|
-
this.timeout = options?.timeout ?? DEFAULT_TIMEOUT;
|
|
1562
|
-
this.maxDistance = options?.maxDistance ?? DEFAULT_MAX_DISTANCE;
|
|
1563
|
-
}
|
|
1564
|
-
feed(e) {
|
|
1565
|
-
const now = Date.now();
|
|
1566
|
-
const x = e.clientX;
|
|
1567
|
-
const y = e.clientY;
|
|
1568
|
-
if (this.hasPendingTap) {
|
|
1569
|
-
const elapsed = now - this.lastTapTime;
|
|
1570
|
-
const dx = x - this.lastTapX;
|
|
1571
|
-
const dy = y - this.lastTapY;
|
|
1572
|
-
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
1573
|
-
if (elapsed <= this.timeout && dist <= this.maxDistance) {
|
|
1574
|
-
this.reset();
|
|
1575
|
-
return true;
|
|
1576
|
-
}
|
|
1577
|
-
}
|
|
1578
|
-
this.lastTapTime = now;
|
|
1579
|
-
this.lastTapX = x;
|
|
1580
|
-
this.lastTapY = y;
|
|
1581
|
-
this.hasPendingTap = true;
|
|
1582
|
-
return false;
|
|
1583
|
-
}
|
|
1584
|
-
reset() {
|
|
1585
|
-
this.hasPendingTap = false;
|
|
1586
|
-
this.lastTapTime = 0;
|
|
1587
|
-
this.lastTapX = 0;
|
|
1588
|
-
this.lastTapY = 0;
|
|
1560
|
+
// src/core/geometry.ts
|
|
1561
|
+
function distSqToSegment(p, a, b) {
|
|
1562
|
+
const abx = b.x - a.x;
|
|
1563
|
+
const aby = b.y - a.y;
|
|
1564
|
+
const apx = p.x - a.x;
|
|
1565
|
+
const apy = p.y - a.y;
|
|
1566
|
+
const lenSq = abx * abx + aby * aby;
|
|
1567
|
+
if (lenSq === 0) {
|
|
1568
|
+
return apx * apx + apy * apy;
|
|
1589
1569
|
}
|
|
1590
|
-
|
|
1570
|
+
const t = Math.max(0, Math.min(1, (apx * abx + apy * aby) / lenSq));
|
|
1571
|
+
const dx = p.x - (a.x + t * abx);
|
|
1572
|
+
const dy = p.y - (a.y + t * aby);
|
|
1573
|
+
return dx * dx + dy * dy;
|
|
1574
|
+
}
|
|
1591
1575
|
|
|
1592
1576
|
// src/elements/arrow-geometry.ts
|
|
1593
1577
|
function getArrowControlPoint(from, to, bend) {
|
|
@@ -1677,16 +1661,7 @@ function bezierPoint(from, cp, to, t) {
|
|
|
1677
1661
|
};
|
|
1678
1662
|
}
|
|
1679
1663
|
function isNearLine(point, a, b, threshold) {
|
|
1680
|
-
|
|
1681
|
-
const dy = b.y - a.y;
|
|
1682
|
-
const lenSq = dx * dx + dy * dy;
|
|
1683
|
-
if (lenSq === 0) {
|
|
1684
|
-
return Math.hypot(point.x - a.x, point.y - a.y) <= threshold;
|
|
1685
|
-
}
|
|
1686
|
-
const t = Math.max(0, Math.min(1, ((point.x - a.x) * dx + (point.y - a.y) * dy) / lenSq));
|
|
1687
|
-
const projX = a.x + t * dx;
|
|
1688
|
-
const projY = a.y + t * dy;
|
|
1689
|
-
return Math.hypot(point.x - projX, point.y - projY) <= threshold;
|
|
1664
|
+
return distSqToSegment(point, a, b) <= threshold * threshold;
|
|
1690
1665
|
}
|
|
1691
1666
|
|
|
1692
1667
|
// src/elements/element-bounds.ts
|
|
@@ -2227,51 +2202,6 @@ function updateBoundArrow(arrow, store) {
|
|
|
2227
2202
|
}
|
|
2228
2203
|
return Object.keys(updates).length > 0 ? updates : null;
|
|
2229
2204
|
}
|
|
2230
|
-
function clearStaleBindings(arrow, store) {
|
|
2231
|
-
const updates = {};
|
|
2232
|
-
let hasUpdates = false;
|
|
2233
|
-
if (arrow.fromBinding && !store.getById(arrow.fromBinding.elementId)) {
|
|
2234
|
-
updates.fromBinding = void 0;
|
|
2235
|
-
hasUpdates = true;
|
|
2236
|
-
}
|
|
2237
|
-
if (arrow.toBinding && !store.getById(arrow.toBinding.elementId)) {
|
|
2238
|
-
updates.toBinding = void 0;
|
|
2239
|
-
hasUpdates = true;
|
|
2240
|
-
}
|
|
2241
|
-
return hasUpdates ? updates : null;
|
|
2242
|
-
}
|
|
2243
|
-
function unbindArrow(arrow, store) {
|
|
2244
|
-
const updates = {};
|
|
2245
|
-
if (arrow.fromBinding) {
|
|
2246
|
-
const el = store.getById(arrow.fromBinding.elementId);
|
|
2247
|
-
const bounds = el ? getElementBounds(el) : null;
|
|
2248
|
-
if (bounds) {
|
|
2249
|
-
const angle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 0);
|
|
2250
|
-
const rayTarget = {
|
|
2251
|
-
x: arrow.from.x + Math.cos(angle) * 1e3,
|
|
2252
|
-
y: arrow.from.y + Math.sin(angle) * 1e3
|
|
2253
|
-
};
|
|
2254
|
-
const edge = getEdgeIntersection(bounds, rayTarget);
|
|
2255
|
-
updates.from = edge;
|
|
2256
|
-
updates.position = edge;
|
|
2257
|
-
}
|
|
2258
|
-
updates.fromBinding = void 0;
|
|
2259
|
-
}
|
|
2260
|
-
if (arrow.toBinding) {
|
|
2261
|
-
const el = store.getById(arrow.toBinding.elementId);
|
|
2262
|
-
const bounds = el ? getElementBounds(el) : null;
|
|
2263
|
-
if (bounds) {
|
|
2264
|
-
const angle = getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 1);
|
|
2265
|
-
const rayTarget = {
|
|
2266
|
-
x: arrow.to.x - Math.cos(angle) * 1e3,
|
|
2267
|
-
y: arrow.to.y - Math.sin(angle) * 1e3
|
|
2268
|
-
};
|
|
2269
|
-
updates.to = getEdgeIntersection(bounds, rayTarget);
|
|
2270
|
-
}
|
|
2271
|
-
updates.toBinding = void 0;
|
|
2272
|
-
}
|
|
2273
|
-
return updates;
|
|
2274
|
-
}
|
|
2275
2205
|
|
|
2276
2206
|
// src/elements/grid-renderer.ts
|
|
2277
2207
|
function getSquareGridLines(bounds, cellSize) {
|
|
@@ -3117,9 +3047,9 @@ var ElementRenderer = class {
|
|
|
3117
3047
|
});
|
|
3118
3048
|
}
|
|
3119
3049
|
};
|
|
3120
|
-
img.onerror = () => {
|
|
3050
|
+
img.onerror = (event) => {
|
|
3121
3051
|
this.imageCache.set(src, "failed");
|
|
3122
|
-
this.onImageError?.(src);
|
|
3052
|
+
this.onImageError?.(src, event);
|
|
3123
3053
|
this.onImageLoad?.();
|
|
3124
3054
|
};
|
|
3125
3055
|
return null;
|
|
@@ -3544,7 +3474,10 @@ var NoteEditor = class {
|
|
|
3544
3474
|
this.editingNode.removeAttribute("data-fn-placeholder");
|
|
3545
3475
|
this.editingNode.removeAttribute("data-fn-empty");
|
|
3546
3476
|
const text = sanitizeNoteHtml(this.editingNode.innerHTML);
|
|
3547
|
-
store.
|
|
3477
|
+
const current = store.getById(this.editingId);
|
|
3478
|
+
if (current && (current.type === "note" || current.type === "text") && current.text !== text) {
|
|
3479
|
+
store.update(this.editingId, { text });
|
|
3480
|
+
}
|
|
3548
3481
|
this.editingNode.contentEditable = "false";
|
|
3549
3482
|
Object.assign(this.editingNode.style, {
|
|
3550
3483
|
userSelect: "none",
|
|
@@ -4461,6 +4394,48 @@ var InteractMode = class {
|
|
|
4461
4394
|
};
|
|
4462
4395
|
};
|
|
4463
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
|
+
|
|
4464
4439
|
// src/canvas/dom-node-manager.ts
|
|
4465
4440
|
var DomNodeManager = class {
|
|
4466
4441
|
domNodes = /* @__PURE__ */ new Map();
|
|
@@ -5141,13 +5116,13 @@ var Viewport = class {
|
|
|
5141
5116
|
this.renderLoop.markAllLayersDirty();
|
|
5142
5117
|
this.requestRender();
|
|
5143
5118
|
});
|
|
5144
|
-
this.renderer.setOnImageError((src) => {
|
|
5119
|
+
this.renderer.setOnImageError((src, cause) => {
|
|
5145
5120
|
const elementIds = [];
|
|
5146
5121
|
for (const el of this.store.getAll()) {
|
|
5147
5122
|
if (el.type === "image" && el.src === src) elementIds.push(el.id);
|
|
5148
5123
|
}
|
|
5149
5124
|
if (options.onImageError) {
|
|
5150
|
-
options.onImageError({ src, elementIds });
|
|
5125
|
+
options.onImageError({ src, elementIds, cause });
|
|
5151
5126
|
} else {
|
|
5152
5127
|
console.warn(`[fieldnotes] image failed to load: ${src}`);
|
|
5153
5128
|
}
|
|
@@ -5366,6 +5341,10 @@ var Viewport = class {
|
|
|
5366
5341
|
this.loadState(parseState(json));
|
|
5367
5342
|
}
|
|
5368
5343
|
setTool(name) {
|
|
5344
|
+
if (!this.toolManager.getTool(name)) {
|
|
5345
|
+
console.warn(`[fieldnotes] setTool: no tool registered as "${name}"`);
|
|
5346
|
+
return;
|
|
5347
|
+
}
|
|
5369
5348
|
this.toolManager.setTool(name, this.toolContext);
|
|
5370
5349
|
}
|
|
5371
5350
|
get shortcuts() {
|
|
@@ -5862,20 +5841,6 @@ var PencilTool = class {
|
|
|
5862
5841
|
};
|
|
5863
5842
|
|
|
5864
5843
|
// src/elements/stroke-hit.ts
|
|
5865
|
-
function distSqToSegment(p, a, b) {
|
|
5866
|
-
const abx = b.x - a.x;
|
|
5867
|
-
const aby = b.y - a.y;
|
|
5868
|
-
const apx = p.x - a.x;
|
|
5869
|
-
const apy = p.y - a.y;
|
|
5870
|
-
const lenSq = abx * abx + aby * aby;
|
|
5871
|
-
if (lenSq === 0) {
|
|
5872
|
-
return apx * apx + apy * apy;
|
|
5873
|
-
}
|
|
5874
|
-
const t = Math.max(0, Math.min(1, (apx * abx + apy * aby) / lenSq));
|
|
5875
|
-
const dx = p.x - (a.x + t * abx);
|
|
5876
|
-
const dy = p.y - (a.y + t * aby);
|
|
5877
|
-
return dx * dx + dy * dy;
|
|
5878
|
-
}
|
|
5879
5844
|
function hitTestStroke(stroke, point, radius) {
|
|
5880
5845
|
const bounds = getElementBounds(stroke);
|
|
5881
5846
|
if (!bounds) return false;
|
|
@@ -7472,52 +7437,32 @@ var TemplateTool = class {
|
|
|
7472
7437
|
};
|
|
7473
7438
|
|
|
7474
7439
|
// src/index.ts
|
|
7475
|
-
var VERSION = "0.
|
|
7440
|
+
var VERSION = "0.25.0";
|
|
7476
7441
|
export {
|
|
7477
|
-
AddElementCommand,
|
|
7478
7442
|
ArrowTool,
|
|
7479
7443
|
AutoSave,
|
|
7480
|
-
Background,
|
|
7481
|
-
BatchCommand,
|
|
7482
7444
|
Camera,
|
|
7483
|
-
CreateLayerCommand,
|
|
7484
|
-
DEFAULT_FONT_SIZE_PRESETS,
|
|
7485
7445
|
DEFAULT_NOTE_FONT_SIZE,
|
|
7486
|
-
DoubleTapDetector,
|
|
7487
|
-
ElementRenderer,
|
|
7488
7446
|
ElementStore,
|
|
7489
7447
|
EraserTool,
|
|
7490
|
-
EventBus,
|
|
7491
7448
|
HandTool,
|
|
7492
|
-
HistoryRecorder,
|
|
7493
7449
|
HistoryStack,
|
|
7494
7450
|
ImageTool,
|
|
7495
|
-
InputFilter,
|
|
7496
|
-
InputHandler,
|
|
7497
7451
|
LayerManager,
|
|
7498
7452
|
MeasureTool,
|
|
7499
|
-
NoteEditor,
|
|
7500
7453
|
NoteTool,
|
|
7501
|
-
NoteToolbar,
|
|
7502
7454
|
PencilTool,
|
|
7503
|
-
Quadtree,
|
|
7504
|
-
RemoveElementCommand,
|
|
7505
|
-
RemoveLayerCommand,
|
|
7506
7455
|
SelectTool,
|
|
7507
7456
|
ShapeTool,
|
|
7508
7457
|
TemplateTool,
|
|
7509
7458
|
TextTool,
|
|
7510
7459
|
ToolManager,
|
|
7511
|
-
UpdateElementCommand,
|
|
7512
|
-
UpdateLayerCommand,
|
|
7513
7460
|
VERSION,
|
|
7514
7461
|
Viewport,
|
|
7515
7462
|
boundsIntersect,
|
|
7516
|
-
clearStaleBindings,
|
|
7517
7463
|
createArrow,
|
|
7518
7464
|
createGrid,
|
|
7519
7465
|
createHtmlElement,
|
|
7520
|
-
createId,
|
|
7521
7466
|
createImage,
|
|
7522
7467
|
createNote,
|
|
7523
7468
|
createShape,
|
|
@@ -7526,29 +7471,20 @@ export {
|
|
|
7526
7471
|
createText,
|
|
7527
7472
|
drawHexPath,
|
|
7528
7473
|
exportImage,
|
|
7529
|
-
exportState,
|
|
7530
|
-
findBindTarget,
|
|
7531
|
-
findBoundArrows,
|
|
7532
7474
|
getActiveFormats,
|
|
7533
7475
|
getArrowBounds,
|
|
7534
7476
|
getArrowControlPoint,
|
|
7535
7477
|
getArrowMidpoint,
|
|
7536
7478
|
getArrowTangentAngle,
|
|
7537
7479
|
getBendFromPoint,
|
|
7538
|
-
getEdgeIntersection,
|
|
7539
7480
|
getElementBounds,
|
|
7540
|
-
getElementCenter,
|
|
7541
7481
|
getElementsBoundingBox,
|
|
7542
7482
|
getHexCellsInCone,
|
|
7543
7483
|
getHexCellsInLine,
|
|
7544
7484
|
getHexCellsInRadius,
|
|
7545
7485
|
getHexCellsInSquare,
|
|
7546
7486
|
getHexDistance,
|
|
7547
|
-
isBindable,
|
|
7548
7487
|
isNearBezier,
|
|
7549
|
-
isNoteContentEmpty,
|
|
7550
|
-
parseState,
|
|
7551
|
-
sanitizeNoteHtml,
|
|
7552
7488
|
setFontSize,
|
|
7553
7489
|
smartSnap,
|
|
7554
7490
|
snapPoint,
|
|
@@ -7556,8 +7492,6 @@ export {
|
|
|
7556
7492
|
toggleBold,
|
|
7557
7493
|
toggleItalic,
|
|
7558
7494
|
toggleStrikethrough,
|
|
7559
|
-
toggleUnderline
|
|
7560
|
-
unbindArrow,
|
|
7561
|
-
updateBoundArrow
|
|
7495
|
+
toggleUnderline
|
|
7562
7496
|
};
|
|
7563
7497
|
//# sourceMappingURL=index.js.map
|