@fieldnotes/core 0.54.0 → 0.56.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 +492 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +259 -3
- package/dist/index.d.ts +259 -3
- package/dist/index.js +486 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8675,6 +8675,361 @@ var RemoteLaserOverlay = class {
|
|
|
8675
8675
|
}
|
|
8676
8676
|
};
|
|
8677
8677
|
|
|
8678
|
+
// src/canvas/ping-pulse.ts
|
|
8679
|
+
var RIPPLE_OFFSETS = [0, 0.25];
|
|
8680
|
+
function easeOutCubic(t) {
|
|
8681
|
+
const inv = 1 - t;
|
|
8682
|
+
return 1 - inv * inv * inv;
|
|
8683
|
+
}
|
|
8684
|
+
function renderPingPulse(ctx, x, y, ageMs, style) {
|
|
8685
|
+
if (ageMs < 0 || ageMs >= style.durationMs) return;
|
|
8686
|
+
ctx.save();
|
|
8687
|
+
ctx.strokeStyle = style.color;
|
|
8688
|
+
ctx.fillStyle = style.color;
|
|
8689
|
+
for (const offset of RIPPLE_OFFSETS) {
|
|
8690
|
+
const rippleSpan = style.durationMs * (1 - offset);
|
|
8691
|
+
const rippleAge = ageMs - style.durationMs * offset;
|
|
8692
|
+
if (rippleAge < 0) continue;
|
|
8693
|
+
const progress = Math.min(1, rippleAge / rippleSpan);
|
|
8694
|
+
const rippleRadius = style.radius * easeOutCubic(progress);
|
|
8695
|
+
if (rippleRadius <= 0) continue;
|
|
8696
|
+
ctx.globalAlpha = Math.max(0, 1 - progress);
|
|
8697
|
+
ctx.lineWidth = Math.max(1.5, style.radius / 12);
|
|
8698
|
+
ctx.beginPath();
|
|
8699
|
+
ctx.arc(x, y, rippleRadius, 0, Math.PI * 2);
|
|
8700
|
+
ctx.stroke();
|
|
8701
|
+
}
|
|
8702
|
+
const dotProgress = ageMs / style.durationMs;
|
|
8703
|
+
ctx.globalAlpha = Math.max(0, 1 - Math.max(0, dotProgress - 0.5) * 2);
|
|
8704
|
+
ctx.beginPath();
|
|
8705
|
+
ctx.arc(x, y, Math.max(2, style.radius / 8), 0, Math.PI * 2);
|
|
8706
|
+
ctx.fill();
|
|
8707
|
+
ctx.restore();
|
|
8708
|
+
}
|
|
8709
|
+
|
|
8710
|
+
// src/canvas/remote-ping-overlay.ts
|
|
8711
|
+
var PING_PRESENCE_KIND = "ping";
|
|
8712
|
+
function isPingPresence(data) {
|
|
8713
|
+
if (typeof data !== "object" || data === null) return false;
|
|
8714
|
+
const payload = data;
|
|
8715
|
+
if (payload.kind !== PING_PRESENCE_KIND) return false;
|
|
8716
|
+
if (typeof payload.x !== "number" || !Number.isFinite(payload.x)) return false;
|
|
8717
|
+
if (typeof payload.y !== "number" || !Number.isFinite(payload.y)) return false;
|
|
8718
|
+
if (payload.color !== void 0 && typeof payload.color !== "string") return false;
|
|
8719
|
+
if (payload.durationMs !== void 0 && !(typeof payload.durationMs === "number" && Number.isFinite(payload.durationMs) && payload.durationMs > 0)) {
|
|
8720
|
+
return false;
|
|
8721
|
+
}
|
|
8722
|
+
if (payload.radius !== void 0 && !(typeof payload.radius === "number" && Number.isFinite(payload.radius) && payload.radius > 0)) {
|
|
8723
|
+
return false;
|
|
8724
|
+
}
|
|
8725
|
+
return true;
|
|
8726
|
+
}
|
|
8727
|
+
function toPingPresence(emission) {
|
|
8728
|
+
return {
|
|
8729
|
+
kind: PING_PRESENCE_KIND,
|
|
8730
|
+
x: emission.x,
|
|
8731
|
+
y: emission.y,
|
|
8732
|
+
color: emission.color,
|
|
8733
|
+
durationMs: emission.durationMs,
|
|
8734
|
+
radius: emission.radius
|
|
8735
|
+
};
|
|
8736
|
+
}
|
|
8737
|
+
var DEFAULT_COLOR2 = "#ff3b30";
|
|
8738
|
+
var DEFAULT_DURATION_MS = 1800;
|
|
8739
|
+
var DEFAULT_RADIUS = 48;
|
|
8740
|
+
var DEFAULT_MAX_PINGS = 8;
|
|
8741
|
+
var RemotePingOverlay = class {
|
|
8742
|
+
host;
|
|
8743
|
+
color;
|
|
8744
|
+
durationMs;
|
|
8745
|
+
radius;
|
|
8746
|
+
maxPingsPerSender;
|
|
8747
|
+
pings = /* @__PURE__ */ new Map();
|
|
8748
|
+
unregister;
|
|
8749
|
+
rafId = null;
|
|
8750
|
+
disposed = false;
|
|
8751
|
+
constructor(host, options = {}) {
|
|
8752
|
+
this.host = host;
|
|
8753
|
+
this.color = options.color ?? DEFAULT_COLOR2;
|
|
8754
|
+
this.durationMs = options.durationMs ?? DEFAULT_DURATION_MS;
|
|
8755
|
+
this.radius = options.radius ?? DEFAULT_RADIUS;
|
|
8756
|
+
this.maxPingsPerSender = options.maxPingsPerSender ?? DEFAULT_MAX_PINGS;
|
|
8757
|
+
this.unregister = host.registerOverlay((ctx) => this.renderPings(ctx));
|
|
8758
|
+
}
|
|
8759
|
+
now() {
|
|
8760
|
+
return performance.now();
|
|
8761
|
+
}
|
|
8762
|
+
/**
|
|
8763
|
+
* Applies a presence payload from `sender` (any opaque per-sender key, e.g.
|
|
8764
|
+
* the envelope `from`). Non-ping or malformed payloads are ignored and
|
|
8765
|
+
* reported as `false`, so hosts can feed every presence frame through.
|
|
8766
|
+
*/
|
|
8767
|
+
apply(sender, data) {
|
|
8768
|
+
if (this.disposed || !isPingPresence(data)) return false;
|
|
8769
|
+
let senderPings = this.pings.get(sender);
|
|
8770
|
+
if (!senderPings) {
|
|
8771
|
+
senderPings = [];
|
|
8772
|
+
this.pings.set(sender, senderPings);
|
|
8773
|
+
}
|
|
8774
|
+
senderPings.push({
|
|
8775
|
+
x: data.x,
|
|
8776
|
+
y: data.y,
|
|
8777
|
+
t: this.now(),
|
|
8778
|
+
color: data.color ?? this.color,
|
|
8779
|
+
durationMs: data.durationMs ?? this.durationMs,
|
|
8780
|
+
radius: data.radius ?? this.radius
|
|
8781
|
+
});
|
|
8782
|
+
const excess = senderPings.length - this.maxPingsPerSender;
|
|
8783
|
+
if (excess > 0) senderPings.splice(0, excess);
|
|
8784
|
+
this.ensureAnimating();
|
|
8785
|
+
this.host.requestRender();
|
|
8786
|
+
return true;
|
|
8787
|
+
}
|
|
8788
|
+
/** Removes a sender's pings immediately (presence-leave/disconnect). */
|
|
8789
|
+
remove(sender) {
|
|
8790
|
+
if (this.pings.delete(sender)) this.host.requestRender();
|
|
8791
|
+
}
|
|
8792
|
+
/** Removes every ping immediately. */
|
|
8793
|
+
clear() {
|
|
8794
|
+
if (this.pings.size === 0) return;
|
|
8795
|
+
this.pings.clear();
|
|
8796
|
+
this.host.requestRender();
|
|
8797
|
+
}
|
|
8798
|
+
/** Number of senders with a live (unexpired) ping. */
|
|
8799
|
+
get activeSenderCount() {
|
|
8800
|
+
return this.pings.size;
|
|
8801
|
+
}
|
|
8802
|
+
/** Unregisters the overlay and stops the animation loop. Idempotent. */
|
|
8803
|
+
dispose() {
|
|
8804
|
+
if (this.disposed) return;
|
|
8805
|
+
this.disposed = true;
|
|
8806
|
+
if (this.rafId !== null) {
|
|
8807
|
+
cancelAnimationFrame(this.rafId);
|
|
8808
|
+
this.rafId = null;
|
|
8809
|
+
}
|
|
8810
|
+
this.pings.clear();
|
|
8811
|
+
this.unregister?.();
|
|
8812
|
+
this.unregister = null;
|
|
8813
|
+
}
|
|
8814
|
+
ensureAnimating() {
|
|
8815
|
+
if (this.rafId === null) {
|
|
8816
|
+
this.rafId = requestAnimationFrame(() => this.tick());
|
|
8817
|
+
}
|
|
8818
|
+
}
|
|
8819
|
+
tick() {
|
|
8820
|
+
if (this.disposed) return;
|
|
8821
|
+
const now = this.now();
|
|
8822
|
+
for (const [sender, senderPings] of this.pings) {
|
|
8823
|
+
const live = senderPings.filter((ping) => now - ping.t < ping.durationMs);
|
|
8824
|
+
if (live.length === 0) {
|
|
8825
|
+
this.pings.delete(sender);
|
|
8826
|
+
} else {
|
|
8827
|
+
this.pings.set(sender, live);
|
|
8828
|
+
}
|
|
8829
|
+
}
|
|
8830
|
+
this.host.requestRender();
|
|
8831
|
+
this.rafId = this.pings.size > 0 ? requestAnimationFrame(() => this.tick()) : null;
|
|
8832
|
+
}
|
|
8833
|
+
renderPings(ctx) {
|
|
8834
|
+
if (this.pings.size === 0) return;
|
|
8835
|
+
const now = this.now();
|
|
8836
|
+
for (const senderPings of this.pings.values()) {
|
|
8837
|
+
for (const ping of senderPings) {
|
|
8838
|
+
renderPingPulse(ctx, ping.x, ping.y, now - ping.t, {
|
|
8839
|
+
color: ping.color,
|
|
8840
|
+
durationMs: ping.durationMs,
|
|
8841
|
+
radius: ping.radius
|
|
8842
|
+
});
|
|
8843
|
+
}
|
|
8844
|
+
}
|
|
8845
|
+
}
|
|
8846
|
+
};
|
|
8847
|
+
|
|
8848
|
+
// src/canvas/ping-input.ts
|
|
8849
|
+
var DEFAULT_LONG_PRESS_MS = 600;
|
|
8850
|
+
var DEFAULT_SLOP_PX = 8;
|
|
8851
|
+
var DEFAULT_COLOR3 = "#ff3b30";
|
|
8852
|
+
var DEFAULT_DURATION_MS2 = 1800;
|
|
8853
|
+
var DEFAULT_RADIUS2 = 48;
|
|
8854
|
+
var DEFAULT_MIN_INTERVAL_MS = 300;
|
|
8855
|
+
var PingInput = class {
|
|
8856
|
+
element;
|
|
8857
|
+
host;
|
|
8858
|
+
longPressEnabled;
|
|
8859
|
+
longPressMs;
|
|
8860
|
+
slopPx;
|
|
8861
|
+
color;
|
|
8862
|
+
durationMs;
|
|
8863
|
+
radius;
|
|
8864
|
+
minIntervalMs;
|
|
8865
|
+
shouldPing;
|
|
8866
|
+
press = null;
|
|
8867
|
+
downPointers = /* @__PURE__ */ new Set();
|
|
8868
|
+
lastPointerScreen = null;
|
|
8869
|
+
lastEmitAt = -Infinity;
|
|
8870
|
+
disposed = false;
|
|
8871
|
+
optionListeners = /* @__PURE__ */ new Set();
|
|
8872
|
+
pingListeners = /* @__PURE__ */ new Set();
|
|
8873
|
+
handlePointerDown = (e) => this.onPointerDown(e);
|
|
8874
|
+
handlePointerMove = (e) => this.onPointerMove(e);
|
|
8875
|
+
handlePointerUp = (e) => this.onPointerEnd(e);
|
|
8876
|
+
handlePointerCancel = (e) => this.onPointerEnd(e);
|
|
8877
|
+
handlePointerLeave = (e) => this.onPointerEnd(e);
|
|
8878
|
+
constructor(element, host, options = {}) {
|
|
8879
|
+
this.element = element;
|
|
8880
|
+
this.host = host;
|
|
8881
|
+
this.longPressEnabled = options.longPressEnabled ?? false;
|
|
8882
|
+
this.longPressMs = options.longPressMs ?? DEFAULT_LONG_PRESS_MS;
|
|
8883
|
+
this.slopPx = options.slopPx ?? DEFAULT_SLOP_PX;
|
|
8884
|
+
this.color = options.color ?? DEFAULT_COLOR3;
|
|
8885
|
+
this.durationMs = options.durationMs ?? DEFAULT_DURATION_MS2;
|
|
8886
|
+
this.radius = options.radius ?? DEFAULT_RADIUS2;
|
|
8887
|
+
this.minIntervalMs = options.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS;
|
|
8888
|
+
this.shouldPing = options.shouldPing;
|
|
8889
|
+
const opts = { passive: true };
|
|
8890
|
+
element.addEventListener("pointerdown", this.handlePointerDown, opts);
|
|
8891
|
+
element.addEventListener("pointermove", this.handlePointerMove, opts);
|
|
8892
|
+
element.addEventListener("pointerup", this.handlePointerUp, opts);
|
|
8893
|
+
element.addEventListener("pointercancel", this.handlePointerCancel, opts);
|
|
8894
|
+
element.addEventListener("pointerleave", this.handlePointerLeave, opts);
|
|
8895
|
+
}
|
|
8896
|
+
now() {
|
|
8897
|
+
return performance.now();
|
|
8898
|
+
}
|
|
8899
|
+
getOptions() {
|
|
8900
|
+
return {
|
|
8901
|
+
longPressEnabled: this.longPressEnabled,
|
|
8902
|
+
longPressMs: this.longPressMs,
|
|
8903
|
+
slopPx: this.slopPx,
|
|
8904
|
+
color: this.color,
|
|
8905
|
+
durationMs: this.durationMs,
|
|
8906
|
+
radius: this.radius,
|
|
8907
|
+
minIntervalMs: this.minIntervalMs,
|
|
8908
|
+
...this.shouldPing ? { shouldPing: this.shouldPing } : {}
|
|
8909
|
+
};
|
|
8910
|
+
}
|
|
8911
|
+
setOptions(options) {
|
|
8912
|
+
if (options.longPressEnabled !== void 0) {
|
|
8913
|
+
this.longPressEnabled = options.longPressEnabled;
|
|
8914
|
+
if (!this.longPressEnabled) this.cancelPress();
|
|
8915
|
+
}
|
|
8916
|
+
if (options.longPressMs !== void 0) this.longPressMs = options.longPressMs;
|
|
8917
|
+
if (options.slopPx !== void 0) this.slopPx = options.slopPx;
|
|
8918
|
+
if (options.color !== void 0) this.color = options.color;
|
|
8919
|
+
if (options.durationMs !== void 0) this.durationMs = options.durationMs;
|
|
8920
|
+
if (options.radius !== void 0) this.radius = options.radius;
|
|
8921
|
+
if (options.minIntervalMs !== void 0) this.minIntervalMs = options.minIntervalMs;
|
|
8922
|
+
if (options.shouldPing !== void 0) this.shouldPing = options.shouldPing;
|
|
8923
|
+
for (const listener of this.optionListeners) listener();
|
|
8924
|
+
}
|
|
8925
|
+
onOptionsChange(listener) {
|
|
8926
|
+
this.optionListeners.add(listener);
|
|
8927
|
+
return () => this.optionListeners.delete(listener);
|
|
8928
|
+
}
|
|
8929
|
+
/**
|
|
8930
|
+
* Subscribes to emitted pings. Listeners must not throw; a throwing
|
|
8931
|
+
* listener is isolated so it cannot break input handling or other
|
|
8932
|
+
* listeners.
|
|
8933
|
+
*/
|
|
8934
|
+
onPing(listener) {
|
|
8935
|
+
this.pingListeners.add(listener);
|
|
8936
|
+
return () => this.pingListeners.delete(listener);
|
|
8937
|
+
}
|
|
8938
|
+
/**
|
|
8939
|
+
* Pings at the last tracked pointer position (hover moves and presses),
|
|
8940
|
+
* world-converted at call time. Returns `false` when no pointer has been
|
|
8941
|
+
* seen yet, the host vetoes, or the rate limit drops the ping.
|
|
8942
|
+
*/
|
|
8943
|
+
pingAtPointer() {
|
|
8944
|
+
if (this.disposed || this.lastPointerScreen === null) return false;
|
|
8945
|
+
return this.emit(this.host.screenToWorld(this.lastPointerScreen));
|
|
8946
|
+
}
|
|
8947
|
+
/** Pings a world position directly. Same veto and rate limit as every path. */
|
|
8948
|
+
pingAt(world) {
|
|
8949
|
+
if (this.disposed) return false;
|
|
8950
|
+
return this.emit(world);
|
|
8951
|
+
}
|
|
8952
|
+
/** Removes all DOM listeners and cancels any pending press. Idempotent. */
|
|
8953
|
+
dispose() {
|
|
8954
|
+
if (this.disposed) return;
|
|
8955
|
+
this.disposed = true;
|
|
8956
|
+
this.cancelPress();
|
|
8957
|
+
this.downPointers.clear();
|
|
8958
|
+
this.element.removeEventListener("pointerdown", this.handlePointerDown);
|
|
8959
|
+
this.element.removeEventListener("pointermove", this.handlePointerMove);
|
|
8960
|
+
this.element.removeEventListener("pointerup", this.handlePointerUp);
|
|
8961
|
+
this.element.removeEventListener("pointercancel", this.handlePointerCancel);
|
|
8962
|
+
this.element.removeEventListener("pointerleave", this.handlePointerLeave);
|
|
8963
|
+
this.optionListeners.clear();
|
|
8964
|
+
this.pingListeners.clear();
|
|
8965
|
+
}
|
|
8966
|
+
toLocal(e) {
|
|
8967
|
+
const rect = this.element.getBoundingClientRect();
|
|
8968
|
+
return { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
|
8969
|
+
}
|
|
8970
|
+
onPointerDown(e) {
|
|
8971
|
+
const local = this.toLocal(e);
|
|
8972
|
+
this.lastPointerScreen = local;
|
|
8973
|
+
const hadPointers = this.downPointers.size > 0;
|
|
8974
|
+
this.downPointers.add(e.pointerId);
|
|
8975
|
+
if (hadPointers) {
|
|
8976
|
+
this.cancelPress();
|
|
8977
|
+
return;
|
|
8978
|
+
}
|
|
8979
|
+
if (!this.longPressEnabled) return;
|
|
8980
|
+
if (e.pointerType === "mouse" && e.button !== 0) return;
|
|
8981
|
+
this.press = {
|
|
8982
|
+
pointerId: e.pointerId,
|
|
8983
|
+
x: local.x,
|
|
8984
|
+
y: local.y,
|
|
8985
|
+
timer: setTimeout(() => this.firePress(), this.longPressMs)
|
|
8986
|
+
};
|
|
8987
|
+
}
|
|
8988
|
+
onPointerMove(e) {
|
|
8989
|
+
const local = this.toLocal(e);
|
|
8990
|
+
this.lastPointerScreen = local;
|
|
8991
|
+
if (this.press === null || e.pointerId !== this.press.pointerId) return;
|
|
8992
|
+
const dx = local.x - this.press.x;
|
|
8993
|
+
const dy = local.y - this.press.y;
|
|
8994
|
+
if (Math.hypot(dx, dy) > this.slopPx) this.cancelPress();
|
|
8995
|
+
}
|
|
8996
|
+
onPointerEnd(e) {
|
|
8997
|
+
this.downPointers.delete(e.pointerId);
|
|
8998
|
+
if (this.press !== null && e.pointerId === this.press.pointerId) this.cancelPress();
|
|
8999
|
+
}
|
|
9000
|
+
cancelPress() {
|
|
9001
|
+
if (this.press === null) return;
|
|
9002
|
+
clearTimeout(this.press.timer);
|
|
9003
|
+
this.press = null;
|
|
9004
|
+
}
|
|
9005
|
+
firePress() {
|
|
9006
|
+
if (this.press === null) return;
|
|
9007
|
+
const screen = { x: this.press.x, y: this.press.y };
|
|
9008
|
+
this.press = null;
|
|
9009
|
+
this.emit(this.host.screenToWorld(screen));
|
|
9010
|
+
}
|
|
9011
|
+
emit(world) {
|
|
9012
|
+
if (this.shouldPing && !this.shouldPing()) return false;
|
|
9013
|
+
const t = this.now();
|
|
9014
|
+
if (t - this.lastEmitAt < this.minIntervalMs) return false;
|
|
9015
|
+
this.lastEmitAt = t;
|
|
9016
|
+
const emission = {
|
|
9017
|
+
x: world.x,
|
|
9018
|
+
y: world.y,
|
|
9019
|
+
color: this.color,
|
|
9020
|
+
durationMs: this.durationMs,
|
|
9021
|
+
radius: this.radius
|
|
9022
|
+
};
|
|
9023
|
+
for (const listener of this.pingListeners) {
|
|
9024
|
+
try {
|
|
9025
|
+
listener(emission);
|
|
9026
|
+
} catch {
|
|
9027
|
+
}
|
|
9028
|
+
}
|
|
9029
|
+
return true;
|
|
9030
|
+
}
|
|
9031
|
+
};
|
|
9032
|
+
|
|
8678
9033
|
// src/tools/hand-tool.ts
|
|
8679
9034
|
var HandTool = class {
|
|
8680
9035
|
name = "hand";
|
|
@@ -8936,7 +9291,7 @@ function erasePoints(points, eraser, radius) {
|
|
|
8936
9291
|
}
|
|
8937
9292
|
|
|
8938
9293
|
// src/tools/eraser-tool.ts
|
|
8939
|
-
var
|
|
9294
|
+
var DEFAULT_RADIUS3 = 20;
|
|
8940
9295
|
function makeEraserCursor(radius) {
|
|
8941
9296
|
const size = radius * 2;
|
|
8942
9297
|
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>`;
|
|
@@ -8949,7 +9304,7 @@ var EraserTool = class {
|
|
|
8949
9304
|
cursor;
|
|
8950
9305
|
mode;
|
|
8951
9306
|
constructor(options = {}) {
|
|
8952
|
-
this.radius = options.radius ??
|
|
9307
|
+
this.radius = options.radius ?? DEFAULT_RADIUS3;
|
|
8953
9308
|
this.cursor = makeEraserCursor(this.radius);
|
|
8954
9309
|
this.mode = options.mode ?? "partial";
|
|
8955
9310
|
}
|
|
@@ -11231,7 +11586,7 @@ var TemplateTool = class {
|
|
|
11231
11586
|
};
|
|
11232
11587
|
|
|
11233
11588
|
// src/tools/laser-tool.ts
|
|
11234
|
-
var
|
|
11589
|
+
var DEFAULT_COLOR4 = "#ff3b30";
|
|
11235
11590
|
var DEFAULT_WIDTH2 = 4;
|
|
11236
11591
|
var DEFAULT_FADE_MS2 = 1200;
|
|
11237
11592
|
var LaserTool = class {
|
|
@@ -11247,7 +11602,7 @@ var LaserTool = class {
|
|
|
11247
11602
|
pendingEmission = [];
|
|
11248
11603
|
constructor(options = {}) {
|
|
11249
11604
|
this.name = options.name ?? "laser";
|
|
11250
|
-
this.color = options.color ??
|
|
11605
|
+
this.color = options.color ?? DEFAULT_COLOR4;
|
|
11251
11606
|
this.width = options.width ?? DEFAULT_WIDTH2;
|
|
11252
11607
|
this.fadeMs = options.fadeMs ?? DEFAULT_FADE_MS2;
|
|
11253
11608
|
}
|
|
@@ -11374,8 +11729,128 @@ var LaserTool = class {
|
|
|
11374
11729
|
}
|
|
11375
11730
|
};
|
|
11376
11731
|
|
|
11732
|
+
// src/tools/ping-tool.ts
|
|
11733
|
+
var DEFAULT_COLOR5 = "#ff3b30";
|
|
11734
|
+
var DEFAULT_DURATION_MS3 = 1800;
|
|
11735
|
+
var DEFAULT_RADIUS4 = 48;
|
|
11736
|
+
var DEFAULT_MIN_INTERVAL_MS2 = 300;
|
|
11737
|
+
var PingTool = class {
|
|
11738
|
+
name;
|
|
11739
|
+
color;
|
|
11740
|
+
durationMs;
|
|
11741
|
+
radius;
|
|
11742
|
+
minIntervalMs;
|
|
11743
|
+
pings = [];
|
|
11744
|
+
lastEmitAt = -Infinity;
|
|
11745
|
+
rafId = null;
|
|
11746
|
+
optionListeners = /* @__PURE__ */ new Set();
|
|
11747
|
+
pingListeners = /* @__PURE__ */ new Set();
|
|
11748
|
+
constructor(options = {}) {
|
|
11749
|
+
this.name = options.name ?? "ping";
|
|
11750
|
+
this.color = options.color ?? DEFAULT_COLOR5;
|
|
11751
|
+
this.durationMs = options.durationMs ?? DEFAULT_DURATION_MS3;
|
|
11752
|
+
this.radius = options.radius ?? DEFAULT_RADIUS4;
|
|
11753
|
+
this.minIntervalMs = options.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS2;
|
|
11754
|
+
}
|
|
11755
|
+
now() {
|
|
11756
|
+
return performance.now();
|
|
11757
|
+
}
|
|
11758
|
+
onActivate(ctx) {
|
|
11759
|
+
ctx.setCursor?.("crosshair");
|
|
11760
|
+
}
|
|
11761
|
+
onDeactivate(ctx) {
|
|
11762
|
+
if (this.rafId !== null) {
|
|
11763
|
+
cancelAnimationFrame(this.rafId);
|
|
11764
|
+
this.rafId = null;
|
|
11765
|
+
}
|
|
11766
|
+
this.pings = [];
|
|
11767
|
+
ctx.setCursor?.("default");
|
|
11768
|
+
ctx.requestRender();
|
|
11769
|
+
}
|
|
11770
|
+
getOptions() {
|
|
11771
|
+
return {
|
|
11772
|
+
name: this.name,
|
|
11773
|
+
color: this.color,
|
|
11774
|
+
durationMs: this.durationMs,
|
|
11775
|
+
radius: this.radius,
|
|
11776
|
+
minIntervalMs: this.minIntervalMs
|
|
11777
|
+
};
|
|
11778
|
+
}
|
|
11779
|
+
setOptions(options) {
|
|
11780
|
+
if (options.color !== void 0) this.color = options.color;
|
|
11781
|
+
if (options.durationMs !== void 0) this.durationMs = options.durationMs;
|
|
11782
|
+
if (options.radius !== void 0) this.radius = options.radius;
|
|
11783
|
+
if (options.minIntervalMs !== void 0) this.minIntervalMs = options.minIntervalMs;
|
|
11784
|
+
this.notifyOptionsChange();
|
|
11785
|
+
}
|
|
11786
|
+
onOptionsChange(listener) {
|
|
11787
|
+
this.optionListeners.add(listener);
|
|
11788
|
+
return () => this.optionListeners.delete(listener);
|
|
11789
|
+
}
|
|
11790
|
+
/**
|
|
11791
|
+
* Subscribes to accepted pings. Listeners must not throw; a throwing
|
|
11792
|
+
* listener is isolated so it cannot break the tap handling or other
|
|
11793
|
+
* listeners.
|
|
11794
|
+
*/
|
|
11795
|
+
onPing(listener) {
|
|
11796
|
+
this.pingListeners.add(listener);
|
|
11797
|
+
return () => this.pingListeners.delete(listener);
|
|
11798
|
+
}
|
|
11799
|
+
onPointerDown(state, ctx) {
|
|
11800
|
+
const t = this.now();
|
|
11801
|
+
if (t - this.lastEmitAt < this.minIntervalMs) return;
|
|
11802
|
+
this.lastEmitAt = t;
|
|
11803
|
+
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
11804
|
+
this.pings.push({ x: world.x, y: world.y, t });
|
|
11805
|
+
const emission = {
|
|
11806
|
+
x: world.x,
|
|
11807
|
+
y: world.y,
|
|
11808
|
+
color: this.color,
|
|
11809
|
+
durationMs: this.durationMs,
|
|
11810
|
+
radius: this.radius
|
|
11811
|
+
};
|
|
11812
|
+
for (const listener of this.pingListeners) {
|
|
11813
|
+
try {
|
|
11814
|
+
listener(emission);
|
|
11815
|
+
} catch {
|
|
11816
|
+
}
|
|
11817
|
+
}
|
|
11818
|
+
this.ensureAnimating(ctx);
|
|
11819
|
+
ctx.requestRender();
|
|
11820
|
+
}
|
|
11821
|
+
onPointerMove(_state, _ctx) {
|
|
11822
|
+
}
|
|
11823
|
+
onPointerUp(_state, _ctx) {
|
|
11824
|
+
}
|
|
11825
|
+
renderOverlay(ctx) {
|
|
11826
|
+
if (this.pings.length === 0) return;
|
|
11827
|
+
const now = this.now();
|
|
11828
|
+
for (const ping of this.pings) {
|
|
11829
|
+
renderPingPulse(ctx, ping.x, ping.y, now - ping.t, {
|
|
11830
|
+
color: this.color,
|
|
11831
|
+
durationMs: this.durationMs,
|
|
11832
|
+
radius: this.radius
|
|
11833
|
+
});
|
|
11834
|
+
}
|
|
11835
|
+
}
|
|
11836
|
+
ensureAnimating(ctx) {
|
|
11837
|
+
if (this.rafId === null) {
|
|
11838
|
+
this.rafId = requestAnimationFrame(() => this.tick(ctx));
|
|
11839
|
+
}
|
|
11840
|
+
}
|
|
11841
|
+
tick(ctx) {
|
|
11842
|
+
const cutoff = this.now() - this.durationMs;
|
|
11843
|
+
this.pings = this.pings.filter((ping) => ping.t > cutoff);
|
|
11844
|
+
ctx.requestRender();
|
|
11845
|
+
this.rafId = this.pings.length > 0 ? requestAnimationFrame(() => this.tick(ctx)) : null;
|
|
11846
|
+
}
|
|
11847
|
+
notifyOptionsChange() {
|
|
11848
|
+
for (const listener of this.optionListeners) listener();
|
|
11849
|
+
}
|
|
11850
|
+
};
|
|
11851
|
+
|
|
11377
11852
|
// src/index.ts
|
|
11378
|
-
var VERSION = "0.
|
|
11853
|
+
var VERSION = "0.56.0";
|
|
11379
11854
|
export {
|
|
11380
11855
|
ArrowTool,
|
|
11381
11856
|
AutoSave,
|
|
@@ -11394,8 +11869,12 @@ export {
|
|
|
11394
11869
|
MeasureTool,
|
|
11395
11870
|
MemoryAdapter,
|
|
11396
11871
|
NoteTool,
|
|
11872
|
+
PING_PRESENCE_KIND,
|
|
11397
11873
|
PencilTool,
|
|
11874
|
+
PingInput,
|
|
11875
|
+
PingTool,
|
|
11398
11876
|
RemoteLaserOverlay,
|
|
11877
|
+
RemotePingOverlay,
|
|
11399
11878
|
SelectTool,
|
|
11400
11879
|
ShapeTool,
|
|
11401
11880
|
TemplateTool,
|
|
@@ -11433,12 +11912,14 @@ export {
|
|
|
11433
11912
|
getHexDistance,
|
|
11434
11913
|
isLaserTrailPresence,
|
|
11435
11914
|
isNearBezier,
|
|
11915
|
+
isPingPresence,
|
|
11436
11916
|
setFontSize,
|
|
11437
11917
|
smartSnap,
|
|
11438
11918
|
snapPoint,
|
|
11439
11919
|
snapToHexCenter,
|
|
11440
11920
|
styleToPatch,
|
|
11441
11921
|
toLaserTrailPresence,
|
|
11922
|
+
toPingPresence,
|
|
11442
11923
|
toggleBold,
|
|
11443
11924
|
toggleItalic,
|
|
11444
11925
|
toggleStrikethrough,
|