@fieldnotes/core 0.53.0 → 0.55.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 +545 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +291 -34
- package/dist/index.d.ts +291 -34
- package/dist/index.js +536 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -30,13 +30,18 @@ __export(index_exports, {
|
|
|
30
30
|
HistoryStack: () => HistoryStack,
|
|
31
31
|
ImageTool: () => ImageTool,
|
|
32
32
|
IndexedDBAdapter: () => IndexedDBAdapter,
|
|
33
|
+
LASER_TRAIL_PRESENCE_KIND: () => LASER_TRAIL_PRESENCE_KIND,
|
|
33
34
|
LaserTool: () => LaserTool,
|
|
34
35
|
LayerManager: () => LayerManager,
|
|
35
36
|
LocalStorageAdapter: () => LocalStorageAdapter,
|
|
36
37
|
MeasureTool: () => MeasureTool,
|
|
37
38
|
MemoryAdapter: () => MemoryAdapter,
|
|
38
39
|
NoteTool: () => NoteTool,
|
|
40
|
+
PING_PRESENCE_KIND: () => PING_PRESENCE_KIND,
|
|
39
41
|
PencilTool: () => PencilTool,
|
|
42
|
+
PingTool: () => PingTool,
|
|
43
|
+
RemoteLaserOverlay: () => RemoteLaserOverlay,
|
|
44
|
+
RemotePingOverlay: () => RemotePingOverlay,
|
|
40
45
|
SelectTool: () => SelectTool,
|
|
41
46
|
ShapeTool: () => ShapeTool,
|
|
42
47
|
TemplateTool: () => TemplateTool,
|
|
@@ -72,12 +77,16 @@ __export(index_exports, {
|
|
|
72
77
|
getHexCellsInRectangle: () => getHexCellsInRectangle,
|
|
73
78
|
getHexCellsInSquare: () => getHexCellsInSquare,
|
|
74
79
|
getHexDistance: () => getHexDistance,
|
|
80
|
+
isLaserTrailPresence: () => isLaserTrailPresence,
|
|
75
81
|
isNearBezier: () => isNearBezier,
|
|
82
|
+
isPingPresence: () => isPingPresence,
|
|
76
83
|
setFontSize: () => setFontSize,
|
|
77
84
|
smartSnap: () => smartSnap,
|
|
78
85
|
snapPoint: () => snapPoint,
|
|
79
86
|
snapToHexCenter: () => snapToHexCenter,
|
|
80
87
|
styleToPatch: () => styleToPatch,
|
|
88
|
+
toLaserTrailPresence: () => toLaserTrailPresence,
|
|
89
|
+
toPingPresence: () => toPingPresence,
|
|
81
90
|
toggleBold: () => toggleBold,
|
|
82
91
|
toggleItalic: () => toggleItalic,
|
|
83
92
|
toggleStrikethrough: () => toggleStrikethrough,
|
|
@@ -6922,6 +6931,7 @@ var RenderLoop = class {
|
|
|
6922
6931
|
// set on recenter/viewport-change; consumed by the grid block
|
|
6923
6932
|
stats = new RenderStats();
|
|
6924
6933
|
layerGroups = /* @__PURE__ */ new Map();
|
|
6934
|
+
overlays = /* @__PURE__ */ new Set();
|
|
6925
6935
|
gridCacheCanvas = null;
|
|
6926
6936
|
gridCacheCtx = null;
|
|
6927
6937
|
lastGridRefs = [];
|
|
@@ -6980,6 +6990,29 @@ var RenderLoop = class {
|
|
|
6980
6990
|
getStats() {
|
|
6981
6991
|
return this.stats.getSnapshot();
|
|
6982
6992
|
}
|
|
6993
|
+
/**
|
|
6994
|
+
* Registers a viewport overlay drawn beneath the active tool's own overlay.
|
|
6995
|
+
* Returns an idempotent unsubscribe that also erases the overlay's last
|
|
6996
|
+
* frame.
|
|
6997
|
+
*/
|
|
6998
|
+
registerOverlay(draw) {
|
|
6999
|
+
this.overlays.add(draw);
|
|
7000
|
+
this.requestRender();
|
|
7001
|
+
return () => {
|
|
7002
|
+
if (this.overlays.delete(draw)) this.requestRender();
|
|
7003
|
+
};
|
|
7004
|
+
}
|
|
7005
|
+
drawRegisteredOverlays(ctx) {
|
|
7006
|
+
for (const draw of this.overlays) {
|
|
7007
|
+
ctx.save();
|
|
7008
|
+
try {
|
|
7009
|
+
draw(ctx);
|
|
7010
|
+
} catch {
|
|
7011
|
+
} finally {
|
|
7012
|
+
ctx.restore();
|
|
7013
|
+
}
|
|
7014
|
+
}
|
|
7015
|
+
}
|
|
6983
7016
|
compositeLayerCache(ctx, layerId, opacity) {
|
|
6984
7017
|
const cached = this.layerCache.getCanvas(layerId);
|
|
6985
7018
|
const offset = this.marginViewport.compositeOffset(
|
|
@@ -7113,7 +7146,8 @@ var RenderLoop = class {
|
|
|
7113
7146
|
}
|
|
7114
7147
|
const activeTool = this.toolManager.activeTool;
|
|
7115
7148
|
const overlayOrder = visibleElements.length + 1;
|
|
7116
|
-
|
|
7149
|
+
const hasOverlay = activeTool?.renderOverlay !== void 0 || this.overlays.size > 0;
|
|
7150
|
+
if (hybridActive && hasOverlay) hybridOrders.add(overlayOrder);
|
|
7117
7151
|
this.hybridSurface.beginFrame(hybridOrders, this.canvasEl.width, this.canvasEl.height);
|
|
7118
7152
|
for (const [layerId, elements] of this.layerGroups) {
|
|
7119
7153
|
const isActiveDrawingLayer = layerId === this.activeDrawingLayerId;
|
|
@@ -7223,7 +7257,7 @@ var RenderLoop = class {
|
|
|
7223
7257
|
hybridCtx.restore();
|
|
7224
7258
|
}
|
|
7225
7259
|
const overlayT0 = performance.now();
|
|
7226
|
-
if (hybridActive &&
|
|
7260
|
+
if (hybridActive && hasOverlay) {
|
|
7227
7261
|
const overlayCtx = this.hybridSurface.getContext(overlayOrder);
|
|
7228
7262
|
if (overlayCtx) {
|
|
7229
7263
|
overlayCtx.clearRect(0, 0, this.canvasEl.width, this.canvasEl.height);
|
|
@@ -7231,11 +7265,13 @@ var RenderLoop = class {
|
|
|
7231
7265
|
overlayCtx.scale(dpr, dpr);
|
|
7232
7266
|
overlayCtx.translate(this.camera.position.x, this.camera.position.y);
|
|
7233
7267
|
overlayCtx.scale(this.camera.zoom, this.camera.zoom);
|
|
7234
|
-
|
|
7268
|
+
this.drawRegisteredOverlays(overlayCtx);
|
|
7269
|
+
if (activeTool?.renderOverlay) activeTool.renderOverlay(overlayCtx);
|
|
7235
7270
|
overlayCtx.restore();
|
|
7236
7271
|
}
|
|
7237
|
-
} else if (
|
|
7238
|
-
|
|
7272
|
+
} else if (hasOverlay) {
|
|
7273
|
+
this.drawRegisteredOverlays(ctx);
|
|
7274
|
+
if (activeTool?.renderOverlay) activeTool.renderOverlay(ctx);
|
|
7239
7275
|
}
|
|
7240
7276
|
const overlayMs = performance.now() - overlayT0;
|
|
7241
7277
|
ctx.restore();
|
|
@@ -8229,6 +8265,17 @@ var Viewport = class {
|
|
|
8229
8265
|
requestRender() {
|
|
8230
8266
|
this.renderLoop.requestRender();
|
|
8231
8267
|
}
|
|
8268
|
+
/**
|
|
8269
|
+
* Registers a world-space overlay drawn above elements on every frame,
|
|
8270
|
+
* regardless of the active tool — the surface for remote presence visuals
|
|
8271
|
+
* such as laser trails, cursors, pings, and shared rulers. Overlays draw
|
|
8272
|
+
* beneath the active tool's own `renderOverlay` and never touch elements,
|
|
8273
|
+
* history, or persisted state. Returns an idempotent unsubscribe that also
|
|
8274
|
+
* erases the overlay's last frame.
|
|
8275
|
+
*/
|
|
8276
|
+
registerOverlay(draw) {
|
|
8277
|
+
return this.renderLoop.registerOverlay(draw);
|
|
8278
|
+
}
|
|
8232
8279
|
exportState() {
|
|
8233
8280
|
return exportState(
|
|
8234
8281
|
this.store.snapshot(),
|
|
@@ -8576,6 +8623,324 @@ var Viewport = class {
|
|
|
8576
8623
|
}
|
|
8577
8624
|
};
|
|
8578
8625
|
|
|
8626
|
+
// src/canvas/remote-laser-overlay.ts
|
|
8627
|
+
var LASER_TRAIL_PRESENCE_KIND = "laser";
|
|
8628
|
+
function isFinitePoint(value) {
|
|
8629
|
+
if (typeof value !== "object" || value === null) return false;
|
|
8630
|
+
const point = value;
|
|
8631
|
+
return typeof point.x === "number" && Number.isFinite(point.x) && typeof point.y === "number" && Number.isFinite(point.y);
|
|
8632
|
+
}
|
|
8633
|
+
function isLaserTrailPresence(data) {
|
|
8634
|
+
if (typeof data !== "object" || data === null) return false;
|
|
8635
|
+
const payload = data;
|
|
8636
|
+
if (payload.kind !== LASER_TRAIL_PRESENCE_KIND) return false;
|
|
8637
|
+
if (!Array.isArray(payload.points) || !payload.points.every(isFinitePoint)) return false;
|
|
8638
|
+
if (payload.color !== void 0 && typeof payload.color !== "string") return false;
|
|
8639
|
+
if (payload.width !== void 0 && !(typeof payload.width === "number" && payload.width > 0)) {
|
|
8640
|
+
return false;
|
|
8641
|
+
}
|
|
8642
|
+
if (payload.fadeMs !== void 0 && !(typeof payload.fadeMs === "number" && payload.fadeMs > 0)) {
|
|
8643
|
+
return false;
|
|
8644
|
+
}
|
|
8645
|
+
return true;
|
|
8646
|
+
}
|
|
8647
|
+
function toLaserTrailPresence(emission) {
|
|
8648
|
+
return {
|
|
8649
|
+
kind: LASER_TRAIL_PRESENCE_KIND,
|
|
8650
|
+
points: emission.points.map((point) => ({ x: point.x, y: point.y })),
|
|
8651
|
+
color: emission.color,
|
|
8652
|
+
width: emission.width,
|
|
8653
|
+
fadeMs: emission.fadeMs
|
|
8654
|
+
};
|
|
8655
|
+
}
|
|
8656
|
+
var DEFAULT_COLOR = "#ff3b30";
|
|
8657
|
+
var DEFAULT_WIDTH = 4;
|
|
8658
|
+
var DEFAULT_FADE_MS = 1200;
|
|
8659
|
+
var DEFAULT_MAX_POINTS = 512;
|
|
8660
|
+
var RemoteLaserOverlay = class {
|
|
8661
|
+
host;
|
|
8662
|
+
color;
|
|
8663
|
+
width;
|
|
8664
|
+
fadeMs;
|
|
8665
|
+
maxPointsPerSender;
|
|
8666
|
+
trails = /* @__PURE__ */ new Map();
|
|
8667
|
+
unregister;
|
|
8668
|
+
rafId = null;
|
|
8669
|
+
disposed = false;
|
|
8670
|
+
constructor(host, options = {}) {
|
|
8671
|
+
this.host = host;
|
|
8672
|
+
this.color = options.color ?? DEFAULT_COLOR;
|
|
8673
|
+
this.width = options.width ?? DEFAULT_WIDTH;
|
|
8674
|
+
this.fadeMs = options.fadeMs ?? DEFAULT_FADE_MS;
|
|
8675
|
+
this.maxPointsPerSender = options.maxPointsPerSender ?? DEFAULT_MAX_POINTS;
|
|
8676
|
+
this.unregister = host.registerOverlay((ctx) => this.renderTrails(ctx));
|
|
8677
|
+
}
|
|
8678
|
+
now() {
|
|
8679
|
+
return performance.now();
|
|
8680
|
+
}
|
|
8681
|
+
/**
|
|
8682
|
+
* Applies a presence payload from `sender` (any opaque per-sender key, e.g.
|
|
8683
|
+
* the envelope `from`). Non-laser or malformed payloads are ignored and
|
|
8684
|
+
* reported as `false`, so hosts can feed every presence frame through.
|
|
8685
|
+
*/
|
|
8686
|
+
apply(sender, data) {
|
|
8687
|
+
if (this.disposed || !isLaserTrailPresence(data)) return false;
|
|
8688
|
+
let trail = this.trails.get(sender);
|
|
8689
|
+
if (!trail) {
|
|
8690
|
+
trail = { points: [], color: this.color, width: this.width, fadeMs: this.fadeMs };
|
|
8691
|
+
this.trails.set(sender, trail);
|
|
8692
|
+
}
|
|
8693
|
+
trail.color = data.color ?? this.color;
|
|
8694
|
+
trail.width = data.width ?? this.width;
|
|
8695
|
+
trail.fadeMs = data.fadeMs ?? this.fadeMs;
|
|
8696
|
+
const t = this.now();
|
|
8697
|
+
for (const point of data.points) {
|
|
8698
|
+
trail.points.push({ x: point.x, y: point.y, t });
|
|
8699
|
+
}
|
|
8700
|
+
const excess = trail.points.length - this.maxPointsPerSender;
|
|
8701
|
+
if (excess > 0) trail.points.splice(0, excess);
|
|
8702
|
+
this.ensureAnimating();
|
|
8703
|
+
this.host.requestRender();
|
|
8704
|
+
return true;
|
|
8705
|
+
}
|
|
8706
|
+
/** Removes a sender's trail immediately (presence-leave/disconnect). */
|
|
8707
|
+
remove(sender) {
|
|
8708
|
+
if (this.trails.delete(sender)) this.host.requestRender();
|
|
8709
|
+
}
|
|
8710
|
+
/** Removes every trail immediately. */
|
|
8711
|
+
clear() {
|
|
8712
|
+
if (this.trails.size === 0) return;
|
|
8713
|
+
this.trails.clear();
|
|
8714
|
+
this.host.requestRender();
|
|
8715
|
+
}
|
|
8716
|
+
/** Number of senders with a live (unfaded) trail. */
|
|
8717
|
+
get activeSenderCount() {
|
|
8718
|
+
return this.trails.size;
|
|
8719
|
+
}
|
|
8720
|
+
/** Unregisters the overlay and stops the fade loop. Idempotent. */
|
|
8721
|
+
dispose() {
|
|
8722
|
+
if (this.disposed) return;
|
|
8723
|
+
this.disposed = true;
|
|
8724
|
+
if (this.rafId !== null) {
|
|
8725
|
+
cancelAnimationFrame(this.rafId);
|
|
8726
|
+
this.rafId = null;
|
|
8727
|
+
}
|
|
8728
|
+
this.trails.clear();
|
|
8729
|
+
this.unregister?.();
|
|
8730
|
+
this.unregister = null;
|
|
8731
|
+
}
|
|
8732
|
+
ensureAnimating() {
|
|
8733
|
+
if (this.rafId === null) {
|
|
8734
|
+
this.rafId = requestAnimationFrame(() => this.tick());
|
|
8735
|
+
}
|
|
8736
|
+
}
|
|
8737
|
+
tick() {
|
|
8738
|
+
if (this.disposed) return;
|
|
8739
|
+
const now = this.now();
|
|
8740
|
+
for (const [sender, trail] of this.trails) {
|
|
8741
|
+
const cutoff = now - trail.fadeMs;
|
|
8742
|
+
trail.points = trail.points.filter((point) => point.t >= cutoff);
|
|
8743
|
+
if (trail.points.length === 0) this.trails.delete(sender);
|
|
8744
|
+
}
|
|
8745
|
+
this.host.requestRender();
|
|
8746
|
+
this.rafId = this.trails.size > 0 ? requestAnimationFrame(() => this.tick()) : null;
|
|
8747
|
+
}
|
|
8748
|
+
renderTrails(ctx) {
|
|
8749
|
+
if (this.trails.size === 0) return;
|
|
8750
|
+
const now = this.now();
|
|
8751
|
+
for (const trail of this.trails.values()) {
|
|
8752
|
+
if (trail.points.length < 2) continue;
|
|
8753
|
+
ctx.save();
|
|
8754
|
+
ctx.strokeStyle = trail.color;
|
|
8755
|
+
ctx.lineWidth = trail.width;
|
|
8756
|
+
ctx.lineCap = "round";
|
|
8757
|
+
ctx.lineJoin = "round";
|
|
8758
|
+
for (let i = 0; i < trail.points.length - 1; i++) {
|
|
8759
|
+
const a = trail.points[i];
|
|
8760
|
+
const b = trail.points[i + 1];
|
|
8761
|
+
if (!a || !b) continue;
|
|
8762
|
+
const age = now - b.t;
|
|
8763
|
+
ctx.globalAlpha = Math.max(0, 1 - age / trail.fadeMs);
|
|
8764
|
+
ctx.beginPath();
|
|
8765
|
+
ctx.moveTo(a.x, a.y);
|
|
8766
|
+
ctx.lineTo(b.x, b.y);
|
|
8767
|
+
ctx.stroke();
|
|
8768
|
+
}
|
|
8769
|
+
ctx.restore();
|
|
8770
|
+
}
|
|
8771
|
+
}
|
|
8772
|
+
};
|
|
8773
|
+
|
|
8774
|
+
// src/canvas/ping-pulse.ts
|
|
8775
|
+
var RIPPLE_OFFSETS = [0, 0.25];
|
|
8776
|
+
function easeOutCubic(t) {
|
|
8777
|
+
const inv = 1 - t;
|
|
8778
|
+
return 1 - inv * inv * inv;
|
|
8779
|
+
}
|
|
8780
|
+
function renderPingPulse(ctx, x, y, ageMs, style) {
|
|
8781
|
+
if (ageMs < 0 || ageMs >= style.durationMs) return;
|
|
8782
|
+
ctx.save();
|
|
8783
|
+
ctx.strokeStyle = style.color;
|
|
8784
|
+
ctx.fillStyle = style.color;
|
|
8785
|
+
for (const offset of RIPPLE_OFFSETS) {
|
|
8786
|
+
const rippleSpan = style.durationMs * (1 - offset);
|
|
8787
|
+
const rippleAge = ageMs - style.durationMs * offset;
|
|
8788
|
+
if (rippleAge < 0) continue;
|
|
8789
|
+
const progress = Math.min(1, rippleAge / rippleSpan);
|
|
8790
|
+
const rippleRadius = style.radius * easeOutCubic(progress);
|
|
8791
|
+
if (rippleRadius <= 0) continue;
|
|
8792
|
+
ctx.globalAlpha = Math.max(0, 1 - progress);
|
|
8793
|
+
ctx.lineWidth = Math.max(1.5, style.radius / 12);
|
|
8794
|
+
ctx.beginPath();
|
|
8795
|
+
ctx.arc(x, y, rippleRadius, 0, Math.PI * 2);
|
|
8796
|
+
ctx.stroke();
|
|
8797
|
+
}
|
|
8798
|
+
const dotProgress = ageMs / style.durationMs;
|
|
8799
|
+
ctx.globalAlpha = Math.max(0, 1 - Math.max(0, dotProgress - 0.5) * 2);
|
|
8800
|
+
ctx.beginPath();
|
|
8801
|
+
ctx.arc(x, y, Math.max(2, style.radius / 8), 0, Math.PI * 2);
|
|
8802
|
+
ctx.fill();
|
|
8803
|
+
ctx.restore();
|
|
8804
|
+
}
|
|
8805
|
+
|
|
8806
|
+
// src/canvas/remote-ping-overlay.ts
|
|
8807
|
+
var PING_PRESENCE_KIND = "ping";
|
|
8808
|
+
function isPingPresence(data) {
|
|
8809
|
+
if (typeof data !== "object" || data === null) return false;
|
|
8810
|
+
const payload = data;
|
|
8811
|
+
if (payload.kind !== PING_PRESENCE_KIND) return false;
|
|
8812
|
+
if (typeof payload.x !== "number" || !Number.isFinite(payload.x)) return false;
|
|
8813
|
+
if (typeof payload.y !== "number" || !Number.isFinite(payload.y)) return false;
|
|
8814
|
+
if (payload.color !== void 0 && typeof payload.color !== "string") return false;
|
|
8815
|
+
if (payload.durationMs !== void 0 && !(typeof payload.durationMs === "number" && Number.isFinite(payload.durationMs) && payload.durationMs > 0)) {
|
|
8816
|
+
return false;
|
|
8817
|
+
}
|
|
8818
|
+
if (payload.radius !== void 0 && !(typeof payload.radius === "number" && Number.isFinite(payload.radius) && payload.radius > 0)) {
|
|
8819
|
+
return false;
|
|
8820
|
+
}
|
|
8821
|
+
return true;
|
|
8822
|
+
}
|
|
8823
|
+
function toPingPresence(emission) {
|
|
8824
|
+
return {
|
|
8825
|
+
kind: PING_PRESENCE_KIND,
|
|
8826
|
+
x: emission.x,
|
|
8827
|
+
y: emission.y,
|
|
8828
|
+
color: emission.color,
|
|
8829
|
+
durationMs: emission.durationMs,
|
|
8830
|
+
radius: emission.radius
|
|
8831
|
+
};
|
|
8832
|
+
}
|
|
8833
|
+
var DEFAULT_COLOR2 = "#ff3b30";
|
|
8834
|
+
var DEFAULT_DURATION_MS = 1800;
|
|
8835
|
+
var DEFAULT_RADIUS = 48;
|
|
8836
|
+
var DEFAULT_MAX_PINGS = 8;
|
|
8837
|
+
var RemotePingOverlay = class {
|
|
8838
|
+
host;
|
|
8839
|
+
color;
|
|
8840
|
+
durationMs;
|
|
8841
|
+
radius;
|
|
8842
|
+
maxPingsPerSender;
|
|
8843
|
+
pings = /* @__PURE__ */ new Map();
|
|
8844
|
+
unregister;
|
|
8845
|
+
rafId = null;
|
|
8846
|
+
disposed = false;
|
|
8847
|
+
constructor(host, options = {}) {
|
|
8848
|
+
this.host = host;
|
|
8849
|
+
this.color = options.color ?? DEFAULT_COLOR2;
|
|
8850
|
+
this.durationMs = options.durationMs ?? DEFAULT_DURATION_MS;
|
|
8851
|
+
this.radius = options.radius ?? DEFAULT_RADIUS;
|
|
8852
|
+
this.maxPingsPerSender = options.maxPingsPerSender ?? DEFAULT_MAX_PINGS;
|
|
8853
|
+
this.unregister = host.registerOverlay((ctx) => this.renderPings(ctx));
|
|
8854
|
+
}
|
|
8855
|
+
now() {
|
|
8856
|
+
return performance.now();
|
|
8857
|
+
}
|
|
8858
|
+
/**
|
|
8859
|
+
* Applies a presence payload from `sender` (any opaque per-sender key, e.g.
|
|
8860
|
+
* the envelope `from`). Non-ping or malformed payloads are ignored and
|
|
8861
|
+
* reported as `false`, so hosts can feed every presence frame through.
|
|
8862
|
+
*/
|
|
8863
|
+
apply(sender, data) {
|
|
8864
|
+
if (this.disposed || !isPingPresence(data)) return false;
|
|
8865
|
+
let senderPings = this.pings.get(sender);
|
|
8866
|
+
if (!senderPings) {
|
|
8867
|
+
senderPings = [];
|
|
8868
|
+
this.pings.set(sender, senderPings);
|
|
8869
|
+
}
|
|
8870
|
+
senderPings.push({
|
|
8871
|
+
x: data.x,
|
|
8872
|
+
y: data.y,
|
|
8873
|
+
t: this.now(),
|
|
8874
|
+
color: data.color ?? this.color,
|
|
8875
|
+
durationMs: data.durationMs ?? this.durationMs,
|
|
8876
|
+
radius: data.radius ?? this.radius
|
|
8877
|
+
});
|
|
8878
|
+
const excess = senderPings.length - this.maxPingsPerSender;
|
|
8879
|
+
if (excess > 0) senderPings.splice(0, excess);
|
|
8880
|
+
this.ensureAnimating();
|
|
8881
|
+
this.host.requestRender();
|
|
8882
|
+
return true;
|
|
8883
|
+
}
|
|
8884
|
+
/** Removes a sender's pings immediately (presence-leave/disconnect). */
|
|
8885
|
+
remove(sender) {
|
|
8886
|
+
if (this.pings.delete(sender)) this.host.requestRender();
|
|
8887
|
+
}
|
|
8888
|
+
/** Removes every ping immediately. */
|
|
8889
|
+
clear() {
|
|
8890
|
+
if (this.pings.size === 0) return;
|
|
8891
|
+
this.pings.clear();
|
|
8892
|
+
this.host.requestRender();
|
|
8893
|
+
}
|
|
8894
|
+
/** Number of senders with a live (unexpired) ping. */
|
|
8895
|
+
get activeSenderCount() {
|
|
8896
|
+
return this.pings.size;
|
|
8897
|
+
}
|
|
8898
|
+
/** Unregisters the overlay and stops the animation loop. Idempotent. */
|
|
8899
|
+
dispose() {
|
|
8900
|
+
if (this.disposed) return;
|
|
8901
|
+
this.disposed = true;
|
|
8902
|
+
if (this.rafId !== null) {
|
|
8903
|
+
cancelAnimationFrame(this.rafId);
|
|
8904
|
+
this.rafId = null;
|
|
8905
|
+
}
|
|
8906
|
+
this.pings.clear();
|
|
8907
|
+
this.unregister?.();
|
|
8908
|
+
this.unregister = null;
|
|
8909
|
+
}
|
|
8910
|
+
ensureAnimating() {
|
|
8911
|
+
if (this.rafId === null) {
|
|
8912
|
+
this.rafId = requestAnimationFrame(() => this.tick());
|
|
8913
|
+
}
|
|
8914
|
+
}
|
|
8915
|
+
tick() {
|
|
8916
|
+
if (this.disposed) return;
|
|
8917
|
+
const now = this.now();
|
|
8918
|
+
for (const [sender, senderPings] of this.pings) {
|
|
8919
|
+
const live = senderPings.filter((ping) => now - ping.t < ping.durationMs);
|
|
8920
|
+
if (live.length === 0) {
|
|
8921
|
+
this.pings.delete(sender);
|
|
8922
|
+
} else {
|
|
8923
|
+
this.pings.set(sender, live);
|
|
8924
|
+
}
|
|
8925
|
+
}
|
|
8926
|
+
this.host.requestRender();
|
|
8927
|
+
this.rafId = this.pings.size > 0 ? requestAnimationFrame(() => this.tick()) : null;
|
|
8928
|
+
}
|
|
8929
|
+
renderPings(ctx) {
|
|
8930
|
+
if (this.pings.size === 0) return;
|
|
8931
|
+
const now = this.now();
|
|
8932
|
+
for (const senderPings of this.pings.values()) {
|
|
8933
|
+
for (const ping of senderPings) {
|
|
8934
|
+
renderPingPulse(ctx, ping.x, ping.y, now - ping.t, {
|
|
8935
|
+
color: ping.color,
|
|
8936
|
+
durationMs: ping.durationMs,
|
|
8937
|
+
radius: ping.radius
|
|
8938
|
+
});
|
|
8939
|
+
}
|
|
8940
|
+
}
|
|
8941
|
+
}
|
|
8942
|
+
};
|
|
8943
|
+
|
|
8579
8944
|
// src/tools/hand-tool.ts
|
|
8580
8945
|
var HandTool = class {
|
|
8581
8946
|
name = "hand";
|
|
@@ -8837,7 +9202,7 @@ function erasePoints(points, eraser, radius) {
|
|
|
8837
9202
|
}
|
|
8838
9203
|
|
|
8839
9204
|
// src/tools/eraser-tool.ts
|
|
8840
|
-
var
|
|
9205
|
+
var DEFAULT_RADIUS2 = 20;
|
|
8841
9206
|
function makeEraserCursor(radius) {
|
|
8842
9207
|
const size = radius * 2;
|
|
8843
9208
|
const svg = `<svg xmlns='http://www.w3.org/2000/svg' width='${size}' height='${size}'><circle cx='${radius}' cy='${radius}' r='${radius - 1}' fill='none' stroke='%23666' stroke-width='1.5'/></svg>`;
|
|
@@ -8850,7 +9215,7 @@ var EraserTool = class {
|
|
|
8850
9215
|
cursor;
|
|
8851
9216
|
mode;
|
|
8852
9217
|
constructor(options = {}) {
|
|
8853
|
-
this.radius = options.radius ??
|
|
9218
|
+
this.radius = options.radius ?? DEFAULT_RADIUS2;
|
|
8854
9219
|
this.cursor = makeEraserCursor(this.radius);
|
|
8855
9220
|
this.mode = options.mode ?? "partial";
|
|
8856
9221
|
}
|
|
@@ -11132,9 +11497,9 @@ var TemplateTool = class {
|
|
|
11132
11497
|
};
|
|
11133
11498
|
|
|
11134
11499
|
// src/tools/laser-tool.ts
|
|
11135
|
-
var
|
|
11136
|
-
var
|
|
11137
|
-
var
|
|
11500
|
+
var DEFAULT_COLOR3 = "#ff3b30";
|
|
11501
|
+
var DEFAULT_WIDTH2 = 4;
|
|
11502
|
+
var DEFAULT_FADE_MS2 = 1200;
|
|
11138
11503
|
var LaserTool = class {
|
|
11139
11504
|
name;
|
|
11140
11505
|
color;
|
|
@@ -11144,11 +11509,13 @@ var LaserTool = class {
|
|
|
11144
11509
|
rafId = null;
|
|
11145
11510
|
drawing = false;
|
|
11146
11511
|
optionListeners = /* @__PURE__ */ new Set();
|
|
11512
|
+
trailListeners = /* @__PURE__ */ new Set();
|
|
11513
|
+
pendingEmission = [];
|
|
11147
11514
|
constructor(options = {}) {
|
|
11148
11515
|
this.name = options.name ?? "laser";
|
|
11149
|
-
this.color = options.color ??
|
|
11150
|
-
this.width = options.width ??
|
|
11151
|
-
this.fadeMs = options.fadeMs ??
|
|
11516
|
+
this.color = options.color ?? DEFAULT_COLOR3;
|
|
11517
|
+
this.width = options.width ?? DEFAULT_WIDTH2;
|
|
11518
|
+
this.fadeMs = options.fadeMs ?? DEFAULT_FADE_MS2;
|
|
11152
11519
|
}
|
|
11153
11520
|
now() {
|
|
11154
11521
|
return performance.now();
|
|
@@ -11163,6 +11530,7 @@ var LaserTool = class {
|
|
|
11163
11530
|
}
|
|
11164
11531
|
this.trail = [];
|
|
11165
11532
|
this.drawing = false;
|
|
11533
|
+
this.pendingEmission = [];
|
|
11166
11534
|
ctx.setCursor?.("default");
|
|
11167
11535
|
ctx.requestRender();
|
|
11168
11536
|
}
|
|
@@ -11178,6 +11546,16 @@ var LaserTool = class {
|
|
|
11178
11546
|
this.optionListeners.add(listener);
|
|
11179
11547
|
return () => this.optionListeners.delete(listener);
|
|
11180
11548
|
}
|
|
11549
|
+
/**
|
|
11550
|
+
* Subscribes to coalesced trail emissions. Points recorded by pointer input
|
|
11551
|
+
* are batched and delivered at most once per animation frame, keeping
|
|
11552
|
+
* outgoing presence packets small. Listeners must not throw; a throwing
|
|
11553
|
+
* listener is isolated so it cannot stall the fade loop.
|
|
11554
|
+
*/
|
|
11555
|
+
onTrail(listener) {
|
|
11556
|
+
this.trailListeners.add(listener);
|
|
11557
|
+
return () => this.trailListeners.delete(listener);
|
|
11558
|
+
}
|
|
11181
11559
|
setOptions(options) {
|
|
11182
11560
|
if (options.color !== void 0) this.color = options.color;
|
|
11183
11561
|
if (options.width !== void 0) this.width = options.width;
|
|
@@ -11186,14 +11564,18 @@ var LaserTool = class {
|
|
|
11186
11564
|
}
|
|
11187
11565
|
onPointerDown(state, ctx) {
|
|
11188
11566
|
this.drawing = true;
|
|
11189
|
-
|
|
11190
|
-
this.trail.push({ x: world.x, y: world.y, t: this.now() });
|
|
11191
|
-
this.ensureAnimating(ctx);
|
|
11567
|
+
this.recordPoint(state, ctx);
|
|
11192
11568
|
}
|
|
11193
11569
|
onPointerMove(state, ctx) {
|
|
11194
11570
|
if (!this.drawing) return;
|
|
11571
|
+
this.recordPoint(state, ctx);
|
|
11572
|
+
}
|
|
11573
|
+
recordPoint(state, ctx) {
|
|
11195
11574
|
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
11196
11575
|
this.trail.push({ x: world.x, y: world.y, t: this.now() });
|
|
11576
|
+
if (this.trailListeners.size > 0) {
|
|
11577
|
+
this.pendingEmission.push({ x: world.x, y: world.y });
|
|
11578
|
+
}
|
|
11197
11579
|
this.ensureAnimating(ctx);
|
|
11198
11580
|
}
|
|
11199
11581
|
onPointerUp(_state, _ctx) {
|
|
@@ -11225,7 +11607,24 @@ var LaserTool = class {
|
|
|
11225
11607
|
this.rafId = requestAnimationFrame(() => this.tick(ctx));
|
|
11226
11608
|
}
|
|
11227
11609
|
}
|
|
11610
|
+
flushEmission() {
|
|
11611
|
+
if (this.pendingEmission.length === 0) return;
|
|
11612
|
+
const emission = {
|
|
11613
|
+
points: this.pendingEmission,
|
|
11614
|
+
color: this.color,
|
|
11615
|
+
width: this.width,
|
|
11616
|
+
fadeMs: this.fadeMs
|
|
11617
|
+
};
|
|
11618
|
+
this.pendingEmission = [];
|
|
11619
|
+
for (const listener of this.trailListeners) {
|
|
11620
|
+
try {
|
|
11621
|
+
listener(emission);
|
|
11622
|
+
} catch {
|
|
11623
|
+
}
|
|
11624
|
+
}
|
|
11625
|
+
}
|
|
11228
11626
|
tick(ctx) {
|
|
11627
|
+
this.flushEmission();
|
|
11229
11628
|
const cutoff = this.now() - this.fadeMs;
|
|
11230
11629
|
this.trail = this.trail.filter((p) => p.t >= cutoff);
|
|
11231
11630
|
if (this.trail.length > 0) {
|
|
@@ -11241,8 +11640,128 @@ var LaserTool = class {
|
|
|
11241
11640
|
}
|
|
11242
11641
|
};
|
|
11243
11642
|
|
|
11643
|
+
// src/tools/ping-tool.ts
|
|
11644
|
+
var DEFAULT_COLOR4 = "#ff3b30";
|
|
11645
|
+
var DEFAULT_DURATION_MS2 = 1800;
|
|
11646
|
+
var DEFAULT_RADIUS3 = 48;
|
|
11647
|
+
var DEFAULT_MIN_INTERVAL_MS = 300;
|
|
11648
|
+
var PingTool = class {
|
|
11649
|
+
name;
|
|
11650
|
+
color;
|
|
11651
|
+
durationMs;
|
|
11652
|
+
radius;
|
|
11653
|
+
minIntervalMs;
|
|
11654
|
+
pings = [];
|
|
11655
|
+
lastEmitAt = -Infinity;
|
|
11656
|
+
rafId = null;
|
|
11657
|
+
optionListeners = /* @__PURE__ */ new Set();
|
|
11658
|
+
pingListeners = /* @__PURE__ */ new Set();
|
|
11659
|
+
constructor(options = {}) {
|
|
11660
|
+
this.name = options.name ?? "ping";
|
|
11661
|
+
this.color = options.color ?? DEFAULT_COLOR4;
|
|
11662
|
+
this.durationMs = options.durationMs ?? DEFAULT_DURATION_MS2;
|
|
11663
|
+
this.radius = options.radius ?? DEFAULT_RADIUS3;
|
|
11664
|
+
this.minIntervalMs = options.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS;
|
|
11665
|
+
}
|
|
11666
|
+
now() {
|
|
11667
|
+
return performance.now();
|
|
11668
|
+
}
|
|
11669
|
+
onActivate(ctx) {
|
|
11670
|
+
ctx.setCursor?.("crosshair");
|
|
11671
|
+
}
|
|
11672
|
+
onDeactivate(ctx) {
|
|
11673
|
+
if (this.rafId !== null) {
|
|
11674
|
+
cancelAnimationFrame(this.rafId);
|
|
11675
|
+
this.rafId = null;
|
|
11676
|
+
}
|
|
11677
|
+
this.pings = [];
|
|
11678
|
+
ctx.setCursor?.("default");
|
|
11679
|
+
ctx.requestRender();
|
|
11680
|
+
}
|
|
11681
|
+
getOptions() {
|
|
11682
|
+
return {
|
|
11683
|
+
name: this.name,
|
|
11684
|
+
color: this.color,
|
|
11685
|
+
durationMs: this.durationMs,
|
|
11686
|
+
radius: this.radius,
|
|
11687
|
+
minIntervalMs: this.minIntervalMs
|
|
11688
|
+
};
|
|
11689
|
+
}
|
|
11690
|
+
setOptions(options) {
|
|
11691
|
+
if (options.color !== void 0) this.color = options.color;
|
|
11692
|
+
if (options.durationMs !== void 0) this.durationMs = options.durationMs;
|
|
11693
|
+
if (options.radius !== void 0) this.radius = options.radius;
|
|
11694
|
+
if (options.minIntervalMs !== void 0) this.minIntervalMs = options.minIntervalMs;
|
|
11695
|
+
this.notifyOptionsChange();
|
|
11696
|
+
}
|
|
11697
|
+
onOptionsChange(listener) {
|
|
11698
|
+
this.optionListeners.add(listener);
|
|
11699
|
+
return () => this.optionListeners.delete(listener);
|
|
11700
|
+
}
|
|
11701
|
+
/**
|
|
11702
|
+
* Subscribes to accepted pings. Listeners must not throw; a throwing
|
|
11703
|
+
* listener is isolated so it cannot break the tap handling or other
|
|
11704
|
+
* listeners.
|
|
11705
|
+
*/
|
|
11706
|
+
onPing(listener) {
|
|
11707
|
+
this.pingListeners.add(listener);
|
|
11708
|
+
return () => this.pingListeners.delete(listener);
|
|
11709
|
+
}
|
|
11710
|
+
onPointerDown(state, ctx) {
|
|
11711
|
+
const t = this.now();
|
|
11712
|
+
if (t - this.lastEmitAt < this.minIntervalMs) return;
|
|
11713
|
+
this.lastEmitAt = t;
|
|
11714
|
+
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
11715
|
+
this.pings.push({ x: world.x, y: world.y, t });
|
|
11716
|
+
const emission = {
|
|
11717
|
+
x: world.x,
|
|
11718
|
+
y: world.y,
|
|
11719
|
+
color: this.color,
|
|
11720
|
+
durationMs: this.durationMs,
|
|
11721
|
+
radius: this.radius
|
|
11722
|
+
};
|
|
11723
|
+
for (const listener of this.pingListeners) {
|
|
11724
|
+
try {
|
|
11725
|
+
listener(emission);
|
|
11726
|
+
} catch {
|
|
11727
|
+
}
|
|
11728
|
+
}
|
|
11729
|
+
this.ensureAnimating(ctx);
|
|
11730
|
+
ctx.requestRender();
|
|
11731
|
+
}
|
|
11732
|
+
onPointerMove(_state, _ctx) {
|
|
11733
|
+
}
|
|
11734
|
+
onPointerUp(_state, _ctx) {
|
|
11735
|
+
}
|
|
11736
|
+
renderOverlay(ctx) {
|
|
11737
|
+
if (this.pings.length === 0) return;
|
|
11738
|
+
const now = this.now();
|
|
11739
|
+
for (const ping of this.pings) {
|
|
11740
|
+
renderPingPulse(ctx, ping.x, ping.y, now - ping.t, {
|
|
11741
|
+
color: this.color,
|
|
11742
|
+
durationMs: this.durationMs,
|
|
11743
|
+
radius: this.radius
|
|
11744
|
+
});
|
|
11745
|
+
}
|
|
11746
|
+
}
|
|
11747
|
+
ensureAnimating(ctx) {
|
|
11748
|
+
if (this.rafId === null) {
|
|
11749
|
+
this.rafId = requestAnimationFrame(() => this.tick(ctx));
|
|
11750
|
+
}
|
|
11751
|
+
}
|
|
11752
|
+
tick(ctx) {
|
|
11753
|
+
const cutoff = this.now() - this.durationMs;
|
|
11754
|
+
this.pings = this.pings.filter((ping) => ping.t > cutoff);
|
|
11755
|
+
ctx.requestRender();
|
|
11756
|
+
this.rafId = this.pings.length > 0 ? requestAnimationFrame(() => this.tick(ctx)) : null;
|
|
11757
|
+
}
|
|
11758
|
+
notifyOptionsChange() {
|
|
11759
|
+
for (const listener of this.optionListeners) listener();
|
|
11760
|
+
}
|
|
11761
|
+
};
|
|
11762
|
+
|
|
11244
11763
|
// src/index.ts
|
|
11245
|
-
var VERSION = "0.
|
|
11764
|
+
var VERSION = "0.55.0";
|
|
11246
11765
|
// Annotate the CommonJS export names for ESM import in node:
|
|
11247
11766
|
0 && (module.exports = {
|
|
11248
11767
|
ArrowTool,
|
|
@@ -11255,13 +11774,18 @@ var VERSION = "0.53.0";
|
|
|
11255
11774
|
HistoryStack,
|
|
11256
11775
|
ImageTool,
|
|
11257
11776
|
IndexedDBAdapter,
|
|
11777
|
+
LASER_TRAIL_PRESENCE_KIND,
|
|
11258
11778
|
LaserTool,
|
|
11259
11779
|
LayerManager,
|
|
11260
11780
|
LocalStorageAdapter,
|
|
11261
11781
|
MeasureTool,
|
|
11262
11782
|
MemoryAdapter,
|
|
11263
11783
|
NoteTool,
|
|
11784
|
+
PING_PRESENCE_KIND,
|
|
11264
11785
|
PencilTool,
|
|
11786
|
+
PingTool,
|
|
11787
|
+
RemoteLaserOverlay,
|
|
11788
|
+
RemotePingOverlay,
|
|
11265
11789
|
SelectTool,
|
|
11266
11790
|
ShapeTool,
|
|
11267
11791
|
TemplateTool,
|
|
@@ -11297,12 +11821,16 @@ var VERSION = "0.53.0";
|
|
|
11297
11821
|
getHexCellsInRectangle,
|
|
11298
11822
|
getHexCellsInSquare,
|
|
11299
11823
|
getHexDistance,
|
|
11824
|
+
isLaserTrailPresence,
|
|
11300
11825
|
isNearBezier,
|
|
11826
|
+
isPingPresence,
|
|
11301
11827
|
setFontSize,
|
|
11302
11828
|
smartSnap,
|
|
11303
11829
|
snapPoint,
|
|
11304
11830
|
snapToHexCenter,
|
|
11305
11831
|
styleToPatch,
|
|
11832
|
+
toLaserTrailPresence,
|
|
11833
|
+
toPingPresence,
|
|
11306
11834
|
toggleBold,
|
|
11307
11835
|
toggleItalic,
|
|
11308
11836
|
toggleStrikethrough,
|