@fieldnotes/core 0.56.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.cjs CHANGED
@@ -34,6 +34,7 @@ __export(index_exports, {
34
34
  LaserTool: () => LaserTool,
35
35
  LayerManager: () => LayerManager,
36
36
  LocalStorageAdapter: () => LocalStorageAdapter,
37
+ MEASURE_PRESENCE_KIND: () => MEASURE_PRESENCE_KIND,
37
38
  MeasureTool: () => MeasureTool,
38
39
  MemoryAdapter: () => MemoryAdapter,
39
40
  NoteTool: () => NoteTool,
@@ -42,6 +43,7 @@ __export(index_exports, {
42
43
  PingInput: () => PingInput,
43
44
  PingTool: () => PingTool,
44
45
  RemoteLaserOverlay: () => RemoteLaserOverlay,
46
+ RemoteMeasureOverlay: () => RemoteMeasureOverlay,
45
47
  RemotePingOverlay: () => RemotePingOverlay,
46
48
  SelectTool: () => SelectTool,
47
49
  ShapeTool: () => ShapeTool,
@@ -79,6 +81,7 @@ __export(index_exports, {
79
81
  getHexCellsInSquare: () => getHexCellsInSquare,
80
82
  getHexDistance: () => getHexDistance,
81
83
  isLaserTrailPresence: () => isLaserTrailPresence,
84
+ isMeasurePresence: () => isMeasurePresence,
82
85
  isNearBezier: () => isNearBezier,
83
86
  isPingPresence: () => isPingPresence,
84
87
  setFontSize: () => setFontSize,
@@ -87,6 +90,7 @@ __export(index_exports, {
87
90
  snapToHexCenter: () => snapToHexCenter,
88
91
  styleToPatch: () => styleToPatch,
89
92
  toLaserTrailPresence: () => toLaserTrailPresence,
93
+ toMeasurePresence: () => toMeasurePresence,
90
94
  toPingPresence: () => toPingPresence,
91
95
  toggleBold: () => toggleBold,
92
96
  toggleItalic: () => toggleItalic,
@@ -8942,10 +8946,218 @@ var RemotePingOverlay = class {
8942
8946
  }
8943
8947
  };
8944
8948
 
8949
+ // src/canvas/measure-render.ts
8950
+ function formatMeasureLabel(feet) {
8951
+ return `${Math.round(feet)} ft`;
8952
+ }
8953
+ function drawMeasurement(ctx, m, opts = {}) {
8954
+ ctx.save();
8955
+ if (opts.alpha !== void 0) ctx.globalAlpha = opts.alpha;
8956
+ ctx.strokeStyle = m.color;
8957
+ ctx.setLineDash([8, 4]);
8958
+ ctx.lineWidth = 2;
8959
+ ctx.beginPath();
8960
+ ctx.moveTo(m.start.x, m.start.y);
8961
+ ctx.lineTo(m.end.x, m.end.y);
8962
+ ctx.stroke();
8963
+ ctx.setLineDash([]);
8964
+ ctx.fillStyle = m.color;
8965
+ const dotRadius = 4;
8966
+ ctx.beginPath();
8967
+ ctx.arc(m.start.x, m.start.y, dotRadius, 0, Math.PI * 2);
8968
+ ctx.fill();
8969
+ ctx.beginPath();
8970
+ ctx.arc(m.end.x, m.end.y, dotRadius, 0, Math.PI * 2);
8971
+ ctx.fill();
8972
+ const label = formatMeasureLabel(m.feet);
8973
+ const midX = (m.start.x + m.end.x) / 2;
8974
+ const midY = (m.start.y + m.end.y) / 2;
8975
+ ctx.font = "14px sans-serif";
8976
+ const metrics = ctx.measureText(label);
8977
+ const padX = 6;
8978
+ const padY = 4;
8979
+ const textH = 14;
8980
+ ctx.fillStyle = "rgba(0, 0, 0, 0.75)";
8981
+ ctx.beginPath();
8982
+ ctx.roundRect(
8983
+ midX - metrics.width / 2 - padX,
8984
+ midY - textH / 2 - padY,
8985
+ metrics.width + padX * 2,
8986
+ textH + padY * 2,
8987
+ 4
8988
+ );
8989
+ ctx.fill();
8990
+ ctx.fillStyle = "#FFFFFF";
8991
+ ctx.textAlign = "center";
8992
+ ctx.textBaseline = "middle";
8993
+ ctx.fillText(label, midX, midY);
8994
+ ctx.restore();
8995
+ }
8996
+
8997
+ // src/canvas/remote-measure-overlay.ts
8998
+ var MEASURE_PRESENCE_KIND = "measure";
8999
+ function isFinitePoint2(value) {
9000
+ if (typeof value !== "object" || value === null) return false;
9001
+ const point = value;
9002
+ return typeof point.x === "number" && Number.isFinite(point.x) && typeof point.y === "number" && Number.isFinite(point.y);
9003
+ }
9004
+ function isMeasurePresence(data) {
9005
+ if (typeof data !== "object" || data === null) return false;
9006
+ const payload = data;
9007
+ if (payload.kind !== MEASURE_PRESENCE_KIND) return false;
9008
+ if ("cleared" in payload) return payload.cleared === true;
9009
+ if (!isFinitePoint2(payload.start) || !isFinitePoint2(payload.end)) return false;
9010
+ if (typeof payload.cells !== "number" || !Number.isFinite(payload.cells)) return false;
9011
+ if (typeof payload.feet !== "number" || !Number.isFinite(payload.feet)) return false;
9012
+ if (payload.color !== void 0 && typeof payload.color !== "string") return false;
9013
+ return true;
9014
+ }
9015
+ function toMeasurePresence(emission) {
9016
+ if (emission === null) return { kind: MEASURE_PRESENCE_KIND, cleared: true };
9017
+ return {
9018
+ kind: MEASURE_PRESENCE_KIND,
9019
+ start: emission.start,
9020
+ end: emission.end,
9021
+ cells: emission.cells,
9022
+ feet: emission.feet,
9023
+ color: emission.color
9024
+ };
9025
+ }
9026
+ var DEFAULT_COLOR3 = "#FF5722";
9027
+ var DEFAULT_HOLD_MS = 1500;
9028
+ var DEFAULT_FADE_MS2 = 400;
9029
+ var DEFAULT_MAX_AGE_MS = 3e4;
9030
+ var RemoteMeasureOverlay = class {
9031
+ host;
9032
+ color;
9033
+ holdMs;
9034
+ fadeMs;
9035
+ maxAgeMs;
9036
+ measurements = /* @__PURE__ */ new Map();
9037
+ unregister;
9038
+ rafId = null;
9039
+ disposed = false;
9040
+ constructor(host, options = {}) {
9041
+ this.host = host;
9042
+ this.color = options.color ?? DEFAULT_COLOR3;
9043
+ this.holdMs = options.holdMs ?? DEFAULT_HOLD_MS;
9044
+ this.fadeMs = options.fadeMs ?? DEFAULT_FADE_MS2;
9045
+ this.maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
9046
+ this.unregister = host.registerOverlay((ctx) => this.renderMeasurements(ctx));
9047
+ }
9048
+ now() {
9049
+ return performance.now();
9050
+ }
9051
+ /**
9052
+ * Applies a presence payload from `sender` (any opaque per-sender key, e.g.
9053
+ * the envelope `from`). Non-measure or malformed payloads are ignored and
9054
+ * reported as `false`, so hosts can feed every presence frame through.
9055
+ */
9056
+ apply(sender, data) {
9057
+ if (this.disposed || !isMeasurePresence(data)) return false;
9058
+ if ("cleared" in data) {
9059
+ this.beginLinger(sender);
9060
+ return true;
9061
+ }
9062
+ const existing = this.measurements.get(sender);
9063
+ if (existing?.expiryTimer != null) clearTimeout(existing.expiryTimer);
9064
+ this.measurements.set(sender, {
9065
+ start: data.start,
9066
+ end: data.end,
9067
+ feet: data.feet,
9068
+ color: data.color ?? this.color,
9069
+ clearedAt: null,
9070
+ expiryTimer: setTimeout(() => this.beginLinger(sender), this.maxAgeMs)
9071
+ });
9072
+ this.host.requestRender();
9073
+ return true;
9074
+ }
9075
+ /** Removes a sender's ruler immediately (presence-leave/disconnect). */
9076
+ remove(sender) {
9077
+ const entry = this.measurements.get(sender);
9078
+ if (!entry) return;
9079
+ if (entry.expiryTimer != null) clearTimeout(entry.expiryTimer);
9080
+ this.measurements.delete(sender);
9081
+ this.host.requestRender();
9082
+ }
9083
+ /** Removes every ruler immediately. */
9084
+ clear() {
9085
+ if (this.measurements.size === 0) return;
9086
+ for (const entry of this.measurements.values()) {
9087
+ if (entry.expiryTimer != null) clearTimeout(entry.expiryTimer);
9088
+ }
9089
+ this.measurements.clear();
9090
+ this.host.requestRender();
9091
+ }
9092
+ /** Number of senders with a visible (active or lingering) ruler. */
9093
+ get activeSenderCount() {
9094
+ return this.measurements.size;
9095
+ }
9096
+ /** Unregisters the overlay, cancels timers, stops animating. Idempotent. */
9097
+ dispose() {
9098
+ if (this.disposed) return;
9099
+ this.disposed = true;
9100
+ if (this.rafId !== null) {
9101
+ cancelAnimationFrame(this.rafId);
9102
+ this.rafId = null;
9103
+ }
9104
+ for (const entry of this.measurements.values()) {
9105
+ if (entry.expiryTimer != null) clearTimeout(entry.expiryTimer);
9106
+ }
9107
+ this.measurements.clear();
9108
+ this.unregister?.();
9109
+ this.unregister = null;
9110
+ this.host.requestRender();
9111
+ }
9112
+ beginLinger(sender) {
9113
+ const entry = this.measurements.get(sender);
9114
+ if (!entry || entry.clearedAt !== null) return;
9115
+ if (entry.expiryTimer != null) {
9116
+ clearTimeout(entry.expiryTimer);
9117
+ entry.expiryTimer = null;
9118
+ }
9119
+ entry.clearedAt = this.now();
9120
+ this.ensureAnimating();
9121
+ this.host.requestRender();
9122
+ }
9123
+ ensureAnimating() {
9124
+ if (this.rafId === null) {
9125
+ this.rafId = requestAnimationFrame(() => this.tick());
9126
+ }
9127
+ }
9128
+ tick() {
9129
+ if (this.disposed) return;
9130
+ const now = this.now();
9131
+ let lingering = 0;
9132
+ for (const [sender, entry] of this.measurements) {
9133
+ if (entry.clearedAt === null) continue;
9134
+ if (now - entry.clearedAt >= this.holdMs + this.fadeMs) {
9135
+ this.measurements.delete(sender);
9136
+ } else {
9137
+ lingering += 1;
9138
+ }
9139
+ }
9140
+ this.host.requestRender();
9141
+ this.rafId = lingering > 0 ? requestAnimationFrame(() => this.tick()) : null;
9142
+ }
9143
+ renderMeasurements(ctx) {
9144
+ if (this.measurements.size === 0) return;
9145
+ const now = this.now();
9146
+ for (const entry of this.measurements.values()) {
9147
+ let alpha = 1;
9148
+ if (entry.clearedAt !== null) {
9149
+ const fadeAge = now - entry.clearedAt - this.holdMs;
9150
+ if (fadeAge > 0) alpha = Math.max(0, 1 - fadeAge / this.fadeMs);
9151
+ }
9152
+ drawMeasurement(ctx, entry, { alpha });
9153
+ }
9154
+ }
9155
+ };
9156
+
8945
9157
  // src/canvas/ping-input.ts
8946
9158
  var DEFAULT_LONG_PRESS_MS = 600;
8947
9159
  var DEFAULT_SLOP_PX = 8;
8948
- var DEFAULT_COLOR3 = "#ff3b30";
9160
+ var DEFAULT_COLOR4 = "#ff3b30";
8949
9161
  var DEFAULT_DURATION_MS2 = 1800;
8950
9162
  var DEFAULT_RADIUS2 = 48;
8951
9163
  var DEFAULT_MIN_INTERVAL_MS = 300;
@@ -8978,7 +9190,7 @@ var PingInput = class {
8978
9190
  this.longPressEnabled = options.longPressEnabled ?? false;
8979
9191
  this.longPressMs = options.longPressMs ?? DEFAULT_LONG_PRESS_MS;
8980
9192
  this.slopPx = options.slopPx ?? DEFAULT_SLOP_PX;
8981
- this.color = options.color ?? DEFAULT_COLOR3;
9193
+ this.color = options.color ?? DEFAULT_COLOR4;
8982
9194
  this.durationMs = options.durationMs ?? DEFAULT_DURATION_MS2;
8983
9195
  this.radius = options.radius ?? DEFAULT_RADIUS2;
8984
9196
  this.minIntervalMs = options.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS;
@@ -11268,21 +11480,37 @@ var MeasureTool = class {
11268
11480
  gridType;
11269
11481
  hexOrientation;
11270
11482
  feetPerCell;
11483
+ color;
11271
11484
  optionListeners = /* @__PURE__ */ new Set();
11485
+ measurementListeners = /* @__PURE__ */ new Set();
11486
+ emissionRafId = null;
11272
11487
  constructor(options = {}) {
11273
11488
  this.feetPerCell = options.feetPerCell ?? 5;
11489
+ this.color = options.color ?? "#FF5722";
11274
11490
  }
11275
11491
  getOptions() {
11276
- return { feetPerCell: this.feetPerCell };
11492
+ return { feetPerCell: this.feetPerCell, color: this.color };
11277
11493
  }
11278
11494
  setOptions(options) {
11279
11495
  if (options.feetPerCell !== void 0) this.feetPerCell = options.feetPerCell;
11496
+ if (options.color !== void 0) this.color = options.color;
11280
11497
  this.notifyOptionsChange();
11281
11498
  }
11282
11499
  onOptionsChange(listener) {
11283
11500
  this.optionListeners.add(listener);
11284
11501
  return () => this.optionListeners.delete(listener);
11285
11502
  }
11503
+ /**
11504
+ * Subscribes to raf-coalesced measurement snapshots. While a measurement is
11505
+ * in progress, listeners receive at most one snapshot per animation frame
11506
+ * carrying the latest state; `null` is delivered synchronously when the
11507
+ * measurement clears (pointer-up or deactivate). Emissions are ephemeral by
11508
+ * contract: presence only — never elements, history, or persisted state.
11509
+ */
11510
+ onMeasurement(listener) {
11511
+ this.measurementListeners.add(listener);
11512
+ return () => this.measurementListeners.delete(listener);
11513
+ }
11286
11514
  onPointerDown(state, ctx) {
11287
11515
  this.gridSize = ctx.gridSize ?? 1;
11288
11516
  this.gridType = ctx.gridType;
@@ -11290,22 +11518,27 @@ var MeasureTool = class {
11290
11518
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
11291
11519
  this.start = this.snapToGrid(world, ctx);
11292
11520
  this.end = { ...this.start };
11521
+ this.scheduleEmission();
11293
11522
  }
11294
11523
  onPointerMove(state, ctx) {
11295
11524
  if (!this.start) return;
11296
11525
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
11297
11526
  this.end = this.snapToGrid(world, ctx);
11298
11527
  ctx.requestRender();
11528
+ this.scheduleEmission();
11299
11529
  }
11300
11530
  onPointerUp(_state, ctx) {
11301
11531
  if (!this.start) return;
11302
11532
  this.start = null;
11303
11533
  this.end = null;
11304
11534
  ctx.requestRender();
11535
+ this.emitClear();
11305
11536
  }
11306
11537
  onDeactivate(_ctx) {
11538
+ const wasActive = this.start !== null;
11307
11539
  this.start = null;
11308
11540
  this.end = null;
11541
+ if (wasActive) this.emitClear();
11309
11542
  }
11310
11543
  getMeasurement() {
11311
11544
  if (!this.start || !this.end) return null;
@@ -11331,46 +11564,7 @@ var MeasureTool = class {
11331
11564
  renderOverlay(ctx) {
11332
11565
  const m = this.getMeasurement();
11333
11566
  if (!m) return;
11334
- ctx.save();
11335
- ctx.strokeStyle = "#FF5722";
11336
- ctx.setLineDash([8, 4]);
11337
- ctx.lineWidth = 2;
11338
- ctx.beginPath();
11339
- ctx.moveTo(m.start.x, m.start.y);
11340
- ctx.lineTo(m.end.x, m.end.y);
11341
- ctx.stroke();
11342
- ctx.setLineDash([]);
11343
- ctx.fillStyle = "#FF5722";
11344
- const dotRadius = 4;
11345
- ctx.beginPath();
11346
- ctx.arc(m.start.x, m.start.y, dotRadius, 0, Math.PI * 2);
11347
- ctx.fill();
11348
- ctx.beginPath();
11349
- ctx.arc(m.end.x, m.end.y, dotRadius, 0, Math.PI * 2);
11350
- ctx.fill();
11351
- const label = `${Math.round(m.feet)} ft`;
11352
- const midX = (m.start.x + m.end.x) / 2;
11353
- const midY = (m.start.y + m.end.y) / 2;
11354
- ctx.font = "14px sans-serif";
11355
- const metrics = ctx.measureText(label);
11356
- const padX = 6;
11357
- const padY = 4;
11358
- const textH = 14;
11359
- ctx.fillStyle = "rgba(0, 0, 0, 0.75)";
11360
- ctx.beginPath();
11361
- ctx.roundRect(
11362
- midX - metrics.width / 2 - padX,
11363
- midY - textH / 2 - padY,
11364
- metrics.width + padX * 2,
11365
- textH + padY * 2,
11366
- 4
11367
- );
11368
- ctx.fill();
11369
- ctx.fillStyle = "#FFFFFF";
11370
- ctx.textAlign = "center";
11371
- ctx.textBaseline = "middle";
11372
- ctx.fillText(label, midX, midY);
11373
- ctx.restore();
11567
+ drawMeasurement(ctx, { start: m.start, end: m.end, feet: m.feet, color: this.color });
11374
11568
  }
11375
11569
  snapToGrid(point, ctx) {
11376
11570
  if (!ctx.gridSize) return point;
@@ -11388,6 +11582,32 @@ var MeasureTool = class {
11388
11582
  notifyOptionsChange() {
11389
11583
  for (const listener of this.optionListeners) listener();
11390
11584
  }
11585
+ scheduleEmission() {
11586
+ if (this.measurementListeners.size === 0) return;
11587
+ if (this.emissionRafId !== null) return;
11588
+ this.emissionRafId = requestAnimationFrame(() => {
11589
+ this.emissionRafId = null;
11590
+ const m = this.getMeasurement();
11591
+ if (!m) return;
11592
+ this.emit({ ...m, color: this.color });
11593
+ });
11594
+ }
11595
+ emitClear() {
11596
+ if (this.emissionRafId !== null) {
11597
+ cancelAnimationFrame(this.emissionRafId);
11598
+ this.emissionRafId = null;
11599
+ }
11600
+ if (this.measurementListeners.size === 0) return;
11601
+ this.emit(null);
11602
+ }
11603
+ emit(emission) {
11604
+ for (const listener of this.measurementListeners) {
11605
+ try {
11606
+ listener(emission);
11607
+ } catch {
11608
+ }
11609
+ }
11610
+ }
11391
11611
  };
11392
11612
 
11393
11613
  // src/tools/template-tool.ts
@@ -11683,9 +11903,9 @@ var TemplateTool = class {
11683
11903
  };
11684
11904
 
11685
11905
  // src/tools/laser-tool.ts
11686
- var DEFAULT_COLOR4 = "#ff3b30";
11906
+ var DEFAULT_COLOR5 = "#ff3b30";
11687
11907
  var DEFAULT_WIDTH2 = 4;
11688
- var DEFAULT_FADE_MS2 = 1200;
11908
+ var DEFAULT_FADE_MS3 = 1200;
11689
11909
  var LaserTool = class {
11690
11910
  name;
11691
11911
  color;
@@ -11699,9 +11919,9 @@ var LaserTool = class {
11699
11919
  pendingEmission = [];
11700
11920
  constructor(options = {}) {
11701
11921
  this.name = options.name ?? "laser";
11702
- this.color = options.color ?? DEFAULT_COLOR4;
11922
+ this.color = options.color ?? DEFAULT_COLOR5;
11703
11923
  this.width = options.width ?? DEFAULT_WIDTH2;
11704
- this.fadeMs = options.fadeMs ?? DEFAULT_FADE_MS2;
11924
+ this.fadeMs = options.fadeMs ?? DEFAULT_FADE_MS3;
11705
11925
  }
11706
11926
  now() {
11707
11927
  return performance.now();
@@ -11827,7 +12047,7 @@ var LaserTool = class {
11827
12047
  };
11828
12048
 
11829
12049
  // src/tools/ping-tool.ts
11830
- var DEFAULT_COLOR5 = "#ff3b30";
12050
+ var DEFAULT_COLOR6 = "#ff3b30";
11831
12051
  var DEFAULT_DURATION_MS3 = 1800;
11832
12052
  var DEFAULT_RADIUS4 = 48;
11833
12053
  var DEFAULT_MIN_INTERVAL_MS2 = 300;
@@ -11844,7 +12064,7 @@ var PingTool = class {
11844
12064
  pingListeners = /* @__PURE__ */ new Set();
11845
12065
  constructor(options = {}) {
11846
12066
  this.name = options.name ?? "ping";
11847
- this.color = options.color ?? DEFAULT_COLOR5;
12067
+ this.color = options.color ?? DEFAULT_COLOR6;
11848
12068
  this.durationMs = options.durationMs ?? DEFAULT_DURATION_MS3;
11849
12069
  this.radius = options.radius ?? DEFAULT_RADIUS4;
11850
12070
  this.minIntervalMs = options.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS2;
@@ -11947,7 +12167,7 @@ var PingTool = class {
11947
12167
  };
11948
12168
 
11949
12169
  // src/index.ts
11950
- var VERSION = "0.56.0";
12170
+ var VERSION = "0.57.0";
11951
12171
  // Annotate the CommonJS export names for ESM import in node:
11952
12172
  0 && (module.exports = {
11953
12173
  ArrowTool,
@@ -11964,6 +12184,7 @@ var VERSION = "0.56.0";
11964
12184
  LaserTool,
11965
12185
  LayerManager,
11966
12186
  LocalStorageAdapter,
12187
+ MEASURE_PRESENCE_KIND,
11967
12188
  MeasureTool,
11968
12189
  MemoryAdapter,
11969
12190
  NoteTool,
@@ -11972,6 +12193,7 @@ var VERSION = "0.56.0";
11972
12193
  PingInput,
11973
12194
  PingTool,
11974
12195
  RemoteLaserOverlay,
12196
+ RemoteMeasureOverlay,
11975
12197
  RemotePingOverlay,
11976
12198
  SelectTool,
11977
12199
  ShapeTool,
@@ -12009,6 +12231,7 @@ var VERSION = "0.56.0";
12009
12231
  getHexCellsInSquare,
12010
12232
  getHexDistance,
12011
12233
  isLaserTrailPresence,
12234
+ isMeasurePresence,
12012
12235
  isNearBezier,
12013
12236
  isPingPresence,
12014
12237
  setFontSize,
@@ -12017,6 +12240,7 @@ var VERSION = "0.56.0";
12017
12240
  snapToHexCenter,
12018
12241
  styleToPatch,
12019
12242
  toLaserTrailPresence,
12243
+ toMeasurePresence,
12020
12244
  toPingPresence,
12021
12245
  toggleBold,
12022
12246
  toggleItalic,