@fieldnotes/core 0.60.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 CHANGED
@@ -23,9 +23,11 @@ __export(index_exports, {
23
23
  ArrowTool: () => ArrowTool,
24
24
  AutoSave: () => AutoSave,
25
25
  Camera: () => Camera,
26
+ CameraAnimator: () => CameraAnimator,
26
27
  DEFAULT_NOTE_FONT_SIZE: () => DEFAULT_NOTE_FONT_SIZE,
27
28
  ElementStore: () => ElementStore,
28
29
  EraserTool: () => EraserTool,
30
+ FOCUS_PRESENCE_KIND: () => FOCUS_PRESENCE_KIND,
29
31
  HandTool: () => HandTool,
30
32
  HistoryStack: () => HistoryStack,
31
33
  ImageTool: () => ImageTool,
@@ -43,6 +45,7 @@ __export(index_exports, {
43
45
  PencilTool: () => PencilTool,
44
46
  PingInput: () => PingInput,
45
47
  PingTool: () => PingTool,
48
+ RemoteFocusReceiver: () => RemoteFocusReceiver,
46
49
  RemoteLaserOverlay: () => RemoteLaserOverlay,
47
50
  RemoteMeasureOverlay: () => RemoteMeasureOverlay,
48
51
  RemotePingOverlay: () => RemotePingOverlay,
@@ -53,7 +56,10 @@ __export(index_exports, {
53
56
  ToolManager: () => ToolManager,
54
57
  VERSION: () => VERSION,
55
58
  Viewport: () => Viewport,
59
+ applyCameraView: () => applyCameraView,
56
60
  boundsIntersect: () => boundsIntersect,
61
+ cameraOriginForView: () => cameraOriginForView,
62
+ captureCameraView: () => captureCameraView,
57
63
  createArrow: () => createArrow,
58
64
  createGrid: () => createGrid,
59
65
  createHtmlElement: () => createHtmlElement,
@@ -66,6 +72,7 @@ __export(index_exports, {
66
72
  drawHexPath: () => drawHexPath,
67
73
  exportImage: () => exportImage,
68
74
  exportSvg: () => exportSvg,
75
+ fitZoomForView: () => fitZoomForView,
69
76
  getActiveFormats: () => getActiveFormats,
70
77
  getArrowBounds: () => getArrowBounds,
71
78
  getArrowControlPoint: () => getArrowControlPoint,
@@ -81,6 +88,7 @@ __export(index_exports, {
81
88
  getHexCellsInRectangle: () => getHexCellsInRectangle,
82
89
  getHexCellsInSquare: () => getHexCellsInSquare,
83
90
  getHexDistance: () => getHexDistance,
91
+ isFocusPresence: () => isFocusPresence,
84
92
  isLaserTrailPresence: () => isLaserTrailPresence,
85
93
  isMeasurePresence: () => isMeasurePresence,
86
94
  isNearBezier: () => isNearBezier,
@@ -90,6 +98,7 @@ __export(index_exports, {
90
98
  snapPoint: () => snapPoint,
91
99
  snapToHexCenter: () => snapToHexCenter,
92
100
  styleToPatch: () => styleToPatch,
101
+ toFocusPresence: () => toFocusPresence,
93
102
  toLaserTrailPresence: () => toLaserTrailPresence,
94
103
  toMeasurePresence: () => toMeasurePresence,
95
104
  toPingPresence: () => toPingPresence,
@@ -8586,6 +8595,17 @@ var Viewport = class _Viewport {
8586
8595
  getVisibleRect() {
8587
8596
  return this.camera.getVisibleRect(this.canvasEl.clientWidth, this.canvasEl.clientHeight);
8588
8597
  }
8598
+ /**
8599
+ * Size in CSS pixels of the canvas that `getVisibleRect()` measures.
8600
+ * Exposed because `canvasEl` is private: consumers can only reach the
8601
+ * wrapper (via `domLayer.parentElement`), so without this accessor the
8602
+ * canonical size behind `getVisibleRect()` is unreachable and callers
8603
+ * resort to `getVisibleRect().w * camera.zoom`. Capture and restore must
8604
+ * measure the same element or saved views do not round-trip.
8605
+ */
8606
+ getCanvasSize() {
8607
+ return { w: this.canvasEl.clientWidth, h: this.canvasEl.clientHeight };
8608
+ }
8589
8609
  /** Centers the camera on a world point without changing zoom. */
8590
8610
  centerCameraAt(world) {
8591
8611
  const z = this.camera.zoom;
@@ -9734,6 +9754,361 @@ var PingInput = class {
9734
9754
  }
9735
9755
  };
9736
9756
 
9757
+ // src/canvas/camera-view.ts
9758
+ function assertValidView(view) {
9759
+ const { x, y, w, h } = view;
9760
+ if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(w) || !Number.isFinite(h)) {
9761
+ throw new Error("[fieldnotes] CameraView requires finite x, y, w, and h");
9762
+ }
9763
+ if (w <= 0 || h <= 0) {
9764
+ throw new Error("[fieldnotes] CameraView requires positive w and h");
9765
+ }
9766
+ }
9767
+ function assertCanvasDims(canvasW, canvasH) {
9768
+ if (!Number.isFinite(canvasW) || !Number.isFinite(canvasH)) {
9769
+ throw new Error("[fieldnotes] canvas dimensions must be finite");
9770
+ }
9771
+ if (canvasW < 0 || canvasH < 0) {
9772
+ throw new Error("[fieldnotes] canvas dimensions must not be negative");
9773
+ }
9774
+ }
9775
+ function canvasDimsUsable(canvasW, canvasH) {
9776
+ return Number.isFinite(canvasW) && Number.isFinite(canvasH) && canvasW >= 0 && canvasH >= 0;
9777
+ }
9778
+ function captureCameraView(viewport) {
9779
+ const rect = viewport.getVisibleRect();
9780
+ return { x: rect.x, y: rect.y, w: rect.w, h: rect.h };
9781
+ }
9782
+ function fitZoomForView(view, canvasW, canvasH) {
9783
+ assertValidView(view);
9784
+ assertCanvasDims(canvasW, canvasH);
9785
+ if (canvasW === 0 || canvasH === 0) {
9786
+ throw new Error("[fieldnotes] fitZoomForView requires a non-zero canvas size");
9787
+ }
9788
+ return Math.min(canvasW / view.w, canvasH / view.h);
9789
+ }
9790
+ function cameraOriginForView(view, zoom, canvasW, canvasH) {
9791
+ return {
9792
+ x: canvasW / 2 - (view.x + view.w / 2) * zoom,
9793
+ y: canvasH / 2 - (view.y + view.h / 2) * zoom
9794
+ };
9795
+ }
9796
+ function applyCameraView(camera, view, canvasW, canvasH) {
9797
+ assertValidView(view);
9798
+ assertCanvasDims(canvasW, canvasH);
9799
+ if (canvasW === 0 || canvasH === 0) return;
9800
+ camera.setZoom(fitZoomForView(view, canvasW, canvasH));
9801
+ const origin = cameraOriginForView(view, camera.zoom, canvasW, canvasH);
9802
+ camera.moveTo(origin.x, origin.y);
9803
+ }
9804
+
9805
+ // src/canvas/camera-animator.ts
9806
+ var DEFAULT_DURATION_MS3 = 400;
9807
+ var FRAMED_EPSILON = 1e-6;
9808
+ var easeOutCubic2 = (t) => 1 - Math.pow(1 - t, 3);
9809
+ function lerp(a, b, k) {
9810
+ return a + (b - a) * k;
9811
+ }
9812
+ function lerpView(from, to, k) {
9813
+ return {
9814
+ x: lerp(from.x, to.x, k),
9815
+ y: lerp(from.y, to.y, k),
9816
+ w: lerp(from.w, to.w, k),
9817
+ h: lerp(from.h, to.h, k)
9818
+ };
9819
+ }
9820
+ function viewsClose(a, b) {
9821
+ return Math.abs(a.x - b.x) < FRAMED_EPSILON && Math.abs(a.y - b.y) < FRAMED_EPSILON && Math.abs(a.w - b.w) < FRAMED_EPSILON && Math.abs(a.h - b.h) < FRAMED_EPSILON;
9822
+ }
9823
+ var CameraAnimator = class {
9824
+ camera;
9825
+ getCanvasSize;
9826
+ frames;
9827
+ now;
9828
+ durationMs;
9829
+ easing;
9830
+ rafId = null;
9831
+ from = null;
9832
+ to = null;
9833
+ startedAt = 0;
9834
+ endListeners = /* @__PURE__ */ new Set();
9835
+ /**
9836
+ * Monotonic operation counter. `animateTo`/`jumpTo` claim a generation
9837
+ * before emitting 'superseded'; if an onEnd listener starts a newer
9838
+ * operation during that emit, the outer call sees a bumped counter and
9839
+ * bails instead of overwriting the nested animation's state. Without this,
9840
+ * the nested animation would run to completion having never reported an end
9841
+ * reason, breaking the exactly-one guarantee the spec makes.
9842
+ */
9843
+ generation = 0;
9844
+ lastWrite = null;
9845
+ disposed = false;
9846
+ detachListeners = null;
9847
+ constructor(element, camera, options) {
9848
+ if (options.frames !== void 0) {
9849
+ const { requestFrame, cancelFrame } = options.frames;
9850
+ if (typeof requestFrame !== "function" || typeof cancelFrame !== "function") {
9851
+ throw new Error(
9852
+ "[fieldnotes] CameraAnimator: `frames` must supply both requestFrame and cancelFrame"
9853
+ );
9854
+ }
9855
+ }
9856
+ this.camera = camera;
9857
+ this.getCanvasSize = options.getCanvasSize;
9858
+ this.frames = options.frames ?? {
9859
+ requestFrame: (cb) => requestAnimationFrame(cb),
9860
+ cancelFrame: (id) => cancelAnimationFrame(id)
9861
+ };
9862
+ this.now = options.now ?? (() => performance.now());
9863
+ this.durationMs = options.durationMs ?? DEFAULT_DURATION_MS3;
9864
+ this.easing = options.easing ?? easeOutCubic2;
9865
+ if (options.interactive ?? true) {
9866
+ const onUserInput = () => {
9867
+ this.end("cancelled");
9868
+ };
9869
+ const types = ["pointerdown", "wheel", "keydown"];
9870
+ for (const type of types) {
9871
+ element.addEventListener(type, onUserInput, { passive: true });
9872
+ }
9873
+ this.detachListeners = () => {
9874
+ for (const type of types) {
9875
+ element.removeEventListener(type, onUserInput);
9876
+ }
9877
+ };
9878
+ }
9879
+ }
9880
+ get animating() {
9881
+ return this.to !== null;
9882
+ }
9883
+ onEnd(listener) {
9884
+ this.endListeners.add(listener);
9885
+ return () => this.endListeners.delete(listener);
9886
+ }
9887
+ animateTo(view) {
9888
+ const size = this.validateAndMeasure(view);
9889
+ if (size === null) return;
9890
+ const current = this.camera.getVisibleRect(size.w, size.h);
9891
+ const generation = ++this.generation;
9892
+ this.end("superseded");
9893
+ if (this.generation !== generation) return;
9894
+ if (viewsClose(current, view)) {
9895
+ applyCameraView(this.camera, view, size.w, size.h);
9896
+ this.emit("complete");
9897
+ return;
9898
+ }
9899
+ this.from = current;
9900
+ this.to = view;
9901
+ this.startedAt = this.now();
9902
+ this.lastWrite = null;
9903
+ this.rafId = this.frames.requestFrame(this.step);
9904
+ }
9905
+ jumpTo(view) {
9906
+ const size = this.validateAndMeasure(view);
9907
+ if (size === null) return;
9908
+ const generation = ++this.generation;
9909
+ this.end("superseded");
9910
+ if (this.generation !== generation) return;
9911
+ applyCameraView(this.camera, view, size.w, size.h);
9912
+ this.lastWrite = null;
9913
+ }
9914
+ cancel() {
9915
+ if (this.disposed) return;
9916
+ this.end("cancelled");
9917
+ }
9918
+ /**
9919
+ * Terminal. Order is load-bearing: the flag is set BEFORE any listener runs,
9920
+ * because an onEnd listener can call animateTo during the disposal callback.
9921
+ * With the flag set last, that call would start a real animation which the
9922
+ * listener clear then silently discards — a second animation with no end
9923
+ * reason, breaking the exactly-one guarantee.
9924
+ */
9925
+ dispose() {
9926
+ if (this.disposed) return;
9927
+ this.disposed = true;
9928
+ this.generation++;
9929
+ const wasAnimating = this.to !== null;
9930
+ this.clearFrame();
9931
+ this.from = null;
9932
+ this.to = null;
9933
+ this.lastWrite = null;
9934
+ if (wasAnimating) this.emit("cancelled");
9935
+ this.endListeners.clear();
9936
+ this.detachListeners?.();
9937
+ this.detachListeners = null;
9938
+ }
9939
+ /**
9940
+ * Steps 1-3 of the public-call contract. Returns null when the caller must
9941
+ * stop, having already handled termination.
9942
+ *
9943
+ * The disposed check precedes validation deliberately: ordering it after
9944
+ * would make `disposed.animateTo(invalidView)` both required to throw and
9945
+ * required to stay silent. Disposal wins — a terminal animator is inert for
9946
+ * every input, and post-disposal calls are exactly the racy teardown paths
9947
+ * where a throw is least useful.
9948
+ */
9949
+ validateAndMeasure(view) {
9950
+ if (this.disposed) return null;
9951
+ assertValidView(view);
9952
+ const size = this.getCanvasSize();
9953
+ assertCanvasDims(size.w, size.h);
9954
+ if (size.w === 0 || size.h === 0) {
9955
+ this.end("cancelled");
9956
+ return null;
9957
+ }
9958
+ return size;
9959
+ }
9960
+ step = () => {
9961
+ if (this.disposed || this.to === null || this.from === null) return;
9962
+ if (this.foreignWrite()) {
9963
+ this.end("cancelled");
9964
+ return;
9965
+ }
9966
+ const size = this.getCanvasSize();
9967
+ if (!canvasDimsUsable(size.w, size.h)) {
9968
+ this.end("cancelled");
9969
+ return;
9970
+ }
9971
+ if (size.w === 0 || size.h === 0) {
9972
+ this.end("cancelled");
9973
+ return;
9974
+ }
9975
+ const elapsed = this.now() - this.startedAt;
9976
+ const t = this.durationMs <= 0 ? 1 : Math.min(1, elapsed / this.durationMs);
9977
+ const view = lerpView(this.from, this.to, this.easing(t));
9978
+ applyCameraView(this.camera, view, size.w, size.h);
9979
+ this.recordWrite();
9980
+ if (t >= 1) {
9981
+ this.end("complete");
9982
+ return;
9983
+ }
9984
+ this.rafId = this.frames.requestFrame(this.step);
9985
+ };
9986
+ recordWrite() {
9987
+ this.lastWrite = {
9988
+ x: this.camera.position.x,
9989
+ y: this.camera.position.y,
9990
+ zoom: this.camera.zoom
9991
+ };
9992
+ }
9993
+ foreignWrite() {
9994
+ if (this.lastWrite === null) return false;
9995
+ const eps = 1e-6;
9996
+ return Math.abs(this.camera.position.x - this.lastWrite.x) > eps || Math.abs(this.camera.position.y - this.lastWrite.y) > eps || Math.abs(this.camera.zoom - this.lastWrite.zoom) > eps;
9997
+ }
9998
+ /** Terminates an in-flight animation with `reason`. No-op when idle. */
9999
+ end(reason) {
10000
+ if (this.disposed || this.to === null) return;
10001
+ this.clearFrame();
10002
+ this.from = null;
10003
+ this.to = null;
10004
+ this.lastWrite = null;
10005
+ this.emit(reason);
10006
+ }
10007
+ clearFrame() {
10008
+ if (this.rafId !== null) {
10009
+ this.frames.cancelFrame(this.rafId);
10010
+ this.rafId = null;
10011
+ }
10012
+ }
10013
+ emit(reason) {
10014
+ for (const listener of [...this.endListeners]) {
10015
+ try {
10016
+ listener(reason);
10017
+ } catch {
10018
+ }
10019
+ }
10020
+ }
10021
+ };
10022
+
10023
+ // src/canvas/focus-presence.ts
10024
+ var FOCUS_PRESENCE_KIND = "focus";
10025
+ var AUDIENCES = ["all", "players", "display"];
10026
+ function isPositiveFinite(value) {
10027
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
10028
+ }
10029
+ function isFiniteNumber2(value) {
10030
+ return typeof value === "number" && Number.isFinite(value);
10031
+ }
10032
+ function isFocusPresence(data) {
10033
+ if (typeof data !== "object" || data === null) return false;
10034
+ const payload = data;
10035
+ if (payload.kind !== FOCUS_PRESENCE_KIND) return false;
10036
+ if (!isFiniteNumber2(payload.x) || !isFiniteNumber2(payload.y)) return false;
10037
+ if (!isPositiveFinite(payload.w) || !isPositiveFinite(payload.h)) return false;
10038
+ if (typeof payload.audience !== "string" || !AUDIENCES.some((a) => a === payload.audience)) {
10039
+ return false;
10040
+ }
10041
+ if (payload.color !== void 0 && typeof payload.color !== "string") return false;
10042
+ return true;
10043
+ }
10044
+ function toFocusPresence(view, audience, color) {
10045
+ return {
10046
+ kind: FOCUS_PRESENCE_KIND,
10047
+ x: view.x,
10048
+ y: view.y,
10049
+ w: view.w,
10050
+ h: view.h,
10051
+ audience,
10052
+ ...color === void 0 ? {} : { color }
10053
+ };
10054
+ }
10055
+
10056
+ // src/canvas/remote-focus-receiver.ts
10057
+ function audienceIncludes(audience, role) {
10058
+ if (role === "dm") return false;
10059
+ if (audience === "all") return true;
10060
+ if (audience === "players") return role === "player";
10061
+ return role === "display";
10062
+ }
10063
+ var RemoteFocusReceiver = class {
10064
+ role;
10065
+ animator;
10066
+ animate;
10067
+ pulseColor;
10068
+ overlay;
10069
+ disposed = false;
10070
+ constructor(host, options) {
10071
+ this.role = options.role;
10072
+ this.animator = options.animator;
10073
+ this.animate = options.animate ?? true;
10074
+ this.pulseColor = options.pulseColor;
10075
+ this.overlay = options.pulse ?? true ? new RemotePingOverlay(host, {
10076
+ ...options.pulseColor === void 0 ? {} : { color: options.pulseColor },
10077
+ ...options.pulseDurationMs === void 0 ? {} : { durationMs: options.pulseDurationMs },
10078
+ ...options.pulseRadius === void 0 ? {} : { radius: options.pulseRadius },
10079
+ maxPingsPerSender: 1
10080
+ }) : null;
10081
+ }
10082
+ /**
10083
+ * Applies a presence payload from `sender`. Returns `false` for payloads
10084
+ * that are not focus frames, or are addressed to a different role, so hosts
10085
+ * can feed every presence frame through without disturbing other handlers.
10086
+ */
10087
+ apply(from, data) {
10088
+ if (this.disposed || !isFocusPresence(data)) return false;
10089
+ if (!audienceIncludes(data.audience, this.role)) return false;
10090
+ const view = { x: data.x, y: data.y, w: data.w, h: data.h };
10091
+ if (this.animate) {
10092
+ this.animator.animateTo(view);
10093
+ } else {
10094
+ this.animator.jumpTo(view);
10095
+ }
10096
+ this.overlay?.apply(from, {
10097
+ kind: "ping",
10098
+ x: view.x + view.w / 2,
10099
+ y: view.y + view.h / 2,
10100
+ color: data.color ?? this.pulseColor
10101
+ });
10102
+ return true;
10103
+ }
10104
+ /** Idempotent. Does NOT dispose the animator — the host owns that. */
10105
+ dispose() {
10106
+ if (this.disposed) return;
10107
+ this.disposed = true;
10108
+ this.overlay?.dispose();
10109
+ }
10110
+ };
10111
+
9737
10112
  // src/tools/hand-tool.ts
9738
10113
  var HandTool = class {
9739
10114
  name = "hand";
@@ -9922,7 +10297,7 @@ function hitTestStroke(stroke, point, radius) {
9922
10297
  }
9923
10298
 
9924
10299
  // src/elements/stroke-erase.ts
9925
- function lerp(a, b, t) {
10300
+ function lerp2(a, b, t) {
9926
10301
  return {
9927
10302
  x: a.x + (b.x - a.x) * t,
9928
10303
  y: a.y + (b.y - a.y) * t,
@@ -9981,13 +10356,13 @@ function erasePoints(points, eraser, radius) {
9981
10356
  erased = true;
9982
10357
  if (tLo > 0) {
9983
10358
  if (current.length === 0) current.push(a);
9984
- current.push(lerp(a, b, tLo));
10359
+ current.push(lerp2(a, b, tLo));
9985
10360
  flush();
9986
10361
  } else {
9987
10362
  flush();
9988
10363
  }
9989
10364
  if (tHi < 1) {
9990
- current = [lerp(a, b, tHi), b];
10365
+ current = [lerp2(a, b, tHi), b];
9991
10366
  }
9992
10367
  }
9993
10368
  flush();
@@ -12443,7 +12818,7 @@ var LaserTool = class {
12443
12818
 
12444
12819
  // src/tools/ping-tool.ts
12445
12820
  var DEFAULT_COLOR6 = "#ff3b30";
12446
- var DEFAULT_DURATION_MS3 = 1800;
12821
+ var DEFAULT_DURATION_MS4 = 1800;
12447
12822
  var DEFAULT_RADIUS4 = 48;
12448
12823
  var DEFAULT_MIN_INTERVAL_MS2 = 300;
12449
12824
  var PingTool = class {
@@ -12460,7 +12835,7 @@ var PingTool = class {
12460
12835
  constructor(options = {}) {
12461
12836
  this.name = options.name ?? "ping";
12462
12837
  this.color = options.color ?? DEFAULT_COLOR6;
12463
- this.durationMs = options.durationMs ?? DEFAULT_DURATION_MS3;
12838
+ this.durationMs = options.durationMs ?? DEFAULT_DURATION_MS4;
12464
12839
  this.radius = options.radius ?? DEFAULT_RADIUS4;
12465
12840
  this.minIntervalMs = options.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS2;
12466
12841
  }
@@ -12562,15 +12937,17 @@ var PingTool = class {
12562
12937
  };
12563
12938
 
12564
12939
  // src/index.ts
12565
- var VERSION = "0.60.0";
12940
+ var VERSION = "0.61.0";
12566
12941
  // Annotate the CommonJS export names for ESM import in node:
12567
12942
  0 && (module.exports = {
12568
12943
  ArrowTool,
12569
12944
  AutoSave,
12570
12945
  Camera,
12946
+ CameraAnimator,
12571
12947
  DEFAULT_NOTE_FONT_SIZE,
12572
12948
  ElementStore,
12573
12949
  EraserTool,
12950
+ FOCUS_PRESENCE_KIND,
12574
12951
  HandTool,
12575
12952
  HistoryStack,
12576
12953
  ImageTool,
@@ -12588,6 +12965,7 @@ var VERSION = "0.60.0";
12588
12965
  PencilTool,
12589
12966
  PingInput,
12590
12967
  PingTool,
12968
+ RemoteFocusReceiver,
12591
12969
  RemoteLaserOverlay,
12592
12970
  RemoteMeasureOverlay,
12593
12971
  RemotePingOverlay,
@@ -12598,7 +12976,10 @@ var VERSION = "0.60.0";
12598
12976
  ToolManager,
12599
12977
  VERSION,
12600
12978
  Viewport,
12979
+ applyCameraView,
12601
12980
  boundsIntersect,
12981
+ cameraOriginForView,
12982
+ captureCameraView,
12602
12983
  createArrow,
12603
12984
  createGrid,
12604
12985
  createHtmlElement,
@@ -12611,6 +12992,7 @@ var VERSION = "0.60.0";
12611
12992
  drawHexPath,
12612
12993
  exportImage,
12613
12994
  exportSvg,
12995
+ fitZoomForView,
12614
12996
  getActiveFormats,
12615
12997
  getArrowBounds,
12616
12998
  getArrowControlPoint,
@@ -12626,6 +13008,7 @@ var VERSION = "0.60.0";
12626
13008
  getHexCellsInRectangle,
12627
13009
  getHexCellsInSquare,
12628
13010
  getHexDistance,
13011
+ isFocusPresence,
12629
13012
  isLaserTrailPresence,
12630
13013
  isMeasurePresence,
12631
13014
  isNearBezier,
@@ -12635,6 +13018,7 @@ var VERSION = "0.60.0";
12635
13018
  snapPoint,
12636
13019
  snapToHexCenter,
12637
13020
  styleToPatch,
13021
+ toFocusPresence,
12638
13022
  toLaserTrailPresence,
12639
13023
  toMeasurePresence,
12640
13024
  toPingPresence,