@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.js CHANGED
@@ -8845,10 +8845,218 @@ 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
+
8848
9056
  // src/canvas/ping-input.ts
8849
9057
  var DEFAULT_LONG_PRESS_MS = 600;
8850
9058
  var DEFAULT_SLOP_PX = 8;
8851
- var DEFAULT_COLOR3 = "#ff3b30";
9059
+ var DEFAULT_COLOR4 = "#ff3b30";
8852
9060
  var DEFAULT_DURATION_MS2 = 1800;
8853
9061
  var DEFAULT_RADIUS2 = 48;
8854
9062
  var DEFAULT_MIN_INTERVAL_MS = 300;
@@ -8881,7 +9089,7 @@ var PingInput = class {
8881
9089
  this.longPressEnabled = options.longPressEnabled ?? false;
8882
9090
  this.longPressMs = options.longPressMs ?? DEFAULT_LONG_PRESS_MS;
8883
9091
  this.slopPx = options.slopPx ?? DEFAULT_SLOP_PX;
8884
- this.color = options.color ?? DEFAULT_COLOR3;
9092
+ this.color = options.color ?? DEFAULT_COLOR4;
8885
9093
  this.durationMs = options.durationMs ?? DEFAULT_DURATION_MS2;
8886
9094
  this.radius = options.radius ?? DEFAULT_RADIUS2;
8887
9095
  this.minIntervalMs = options.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS;
@@ -11171,21 +11379,37 @@ var MeasureTool = class {
11171
11379
  gridType;
11172
11380
  hexOrientation;
11173
11381
  feetPerCell;
11382
+ color;
11174
11383
  optionListeners = /* @__PURE__ */ new Set();
11384
+ measurementListeners = /* @__PURE__ */ new Set();
11385
+ emissionRafId = null;
11175
11386
  constructor(options = {}) {
11176
11387
  this.feetPerCell = options.feetPerCell ?? 5;
11388
+ this.color = options.color ?? "#FF5722";
11177
11389
  }
11178
11390
  getOptions() {
11179
- return { feetPerCell: this.feetPerCell };
11391
+ return { feetPerCell: this.feetPerCell, color: this.color };
11180
11392
  }
11181
11393
  setOptions(options) {
11182
11394
  if (options.feetPerCell !== void 0) this.feetPerCell = options.feetPerCell;
11395
+ if (options.color !== void 0) this.color = options.color;
11183
11396
  this.notifyOptionsChange();
11184
11397
  }
11185
11398
  onOptionsChange(listener) {
11186
11399
  this.optionListeners.add(listener);
11187
11400
  return () => this.optionListeners.delete(listener);
11188
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
+ }
11189
11413
  onPointerDown(state, ctx) {
11190
11414
  this.gridSize = ctx.gridSize ?? 1;
11191
11415
  this.gridType = ctx.gridType;
@@ -11193,22 +11417,27 @@ var MeasureTool = class {
11193
11417
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
11194
11418
  this.start = this.snapToGrid(world, ctx);
11195
11419
  this.end = { ...this.start };
11420
+ this.scheduleEmission();
11196
11421
  }
11197
11422
  onPointerMove(state, ctx) {
11198
11423
  if (!this.start) return;
11199
11424
  const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
11200
11425
  this.end = this.snapToGrid(world, ctx);
11201
11426
  ctx.requestRender();
11427
+ this.scheduleEmission();
11202
11428
  }
11203
11429
  onPointerUp(_state, ctx) {
11204
11430
  if (!this.start) return;
11205
11431
  this.start = null;
11206
11432
  this.end = null;
11207
11433
  ctx.requestRender();
11434
+ this.emitClear();
11208
11435
  }
11209
11436
  onDeactivate(_ctx) {
11437
+ const wasActive = this.start !== null;
11210
11438
  this.start = null;
11211
11439
  this.end = null;
11440
+ if (wasActive) this.emitClear();
11212
11441
  }
11213
11442
  getMeasurement() {
11214
11443
  if (!this.start || !this.end) return null;
@@ -11234,46 +11463,7 @@ var MeasureTool = class {
11234
11463
  renderOverlay(ctx) {
11235
11464
  const m = this.getMeasurement();
11236
11465
  if (!m) return;
11237
- ctx.save();
11238
- ctx.strokeStyle = "#FF5722";
11239
- ctx.setLineDash([8, 4]);
11240
- ctx.lineWidth = 2;
11241
- ctx.beginPath();
11242
- ctx.moveTo(m.start.x, m.start.y);
11243
- ctx.lineTo(m.end.x, m.end.y);
11244
- ctx.stroke();
11245
- ctx.setLineDash([]);
11246
- ctx.fillStyle = "#FF5722";
11247
- const dotRadius = 4;
11248
- ctx.beginPath();
11249
- ctx.arc(m.start.x, m.start.y, dotRadius, 0, Math.PI * 2);
11250
- ctx.fill();
11251
- ctx.beginPath();
11252
- ctx.arc(m.end.x, m.end.y, dotRadius, 0, Math.PI * 2);
11253
- ctx.fill();
11254
- const label = `${Math.round(m.feet)} ft`;
11255
- const midX = (m.start.x + m.end.x) / 2;
11256
- const midY = (m.start.y + m.end.y) / 2;
11257
- ctx.font = "14px sans-serif";
11258
- const metrics = ctx.measureText(label);
11259
- const padX = 6;
11260
- const padY = 4;
11261
- const textH = 14;
11262
- ctx.fillStyle = "rgba(0, 0, 0, 0.75)";
11263
- ctx.beginPath();
11264
- ctx.roundRect(
11265
- midX - metrics.width / 2 - padX,
11266
- midY - textH / 2 - padY,
11267
- metrics.width + padX * 2,
11268
- textH + padY * 2,
11269
- 4
11270
- );
11271
- ctx.fill();
11272
- ctx.fillStyle = "#FFFFFF";
11273
- ctx.textAlign = "center";
11274
- ctx.textBaseline = "middle";
11275
- ctx.fillText(label, midX, midY);
11276
- ctx.restore();
11466
+ drawMeasurement(ctx, { start: m.start, end: m.end, feet: m.feet, color: this.color });
11277
11467
  }
11278
11468
  snapToGrid(point, ctx) {
11279
11469
  if (!ctx.gridSize) return point;
@@ -11291,6 +11481,32 @@ var MeasureTool = class {
11291
11481
  notifyOptionsChange() {
11292
11482
  for (const listener of this.optionListeners) listener();
11293
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
+ }
11294
11510
  };
11295
11511
 
11296
11512
  // src/tools/template-tool.ts
@@ -11586,9 +11802,9 @@ var TemplateTool = class {
11586
11802
  };
11587
11803
 
11588
11804
  // src/tools/laser-tool.ts
11589
- var DEFAULT_COLOR4 = "#ff3b30";
11805
+ var DEFAULT_COLOR5 = "#ff3b30";
11590
11806
  var DEFAULT_WIDTH2 = 4;
11591
- var DEFAULT_FADE_MS2 = 1200;
11807
+ var DEFAULT_FADE_MS3 = 1200;
11592
11808
  var LaserTool = class {
11593
11809
  name;
11594
11810
  color;
@@ -11602,9 +11818,9 @@ var LaserTool = class {
11602
11818
  pendingEmission = [];
11603
11819
  constructor(options = {}) {
11604
11820
  this.name = options.name ?? "laser";
11605
- this.color = options.color ?? DEFAULT_COLOR4;
11821
+ this.color = options.color ?? DEFAULT_COLOR5;
11606
11822
  this.width = options.width ?? DEFAULT_WIDTH2;
11607
- this.fadeMs = options.fadeMs ?? DEFAULT_FADE_MS2;
11823
+ this.fadeMs = options.fadeMs ?? DEFAULT_FADE_MS3;
11608
11824
  }
11609
11825
  now() {
11610
11826
  return performance.now();
@@ -11730,7 +11946,7 @@ var LaserTool = class {
11730
11946
  };
11731
11947
 
11732
11948
  // src/tools/ping-tool.ts
11733
- var DEFAULT_COLOR5 = "#ff3b30";
11949
+ var DEFAULT_COLOR6 = "#ff3b30";
11734
11950
  var DEFAULT_DURATION_MS3 = 1800;
11735
11951
  var DEFAULT_RADIUS4 = 48;
11736
11952
  var DEFAULT_MIN_INTERVAL_MS2 = 300;
@@ -11747,7 +11963,7 @@ var PingTool = class {
11747
11963
  pingListeners = /* @__PURE__ */ new Set();
11748
11964
  constructor(options = {}) {
11749
11965
  this.name = options.name ?? "ping";
11750
- this.color = options.color ?? DEFAULT_COLOR5;
11966
+ this.color = options.color ?? DEFAULT_COLOR6;
11751
11967
  this.durationMs = options.durationMs ?? DEFAULT_DURATION_MS3;
11752
11968
  this.radius = options.radius ?? DEFAULT_RADIUS4;
11753
11969
  this.minIntervalMs = options.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS2;
@@ -11850,7 +12066,7 @@ var PingTool = class {
11850
12066
  };
11851
12067
 
11852
12068
  // src/index.ts
11853
- var VERSION = "0.56.0";
12069
+ var VERSION = "0.57.0";
11854
12070
  export {
11855
12071
  ArrowTool,
11856
12072
  AutoSave,
@@ -11866,6 +12082,7 @@ export {
11866
12082
  LaserTool,
11867
12083
  LayerManager,
11868
12084
  LocalStorageAdapter,
12085
+ MEASURE_PRESENCE_KIND,
11869
12086
  MeasureTool,
11870
12087
  MemoryAdapter,
11871
12088
  NoteTool,
@@ -11874,6 +12091,7 @@ export {
11874
12091
  PingInput,
11875
12092
  PingTool,
11876
12093
  RemoteLaserOverlay,
12094
+ RemoteMeasureOverlay,
11877
12095
  RemotePingOverlay,
11878
12096
  SelectTool,
11879
12097
  ShapeTool,
@@ -11911,6 +12129,7 @@ export {
11911
12129
  getHexCellsInSquare,
11912
12130
  getHexDistance,
11913
12131
  isLaserTrailPresence,
12132
+ isMeasurePresence,
11914
12133
  isNearBezier,
11915
12134
  isPingPresence,
11916
12135
  setFontSize,
@@ -11919,6 +12138,7 @@ export {
11919
12138
  snapToHexCenter,
11920
12139
  styleToPatch,
11921
12140
  toLaserTrailPresence,
12141
+ toMeasurePresence,
11922
12142
  toPingPresence,
11923
12143
  toggleBold,
11924
12144
  toggleItalic,