@fieldnotes/core 0.55.0 → 0.57.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
@@ -8845,6 +8845,399 @@ var RemotePingOverlay = class {
8845
8845
  }
8846
8846
  };
8847
8847
 
8848
+ // src/canvas/measure-render.ts
8849
+ function formatMeasureLabel(feet) {
8850
+ return `${Math.round(feet)} ft`;
8851
+ }
8852
+ function drawMeasurement(ctx, m, opts = {}) {
8853
+ ctx.save();
8854
+ if (opts.alpha !== void 0) ctx.globalAlpha = opts.alpha;
8855
+ ctx.strokeStyle = m.color;
8856
+ ctx.setLineDash([8, 4]);
8857
+ ctx.lineWidth = 2;
8858
+ ctx.beginPath();
8859
+ ctx.moveTo(m.start.x, m.start.y);
8860
+ ctx.lineTo(m.end.x, m.end.y);
8861
+ ctx.stroke();
8862
+ ctx.setLineDash([]);
8863
+ ctx.fillStyle = m.color;
8864
+ const dotRadius = 4;
8865
+ ctx.beginPath();
8866
+ ctx.arc(m.start.x, m.start.y, dotRadius, 0, Math.PI * 2);
8867
+ ctx.fill();
8868
+ ctx.beginPath();
8869
+ ctx.arc(m.end.x, m.end.y, dotRadius, 0, Math.PI * 2);
8870
+ ctx.fill();
8871
+ const label = formatMeasureLabel(m.feet);
8872
+ const midX = (m.start.x + m.end.x) / 2;
8873
+ const midY = (m.start.y + m.end.y) / 2;
8874
+ ctx.font = "14px sans-serif";
8875
+ const metrics = ctx.measureText(label);
8876
+ const padX = 6;
8877
+ const padY = 4;
8878
+ const textH = 14;
8879
+ ctx.fillStyle = "rgba(0, 0, 0, 0.75)";
8880
+ ctx.beginPath();
8881
+ ctx.roundRect(
8882
+ midX - metrics.width / 2 - padX,
8883
+ midY - textH / 2 - padY,
8884
+ metrics.width + padX * 2,
8885
+ textH + padY * 2,
8886
+ 4
8887
+ );
8888
+ ctx.fill();
8889
+ ctx.fillStyle = "#FFFFFF";
8890
+ ctx.textAlign = "center";
8891
+ ctx.textBaseline = "middle";
8892
+ ctx.fillText(label, midX, midY);
8893
+ ctx.restore();
8894
+ }
8895
+
8896
+ // src/canvas/remote-measure-overlay.ts
8897
+ var MEASURE_PRESENCE_KIND = "measure";
8898
+ function isFinitePoint2(value) {
8899
+ if (typeof value !== "object" || value === null) return false;
8900
+ const point = value;
8901
+ return typeof point.x === "number" && Number.isFinite(point.x) && typeof point.y === "number" && Number.isFinite(point.y);
8902
+ }
8903
+ function isMeasurePresence(data) {
8904
+ if (typeof data !== "object" || data === null) return false;
8905
+ const payload = data;
8906
+ if (payload.kind !== MEASURE_PRESENCE_KIND) return false;
8907
+ if ("cleared" in payload) return payload.cleared === true;
8908
+ if (!isFinitePoint2(payload.start) || !isFinitePoint2(payload.end)) return false;
8909
+ if (typeof payload.cells !== "number" || !Number.isFinite(payload.cells)) return false;
8910
+ if (typeof payload.feet !== "number" || !Number.isFinite(payload.feet)) return false;
8911
+ if (payload.color !== void 0 && typeof payload.color !== "string") return false;
8912
+ return true;
8913
+ }
8914
+ function toMeasurePresence(emission) {
8915
+ if (emission === null) return { kind: MEASURE_PRESENCE_KIND, cleared: true };
8916
+ return {
8917
+ kind: MEASURE_PRESENCE_KIND,
8918
+ start: emission.start,
8919
+ end: emission.end,
8920
+ cells: emission.cells,
8921
+ feet: emission.feet,
8922
+ color: emission.color
8923
+ };
8924
+ }
8925
+ var DEFAULT_COLOR3 = "#FF5722";
8926
+ var DEFAULT_HOLD_MS = 1500;
8927
+ var DEFAULT_FADE_MS2 = 400;
8928
+ var DEFAULT_MAX_AGE_MS = 3e4;
8929
+ var RemoteMeasureOverlay = class {
8930
+ host;
8931
+ color;
8932
+ holdMs;
8933
+ fadeMs;
8934
+ maxAgeMs;
8935
+ measurements = /* @__PURE__ */ new Map();
8936
+ unregister;
8937
+ rafId = null;
8938
+ disposed = false;
8939
+ constructor(host, options = {}) {
8940
+ this.host = host;
8941
+ this.color = options.color ?? DEFAULT_COLOR3;
8942
+ this.holdMs = options.holdMs ?? DEFAULT_HOLD_MS;
8943
+ this.fadeMs = options.fadeMs ?? DEFAULT_FADE_MS2;
8944
+ this.maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
8945
+ this.unregister = host.registerOverlay((ctx) => this.renderMeasurements(ctx));
8946
+ }
8947
+ now() {
8948
+ return performance.now();
8949
+ }
8950
+ /**
8951
+ * Applies a presence payload from `sender` (any opaque per-sender key, e.g.
8952
+ * the envelope `from`). Non-measure or malformed payloads are ignored and
8953
+ * reported as `false`, so hosts can feed every presence frame through.
8954
+ */
8955
+ apply(sender, data) {
8956
+ if (this.disposed || !isMeasurePresence(data)) return false;
8957
+ if ("cleared" in data) {
8958
+ this.beginLinger(sender);
8959
+ return true;
8960
+ }
8961
+ const existing = this.measurements.get(sender);
8962
+ if (existing?.expiryTimer != null) clearTimeout(existing.expiryTimer);
8963
+ this.measurements.set(sender, {
8964
+ start: data.start,
8965
+ end: data.end,
8966
+ feet: data.feet,
8967
+ color: data.color ?? this.color,
8968
+ clearedAt: null,
8969
+ expiryTimer: setTimeout(() => this.beginLinger(sender), this.maxAgeMs)
8970
+ });
8971
+ this.host.requestRender();
8972
+ return true;
8973
+ }
8974
+ /** Removes a sender's ruler immediately (presence-leave/disconnect). */
8975
+ remove(sender) {
8976
+ const entry = this.measurements.get(sender);
8977
+ if (!entry) return;
8978
+ if (entry.expiryTimer != null) clearTimeout(entry.expiryTimer);
8979
+ this.measurements.delete(sender);
8980
+ this.host.requestRender();
8981
+ }
8982
+ /** Removes every ruler immediately. */
8983
+ clear() {
8984
+ if (this.measurements.size === 0) return;
8985
+ for (const entry of this.measurements.values()) {
8986
+ if (entry.expiryTimer != null) clearTimeout(entry.expiryTimer);
8987
+ }
8988
+ this.measurements.clear();
8989
+ this.host.requestRender();
8990
+ }
8991
+ /** Number of senders with a visible (active or lingering) ruler. */
8992
+ get activeSenderCount() {
8993
+ return this.measurements.size;
8994
+ }
8995
+ /** Unregisters the overlay, cancels timers, stops animating. Idempotent. */
8996
+ dispose() {
8997
+ if (this.disposed) return;
8998
+ this.disposed = true;
8999
+ if (this.rafId !== null) {
9000
+ cancelAnimationFrame(this.rafId);
9001
+ this.rafId = null;
9002
+ }
9003
+ for (const entry of this.measurements.values()) {
9004
+ if (entry.expiryTimer != null) clearTimeout(entry.expiryTimer);
9005
+ }
9006
+ this.measurements.clear();
9007
+ this.unregister?.();
9008
+ this.unregister = null;
9009
+ this.host.requestRender();
9010
+ }
9011
+ beginLinger(sender) {
9012
+ const entry = this.measurements.get(sender);
9013
+ if (!entry || entry.clearedAt !== null) return;
9014
+ if (entry.expiryTimer != null) {
9015
+ clearTimeout(entry.expiryTimer);
9016
+ entry.expiryTimer = null;
9017
+ }
9018
+ entry.clearedAt = this.now();
9019
+ this.ensureAnimating();
9020
+ this.host.requestRender();
9021
+ }
9022
+ ensureAnimating() {
9023
+ if (this.rafId === null) {
9024
+ this.rafId = requestAnimationFrame(() => this.tick());
9025
+ }
9026
+ }
9027
+ tick() {
9028
+ if (this.disposed) return;
9029
+ const now = this.now();
9030
+ let lingering = 0;
9031
+ for (const [sender, entry] of this.measurements) {
9032
+ if (entry.clearedAt === null) continue;
9033
+ if (now - entry.clearedAt >= this.holdMs + this.fadeMs) {
9034
+ this.measurements.delete(sender);
9035
+ } else {
9036
+ lingering += 1;
9037
+ }
9038
+ }
9039
+ this.host.requestRender();
9040
+ this.rafId = lingering > 0 ? requestAnimationFrame(() => this.tick()) : null;
9041
+ }
9042
+ renderMeasurements(ctx) {
9043
+ if (this.measurements.size === 0) return;
9044
+ const now = this.now();
9045
+ for (const entry of this.measurements.values()) {
9046
+ let alpha = 1;
9047
+ if (entry.clearedAt !== null) {
9048
+ const fadeAge = now - entry.clearedAt - this.holdMs;
9049
+ if (fadeAge > 0) alpha = Math.max(0, 1 - fadeAge / this.fadeMs);
9050
+ }
9051
+ drawMeasurement(ctx, entry, { alpha });
9052
+ }
9053
+ }
9054
+ };
9055
+
9056
+ // src/canvas/ping-input.ts
9057
+ var DEFAULT_LONG_PRESS_MS = 600;
9058
+ var DEFAULT_SLOP_PX = 8;
9059
+ var DEFAULT_COLOR4 = "#ff3b30";
9060
+ var DEFAULT_DURATION_MS2 = 1800;
9061
+ var DEFAULT_RADIUS2 = 48;
9062
+ var DEFAULT_MIN_INTERVAL_MS = 300;
9063
+ var PingInput = class {
9064
+ element;
9065
+ host;
9066
+ longPressEnabled;
9067
+ longPressMs;
9068
+ slopPx;
9069
+ color;
9070
+ durationMs;
9071
+ radius;
9072
+ minIntervalMs;
9073
+ shouldPing;
9074
+ press = null;
9075
+ downPointers = /* @__PURE__ */ new Set();
9076
+ lastPointerScreen = null;
9077
+ lastEmitAt = -Infinity;
9078
+ disposed = false;
9079
+ optionListeners = /* @__PURE__ */ new Set();
9080
+ pingListeners = /* @__PURE__ */ new Set();
9081
+ handlePointerDown = (e) => this.onPointerDown(e);
9082
+ handlePointerMove = (e) => this.onPointerMove(e);
9083
+ handlePointerUp = (e) => this.onPointerEnd(e);
9084
+ handlePointerCancel = (e) => this.onPointerEnd(e);
9085
+ handlePointerLeave = (e) => this.onPointerEnd(e);
9086
+ constructor(element, host, options = {}) {
9087
+ this.element = element;
9088
+ this.host = host;
9089
+ this.longPressEnabled = options.longPressEnabled ?? false;
9090
+ this.longPressMs = options.longPressMs ?? DEFAULT_LONG_PRESS_MS;
9091
+ this.slopPx = options.slopPx ?? DEFAULT_SLOP_PX;
9092
+ this.color = options.color ?? DEFAULT_COLOR4;
9093
+ this.durationMs = options.durationMs ?? DEFAULT_DURATION_MS2;
9094
+ this.radius = options.radius ?? DEFAULT_RADIUS2;
9095
+ this.minIntervalMs = options.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS;
9096
+ this.shouldPing = options.shouldPing;
9097
+ const opts = { passive: true };
9098
+ element.addEventListener("pointerdown", this.handlePointerDown, opts);
9099
+ element.addEventListener("pointermove", this.handlePointerMove, opts);
9100
+ element.addEventListener("pointerup", this.handlePointerUp, opts);
9101
+ element.addEventListener("pointercancel", this.handlePointerCancel, opts);
9102
+ element.addEventListener("pointerleave", this.handlePointerLeave, opts);
9103
+ }
9104
+ now() {
9105
+ return performance.now();
9106
+ }
9107
+ getOptions() {
9108
+ return {
9109
+ longPressEnabled: this.longPressEnabled,
9110
+ longPressMs: this.longPressMs,
9111
+ slopPx: this.slopPx,
9112
+ color: this.color,
9113
+ durationMs: this.durationMs,
9114
+ radius: this.radius,
9115
+ minIntervalMs: this.minIntervalMs,
9116
+ ...this.shouldPing ? { shouldPing: this.shouldPing } : {}
9117
+ };
9118
+ }
9119
+ setOptions(options) {
9120
+ if (options.longPressEnabled !== void 0) {
9121
+ this.longPressEnabled = options.longPressEnabled;
9122
+ if (!this.longPressEnabled) this.cancelPress();
9123
+ }
9124
+ if (options.longPressMs !== void 0) this.longPressMs = options.longPressMs;
9125
+ if (options.slopPx !== void 0) this.slopPx = options.slopPx;
9126
+ if (options.color !== void 0) this.color = options.color;
9127
+ if (options.durationMs !== void 0) this.durationMs = options.durationMs;
9128
+ if (options.radius !== void 0) this.radius = options.radius;
9129
+ if (options.minIntervalMs !== void 0) this.minIntervalMs = options.minIntervalMs;
9130
+ if (options.shouldPing !== void 0) this.shouldPing = options.shouldPing;
9131
+ for (const listener of this.optionListeners) listener();
9132
+ }
9133
+ onOptionsChange(listener) {
9134
+ this.optionListeners.add(listener);
9135
+ return () => this.optionListeners.delete(listener);
9136
+ }
9137
+ /**
9138
+ * Subscribes to emitted pings. Listeners must not throw; a throwing
9139
+ * listener is isolated so it cannot break input handling or other
9140
+ * listeners.
9141
+ */
9142
+ onPing(listener) {
9143
+ this.pingListeners.add(listener);
9144
+ return () => this.pingListeners.delete(listener);
9145
+ }
9146
+ /**
9147
+ * Pings at the last tracked pointer position (hover moves and presses),
9148
+ * world-converted at call time. Returns `false` when no pointer has been
9149
+ * seen yet, the host vetoes, or the rate limit drops the ping.
9150
+ */
9151
+ pingAtPointer() {
9152
+ if (this.disposed || this.lastPointerScreen === null) return false;
9153
+ return this.emit(this.host.screenToWorld(this.lastPointerScreen));
9154
+ }
9155
+ /** Pings a world position directly. Same veto and rate limit as every path. */
9156
+ pingAt(world) {
9157
+ if (this.disposed) return false;
9158
+ return this.emit(world);
9159
+ }
9160
+ /** Removes all DOM listeners and cancels any pending press. Idempotent. */
9161
+ dispose() {
9162
+ if (this.disposed) return;
9163
+ this.disposed = true;
9164
+ this.cancelPress();
9165
+ this.downPointers.clear();
9166
+ this.element.removeEventListener("pointerdown", this.handlePointerDown);
9167
+ this.element.removeEventListener("pointermove", this.handlePointerMove);
9168
+ this.element.removeEventListener("pointerup", this.handlePointerUp);
9169
+ this.element.removeEventListener("pointercancel", this.handlePointerCancel);
9170
+ this.element.removeEventListener("pointerleave", this.handlePointerLeave);
9171
+ this.optionListeners.clear();
9172
+ this.pingListeners.clear();
9173
+ }
9174
+ toLocal(e) {
9175
+ const rect = this.element.getBoundingClientRect();
9176
+ return { x: e.clientX - rect.left, y: e.clientY - rect.top };
9177
+ }
9178
+ onPointerDown(e) {
9179
+ const local = this.toLocal(e);
9180
+ this.lastPointerScreen = local;
9181
+ const hadPointers = this.downPointers.size > 0;
9182
+ this.downPointers.add(e.pointerId);
9183
+ if (hadPointers) {
9184
+ this.cancelPress();
9185
+ return;
9186
+ }
9187
+ if (!this.longPressEnabled) return;
9188
+ if (e.pointerType === "mouse" && e.button !== 0) return;
9189
+ this.press = {
9190
+ pointerId: e.pointerId,
9191
+ x: local.x,
9192
+ y: local.y,
9193
+ timer: setTimeout(() => this.firePress(), this.longPressMs)
9194
+ };
9195
+ }
9196
+ onPointerMove(e) {
9197
+ const local = this.toLocal(e);
9198
+ this.lastPointerScreen = local;
9199
+ if (this.press === null || e.pointerId !== this.press.pointerId) return;
9200
+ const dx = local.x - this.press.x;
9201
+ const dy = local.y - this.press.y;
9202
+ if (Math.hypot(dx, dy) > this.slopPx) this.cancelPress();
9203
+ }
9204
+ onPointerEnd(e) {
9205
+ this.downPointers.delete(e.pointerId);
9206
+ if (this.press !== null && e.pointerId === this.press.pointerId) this.cancelPress();
9207
+ }
9208
+ cancelPress() {
9209
+ if (this.press === null) return;
9210
+ clearTimeout(this.press.timer);
9211
+ this.press = null;
9212
+ }
9213
+ firePress() {
9214
+ if (this.press === null) return;
9215
+ const screen = { x: this.press.x, y: this.press.y };
9216
+ this.press = null;
9217
+ this.emit(this.host.screenToWorld(screen));
9218
+ }
9219
+ emit(world) {
9220
+ if (this.shouldPing && !this.shouldPing()) return false;
9221
+ const t = this.now();
9222
+ if (t - this.lastEmitAt < this.minIntervalMs) return false;
9223
+ this.lastEmitAt = t;
9224
+ const emission = {
9225
+ x: world.x,
9226
+ y: world.y,
9227
+ color: this.color,
9228
+ durationMs: this.durationMs,
9229
+ radius: this.radius
9230
+ };
9231
+ for (const listener of this.pingListeners) {
9232
+ try {
9233
+ listener(emission);
9234
+ } catch {
9235
+ }
9236
+ }
9237
+ return true;
9238
+ }
9239
+ };
9240
+
8848
9241
  // src/tools/hand-tool.ts
8849
9242
  var HandTool = class {
8850
9243
  name = "hand";
@@ -9106,7 +9499,7 @@ function erasePoints(points, eraser, radius) {
9106
9499
  }
9107
9500
 
9108
9501
  // src/tools/eraser-tool.ts
9109
- var DEFAULT_RADIUS2 = 20;
9502
+ var DEFAULT_RADIUS3 = 20;
9110
9503
  function makeEraserCursor(radius) {
9111
9504
  const size = radius * 2;
9112
9505
  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>`;
@@ -9119,7 +9512,7 @@ var EraserTool = class {
9119
9512
  cursor;
9120
9513
  mode;
9121
9514
  constructor(options = {}) {
9122
- this.radius = options.radius ?? DEFAULT_RADIUS2;
9515
+ this.radius = options.radius ?? DEFAULT_RADIUS3;
9123
9516
  this.cursor = makeEraserCursor(this.radius);
9124
9517
  this.mode = options.mode ?? "partial";
9125
9518
  }
@@ -10986,21 +11379,37 @@ var MeasureTool = class {
10986
11379
  gridType;
10987
11380
  hexOrientation;
10988
11381
  feetPerCell;
11382
+ color;
10989
11383
  optionListeners = /* @__PURE__ */ new Set();
11384
+ measurementListeners = /* @__PURE__ */ new Set();
11385
+ emissionRafId = null;
10990
11386
  constructor(options = {}) {
10991
11387
  this.feetPerCell = options.feetPerCell ?? 5;
11388
+ this.color = options.color ?? "#FF5722";
10992
11389
  }
10993
11390
  getOptions() {
10994
- return { feetPerCell: this.feetPerCell };
11391
+ return { feetPerCell: this.feetPerCell, color: this.color };
10995
11392
  }
10996
11393
  setOptions(options) {
10997
11394
  if (options.feetPerCell !== void 0) this.feetPerCell = options.feetPerCell;
11395
+ if (options.color !== void 0) this.color = options.color;
10998
11396
  this.notifyOptionsChange();
10999
11397
  }
11000
11398
  onOptionsChange(listener) {
11001
11399
  this.optionListeners.add(listener);
11002
11400
  return () => this.optionListeners.delete(listener);
11003
11401
  }
11402
+ /**
11403
+ * Subscribes to raf-coalesced measurement snapshots. While a measurement is
11404
+ * in progress, listeners receive at most one snapshot per animation frame
11405
+ * carrying the latest state; `null` is delivered synchronously when the
11406
+ * measurement clears (pointer-up or deactivate). Emissions are ephemeral by
11407
+ * contract: presence only — never elements, history, or persisted state.
11408
+ */
11409
+ onMeasurement(listener) {
11410
+ this.measurementListeners.add(listener);
11411
+ return () => this.measurementListeners.delete(listener);
11412
+ }
11004
11413
  onPointerDown(state, ctx) {
11005
11414
  this.gridSize = ctx.gridSize ?? 1;
11006
11415
  this.gridType = ctx.gridType;
@@ -11008,22 +11417,27 @@ var MeasureTool = class {
11008
11417
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
11009
11418
  this.start = this.snapToGrid(world, ctx);
11010
11419
  this.end = { ...this.start };
11420
+ this.scheduleEmission();
11011
11421
  }
11012
11422
  onPointerMove(state, ctx) {
11013
11423
  if (!this.start) return;
11014
11424
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
11015
11425
  this.end = this.snapToGrid(world, ctx);
11016
11426
  ctx.requestRender();
11427
+ this.scheduleEmission();
11017
11428
  }
11018
11429
  onPointerUp(_state, ctx) {
11019
11430
  if (!this.start) return;
11020
11431
  this.start = null;
11021
11432
  this.end = null;
11022
11433
  ctx.requestRender();
11434
+ this.emitClear();
11023
11435
  }
11024
11436
  onDeactivate(_ctx) {
11437
+ const wasActive = this.start !== null;
11025
11438
  this.start = null;
11026
11439
  this.end = null;
11440
+ if (wasActive) this.emitClear();
11027
11441
  }
11028
11442
  getMeasurement() {
11029
11443
  if (!this.start || !this.end) return null;
@@ -11049,46 +11463,7 @@ var MeasureTool = class {
11049
11463
  renderOverlay(ctx) {
11050
11464
  const m = this.getMeasurement();
11051
11465
  if (!m) return;
11052
- ctx.save();
11053
- ctx.strokeStyle = "#FF5722";
11054
- ctx.setLineDash([8, 4]);
11055
- ctx.lineWidth = 2;
11056
- ctx.beginPath();
11057
- ctx.moveTo(m.start.x, m.start.y);
11058
- ctx.lineTo(m.end.x, m.end.y);
11059
- ctx.stroke();
11060
- ctx.setLineDash([]);
11061
- ctx.fillStyle = "#FF5722";
11062
- const dotRadius = 4;
11063
- ctx.beginPath();
11064
- ctx.arc(m.start.x, m.start.y, dotRadius, 0, Math.PI * 2);
11065
- ctx.fill();
11066
- ctx.beginPath();
11067
- ctx.arc(m.end.x, m.end.y, dotRadius, 0, Math.PI * 2);
11068
- ctx.fill();
11069
- const label = `${Math.round(m.feet)} ft`;
11070
- const midX = (m.start.x + m.end.x) / 2;
11071
- const midY = (m.start.y + m.end.y) / 2;
11072
- ctx.font = "14px sans-serif";
11073
- const metrics = ctx.measureText(label);
11074
- const padX = 6;
11075
- const padY = 4;
11076
- const textH = 14;
11077
- ctx.fillStyle = "rgba(0, 0, 0, 0.75)";
11078
- ctx.beginPath();
11079
- ctx.roundRect(
11080
- midX - metrics.width / 2 - padX,
11081
- midY - textH / 2 - padY,
11082
- metrics.width + padX * 2,
11083
- textH + padY * 2,
11084
- 4
11085
- );
11086
- ctx.fill();
11087
- ctx.fillStyle = "#FFFFFF";
11088
- ctx.textAlign = "center";
11089
- ctx.textBaseline = "middle";
11090
- ctx.fillText(label, midX, midY);
11091
- ctx.restore();
11466
+ drawMeasurement(ctx, { start: m.start, end: m.end, feet: m.feet, color: this.color });
11092
11467
  }
11093
11468
  snapToGrid(point, ctx) {
11094
11469
  if (!ctx.gridSize) return point;
@@ -11106,6 +11481,32 @@ var MeasureTool = class {
11106
11481
  notifyOptionsChange() {
11107
11482
  for (const listener of this.optionListeners) listener();
11108
11483
  }
11484
+ scheduleEmission() {
11485
+ if (this.measurementListeners.size === 0) return;
11486
+ if (this.emissionRafId !== null) return;
11487
+ this.emissionRafId = requestAnimationFrame(() => {
11488
+ this.emissionRafId = null;
11489
+ const m = this.getMeasurement();
11490
+ if (!m) return;
11491
+ this.emit({ ...m, color: this.color });
11492
+ });
11493
+ }
11494
+ emitClear() {
11495
+ if (this.emissionRafId !== null) {
11496
+ cancelAnimationFrame(this.emissionRafId);
11497
+ this.emissionRafId = null;
11498
+ }
11499
+ if (this.measurementListeners.size === 0) return;
11500
+ this.emit(null);
11501
+ }
11502
+ emit(emission) {
11503
+ for (const listener of this.measurementListeners) {
11504
+ try {
11505
+ listener(emission);
11506
+ } catch {
11507
+ }
11508
+ }
11509
+ }
11109
11510
  };
11110
11511
 
11111
11512
  // src/tools/template-tool.ts
@@ -11401,9 +11802,9 @@ var TemplateTool = class {
11401
11802
  };
11402
11803
 
11403
11804
  // src/tools/laser-tool.ts
11404
- var DEFAULT_COLOR3 = "#ff3b30";
11805
+ var DEFAULT_COLOR5 = "#ff3b30";
11405
11806
  var DEFAULT_WIDTH2 = 4;
11406
- var DEFAULT_FADE_MS2 = 1200;
11807
+ var DEFAULT_FADE_MS3 = 1200;
11407
11808
  var LaserTool = class {
11408
11809
  name;
11409
11810
  color;
@@ -11417,9 +11818,9 @@ var LaserTool = class {
11417
11818
  pendingEmission = [];
11418
11819
  constructor(options = {}) {
11419
11820
  this.name = options.name ?? "laser";
11420
- this.color = options.color ?? DEFAULT_COLOR3;
11821
+ this.color = options.color ?? DEFAULT_COLOR5;
11421
11822
  this.width = options.width ?? DEFAULT_WIDTH2;
11422
- this.fadeMs = options.fadeMs ?? DEFAULT_FADE_MS2;
11823
+ this.fadeMs = options.fadeMs ?? DEFAULT_FADE_MS3;
11423
11824
  }
11424
11825
  now() {
11425
11826
  return performance.now();
@@ -11545,10 +11946,10 @@ var LaserTool = class {
11545
11946
  };
11546
11947
 
11547
11948
  // src/tools/ping-tool.ts
11548
- var DEFAULT_COLOR4 = "#ff3b30";
11549
- var DEFAULT_DURATION_MS2 = 1800;
11550
- var DEFAULT_RADIUS3 = 48;
11551
- var DEFAULT_MIN_INTERVAL_MS = 300;
11949
+ var DEFAULT_COLOR6 = "#ff3b30";
11950
+ var DEFAULT_DURATION_MS3 = 1800;
11951
+ var DEFAULT_RADIUS4 = 48;
11952
+ var DEFAULT_MIN_INTERVAL_MS2 = 300;
11552
11953
  var PingTool = class {
11553
11954
  name;
11554
11955
  color;
@@ -11562,10 +11963,10 @@ var PingTool = class {
11562
11963
  pingListeners = /* @__PURE__ */ new Set();
11563
11964
  constructor(options = {}) {
11564
11965
  this.name = options.name ?? "ping";
11565
- this.color = options.color ?? DEFAULT_COLOR4;
11566
- this.durationMs = options.durationMs ?? DEFAULT_DURATION_MS2;
11567
- this.radius = options.radius ?? DEFAULT_RADIUS3;
11568
- this.minIntervalMs = options.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS;
11966
+ this.color = options.color ?? DEFAULT_COLOR6;
11967
+ this.durationMs = options.durationMs ?? DEFAULT_DURATION_MS3;
11968
+ this.radius = options.radius ?? DEFAULT_RADIUS4;
11969
+ this.minIntervalMs = options.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS2;
11569
11970
  }
11570
11971
  now() {
11571
11972
  return performance.now();
@@ -11665,7 +12066,7 @@ var PingTool = class {
11665
12066
  };
11666
12067
 
11667
12068
  // src/index.ts
11668
- var VERSION = "0.55.0";
12069
+ var VERSION = "0.57.0";
11669
12070
  export {
11670
12071
  ArrowTool,
11671
12072
  AutoSave,
@@ -11681,13 +12082,16 @@ export {
11681
12082
  LaserTool,
11682
12083
  LayerManager,
11683
12084
  LocalStorageAdapter,
12085
+ MEASURE_PRESENCE_KIND,
11684
12086
  MeasureTool,
11685
12087
  MemoryAdapter,
11686
12088
  NoteTool,
11687
12089
  PING_PRESENCE_KIND,
11688
12090
  PencilTool,
12091
+ PingInput,
11689
12092
  PingTool,
11690
12093
  RemoteLaserOverlay,
12094
+ RemoteMeasureOverlay,
11691
12095
  RemotePingOverlay,
11692
12096
  SelectTool,
11693
12097
  ShapeTool,
@@ -11725,6 +12129,7 @@ export {
11725
12129
  getHexCellsInSquare,
11726
12130
  getHexDistance,
11727
12131
  isLaserTrailPresence,
12132
+ isMeasurePresence,
11728
12133
  isNearBezier,
11729
12134
  isPingPresence,
11730
12135
  setFontSize,
@@ -11733,6 +12138,7 @@ export {
11733
12138
  snapToHexCenter,
11734
12139
  styleToPatch,
11735
12140
  toLaserTrailPresence,
12141
+ toMeasurePresence,
11736
12142
  toPingPresence,
11737
12143
  toggleBold,
11738
12144
  toggleItalic,