@fieldnotes/core 0.59.0 → 0.61.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 +541 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +243 -2
- package/dist/index.d.ts +243 -2
- package/dist/index.js +532 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -23,9 +23,11 @@ __export(index_exports, {
|
|
|
23
23
|
ArrowTool: () => ArrowTool,
|
|
24
24
|
AutoSave: () => AutoSave,
|
|
25
25
|
Camera: () => Camera,
|
|
26
|
+
CameraAnimator: () => CameraAnimator,
|
|
26
27
|
DEFAULT_NOTE_FONT_SIZE: () => DEFAULT_NOTE_FONT_SIZE,
|
|
27
28
|
ElementStore: () => ElementStore,
|
|
28
29
|
EraserTool: () => EraserTool,
|
|
30
|
+
FOCUS_PRESENCE_KIND: () => FOCUS_PRESENCE_KIND,
|
|
29
31
|
HandTool: () => HandTool,
|
|
30
32
|
HistoryStack: () => HistoryStack,
|
|
31
33
|
ImageTool: () => ImageTool,
|
|
@@ -43,6 +45,7 @@ __export(index_exports, {
|
|
|
43
45
|
PencilTool: () => PencilTool,
|
|
44
46
|
PingInput: () => PingInput,
|
|
45
47
|
PingTool: () => PingTool,
|
|
48
|
+
RemoteFocusReceiver: () => RemoteFocusReceiver,
|
|
46
49
|
RemoteLaserOverlay: () => RemoteLaserOverlay,
|
|
47
50
|
RemoteMeasureOverlay: () => RemoteMeasureOverlay,
|
|
48
51
|
RemotePingOverlay: () => RemotePingOverlay,
|
|
@@ -53,7 +56,10 @@ __export(index_exports, {
|
|
|
53
56
|
ToolManager: () => ToolManager,
|
|
54
57
|
VERSION: () => VERSION,
|
|
55
58
|
Viewport: () => Viewport,
|
|
59
|
+
applyCameraView: () => applyCameraView,
|
|
56
60
|
boundsIntersect: () => boundsIntersect,
|
|
61
|
+
cameraOriginForView: () => cameraOriginForView,
|
|
62
|
+
captureCameraView: () => captureCameraView,
|
|
57
63
|
createArrow: () => createArrow,
|
|
58
64
|
createGrid: () => createGrid,
|
|
59
65
|
createHtmlElement: () => createHtmlElement,
|
|
@@ -66,6 +72,7 @@ __export(index_exports, {
|
|
|
66
72
|
drawHexPath: () => drawHexPath,
|
|
67
73
|
exportImage: () => exportImage,
|
|
68
74
|
exportSvg: () => exportSvg,
|
|
75
|
+
fitZoomForView: () => fitZoomForView,
|
|
69
76
|
getActiveFormats: () => getActiveFormats,
|
|
70
77
|
getArrowBounds: () => getArrowBounds,
|
|
71
78
|
getArrowControlPoint: () => getArrowControlPoint,
|
|
@@ -81,6 +88,7 @@ __export(index_exports, {
|
|
|
81
88
|
getHexCellsInRectangle: () => getHexCellsInRectangle,
|
|
82
89
|
getHexCellsInSquare: () => getHexCellsInSquare,
|
|
83
90
|
getHexDistance: () => getHexDistance,
|
|
91
|
+
isFocusPresence: () => isFocusPresence,
|
|
84
92
|
isLaserTrailPresence: () => isLaserTrailPresence,
|
|
85
93
|
isMeasurePresence: () => isMeasurePresence,
|
|
86
94
|
isNearBezier: () => isNearBezier,
|
|
@@ -90,6 +98,7 @@ __export(index_exports, {
|
|
|
90
98
|
snapPoint: () => snapPoint,
|
|
91
99
|
snapToHexCenter: () => snapToHexCenter,
|
|
92
100
|
styleToPatch: () => styleToPatch,
|
|
101
|
+
toFocusPresence: () => toFocusPresence,
|
|
93
102
|
toLaserTrailPresence: () => toLaserTrailPresence,
|
|
94
103
|
toMeasurePresence: () => toMeasurePresence,
|
|
95
104
|
toPingPresence: () => toPingPresence,
|
|
@@ -5272,6 +5281,7 @@ var ToolManager = class {
|
|
|
5272
5281
|
tools = /* @__PURE__ */ new Map();
|
|
5273
5282
|
current = null;
|
|
5274
5283
|
changeListeners = /* @__PURE__ */ new Set();
|
|
5284
|
+
registerListeners = /* @__PURE__ */ new Set();
|
|
5275
5285
|
get activeTool() {
|
|
5276
5286
|
return this.current;
|
|
5277
5287
|
}
|
|
@@ -5280,6 +5290,7 @@ var ToolManager = class {
|
|
|
5280
5290
|
}
|
|
5281
5291
|
register(tool) {
|
|
5282
5292
|
this.tools.set(tool.name, tool);
|
|
5293
|
+
this.registerListeners.forEach((fn) => fn(tool));
|
|
5283
5294
|
}
|
|
5284
5295
|
getTool(name) {
|
|
5285
5296
|
return this.tools.get(name);
|
|
@@ -5305,6 +5316,10 @@ var ToolManager = class {
|
|
|
5305
5316
|
this.changeListeners.add(listener);
|
|
5306
5317
|
return () => this.changeListeners.delete(listener);
|
|
5307
5318
|
}
|
|
5319
|
+
onRegister(listener) {
|
|
5320
|
+
this.registerListeners.add(listener);
|
|
5321
|
+
return () => this.registerListeners.delete(listener);
|
|
5322
|
+
}
|
|
5308
5323
|
};
|
|
5309
5324
|
|
|
5310
5325
|
// src/history/history-stack.ts
|
|
@@ -5496,6 +5511,7 @@ var HistoryRecorder = class {
|
|
|
5496
5511
|
generation = 0;
|
|
5497
5512
|
updateSnapshots = /* @__PURE__ */ new Map();
|
|
5498
5513
|
unsubscribers;
|
|
5514
|
+
transactionEndListeners = /* @__PURE__ */ new Set();
|
|
5499
5515
|
pause() {
|
|
5500
5516
|
this.recording = false;
|
|
5501
5517
|
}
|
|
@@ -5513,19 +5529,35 @@ var HistoryRecorder = class {
|
|
|
5513
5529
|
get currentTransactionId() {
|
|
5514
5530
|
return this.transaction !== null ? this.generation : null;
|
|
5515
5531
|
}
|
|
5532
|
+
onTransactionEnd(listener) {
|
|
5533
|
+
this.transactionEndListeners.add(listener);
|
|
5534
|
+
return () => this.transactionEndListeners.delete(listener);
|
|
5535
|
+
}
|
|
5536
|
+
notifyTransactionEnd() {
|
|
5537
|
+
for (const listener of this.transactionEndListeners) {
|
|
5538
|
+
try {
|
|
5539
|
+
listener();
|
|
5540
|
+
} catch {
|
|
5541
|
+
}
|
|
5542
|
+
}
|
|
5543
|
+
}
|
|
5516
5544
|
commit() {
|
|
5517
5545
|
if (!this.transaction) return;
|
|
5518
5546
|
const finalCommands = this.flushUpdateSnapshots();
|
|
5519
5547
|
const all = [...this.transaction, ...finalCommands];
|
|
5520
5548
|
this.transaction = null;
|
|
5521
5549
|
this.updateSnapshots.clear();
|
|
5522
|
-
if (all.length
|
|
5523
|
-
|
|
5524
|
-
|
|
5550
|
+
if (all.length > 0) {
|
|
5551
|
+
const first = all[0];
|
|
5552
|
+
this.stack.push(all.length === 1 && first ? first : new BatchCommand(all));
|
|
5553
|
+
}
|
|
5554
|
+
this.notifyTransactionEnd();
|
|
5525
5555
|
}
|
|
5526
5556
|
rollback() {
|
|
5557
|
+
const wasOpen = this.transaction !== null;
|
|
5527
5558
|
this.transaction = null;
|
|
5528
5559
|
this.updateSnapshots.clear();
|
|
5560
|
+
if (wasOpen) this.notifyTransactionEnd();
|
|
5529
5561
|
}
|
|
5530
5562
|
destroy() {
|
|
5531
5563
|
this.unsubscribers.forEach((fn) => fn());
|
|
@@ -7817,6 +7849,14 @@ function rotateElementPatch(el, bounds, pivot, delta) {
|
|
|
7817
7849
|
}
|
|
7818
7850
|
|
|
7819
7851
|
// src/canvas/selection-ops.ts
|
|
7852
|
+
var STYLE_FIELDS = [
|
|
7853
|
+
"color",
|
|
7854
|
+
"fillColor",
|
|
7855
|
+
"strokeWidth",
|
|
7856
|
+
"opacity",
|
|
7857
|
+
"fontSize",
|
|
7858
|
+
"strokeStyle"
|
|
7859
|
+
];
|
|
7820
7860
|
function sharedValue(values) {
|
|
7821
7861
|
const present = values.filter((v) => v !== void 0);
|
|
7822
7862
|
if (present.length === 0) return void 0;
|
|
@@ -7851,6 +7891,42 @@ var SelectionOps = class {
|
|
|
7851
7891
|
if (strokeStyle !== void 0) result.strokeStyle = strokeStyle;
|
|
7852
7892
|
return result;
|
|
7853
7893
|
}
|
|
7894
|
+
/**
|
|
7895
|
+
* Unlike `getStyle()` — which returns `{}` for a selection whose elements
|
|
7896
|
+
* have no applicable style fields (e.g. images) — this returns `null` when
|
|
7897
|
+
* no field is applicable, so callers can distinguish "nothing to show" from
|
|
7898
|
+
* "everything shared but empty".
|
|
7899
|
+
*/
|
|
7900
|
+
getStyleDetails() {
|
|
7901
|
+
const ids = this.deps.getSelectedIds();
|
|
7902
|
+
if (ids.length === 0) return null;
|
|
7903
|
+
const styles = [];
|
|
7904
|
+
for (const id of ids) {
|
|
7905
|
+
const el = this.deps.store.getById(id);
|
|
7906
|
+
if (el) styles.push(getElementStyle(el));
|
|
7907
|
+
}
|
|
7908
|
+
if (styles.length === 0) return null;
|
|
7909
|
+
const common = {};
|
|
7910
|
+
const applicable = [];
|
|
7911
|
+
const mixed = [];
|
|
7912
|
+
for (const field of STYLE_FIELDS) {
|
|
7913
|
+
const allValues = styles.map((s) => s[field]);
|
|
7914
|
+
const values = [];
|
|
7915
|
+
for (const v of allValues) {
|
|
7916
|
+
if (v !== void 0) values.push(v);
|
|
7917
|
+
}
|
|
7918
|
+
if (values.length === 0) continue;
|
|
7919
|
+
applicable.push(field);
|
|
7920
|
+
const distinct = new Set(values);
|
|
7921
|
+
if (distinct.size > 1) {
|
|
7922
|
+
mixed.push(field);
|
|
7923
|
+
} else {
|
|
7924
|
+
common[field] = values[0];
|
|
7925
|
+
}
|
|
7926
|
+
}
|
|
7927
|
+
if (applicable.length === 0) return null;
|
|
7928
|
+
return { common, applicable, mixed };
|
|
7929
|
+
}
|
|
7854
7930
|
applyStyle(style) {
|
|
7855
7931
|
const ids = this.deps.getSelectedIds();
|
|
7856
7932
|
if (ids.length === 0) return;
|
|
@@ -8225,7 +8301,7 @@ var ViewportInteractions = class {
|
|
|
8225
8301
|
var EMPTY_IDS = [];
|
|
8226
8302
|
function noop() {
|
|
8227
8303
|
}
|
|
8228
|
-
var Viewport = class {
|
|
8304
|
+
var Viewport = class _Viewport {
|
|
8229
8305
|
constructor(container, options = {}) {
|
|
8230
8306
|
this.container = container;
|
|
8231
8307
|
this.camera = new Camera(options.camera);
|
|
@@ -8234,6 +8310,13 @@ var Viewport = class {
|
|
|
8234
8310
|
this.store = new ElementStore();
|
|
8235
8311
|
this.layerManager = new LayerManager(this.store);
|
|
8236
8312
|
this.toolManager = new ToolManager();
|
|
8313
|
+
this.unsubToolRegister = this.toolManager.onRegister((tool) => {
|
|
8314
|
+
if (_Viewport.isSelectionSource(tool)) this.attachSelectionSource(tool);
|
|
8315
|
+
});
|
|
8316
|
+
const existingSelect = this.getSelectTool();
|
|
8317
|
+
if (existingSelect && _Viewport.isSelectionSource(existingSelect)) {
|
|
8318
|
+
this.attachSelectionSource(existingSelect);
|
|
8319
|
+
}
|
|
8237
8320
|
this.renderer = new ElementRenderer();
|
|
8238
8321
|
this.renderer.setStore(this.store);
|
|
8239
8322
|
this.renderer.setCamera(this.camera);
|
|
@@ -8268,6 +8351,11 @@ var Viewport = class {
|
|
|
8268
8351
|
this.dropHandler = options.onDrop;
|
|
8269
8352
|
this.history = new HistoryStack();
|
|
8270
8353
|
this.historyRecorder = new HistoryRecorder(this.store, this.history, this.layerManager);
|
|
8354
|
+
this.unsubRecorderEnd = this.historyRecorder.onTransactionEnd(() => {
|
|
8355
|
+
if (!this.pendingSelectionPrune) return;
|
|
8356
|
+
this.pendingSelectionPrune = false;
|
|
8357
|
+
this.pruneSelection();
|
|
8358
|
+
});
|
|
8271
8359
|
this.selectionOps = new SelectionOps({
|
|
8272
8360
|
store: this.store,
|
|
8273
8361
|
recorder: this.historyRecorder,
|
|
@@ -8387,6 +8475,7 @@ var Viewport = class {
|
|
|
8387
8475
|
this.domNodeManager.removeDomNode(el.id);
|
|
8388
8476
|
this.renderLoop.markLayerDirty(el.layerId);
|
|
8389
8477
|
this.requestRender();
|
|
8478
|
+
this.handleRemovedElement(el.id);
|
|
8390
8479
|
}),
|
|
8391
8480
|
this.store.on("update", ({ previous, current }) => {
|
|
8392
8481
|
if (current.type === "grid") this.gridController.syncContext();
|
|
@@ -8401,6 +8490,7 @@ var Viewport = class {
|
|
|
8401
8490
|
this.renderLoop.markAllLayersDirty();
|
|
8402
8491
|
this.gridController.syncContext();
|
|
8403
8492
|
this.requestRender();
|
|
8493
|
+
this.pruneSelection();
|
|
8404
8494
|
})
|
|
8405
8495
|
];
|
|
8406
8496
|
this.layerManager.on("change", () => {
|
|
@@ -8470,6 +8560,13 @@ var Viewport = class {
|
|
|
8470
8560
|
minimap = null;
|
|
8471
8561
|
htmlRenderers = /* @__PURE__ */ new Map();
|
|
8472
8562
|
resizeListeners = /* @__PURE__ */ new Set();
|
|
8563
|
+
selectionListeners = /* @__PURE__ */ new Set();
|
|
8564
|
+
detachSelectionSource = null;
|
|
8565
|
+
unsubToolRegister = () => {
|
|
8566
|
+
};
|
|
8567
|
+
pendingSelectionPrune = false;
|
|
8568
|
+
unsubRecorderEnd = () => {
|
|
8569
|
+
};
|
|
8473
8570
|
get ctx() {
|
|
8474
8571
|
return this.canvasEl.getContext("2d");
|
|
8475
8572
|
}
|
|
@@ -8498,6 +8595,17 @@ var Viewport = class {
|
|
|
8498
8595
|
getVisibleRect() {
|
|
8499
8596
|
return this.camera.getVisibleRect(this.canvasEl.clientWidth, this.canvasEl.clientHeight);
|
|
8500
8597
|
}
|
|
8598
|
+
/**
|
|
8599
|
+
* Size in CSS pixels of the canvas that `getVisibleRect()` measures.
|
|
8600
|
+
* Exposed because `canvasEl` is private: consumers can only reach the
|
|
8601
|
+
* wrapper (via `domLayer.parentElement`), so without this accessor the
|
|
8602
|
+
* canonical size behind `getVisibleRect()` is unreachable and callers
|
|
8603
|
+
* resort to `getVisibleRect().w * camera.zoom`. Capture and restore must
|
|
8604
|
+
* measure the same element or saved views do not round-trip.
|
|
8605
|
+
*/
|
|
8606
|
+
getCanvasSize() {
|
|
8607
|
+
return { w: this.canvasEl.clientWidth, h: this.canvasEl.clientHeight };
|
|
8608
|
+
}
|
|
8501
8609
|
/** Centers the camera on a world point without changing zoom. */
|
|
8502
8610
|
centerCameraAt(world) {
|
|
8503
8611
|
const z = this.camera.zoom;
|
|
@@ -8728,6 +8836,42 @@ var Viewport = class {
|
|
|
8728
8836
|
getSelectTool() {
|
|
8729
8837
|
return this.toolManager.getTool("select");
|
|
8730
8838
|
}
|
|
8839
|
+
pruneSelection() {
|
|
8840
|
+
const tool = this.getSelectTool();
|
|
8841
|
+
if (!tool) return;
|
|
8842
|
+
const ids = tool.selectedIds;
|
|
8843
|
+
const filtered = ids.filter((id) => this.store.getById(id) !== void 0);
|
|
8844
|
+
if (filtered.length !== ids.length) tool.setSelection(filtered);
|
|
8845
|
+
}
|
|
8846
|
+
handleRemovedElement(id) {
|
|
8847
|
+
if (!this.getSelectedIds().includes(id)) return;
|
|
8848
|
+
if (this.historyRecorder.currentTransactionId !== null) {
|
|
8849
|
+
this.pendingSelectionPrune = true;
|
|
8850
|
+
return;
|
|
8851
|
+
}
|
|
8852
|
+
this.pruneSelection();
|
|
8853
|
+
}
|
|
8854
|
+
static isSelectionSource(tool) {
|
|
8855
|
+
const candidate = tool;
|
|
8856
|
+
return tool.name === "select" && typeof candidate.onSelectionChange === "function" && typeof candidate.setSelection === "function";
|
|
8857
|
+
}
|
|
8858
|
+
emitSelectionChange() {
|
|
8859
|
+
for (const listener of this.selectionListeners) {
|
|
8860
|
+
try {
|
|
8861
|
+
listener();
|
|
8862
|
+
} catch {
|
|
8863
|
+
}
|
|
8864
|
+
}
|
|
8865
|
+
}
|
|
8866
|
+
attachSelectionSource(tool) {
|
|
8867
|
+
this.detachSelectionSource?.();
|
|
8868
|
+
this.detachSelectionSource = tool.onSelectionChange(() => this.emitSelectionChange());
|
|
8869
|
+
}
|
|
8870
|
+
/**
|
|
8871
|
+
* getSelectedIds() and the onSelectionChange emitter never surface stale ids:
|
|
8872
|
+
* once the enclosing history transaction completes, both reflect
|
|
8873
|
+
* the current selection.
|
|
8874
|
+
*/
|
|
8731
8875
|
getSelectedIds() {
|
|
8732
8876
|
return this.getSelectTool()?.selectedIds ?? EMPTY_IDS;
|
|
8733
8877
|
}
|
|
@@ -8761,13 +8905,29 @@ var Viewport = class {
|
|
|
8761
8905
|
if (items.length === 0) return;
|
|
8762
8906
|
this.contextMenu.open(items, screenPos);
|
|
8763
8907
|
}
|
|
8908
|
+
/**
|
|
8909
|
+
* Persistent, viewport-owned selection-change emitter. Subscribing works
|
|
8910
|
+
* regardless of whether a select tool is registered yet; it forwards
|
|
8911
|
+
* events from whichever select tool is currently attached via
|
|
8912
|
+
* `toolManager.onRegister`. Never delivers stale ids once the enclosing
|
|
8913
|
+
* history transaction completes.
|
|
8914
|
+
*/
|
|
8764
8915
|
onSelectionChange(listener) {
|
|
8765
|
-
|
|
8766
|
-
return
|
|
8916
|
+
this.selectionListeners.add(listener);
|
|
8917
|
+
return () => {
|
|
8918
|
+
this.selectionListeners.delete(listener);
|
|
8919
|
+
};
|
|
8767
8920
|
}
|
|
8768
8921
|
getSelectionStyle() {
|
|
8769
8922
|
return this.selectionOps.getStyle();
|
|
8770
8923
|
}
|
|
8924
|
+
/**
|
|
8925
|
+
* Unlike `getSelectionStyle()` — which returns `{}` for a style-less
|
|
8926
|
+
* selection — this returns `null` when no style field is applicable.
|
|
8927
|
+
*/
|
|
8928
|
+
getSelectionStyleDetails() {
|
|
8929
|
+
return this.selectionOps.getStyleDetails();
|
|
8930
|
+
}
|
|
8771
8931
|
applyStyleToSelection(style) {
|
|
8772
8932
|
this.selectionOps.applyStyle(style);
|
|
8773
8933
|
}
|
|
@@ -8816,6 +8976,11 @@ var Viewport = class {
|
|
|
8816
8976
|
this.inputHandler.destroy();
|
|
8817
8977
|
this.unsubCamera();
|
|
8818
8978
|
this.unsubToolChange();
|
|
8979
|
+
this.unsubToolRegister();
|
|
8980
|
+
this.unsubRecorderEnd();
|
|
8981
|
+
this.detachSelectionSource?.();
|
|
8982
|
+
this.detachSelectionSource = null;
|
|
8983
|
+
this.selectionListeners.clear();
|
|
8819
8984
|
this.unsubStore.forEach((fn) => fn());
|
|
8820
8985
|
this.resizeObserver?.disconnect();
|
|
8821
8986
|
this.resizeObserver = null;
|
|
@@ -9589,6 +9754,361 @@ var PingInput = class {
|
|
|
9589
9754
|
}
|
|
9590
9755
|
};
|
|
9591
9756
|
|
|
9757
|
+
// src/canvas/camera-view.ts
|
|
9758
|
+
function assertValidView(view) {
|
|
9759
|
+
const { x, y, w, h } = view;
|
|
9760
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(w) || !Number.isFinite(h)) {
|
|
9761
|
+
throw new Error("[fieldnotes] CameraView requires finite x, y, w, and h");
|
|
9762
|
+
}
|
|
9763
|
+
if (w <= 0 || h <= 0) {
|
|
9764
|
+
throw new Error("[fieldnotes] CameraView requires positive w and h");
|
|
9765
|
+
}
|
|
9766
|
+
}
|
|
9767
|
+
function assertCanvasDims(canvasW, canvasH) {
|
|
9768
|
+
if (!Number.isFinite(canvasW) || !Number.isFinite(canvasH)) {
|
|
9769
|
+
throw new Error("[fieldnotes] canvas dimensions must be finite");
|
|
9770
|
+
}
|
|
9771
|
+
if (canvasW < 0 || canvasH < 0) {
|
|
9772
|
+
throw new Error("[fieldnotes] canvas dimensions must not be negative");
|
|
9773
|
+
}
|
|
9774
|
+
}
|
|
9775
|
+
function canvasDimsUsable(canvasW, canvasH) {
|
|
9776
|
+
return Number.isFinite(canvasW) && Number.isFinite(canvasH) && canvasW >= 0 && canvasH >= 0;
|
|
9777
|
+
}
|
|
9778
|
+
function captureCameraView(viewport) {
|
|
9779
|
+
const rect = viewport.getVisibleRect();
|
|
9780
|
+
return { x: rect.x, y: rect.y, w: rect.w, h: rect.h };
|
|
9781
|
+
}
|
|
9782
|
+
function fitZoomForView(view, canvasW, canvasH) {
|
|
9783
|
+
assertValidView(view);
|
|
9784
|
+
assertCanvasDims(canvasW, canvasH);
|
|
9785
|
+
if (canvasW === 0 || canvasH === 0) {
|
|
9786
|
+
throw new Error("[fieldnotes] fitZoomForView requires a non-zero canvas size");
|
|
9787
|
+
}
|
|
9788
|
+
return Math.min(canvasW / view.w, canvasH / view.h);
|
|
9789
|
+
}
|
|
9790
|
+
function cameraOriginForView(view, zoom, canvasW, canvasH) {
|
|
9791
|
+
return {
|
|
9792
|
+
x: canvasW / 2 - (view.x + view.w / 2) * zoom,
|
|
9793
|
+
y: canvasH / 2 - (view.y + view.h / 2) * zoom
|
|
9794
|
+
};
|
|
9795
|
+
}
|
|
9796
|
+
function applyCameraView(camera, view, canvasW, canvasH) {
|
|
9797
|
+
assertValidView(view);
|
|
9798
|
+
assertCanvasDims(canvasW, canvasH);
|
|
9799
|
+
if (canvasW === 0 || canvasH === 0) return;
|
|
9800
|
+
camera.setZoom(fitZoomForView(view, canvasW, canvasH));
|
|
9801
|
+
const origin = cameraOriginForView(view, camera.zoom, canvasW, canvasH);
|
|
9802
|
+
camera.moveTo(origin.x, origin.y);
|
|
9803
|
+
}
|
|
9804
|
+
|
|
9805
|
+
// src/canvas/camera-animator.ts
|
|
9806
|
+
var DEFAULT_DURATION_MS3 = 400;
|
|
9807
|
+
var FRAMED_EPSILON = 1e-6;
|
|
9808
|
+
var easeOutCubic2 = (t) => 1 - Math.pow(1 - t, 3);
|
|
9809
|
+
function lerp(a, b, k) {
|
|
9810
|
+
return a + (b - a) * k;
|
|
9811
|
+
}
|
|
9812
|
+
function lerpView(from, to, k) {
|
|
9813
|
+
return {
|
|
9814
|
+
x: lerp(from.x, to.x, k),
|
|
9815
|
+
y: lerp(from.y, to.y, k),
|
|
9816
|
+
w: lerp(from.w, to.w, k),
|
|
9817
|
+
h: lerp(from.h, to.h, k)
|
|
9818
|
+
};
|
|
9819
|
+
}
|
|
9820
|
+
function viewsClose(a, b) {
|
|
9821
|
+
return Math.abs(a.x - b.x) < FRAMED_EPSILON && Math.abs(a.y - b.y) < FRAMED_EPSILON && Math.abs(a.w - b.w) < FRAMED_EPSILON && Math.abs(a.h - b.h) < FRAMED_EPSILON;
|
|
9822
|
+
}
|
|
9823
|
+
var CameraAnimator = class {
|
|
9824
|
+
camera;
|
|
9825
|
+
getCanvasSize;
|
|
9826
|
+
frames;
|
|
9827
|
+
now;
|
|
9828
|
+
durationMs;
|
|
9829
|
+
easing;
|
|
9830
|
+
rafId = null;
|
|
9831
|
+
from = null;
|
|
9832
|
+
to = null;
|
|
9833
|
+
startedAt = 0;
|
|
9834
|
+
endListeners = /* @__PURE__ */ new Set();
|
|
9835
|
+
/**
|
|
9836
|
+
* Monotonic operation counter. `animateTo`/`jumpTo` claim a generation
|
|
9837
|
+
* before emitting 'superseded'; if an onEnd listener starts a newer
|
|
9838
|
+
* operation during that emit, the outer call sees a bumped counter and
|
|
9839
|
+
* bails instead of overwriting the nested animation's state. Without this,
|
|
9840
|
+
* the nested animation would run to completion having never reported an end
|
|
9841
|
+
* reason, breaking the exactly-one guarantee the spec makes.
|
|
9842
|
+
*/
|
|
9843
|
+
generation = 0;
|
|
9844
|
+
lastWrite = null;
|
|
9845
|
+
disposed = false;
|
|
9846
|
+
detachListeners = null;
|
|
9847
|
+
constructor(element, camera, options) {
|
|
9848
|
+
if (options.frames !== void 0) {
|
|
9849
|
+
const { requestFrame, cancelFrame } = options.frames;
|
|
9850
|
+
if (typeof requestFrame !== "function" || typeof cancelFrame !== "function") {
|
|
9851
|
+
throw new Error(
|
|
9852
|
+
"[fieldnotes] CameraAnimator: `frames` must supply both requestFrame and cancelFrame"
|
|
9853
|
+
);
|
|
9854
|
+
}
|
|
9855
|
+
}
|
|
9856
|
+
this.camera = camera;
|
|
9857
|
+
this.getCanvasSize = options.getCanvasSize;
|
|
9858
|
+
this.frames = options.frames ?? {
|
|
9859
|
+
requestFrame: (cb) => requestAnimationFrame(cb),
|
|
9860
|
+
cancelFrame: (id) => cancelAnimationFrame(id)
|
|
9861
|
+
};
|
|
9862
|
+
this.now = options.now ?? (() => performance.now());
|
|
9863
|
+
this.durationMs = options.durationMs ?? DEFAULT_DURATION_MS3;
|
|
9864
|
+
this.easing = options.easing ?? easeOutCubic2;
|
|
9865
|
+
if (options.interactive ?? true) {
|
|
9866
|
+
const onUserInput = () => {
|
|
9867
|
+
this.end("cancelled");
|
|
9868
|
+
};
|
|
9869
|
+
const types = ["pointerdown", "wheel", "keydown"];
|
|
9870
|
+
for (const type of types) {
|
|
9871
|
+
element.addEventListener(type, onUserInput, { passive: true });
|
|
9872
|
+
}
|
|
9873
|
+
this.detachListeners = () => {
|
|
9874
|
+
for (const type of types) {
|
|
9875
|
+
element.removeEventListener(type, onUserInput);
|
|
9876
|
+
}
|
|
9877
|
+
};
|
|
9878
|
+
}
|
|
9879
|
+
}
|
|
9880
|
+
get animating() {
|
|
9881
|
+
return this.to !== null;
|
|
9882
|
+
}
|
|
9883
|
+
onEnd(listener) {
|
|
9884
|
+
this.endListeners.add(listener);
|
|
9885
|
+
return () => this.endListeners.delete(listener);
|
|
9886
|
+
}
|
|
9887
|
+
animateTo(view) {
|
|
9888
|
+
const size = this.validateAndMeasure(view);
|
|
9889
|
+
if (size === null) return;
|
|
9890
|
+
const current = this.camera.getVisibleRect(size.w, size.h);
|
|
9891
|
+
const generation = ++this.generation;
|
|
9892
|
+
this.end("superseded");
|
|
9893
|
+
if (this.generation !== generation) return;
|
|
9894
|
+
if (viewsClose(current, view)) {
|
|
9895
|
+
applyCameraView(this.camera, view, size.w, size.h);
|
|
9896
|
+
this.emit("complete");
|
|
9897
|
+
return;
|
|
9898
|
+
}
|
|
9899
|
+
this.from = current;
|
|
9900
|
+
this.to = view;
|
|
9901
|
+
this.startedAt = this.now();
|
|
9902
|
+
this.lastWrite = null;
|
|
9903
|
+
this.rafId = this.frames.requestFrame(this.step);
|
|
9904
|
+
}
|
|
9905
|
+
jumpTo(view) {
|
|
9906
|
+
const size = this.validateAndMeasure(view);
|
|
9907
|
+
if (size === null) return;
|
|
9908
|
+
const generation = ++this.generation;
|
|
9909
|
+
this.end("superseded");
|
|
9910
|
+
if (this.generation !== generation) return;
|
|
9911
|
+
applyCameraView(this.camera, view, size.w, size.h);
|
|
9912
|
+
this.lastWrite = null;
|
|
9913
|
+
}
|
|
9914
|
+
cancel() {
|
|
9915
|
+
if (this.disposed) return;
|
|
9916
|
+
this.end("cancelled");
|
|
9917
|
+
}
|
|
9918
|
+
/**
|
|
9919
|
+
* Terminal. Order is load-bearing: the flag is set BEFORE any listener runs,
|
|
9920
|
+
* because an onEnd listener can call animateTo during the disposal callback.
|
|
9921
|
+
* With the flag set last, that call would start a real animation which the
|
|
9922
|
+
* listener clear then silently discards — a second animation with no end
|
|
9923
|
+
* reason, breaking the exactly-one guarantee.
|
|
9924
|
+
*/
|
|
9925
|
+
dispose() {
|
|
9926
|
+
if (this.disposed) return;
|
|
9927
|
+
this.disposed = true;
|
|
9928
|
+
this.generation++;
|
|
9929
|
+
const wasAnimating = this.to !== null;
|
|
9930
|
+
this.clearFrame();
|
|
9931
|
+
this.from = null;
|
|
9932
|
+
this.to = null;
|
|
9933
|
+
this.lastWrite = null;
|
|
9934
|
+
if (wasAnimating) this.emit("cancelled");
|
|
9935
|
+
this.endListeners.clear();
|
|
9936
|
+
this.detachListeners?.();
|
|
9937
|
+
this.detachListeners = null;
|
|
9938
|
+
}
|
|
9939
|
+
/**
|
|
9940
|
+
* Steps 1-3 of the public-call contract. Returns null when the caller must
|
|
9941
|
+
* stop, having already handled termination.
|
|
9942
|
+
*
|
|
9943
|
+
* The disposed check precedes validation deliberately: ordering it after
|
|
9944
|
+
* would make `disposed.animateTo(invalidView)` both required to throw and
|
|
9945
|
+
* required to stay silent. Disposal wins — a terminal animator is inert for
|
|
9946
|
+
* every input, and post-disposal calls are exactly the racy teardown paths
|
|
9947
|
+
* where a throw is least useful.
|
|
9948
|
+
*/
|
|
9949
|
+
validateAndMeasure(view) {
|
|
9950
|
+
if (this.disposed) return null;
|
|
9951
|
+
assertValidView(view);
|
|
9952
|
+
const size = this.getCanvasSize();
|
|
9953
|
+
assertCanvasDims(size.w, size.h);
|
|
9954
|
+
if (size.w === 0 || size.h === 0) {
|
|
9955
|
+
this.end("cancelled");
|
|
9956
|
+
return null;
|
|
9957
|
+
}
|
|
9958
|
+
return size;
|
|
9959
|
+
}
|
|
9960
|
+
step = () => {
|
|
9961
|
+
if (this.disposed || this.to === null || this.from === null) return;
|
|
9962
|
+
if (this.foreignWrite()) {
|
|
9963
|
+
this.end("cancelled");
|
|
9964
|
+
return;
|
|
9965
|
+
}
|
|
9966
|
+
const size = this.getCanvasSize();
|
|
9967
|
+
if (!canvasDimsUsable(size.w, size.h)) {
|
|
9968
|
+
this.end("cancelled");
|
|
9969
|
+
return;
|
|
9970
|
+
}
|
|
9971
|
+
if (size.w === 0 || size.h === 0) {
|
|
9972
|
+
this.end("cancelled");
|
|
9973
|
+
return;
|
|
9974
|
+
}
|
|
9975
|
+
const elapsed = this.now() - this.startedAt;
|
|
9976
|
+
const t = this.durationMs <= 0 ? 1 : Math.min(1, elapsed / this.durationMs);
|
|
9977
|
+
const view = lerpView(this.from, this.to, this.easing(t));
|
|
9978
|
+
applyCameraView(this.camera, view, size.w, size.h);
|
|
9979
|
+
this.recordWrite();
|
|
9980
|
+
if (t >= 1) {
|
|
9981
|
+
this.end("complete");
|
|
9982
|
+
return;
|
|
9983
|
+
}
|
|
9984
|
+
this.rafId = this.frames.requestFrame(this.step);
|
|
9985
|
+
};
|
|
9986
|
+
recordWrite() {
|
|
9987
|
+
this.lastWrite = {
|
|
9988
|
+
x: this.camera.position.x,
|
|
9989
|
+
y: this.camera.position.y,
|
|
9990
|
+
zoom: this.camera.zoom
|
|
9991
|
+
};
|
|
9992
|
+
}
|
|
9993
|
+
foreignWrite() {
|
|
9994
|
+
if (this.lastWrite === null) return false;
|
|
9995
|
+
const eps = 1e-6;
|
|
9996
|
+
return Math.abs(this.camera.position.x - this.lastWrite.x) > eps || Math.abs(this.camera.position.y - this.lastWrite.y) > eps || Math.abs(this.camera.zoom - this.lastWrite.zoom) > eps;
|
|
9997
|
+
}
|
|
9998
|
+
/** Terminates an in-flight animation with `reason`. No-op when idle. */
|
|
9999
|
+
end(reason) {
|
|
10000
|
+
if (this.disposed || this.to === null) return;
|
|
10001
|
+
this.clearFrame();
|
|
10002
|
+
this.from = null;
|
|
10003
|
+
this.to = null;
|
|
10004
|
+
this.lastWrite = null;
|
|
10005
|
+
this.emit(reason);
|
|
10006
|
+
}
|
|
10007
|
+
clearFrame() {
|
|
10008
|
+
if (this.rafId !== null) {
|
|
10009
|
+
this.frames.cancelFrame(this.rafId);
|
|
10010
|
+
this.rafId = null;
|
|
10011
|
+
}
|
|
10012
|
+
}
|
|
10013
|
+
emit(reason) {
|
|
10014
|
+
for (const listener of [...this.endListeners]) {
|
|
10015
|
+
try {
|
|
10016
|
+
listener(reason);
|
|
10017
|
+
} catch {
|
|
10018
|
+
}
|
|
10019
|
+
}
|
|
10020
|
+
}
|
|
10021
|
+
};
|
|
10022
|
+
|
|
10023
|
+
// src/canvas/focus-presence.ts
|
|
10024
|
+
var FOCUS_PRESENCE_KIND = "focus";
|
|
10025
|
+
var AUDIENCES = ["all", "players", "display"];
|
|
10026
|
+
function isPositiveFinite(value) {
|
|
10027
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
10028
|
+
}
|
|
10029
|
+
function isFiniteNumber2(value) {
|
|
10030
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
10031
|
+
}
|
|
10032
|
+
function isFocusPresence(data) {
|
|
10033
|
+
if (typeof data !== "object" || data === null) return false;
|
|
10034
|
+
const payload = data;
|
|
10035
|
+
if (payload.kind !== FOCUS_PRESENCE_KIND) return false;
|
|
10036
|
+
if (!isFiniteNumber2(payload.x) || !isFiniteNumber2(payload.y)) return false;
|
|
10037
|
+
if (!isPositiveFinite(payload.w) || !isPositiveFinite(payload.h)) return false;
|
|
10038
|
+
if (typeof payload.audience !== "string" || !AUDIENCES.some((a) => a === payload.audience)) {
|
|
10039
|
+
return false;
|
|
10040
|
+
}
|
|
10041
|
+
if (payload.color !== void 0 && typeof payload.color !== "string") return false;
|
|
10042
|
+
return true;
|
|
10043
|
+
}
|
|
10044
|
+
function toFocusPresence(view, audience, color) {
|
|
10045
|
+
return {
|
|
10046
|
+
kind: FOCUS_PRESENCE_KIND,
|
|
10047
|
+
x: view.x,
|
|
10048
|
+
y: view.y,
|
|
10049
|
+
w: view.w,
|
|
10050
|
+
h: view.h,
|
|
10051
|
+
audience,
|
|
10052
|
+
...color === void 0 ? {} : { color }
|
|
10053
|
+
};
|
|
10054
|
+
}
|
|
10055
|
+
|
|
10056
|
+
// src/canvas/remote-focus-receiver.ts
|
|
10057
|
+
function audienceIncludes(audience, role) {
|
|
10058
|
+
if (role === "dm") return false;
|
|
10059
|
+
if (audience === "all") return true;
|
|
10060
|
+
if (audience === "players") return role === "player";
|
|
10061
|
+
return role === "display";
|
|
10062
|
+
}
|
|
10063
|
+
var RemoteFocusReceiver = class {
|
|
10064
|
+
role;
|
|
10065
|
+
animator;
|
|
10066
|
+
animate;
|
|
10067
|
+
pulseColor;
|
|
10068
|
+
overlay;
|
|
10069
|
+
disposed = false;
|
|
10070
|
+
constructor(host, options) {
|
|
10071
|
+
this.role = options.role;
|
|
10072
|
+
this.animator = options.animator;
|
|
10073
|
+
this.animate = options.animate ?? true;
|
|
10074
|
+
this.pulseColor = options.pulseColor;
|
|
10075
|
+
this.overlay = options.pulse ?? true ? new RemotePingOverlay(host, {
|
|
10076
|
+
...options.pulseColor === void 0 ? {} : { color: options.pulseColor },
|
|
10077
|
+
...options.pulseDurationMs === void 0 ? {} : { durationMs: options.pulseDurationMs },
|
|
10078
|
+
...options.pulseRadius === void 0 ? {} : { radius: options.pulseRadius },
|
|
10079
|
+
maxPingsPerSender: 1
|
|
10080
|
+
}) : null;
|
|
10081
|
+
}
|
|
10082
|
+
/**
|
|
10083
|
+
* Applies a presence payload from `sender`. Returns `false` for payloads
|
|
10084
|
+
* that are not focus frames, or are addressed to a different role, so hosts
|
|
10085
|
+
* can feed every presence frame through without disturbing other handlers.
|
|
10086
|
+
*/
|
|
10087
|
+
apply(from, data) {
|
|
10088
|
+
if (this.disposed || !isFocusPresence(data)) return false;
|
|
10089
|
+
if (!audienceIncludes(data.audience, this.role)) return false;
|
|
10090
|
+
const view = { x: data.x, y: data.y, w: data.w, h: data.h };
|
|
10091
|
+
if (this.animate) {
|
|
10092
|
+
this.animator.animateTo(view);
|
|
10093
|
+
} else {
|
|
10094
|
+
this.animator.jumpTo(view);
|
|
10095
|
+
}
|
|
10096
|
+
this.overlay?.apply(from, {
|
|
10097
|
+
kind: "ping",
|
|
10098
|
+
x: view.x + view.w / 2,
|
|
10099
|
+
y: view.y + view.h / 2,
|
|
10100
|
+
color: data.color ?? this.pulseColor
|
|
10101
|
+
});
|
|
10102
|
+
return true;
|
|
10103
|
+
}
|
|
10104
|
+
/** Idempotent. Does NOT dispose the animator — the host owns that. */
|
|
10105
|
+
dispose() {
|
|
10106
|
+
if (this.disposed) return;
|
|
10107
|
+
this.disposed = true;
|
|
10108
|
+
this.overlay?.dispose();
|
|
10109
|
+
}
|
|
10110
|
+
};
|
|
10111
|
+
|
|
9592
10112
|
// src/tools/hand-tool.ts
|
|
9593
10113
|
var HandTool = class {
|
|
9594
10114
|
name = "hand";
|
|
@@ -9777,7 +10297,7 @@ function hitTestStroke(stroke, point, radius) {
|
|
|
9777
10297
|
}
|
|
9778
10298
|
|
|
9779
10299
|
// src/elements/stroke-erase.ts
|
|
9780
|
-
function
|
|
10300
|
+
function lerp2(a, b, t) {
|
|
9781
10301
|
return {
|
|
9782
10302
|
x: a.x + (b.x - a.x) * t,
|
|
9783
10303
|
y: a.y + (b.y - a.y) * t,
|
|
@@ -9836,13 +10356,13 @@ function erasePoints(points, eraser, radius) {
|
|
|
9836
10356
|
erased = true;
|
|
9837
10357
|
if (tLo > 0) {
|
|
9838
10358
|
if (current.length === 0) current.push(a);
|
|
9839
|
-
current.push(
|
|
10359
|
+
current.push(lerp2(a, b, tLo));
|
|
9840
10360
|
flush();
|
|
9841
10361
|
} else {
|
|
9842
10362
|
flush();
|
|
9843
10363
|
}
|
|
9844
10364
|
if (tHi < 1) {
|
|
9845
|
-
current = [
|
|
10365
|
+
current = [lerp2(a, b, tHi), b];
|
|
9846
10366
|
}
|
|
9847
10367
|
}
|
|
9848
10368
|
flush();
|
|
@@ -12298,7 +12818,7 @@ var LaserTool = class {
|
|
|
12298
12818
|
|
|
12299
12819
|
// src/tools/ping-tool.ts
|
|
12300
12820
|
var DEFAULT_COLOR6 = "#ff3b30";
|
|
12301
|
-
var
|
|
12821
|
+
var DEFAULT_DURATION_MS4 = 1800;
|
|
12302
12822
|
var DEFAULT_RADIUS4 = 48;
|
|
12303
12823
|
var DEFAULT_MIN_INTERVAL_MS2 = 300;
|
|
12304
12824
|
var PingTool = class {
|
|
@@ -12315,7 +12835,7 @@ var PingTool = class {
|
|
|
12315
12835
|
constructor(options = {}) {
|
|
12316
12836
|
this.name = options.name ?? "ping";
|
|
12317
12837
|
this.color = options.color ?? DEFAULT_COLOR6;
|
|
12318
|
-
this.durationMs = options.durationMs ??
|
|
12838
|
+
this.durationMs = options.durationMs ?? DEFAULT_DURATION_MS4;
|
|
12319
12839
|
this.radius = options.radius ?? DEFAULT_RADIUS4;
|
|
12320
12840
|
this.minIntervalMs = options.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS2;
|
|
12321
12841
|
}
|
|
@@ -12417,15 +12937,17 @@ var PingTool = class {
|
|
|
12417
12937
|
};
|
|
12418
12938
|
|
|
12419
12939
|
// src/index.ts
|
|
12420
|
-
var VERSION = "0.
|
|
12940
|
+
var VERSION = "0.61.0";
|
|
12421
12941
|
// Annotate the CommonJS export names for ESM import in node:
|
|
12422
12942
|
0 && (module.exports = {
|
|
12423
12943
|
ArrowTool,
|
|
12424
12944
|
AutoSave,
|
|
12425
12945
|
Camera,
|
|
12946
|
+
CameraAnimator,
|
|
12426
12947
|
DEFAULT_NOTE_FONT_SIZE,
|
|
12427
12948
|
ElementStore,
|
|
12428
12949
|
EraserTool,
|
|
12950
|
+
FOCUS_PRESENCE_KIND,
|
|
12429
12951
|
HandTool,
|
|
12430
12952
|
HistoryStack,
|
|
12431
12953
|
ImageTool,
|
|
@@ -12443,6 +12965,7 @@ var VERSION = "0.59.0";
|
|
|
12443
12965
|
PencilTool,
|
|
12444
12966
|
PingInput,
|
|
12445
12967
|
PingTool,
|
|
12968
|
+
RemoteFocusReceiver,
|
|
12446
12969
|
RemoteLaserOverlay,
|
|
12447
12970
|
RemoteMeasureOverlay,
|
|
12448
12971
|
RemotePingOverlay,
|
|
@@ -12453,7 +12976,10 @@ var VERSION = "0.59.0";
|
|
|
12453
12976
|
ToolManager,
|
|
12454
12977
|
VERSION,
|
|
12455
12978
|
Viewport,
|
|
12979
|
+
applyCameraView,
|
|
12456
12980
|
boundsIntersect,
|
|
12981
|
+
cameraOriginForView,
|
|
12982
|
+
captureCameraView,
|
|
12457
12983
|
createArrow,
|
|
12458
12984
|
createGrid,
|
|
12459
12985
|
createHtmlElement,
|
|
@@ -12466,6 +12992,7 @@ var VERSION = "0.59.0";
|
|
|
12466
12992
|
drawHexPath,
|
|
12467
12993
|
exportImage,
|
|
12468
12994
|
exportSvg,
|
|
12995
|
+
fitZoomForView,
|
|
12469
12996
|
getActiveFormats,
|
|
12470
12997
|
getArrowBounds,
|
|
12471
12998
|
getArrowControlPoint,
|
|
@@ -12481,6 +13008,7 @@ var VERSION = "0.59.0";
|
|
|
12481
13008
|
getHexCellsInRectangle,
|
|
12482
13009
|
getHexCellsInSquare,
|
|
12483
13010
|
getHexDistance,
|
|
13011
|
+
isFocusPresence,
|
|
12484
13012
|
isLaserTrailPresence,
|
|
12485
13013
|
isMeasurePresence,
|
|
12486
13014
|
isNearBezier,
|
|
@@ -12490,6 +13018,7 @@ var VERSION = "0.59.0";
|
|
|
12490
13018
|
snapPoint,
|
|
12491
13019
|
snapToHexCenter,
|
|
12492
13020
|
styleToPatch,
|
|
13021
|
+
toFocusPresence,
|
|
12493
13022
|
toLaserTrailPresence,
|
|
12494
13023
|
toMeasurePresence,
|
|
12495
13024
|
toPingPresence,
|