@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.js
CHANGED
|
@@ -5170,6 +5170,7 @@ var ToolManager = class {
|
|
|
5170
5170
|
tools = /* @__PURE__ */ new Map();
|
|
5171
5171
|
current = null;
|
|
5172
5172
|
changeListeners = /* @__PURE__ */ new Set();
|
|
5173
|
+
registerListeners = /* @__PURE__ */ new Set();
|
|
5173
5174
|
get activeTool() {
|
|
5174
5175
|
return this.current;
|
|
5175
5176
|
}
|
|
@@ -5178,6 +5179,7 @@ var ToolManager = class {
|
|
|
5178
5179
|
}
|
|
5179
5180
|
register(tool) {
|
|
5180
5181
|
this.tools.set(tool.name, tool);
|
|
5182
|
+
this.registerListeners.forEach((fn) => fn(tool));
|
|
5181
5183
|
}
|
|
5182
5184
|
getTool(name) {
|
|
5183
5185
|
return this.tools.get(name);
|
|
@@ -5203,6 +5205,10 @@ var ToolManager = class {
|
|
|
5203
5205
|
this.changeListeners.add(listener);
|
|
5204
5206
|
return () => this.changeListeners.delete(listener);
|
|
5205
5207
|
}
|
|
5208
|
+
onRegister(listener) {
|
|
5209
|
+
this.registerListeners.add(listener);
|
|
5210
|
+
return () => this.registerListeners.delete(listener);
|
|
5211
|
+
}
|
|
5206
5212
|
};
|
|
5207
5213
|
|
|
5208
5214
|
// src/history/history-stack.ts
|
|
@@ -5394,6 +5400,7 @@ var HistoryRecorder = class {
|
|
|
5394
5400
|
generation = 0;
|
|
5395
5401
|
updateSnapshots = /* @__PURE__ */ new Map();
|
|
5396
5402
|
unsubscribers;
|
|
5403
|
+
transactionEndListeners = /* @__PURE__ */ new Set();
|
|
5397
5404
|
pause() {
|
|
5398
5405
|
this.recording = false;
|
|
5399
5406
|
}
|
|
@@ -5411,19 +5418,35 @@ var HistoryRecorder = class {
|
|
|
5411
5418
|
get currentTransactionId() {
|
|
5412
5419
|
return this.transaction !== null ? this.generation : null;
|
|
5413
5420
|
}
|
|
5421
|
+
onTransactionEnd(listener) {
|
|
5422
|
+
this.transactionEndListeners.add(listener);
|
|
5423
|
+
return () => this.transactionEndListeners.delete(listener);
|
|
5424
|
+
}
|
|
5425
|
+
notifyTransactionEnd() {
|
|
5426
|
+
for (const listener of this.transactionEndListeners) {
|
|
5427
|
+
try {
|
|
5428
|
+
listener();
|
|
5429
|
+
} catch {
|
|
5430
|
+
}
|
|
5431
|
+
}
|
|
5432
|
+
}
|
|
5414
5433
|
commit() {
|
|
5415
5434
|
if (!this.transaction) return;
|
|
5416
5435
|
const finalCommands = this.flushUpdateSnapshots();
|
|
5417
5436
|
const all = [...this.transaction, ...finalCommands];
|
|
5418
5437
|
this.transaction = null;
|
|
5419
5438
|
this.updateSnapshots.clear();
|
|
5420
|
-
if (all.length
|
|
5421
|
-
|
|
5422
|
-
|
|
5439
|
+
if (all.length > 0) {
|
|
5440
|
+
const first = all[0];
|
|
5441
|
+
this.stack.push(all.length === 1 && first ? first : new BatchCommand(all));
|
|
5442
|
+
}
|
|
5443
|
+
this.notifyTransactionEnd();
|
|
5423
5444
|
}
|
|
5424
5445
|
rollback() {
|
|
5446
|
+
const wasOpen = this.transaction !== null;
|
|
5425
5447
|
this.transaction = null;
|
|
5426
5448
|
this.updateSnapshots.clear();
|
|
5449
|
+
if (wasOpen) this.notifyTransactionEnd();
|
|
5427
5450
|
}
|
|
5428
5451
|
destroy() {
|
|
5429
5452
|
this.unsubscribers.forEach((fn) => fn());
|
|
@@ -7715,6 +7738,14 @@ function rotateElementPatch(el, bounds, pivot, delta) {
|
|
|
7715
7738
|
}
|
|
7716
7739
|
|
|
7717
7740
|
// src/canvas/selection-ops.ts
|
|
7741
|
+
var STYLE_FIELDS = [
|
|
7742
|
+
"color",
|
|
7743
|
+
"fillColor",
|
|
7744
|
+
"strokeWidth",
|
|
7745
|
+
"opacity",
|
|
7746
|
+
"fontSize",
|
|
7747
|
+
"strokeStyle"
|
|
7748
|
+
];
|
|
7718
7749
|
function sharedValue(values) {
|
|
7719
7750
|
const present = values.filter((v) => v !== void 0);
|
|
7720
7751
|
if (present.length === 0) return void 0;
|
|
@@ -7749,6 +7780,42 @@ var SelectionOps = class {
|
|
|
7749
7780
|
if (strokeStyle !== void 0) result.strokeStyle = strokeStyle;
|
|
7750
7781
|
return result;
|
|
7751
7782
|
}
|
|
7783
|
+
/**
|
|
7784
|
+
* Unlike `getStyle()` — which returns `{}` for a selection whose elements
|
|
7785
|
+
* have no applicable style fields (e.g. images) — this returns `null` when
|
|
7786
|
+
* no field is applicable, so callers can distinguish "nothing to show" from
|
|
7787
|
+
* "everything shared but empty".
|
|
7788
|
+
*/
|
|
7789
|
+
getStyleDetails() {
|
|
7790
|
+
const ids = this.deps.getSelectedIds();
|
|
7791
|
+
if (ids.length === 0) return null;
|
|
7792
|
+
const styles = [];
|
|
7793
|
+
for (const id of ids) {
|
|
7794
|
+
const el = this.deps.store.getById(id);
|
|
7795
|
+
if (el) styles.push(getElementStyle(el));
|
|
7796
|
+
}
|
|
7797
|
+
if (styles.length === 0) return null;
|
|
7798
|
+
const common = {};
|
|
7799
|
+
const applicable = [];
|
|
7800
|
+
const mixed = [];
|
|
7801
|
+
for (const field of STYLE_FIELDS) {
|
|
7802
|
+
const allValues = styles.map((s) => s[field]);
|
|
7803
|
+
const values = [];
|
|
7804
|
+
for (const v of allValues) {
|
|
7805
|
+
if (v !== void 0) values.push(v);
|
|
7806
|
+
}
|
|
7807
|
+
if (values.length === 0) continue;
|
|
7808
|
+
applicable.push(field);
|
|
7809
|
+
const distinct = new Set(values);
|
|
7810
|
+
if (distinct.size > 1) {
|
|
7811
|
+
mixed.push(field);
|
|
7812
|
+
} else {
|
|
7813
|
+
common[field] = values[0];
|
|
7814
|
+
}
|
|
7815
|
+
}
|
|
7816
|
+
if (applicable.length === 0) return null;
|
|
7817
|
+
return { common, applicable, mixed };
|
|
7818
|
+
}
|
|
7752
7819
|
applyStyle(style) {
|
|
7753
7820
|
const ids = this.deps.getSelectedIds();
|
|
7754
7821
|
if (ids.length === 0) return;
|
|
@@ -8123,7 +8190,7 @@ var ViewportInteractions = class {
|
|
|
8123
8190
|
var EMPTY_IDS = [];
|
|
8124
8191
|
function noop() {
|
|
8125
8192
|
}
|
|
8126
|
-
var Viewport = class {
|
|
8193
|
+
var Viewport = class _Viewport {
|
|
8127
8194
|
constructor(container, options = {}) {
|
|
8128
8195
|
this.container = container;
|
|
8129
8196
|
this.camera = new Camera(options.camera);
|
|
@@ -8132,6 +8199,13 @@ var Viewport = class {
|
|
|
8132
8199
|
this.store = new ElementStore();
|
|
8133
8200
|
this.layerManager = new LayerManager(this.store);
|
|
8134
8201
|
this.toolManager = new ToolManager();
|
|
8202
|
+
this.unsubToolRegister = this.toolManager.onRegister((tool) => {
|
|
8203
|
+
if (_Viewport.isSelectionSource(tool)) this.attachSelectionSource(tool);
|
|
8204
|
+
});
|
|
8205
|
+
const existingSelect = this.getSelectTool();
|
|
8206
|
+
if (existingSelect && _Viewport.isSelectionSource(existingSelect)) {
|
|
8207
|
+
this.attachSelectionSource(existingSelect);
|
|
8208
|
+
}
|
|
8135
8209
|
this.renderer = new ElementRenderer();
|
|
8136
8210
|
this.renderer.setStore(this.store);
|
|
8137
8211
|
this.renderer.setCamera(this.camera);
|
|
@@ -8166,6 +8240,11 @@ var Viewport = class {
|
|
|
8166
8240
|
this.dropHandler = options.onDrop;
|
|
8167
8241
|
this.history = new HistoryStack();
|
|
8168
8242
|
this.historyRecorder = new HistoryRecorder(this.store, this.history, this.layerManager);
|
|
8243
|
+
this.unsubRecorderEnd = this.historyRecorder.onTransactionEnd(() => {
|
|
8244
|
+
if (!this.pendingSelectionPrune) return;
|
|
8245
|
+
this.pendingSelectionPrune = false;
|
|
8246
|
+
this.pruneSelection();
|
|
8247
|
+
});
|
|
8169
8248
|
this.selectionOps = new SelectionOps({
|
|
8170
8249
|
store: this.store,
|
|
8171
8250
|
recorder: this.historyRecorder,
|
|
@@ -8285,6 +8364,7 @@ var Viewport = class {
|
|
|
8285
8364
|
this.domNodeManager.removeDomNode(el.id);
|
|
8286
8365
|
this.renderLoop.markLayerDirty(el.layerId);
|
|
8287
8366
|
this.requestRender();
|
|
8367
|
+
this.handleRemovedElement(el.id);
|
|
8288
8368
|
}),
|
|
8289
8369
|
this.store.on("update", ({ previous, current }) => {
|
|
8290
8370
|
if (current.type === "grid") this.gridController.syncContext();
|
|
@@ -8299,6 +8379,7 @@ var Viewport = class {
|
|
|
8299
8379
|
this.renderLoop.markAllLayersDirty();
|
|
8300
8380
|
this.gridController.syncContext();
|
|
8301
8381
|
this.requestRender();
|
|
8382
|
+
this.pruneSelection();
|
|
8302
8383
|
})
|
|
8303
8384
|
];
|
|
8304
8385
|
this.layerManager.on("change", () => {
|
|
@@ -8368,6 +8449,13 @@ var Viewport = class {
|
|
|
8368
8449
|
minimap = null;
|
|
8369
8450
|
htmlRenderers = /* @__PURE__ */ new Map();
|
|
8370
8451
|
resizeListeners = /* @__PURE__ */ new Set();
|
|
8452
|
+
selectionListeners = /* @__PURE__ */ new Set();
|
|
8453
|
+
detachSelectionSource = null;
|
|
8454
|
+
unsubToolRegister = () => {
|
|
8455
|
+
};
|
|
8456
|
+
pendingSelectionPrune = false;
|
|
8457
|
+
unsubRecorderEnd = () => {
|
|
8458
|
+
};
|
|
8371
8459
|
get ctx() {
|
|
8372
8460
|
return this.canvasEl.getContext("2d");
|
|
8373
8461
|
}
|
|
@@ -8396,6 +8484,17 @@ var Viewport = class {
|
|
|
8396
8484
|
getVisibleRect() {
|
|
8397
8485
|
return this.camera.getVisibleRect(this.canvasEl.clientWidth, this.canvasEl.clientHeight);
|
|
8398
8486
|
}
|
|
8487
|
+
/**
|
|
8488
|
+
* Size in CSS pixels of the canvas that `getVisibleRect()` measures.
|
|
8489
|
+
* Exposed because `canvasEl` is private: consumers can only reach the
|
|
8490
|
+
* wrapper (via `domLayer.parentElement`), so without this accessor the
|
|
8491
|
+
* canonical size behind `getVisibleRect()` is unreachable and callers
|
|
8492
|
+
* resort to `getVisibleRect().w * camera.zoom`. Capture and restore must
|
|
8493
|
+
* measure the same element or saved views do not round-trip.
|
|
8494
|
+
*/
|
|
8495
|
+
getCanvasSize() {
|
|
8496
|
+
return { w: this.canvasEl.clientWidth, h: this.canvasEl.clientHeight };
|
|
8497
|
+
}
|
|
8399
8498
|
/** Centers the camera on a world point without changing zoom. */
|
|
8400
8499
|
centerCameraAt(world) {
|
|
8401
8500
|
const z = this.camera.zoom;
|
|
@@ -8626,6 +8725,42 @@ var Viewport = class {
|
|
|
8626
8725
|
getSelectTool() {
|
|
8627
8726
|
return this.toolManager.getTool("select");
|
|
8628
8727
|
}
|
|
8728
|
+
pruneSelection() {
|
|
8729
|
+
const tool = this.getSelectTool();
|
|
8730
|
+
if (!tool) return;
|
|
8731
|
+
const ids = tool.selectedIds;
|
|
8732
|
+
const filtered = ids.filter((id) => this.store.getById(id) !== void 0);
|
|
8733
|
+
if (filtered.length !== ids.length) tool.setSelection(filtered);
|
|
8734
|
+
}
|
|
8735
|
+
handleRemovedElement(id) {
|
|
8736
|
+
if (!this.getSelectedIds().includes(id)) return;
|
|
8737
|
+
if (this.historyRecorder.currentTransactionId !== null) {
|
|
8738
|
+
this.pendingSelectionPrune = true;
|
|
8739
|
+
return;
|
|
8740
|
+
}
|
|
8741
|
+
this.pruneSelection();
|
|
8742
|
+
}
|
|
8743
|
+
static isSelectionSource(tool) {
|
|
8744
|
+
const candidate = tool;
|
|
8745
|
+
return tool.name === "select" && typeof candidate.onSelectionChange === "function" && typeof candidate.setSelection === "function";
|
|
8746
|
+
}
|
|
8747
|
+
emitSelectionChange() {
|
|
8748
|
+
for (const listener of this.selectionListeners) {
|
|
8749
|
+
try {
|
|
8750
|
+
listener();
|
|
8751
|
+
} catch {
|
|
8752
|
+
}
|
|
8753
|
+
}
|
|
8754
|
+
}
|
|
8755
|
+
attachSelectionSource(tool) {
|
|
8756
|
+
this.detachSelectionSource?.();
|
|
8757
|
+
this.detachSelectionSource = tool.onSelectionChange(() => this.emitSelectionChange());
|
|
8758
|
+
}
|
|
8759
|
+
/**
|
|
8760
|
+
* getSelectedIds() and the onSelectionChange emitter never surface stale ids:
|
|
8761
|
+
* once the enclosing history transaction completes, both reflect
|
|
8762
|
+
* the current selection.
|
|
8763
|
+
*/
|
|
8629
8764
|
getSelectedIds() {
|
|
8630
8765
|
return this.getSelectTool()?.selectedIds ?? EMPTY_IDS;
|
|
8631
8766
|
}
|
|
@@ -8659,13 +8794,29 @@ var Viewport = class {
|
|
|
8659
8794
|
if (items.length === 0) return;
|
|
8660
8795
|
this.contextMenu.open(items, screenPos);
|
|
8661
8796
|
}
|
|
8797
|
+
/**
|
|
8798
|
+
* Persistent, viewport-owned selection-change emitter. Subscribing works
|
|
8799
|
+
* regardless of whether a select tool is registered yet; it forwards
|
|
8800
|
+
* events from whichever select tool is currently attached via
|
|
8801
|
+
* `toolManager.onRegister`. Never delivers stale ids once the enclosing
|
|
8802
|
+
* history transaction completes.
|
|
8803
|
+
*/
|
|
8662
8804
|
onSelectionChange(listener) {
|
|
8663
|
-
|
|
8664
|
-
return
|
|
8805
|
+
this.selectionListeners.add(listener);
|
|
8806
|
+
return () => {
|
|
8807
|
+
this.selectionListeners.delete(listener);
|
|
8808
|
+
};
|
|
8665
8809
|
}
|
|
8666
8810
|
getSelectionStyle() {
|
|
8667
8811
|
return this.selectionOps.getStyle();
|
|
8668
8812
|
}
|
|
8813
|
+
/**
|
|
8814
|
+
* Unlike `getSelectionStyle()` — which returns `{}` for a style-less
|
|
8815
|
+
* selection — this returns `null` when no style field is applicable.
|
|
8816
|
+
*/
|
|
8817
|
+
getSelectionStyleDetails() {
|
|
8818
|
+
return this.selectionOps.getStyleDetails();
|
|
8819
|
+
}
|
|
8669
8820
|
applyStyleToSelection(style) {
|
|
8670
8821
|
this.selectionOps.applyStyle(style);
|
|
8671
8822
|
}
|
|
@@ -8714,6 +8865,11 @@ var Viewport = class {
|
|
|
8714
8865
|
this.inputHandler.destroy();
|
|
8715
8866
|
this.unsubCamera();
|
|
8716
8867
|
this.unsubToolChange();
|
|
8868
|
+
this.unsubToolRegister();
|
|
8869
|
+
this.unsubRecorderEnd();
|
|
8870
|
+
this.detachSelectionSource?.();
|
|
8871
|
+
this.detachSelectionSource = null;
|
|
8872
|
+
this.selectionListeners.clear();
|
|
8717
8873
|
this.unsubStore.forEach((fn) => fn());
|
|
8718
8874
|
this.resizeObserver?.disconnect();
|
|
8719
8875
|
this.resizeObserver = null;
|
|
@@ -9487,6 +9643,361 @@ var PingInput = class {
|
|
|
9487
9643
|
}
|
|
9488
9644
|
};
|
|
9489
9645
|
|
|
9646
|
+
// src/canvas/camera-view.ts
|
|
9647
|
+
function assertValidView(view) {
|
|
9648
|
+
const { x, y, w, h } = view;
|
|
9649
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(w) || !Number.isFinite(h)) {
|
|
9650
|
+
throw new Error("[fieldnotes] CameraView requires finite x, y, w, and h");
|
|
9651
|
+
}
|
|
9652
|
+
if (w <= 0 || h <= 0) {
|
|
9653
|
+
throw new Error("[fieldnotes] CameraView requires positive w and h");
|
|
9654
|
+
}
|
|
9655
|
+
}
|
|
9656
|
+
function assertCanvasDims(canvasW, canvasH) {
|
|
9657
|
+
if (!Number.isFinite(canvasW) || !Number.isFinite(canvasH)) {
|
|
9658
|
+
throw new Error("[fieldnotes] canvas dimensions must be finite");
|
|
9659
|
+
}
|
|
9660
|
+
if (canvasW < 0 || canvasH < 0) {
|
|
9661
|
+
throw new Error("[fieldnotes] canvas dimensions must not be negative");
|
|
9662
|
+
}
|
|
9663
|
+
}
|
|
9664
|
+
function canvasDimsUsable(canvasW, canvasH) {
|
|
9665
|
+
return Number.isFinite(canvasW) && Number.isFinite(canvasH) && canvasW >= 0 && canvasH >= 0;
|
|
9666
|
+
}
|
|
9667
|
+
function captureCameraView(viewport) {
|
|
9668
|
+
const rect = viewport.getVisibleRect();
|
|
9669
|
+
return { x: rect.x, y: rect.y, w: rect.w, h: rect.h };
|
|
9670
|
+
}
|
|
9671
|
+
function fitZoomForView(view, canvasW, canvasH) {
|
|
9672
|
+
assertValidView(view);
|
|
9673
|
+
assertCanvasDims(canvasW, canvasH);
|
|
9674
|
+
if (canvasW === 0 || canvasH === 0) {
|
|
9675
|
+
throw new Error("[fieldnotes] fitZoomForView requires a non-zero canvas size");
|
|
9676
|
+
}
|
|
9677
|
+
return Math.min(canvasW / view.w, canvasH / view.h);
|
|
9678
|
+
}
|
|
9679
|
+
function cameraOriginForView(view, zoom, canvasW, canvasH) {
|
|
9680
|
+
return {
|
|
9681
|
+
x: canvasW / 2 - (view.x + view.w / 2) * zoom,
|
|
9682
|
+
y: canvasH / 2 - (view.y + view.h / 2) * zoom
|
|
9683
|
+
};
|
|
9684
|
+
}
|
|
9685
|
+
function applyCameraView(camera, view, canvasW, canvasH) {
|
|
9686
|
+
assertValidView(view);
|
|
9687
|
+
assertCanvasDims(canvasW, canvasH);
|
|
9688
|
+
if (canvasW === 0 || canvasH === 0) return;
|
|
9689
|
+
camera.setZoom(fitZoomForView(view, canvasW, canvasH));
|
|
9690
|
+
const origin = cameraOriginForView(view, camera.zoom, canvasW, canvasH);
|
|
9691
|
+
camera.moveTo(origin.x, origin.y);
|
|
9692
|
+
}
|
|
9693
|
+
|
|
9694
|
+
// src/canvas/camera-animator.ts
|
|
9695
|
+
var DEFAULT_DURATION_MS3 = 400;
|
|
9696
|
+
var FRAMED_EPSILON = 1e-6;
|
|
9697
|
+
var easeOutCubic2 = (t) => 1 - Math.pow(1 - t, 3);
|
|
9698
|
+
function lerp(a, b, k) {
|
|
9699
|
+
return a + (b - a) * k;
|
|
9700
|
+
}
|
|
9701
|
+
function lerpView(from, to, k) {
|
|
9702
|
+
return {
|
|
9703
|
+
x: lerp(from.x, to.x, k),
|
|
9704
|
+
y: lerp(from.y, to.y, k),
|
|
9705
|
+
w: lerp(from.w, to.w, k),
|
|
9706
|
+
h: lerp(from.h, to.h, k)
|
|
9707
|
+
};
|
|
9708
|
+
}
|
|
9709
|
+
function viewsClose(a, b) {
|
|
9710
|
+
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;
|
|
9711
|
+
}
|
|
9712
|
+
var CameraAnimator = class {
|
|
9713
|
+
camera;
|
|
9714
|
+
getCanvasSize;
|
|
9715
|
+
frames;
|
|
9716
|
+
now;
|
|
9717
|
+
durationMs;
|
|
9718
|
+
easing;
|
|
9719
|
+
rafId = null;
|
|
9720
|
+
from = null;
|
|
9721
|
+
to = null;
|
|
9722
|
+
startedAt = 0;
|
|
9723
|
+
endListeners = /* @__PURE__ */ new Set();
|
|
9724
|
+
/**
|
|
9725
|
+
* Monotonic operation counter. `animateTo`/`jumpTo` claim a generation
|
|
9726
|
+
* before emitting 'superseded'; if an onEnd listener starts a newer
|
|
9727
|
+
* operation during that emit, the outer call sees a bumped counter and
|
|
9728
|
+
* bails instead of overwriting the nested animation's state. Without this,
|
|
9729
|
+
* the nested animation would run to completion having never reported an end
|
|
9730
|
+
* reason, breaking the exactly-one guarantee the spec makes.
|
|
9731
|
+
*/
|
|
9732
|
+
generation = 0;
|
|
9733
|
+
lastWrite = null;
|
|
9734
|
+
disposed = false;
|
|
9735
|
+
detachListeners = null;
|
|
9736
|
+
constructor(element, camera, options) {
|
|
9737
|
+
if (options.frames !== void 0) {
|
|
9738
|
+
const { requestFrame, cancelFrame } = options.frames;
|
|
9739
|
+
if (typeof requestFrame !== "function" || typeof cancelFrame !== "function") {
|
|
9740
|
+
throw new Error(
|
|
9741
|
+
"[fieldnotes] CameraAnimator: `frames` must supply both requestFrame and cancelFrame"
|
|
9742
|
+
);
|
|
9743
|
+
}
|
|
9744
|
+
}
|
|
9745
|
+
this.camera = camera;
|
|
9746
|
+
this.getCanvasSize = options.getCanvasSize;
|
|
9747
|
+
this.frames = options.frames ?? {
|
|
9748
|
+
requestFrame: (cb) => requestAnimationFrame(cb),
|
|
9749
|
+
cancelFrame: (id) => cancelAnimationFrame(id)
|
|
9750
|
+
};
|
|
9751
|
+
this.now = options.now ?? (() => performance.now());
|
|
9752
|
+
this.durationMs = options.durationMs ?? DEFAULT_DURATION_MS3;
|
|
9753
|
+
this.easing = options.easing ?? easeOutCubic2;
|
|
9754
|
+
if (options.interactive ?? true) {
|
|
9755
|
+
const onUserInput = () => {
|
|
9756
|
+
this.end("cancelled");
|
|
9757
|
+
};
|
|
9758
|
+
const types = ["pointerdown", "wheel", "keydown"];
|
|
9759
|
+
for (const type of types) {
|
|
9760
|
+
element.addEventListener(type, onUserInput, { passive: true });
|
|
9761
|
+
}
|
|
9762
|
+
this.detachListeners = () => {
|
|
9763
|
+
for (const type of types) {
|
|
9764
|
+
element.removeEventListener(type, onUserInput);
|
|
9765
|
+
}
|
|
9766
|
+
};
|
|
9767
|
+
}
|
|
9768
|
+
}
|
|
9769
|
+
get animating() {
|
|
9770
|
+
return this.to !== null;
|
|
9771
|
+
}
|
|
9772
|
+
onEnd(listener) {
|
|
9773
|
+
this.endListeners.add(listener);
|
|
9774
|
+
return () => this.endListeners.delete(listener);
|
|
9775
|
+
}
|
|
9776
|
+
animateTo(view) {
|
|
9777
|
+
const size = this.validateAndMeasure(view);
|
|
9778
|
+
if (size === null) return;
|
|
9779
|
+
const current = this.camera.getVisibleRect(size.w, size.h);
|
|
9780
|
+
const generation = ++this.generation;
|
|
9781
|
+
this.end("superseded");
|
|
9782
|
+
if (this.generation !== generation) return;
|
|
9783
|
+
if (viewsClose(current, view)) {
|
|
9784
|
+
applyCameraView(this.camera, view, size.w, size.h);
|
|
9785
|
+
this.emit("complete");
|
|
9786
|
+
return;
|
|
9787
|
+
}
|
|
9788
|
+
this.from = current;
|
|
9789
|
+
this.to = view;
|
|
9790
|
+
this.startedAt = this.now();
|
|
9791
|
+
this.lastWrite = null;
|
|
9792
|
+
this.rafId = this.frames.requestFrame(this.step);
|
|
9793
|
+
}
|
|
9794
|
+
jumpTo(view) {
|
|
9795
|
+
const size = this.validateAndMeasure(view);
|
|
9796
|
+
if (size === null) return;
|
|
9797
|
+
const generation = ++this.generation;
|
|
9798
|
+
this.end("superseded");
|
|
9799
|
+
if (this.generation !== generation) return;
|
|
9800
|
+
applyCameraView(this.camera, view, size.w, size.h);
|
|
9801
|
+
this.lastWrite = null;
|
|
9802
|
+
}
|
|
9803
|
+
cancel() {
|
|
9804
|
+
if (this.disposed) return;
|
|
9805
|
+
this.end("cancelled");
|
|
9806
|
+
}
|
|
9807
|
+
/**
|
|
9808
|
+
* Terminal. Order is load-bearing: the flag is set BEFORE any listener runs,
|
|
9809
|
+
* because an onEnd listener can call animateTo during the disposal callback.
|
|
9810
|
+
* With the flag set last, that call would start a real animation which the
|
|
9811
|
+
* listener clear then silently discards — a second animation with no end
|
|
9812
|
+
* reason, breaking the exactly-one guarantee.
|
|
9813
|
+
*/
|
|
9814
|
+
dispose() {
|
|
9815
|
+
if (this.disposed) return;
|
|
9816
|
+
this.disposed = true;
|
|
9817
|
+
this.generation++;
|
|
9818
|
+
const wasAnimating = this.to !== null;
|
|
9819
|
+
this.clearFrame();
|
|
9820
|
+
this.from = null;
|
|
9821
|
+
this.to = null;
|
|
9822
|
+
this.lastWrite = null;
|
|
9823
|
+
if (wasAnimating) this.emit("cancelled");
|
|
9824
|
+
this.endListeners.clear();
|
|
9825
|
+
this.detachListeners?.();
|
|
9826
|
+
this.detachListeners = null;
|
|
9827
|
+
}
|
|
9828
|
+
/**
|
|
9829
|
+
* Steps 1-3 of the public-call contract. Returns null when the caller must
|
|
9830
|
+
* stop, having already handled termination.
|
|
9831
|
+
*
|
|
9832
|
+
* The disposed check precedes validation deliberately: ordering it after
|
|
9833
|
+
* would make `disposed.animateTo(invalidView)` both required to throw and
|
|
9834
|
+
* required to stay silent. Disposal wins — a terminal animator is inert for
|
|
9835
|
+
* every input, and post-disposal calls are exactly the racy teardown paths
|
|
9836
|
+
* where a throw is least useful.
|
|
9837
|
+
*/
|
|
9838
|
+
validateAndMeasure(view) {
|
|
9839
|
+
if (this.disposed) return null;
|
|
9840
|
+
assertValidView(view);
|
|
9841
|
+
const size = this.getCanvasSize();
|
|
9842
|
+
assertCanvasDims(size.w, size.h);
|
|
9843
|
+
if (size.w === 0 || size.h === 0) {
|
|
9844
|
+
this.end("cancelled");
|
|
9845
|
+
return null;
|
|
9846
|
+
}
|
|
9847
|
+
return size;
|
|
9848
|
+
}
|
|
9849
|
+
step = () => {
|
|
9850
|
+
if (this.disposed || this.to === null || this.from === null) return;
|
|
9851
|
+
if (this.foreignWrite()) {
|
|
9852
|
+
this.end("cancelled");
|
|
9853
|
+
return;
|
|
9854
|
+
}
|
|
9855
|
+
const size = this.getCanvasSize();
|
|
9856
|
+
if (!canvasDimsUsable(size.w, size.h)) {
|
|
9857
|
+
this.end("cancelled");
|
|
9858
|
+
return;
|
|
9859
|
+
}
|
|
9860
|
+
if (size.w === 0 || size.h === 0) {
|
|
9861
|
+
this.end("cancelled");
|
|
9862
|
+
return;
|
|
9863
|
+
}
|
|
9864
|
+
const elapsed = this.now() - this.startedAt;
|
|
9865
|
+
const t = this.durationMs <= 0 ? 1 : Math.min(1, elapsed / this.durationMs);
|
|
9866
|
+
const view = lerpView(this.from, this.to, this.easing(t));
|
|
9867
|
+
applyCameraView(this.camera, view, size.w, size.h);
|
|
9868
|
+
this.recordWrite();
|
|
9869
|
+
if (t >= 1) {
|
|
9870
|
+
this.end("complete");
|
|
9871
|
+
return;
|
|
9872
|
+
}
|
|
9873
|
+
this.rafId = this.frames.requestFrame(this.step);
|
|
9874
|
+
};
|
|
9875
|
+
recordWrite() {
|
|
9876
|
+
this.lastWrite = {
|
|
9877
|
+
x: this.camera.position.x,
|
|
9878
|
+
y: this.camera.position.y,
|
|
9879
|
+
zoom: this.camera.zoom
|
|
9880
|
+
};
|
|
9881
|
+
}
|
|
9882
|
+
foreignWrite() {
|
|
9883
|
+
if (this.lastWrite === null) return false;
|
|
9884
|
+
const eps = 1e-6;
|
|
9885
|
+
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;
|
|
9886
|
+
}
|
|
9887
|
+
/** Terminates an in-flight animation with `reason`. No-op when idle. */
|
|
9888
|
+
end(reason) {
|
|
9889
|
+
if (this.disposed || this.to === null) return;
|
|
9890
|
+
this.clearFrame();
|
|
9891
|
+
this.from = null;
|
|
9892
|
+
this.to = null;
|
|
9893
|
+
this.lastWrite = null;
|
|
9894
|
+
this.emit(reason);
|
|
9895
|
+
}
|
|
9896
|
+
clearFrame() {
|
|
9897
|
+
if (this.rafId !== null) {
|
|
9898
|
+
this.frames.cancelFrame(this.rafId);
|
|
9899
|
+
this.rafId = null;
|
|
9900
|
+
}
|
|
9901
|
+
}
|
|
9902
|
+
emit(reason) {
|
|
9903
|
+
for (const listener of [...this.endListeners]) {
|
|
9904
|
+
try {
|
|
9905
|
+
listener(reason);
|
|
9906
|
+
} catch {
|
|
9907
|
+
}
|
|
9908
|
+
}
|
|
9909
|
+
}
|
|
9910
|
+
};
|
|
9911
|
+
|
|
9912
|
+
// src/canvas/focus-presence.ts
|
|
9913
|
+
var FOCUS_PRESENCE_KIND = "focus";
|
|
9914
|
+
var AUDIENCES = ["all", "players", "display"];
|
|
9915
|
+
function isPositiveFinite(value) {
|
|
9916
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
9917
|
+
}
|
|
9918
|
+
function isFiniteNumber2(value) {
|
|
9919
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
9920
|
+
}
|
|
9921
|
+
function isFocusPresence(data) {
|
|
9922
|
+
if (typeof data !== "object" || data === null) return false;
|
|
9923
|
+
const payload = data;
|
|
9924
|
+
if (payload.kind !== FOCUS_PRESENCE_KIND) return false;
|
|
9925
|
+
if (!isFiniteNumber2(payload.x) || !isFiniteNumber2(payload.y)) return false;
|
|
9926
|
+
if (!isPositiveFinite(payload.w) || !isPositiveFinite(payload.h)) return false;
|
|
9927
|
+
if (typeof payload.audience !== "string" || !AUDIENCES.some((a) => a === payload.audience)) {
|
|
9928
|
+
return false;
|
|
9929
|
+
}
|
|
9930
|
+
if (payload.color !== void 0 && typeof payload.color !== "string") return false;
|
|
9931
|
+
return true;
|
|
9932
|
+
}
|
|
9933
|
+
function toFocusPresence(view, audience, color) {
|
|
9934
|
+
return {
|
|
9935
|
+
kind: FOCUS_PRESENCE_KIND,
|
|
9936
|
+
x: view.x,
|
|
9937
|
+
y: view.y,
|
|
9938
|
+
w: view.w,
|
|
9939
|
+
h: view.h,
|
|
9940
|
+
audience,
|
|
9941
|
+
...color === void 0 ? {} : { color }
|
|
9942
|
+
};
|
|
9943
|
+
}
|
|
9944
|
+
|
|
9945
|
+
// src/canvas/remote-focus-receiver.ts
|
|
9946
|
+
function audienceIncludes(audience, role) {
|
|
9947
|
+
if (role === "dm") return false;
|
|
9948
|
+
if (audience === "all") return true;
|
|
9949
|
+
if (audience === "players") return role === "player";
|
|
9950
|
+
return role === "display";
|
|
9951
|
+
}
|
|
9952
|
+
var RemoteFocusReceiver = class {
|
|
9953
|
+
role;
|
|
9954
|
+
animator;
|
|
9955
|
+
animate;
|
|
9956
|
+
pulseColor;
|
|
9957
|
+
overlay;
|
|
9958
|
+
disposed = false;
|
|
9959
|
+
constructor(host, options) {
|
|
9960
|
+
this.role = options.role;
|
|
9961
|
+
this.animator = options.animator;
|
|
9962
|
+
this.animate = options.animate ?? true;
|
|
9963
|
+
this.pulseColor = options.pulseColor;
|
|
9964
|
+
this.overlay = options.pulse ?? true ? new RemotePingOverlay(host, {
|
|
9965
|
+
...options.pulseColor === void 0 ? {} : { color: options.pulseColor },
|
|
9966
|
+
...options.pulseDurationMs === void 0 ? {} : { durationMs: options.pulseDurationMs },
|
|
9967
|
+
...options.pulseRadius === void 0 ? {} : { radius: options.pulseRadius },
|
|
9968
|
+
maxPingsPerSender: 1
|
|
9969
|
+
}) : null;
|
|
9970
|
+
}
|
|
9971
|
+
/**
|
|
9972
|
+
* Applies a presence payload from `sender`. Returns `false` for payloads
|
|
9973
|
+
* that are not focus frames, or are addressed to a different role, so hosts
|
|
9974
|
+
* can feed every presence frame through without disturbing other handlers.
|
|
9975
|
+
*/
|
|
9976
|
+
apply(from, data) {
|
|
9977
|
+
if (this.disposed || !isFocusPresence(data)) return false;
|
|
9978
|
+
if (!audienceIncludes(data.audience, this.role)) return false;
|
|
9979
|
+
const view = { x: data.x, y: data.y, w: data.w, h: data.h };
|
|
9980
|
+
if (this.animate) {
|
|
9981
|
+
this.animator.animateTo(view);
|
|
9982
|
+
} else {
|
|
9983
|
+
this.animator.jumpTo(view);
|
|
9984
|
+
}
|
|
9985
|
+
this.overlay?.apply(from, {
|
|
9986
|
+
kind: "ping",
|
|
9987
|
+
x: view.x + view.w / 2,
|
|
9988
|
+
y: view.y + view.h / 2,
|
|
9989
|
+
color: data.color ?? this.pulseColor
|
|
9990
|
+
});
|
|
9991
|
+
return true;
|
|
9992
|
+
}
|
|
9993
|
+
/** Idempotent. Does NOT dispose the animator — the host owns that. */
|
|
9994
|
+
dispose() {
|
|
9995
|
+
if (this.disposed) return;
|
|
9996
|
+
this.disposed = true;
|
|
9997
|
+
this.overlay?.dispose();
|
|
9998
|
+
}
|
|
9999
|
+
};
|
|
10000
|
+
|
|
9490
10001
|
// src/tools/hand-tool.ts
|
|
9491
10002
|
var HandTool = class {
|
|
9492
10003
|
name = "hand";
|
|
@@ -9675,7 +10186,7 @@ function hitTestStroke(stroke, point, radius) {
|
|
|
9675
10186
|
}
|
|
9676
10187
|
|
|
9677
10188
|
// src/elements/stroke-erase.ts
|
|
9678
|
-
function
|
|
10189
|
+
function lerp2(a, b, t) {
|
|
9679
10190
|
return {
|
|
9680
10191
|
x: a.x + (b.x - a.x) * t,
|
|
9681
10192
|
y: a.y + (b.y - a.y) * t,
|
|
@@ -9734,13 +10245,13 @@ function erasePoints(points, eraser, radius) {
|
|
|
9734
10245
|
erased = true;
|
|
9735
10246
|
if (tLo > 0) {
|
|
9736
10247
|
if (current.length === 0) current.push(a);
|
|
9737
|
-
current.push(
|
|
10248
|
+
current.push(lerp2(a, b, tLo));
|
|
9738
10249
|
flush();
|
|
9739
10250
|
} else {
|
|
9740
10251
|
flush();
|
|
9741
10252
|
}
|
|
9742
10253
|
if (tHi < 1) {
|
|
9743
|
-
current = [
|
|
10254
|
+
current = [lerp2(a, b, tHi), b];
|
|
9744
10255
|
}
|
|
9745
10256
|
}
|
|
9746
10257
|
flush();
|
|
@@ -12196,7 +12707,7 @@ var LaserTool = class {
|
|
|
12196
12707
|
|
|
12197
12708
|
// src/tools/ping-tool.ts
|
|
12198
12709
|
var DEFAULT_COLOR6 = "#ff3b30";
|
|
12199
|
-
var
|
|
12710
|
+
var DEFAULT_DURATION_MS4 = 1800;
|
|
12200
12711
|
var DEFAULT_RADIUS4 = 48;
|
|
12201
12712
|
var DEFAULT_MIN_INTERVAL_MS2 = 300;
|
|
12202
12713
|
var PingTool = class {
|
|
@@ -12213,7 +12724,7 @@ var PingTool = class {
|
|
|
12213
12724
|
constructor(options = {}) {
|
|
12214
12725
|
this.name = options.name ?? "ping";
|
|
12215
12726
|
this.color = options.color ?? DEFAULT_COLOR6;
|
|
12216
|
-
this.durationMs = options.durationMs ??
|
|
12727
|
+
this.durationMs = options.durationMs ?? DEFAULT_DURATION_MS4;
|
|
12217
12728
|
this.radius = options.radius ?? DEFAULT_RADIUS4;
|
|
12218
12729
|
this.minIntervalMs = options.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS2;
|
|
12219
12730
|
}
|
|
@@ -12315,14 +12826,16 @@ var PingTool = class {
|
|
|
12315
12826
|
};
|
|
12316
12827
|
|
|
12317
12828
|
// src/index.ts
|
|
12318
|
-
var VERSION = "0.
|
|
12829
|
+
var VERSION = "0.61.0";
|
|
12319
12830
|
export {
|
|
12320
12831
|
ArrowTool,
|
|
12321
12832
|
AutoSave,
|
|
12322
12833
|
Camera,
|
|
12834
|
+
CameraAnimator,
|
|
12323
12835
|
DEFAULT_NOTE_FONT_SIZE,
|
|
12324
12836
|
ElementStore,
|
|
12325
12837
|
EraserTool,
|
|
12838
|
+
FOCUS_PRESENCE_KIND,
|
|
12326
12839
|
HandTool,
|
|
12327
12840
|
HistoryStack,
|
|
12328
12841
|
ImageTool,
|
|
@@ -12340,6 +12853,7 @@ export {
|
|
|
12340
12853
|
PencilTool,
|
|
12341
12854
|
PingInput,
|
|
12342
12855
|
PingTool,
|
|
12856
|
+
RemoteFocusReceiver,
|
|
12343
12857
|
RemoteLaserOverlay,
|
|
12344
12858
|
RemoteMeasureOverlay,
|
|
12345
12859
|
RemotePingOverlay,
|
|
@@ -12350,7 +12864,10 @@ export {
|
|
|
12350
12864
|
ToolManager,
|
|
12351
12865
|
VERSION,
|
|
12352
12866
|
Viewport,
|
|
12867
|
+
applyCameraView,
|
|
12353
12868
|
boundsIntersect,
|
|
12869
|
+
cameraOriginForView,
|
|
12870
|
+
captureCameraView,
|
|
12354
12871
|
createArrow,
|
|
12355
12872
|
createGrid,
|
|
12356
12873
|
createHtmlElement,
|
|
@@ -12363,6 +12880,7 @@ export {
|
|
|
12363
12880
|
drawHexPath,
|
|
12364
12881
|
exportImage,
|
|
12365
12882
|
exportSvg,
|
|
12883
|
+
fitZoomForView,
|
|
12366
12884
|
getActiveFormats,
|
|
12367
12885
|
getArrowBounds,
|
|
12368
12886
|
getArrowControlPoint,
|
|
@@ -12378,6 +12896,7 @@ export {
|
|
|
12378
12896
|
getHexCellsInRectangle,
|
|
12379
12897
|
getHexCellsInSquare,
|
|
12380
12898
|
getHexDistance,
|
|
12899
|
+
isFocusPresence,
|
|
12381
12900
|
isLaserTrailPresence,
|
|
12382
12901
|
isMeasurePresence,
|
|
12383
12902
|
isNearBezier,
|
|
@@ -12387,6 +12906,7 @@ export {
|
|
|
12387
12906
|
snapPoint,
|
|
12388
12907
|
snapToHexCenter,
|
|
12389
12908
|
styleToPatch,
|
|
12909
|
+
toFocusPresence,
|
|
12390
12910
|
toLaserTrailPresence,
|
|
12391
12911
|
toMeasurePresence,
|
|
12392
12912
|
toPingPresence,
|