@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.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 DEFAULT_RADIUS = 20;
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 ?? DEFAULT_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 DEFAULT_COLOR2 = "#ff3b30";
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 ?? DEFAULT_COLOR2;
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.54.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,