@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.cjs +467 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +261 -35
- package/dist/index.d.ts +261 -35
- package/dist/index.js +462 -56
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -34,13 +34,16 @@ __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,
|
|
40
41
|
PING_PRESENCE_KIND: () => PING_PRESENCE_KIND,
|
|
41
42
|
PencilTool: () => PencilTool,
|
|
43
|
+
PingInput: () => PingInput,
|
|
42
44
|
PingTool: () => PingTool,
|
|
43
45
|
RemoteLaserOverlay: () => RemoteLaserOverlay,
|
|
46
|
+
RemoteMeasureOverlay: () => RemoteMeasureOverlay,
|
|
44
47
|
RemotePingOverlay: () => RemotePingOverlay,
|
|
45
48
|
SelectTool: () => SelectTool,
|
|
46
49
|
ShapeTool: () => ShapeTool,
|
|
@@ -78,6 +81,7 @@ __export(index_exports, {
|
|
|
78
81
|
getHexCellsInSquare: () => getHexCellsInSquare,
|
|
79
82
|
getHexDistance: () => getHexDistance,
|
|
80
83
|
isLaserTrailPresence: () => isLaserTrailPresence,
|
|
84
|
+
isMeasurePresence: () => isMeasurePresence,
|
|
81
85
|
isNearBezier: () => isNearBezier,
|
|
82
86
|
isPingPresence: () => isPingPresence,
|
|
83
87
|
setFontSize: () => setFontSize,
|
|
@@ -86,6 +90,7 @@ __export(index_exports, {
|
|
|
86
90
|
snapToHexCenter: () => snapToHexCenter,
|
|
87
91
|
styleToPatch: () => styleToPatch,
|
|
88
92
|
toLaserTrailPresence: () => toLaserTrailPresence,
|
|
93
|
+
toMeasurePresence: () => toMeasurePresence,
|
|
89
94
|
toPingPresence: () => toPingPresence,
|
|
90
95
|
toggleBold: () => toggleBold,
|
|
91
96
|
toggleItalic: () => toggleItalic,
|
|
@@ -8941,6 +8946,399 @@ var RemotePingOverlay = class {
|
|
|
8941
8946
|
}
|
|
8942
8947
|
};
|
|
8943
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
|
+
|
|
9157
|
+
// src/canvas/ping-input.ts
|
|
9158
|
+
var DEFAULT_LONG_PRESS_MS = 600;
|
|
9159
|
+
var DEFAULT_SLOP_PX = 8;
|
|
9160
|
+
var DEFAULT_COLOR4 = "#ff3b30";
|
|
9161
|
+
var DEFAULT_DURATION_MS2 = 1800;
|
|
9162
|
+
var DEFAULT_RADIUS2 = 48;
|
|
9163
|
+
var DEFAULT_MIN_INTERVAL_MS = 300;
|
|
9164
|
+
var PingInput = class {
|
|
9165
|
+
element;
|
|
9166
|
+
host;
|
|
9167
|
+
longPressEnabled;
|
|
9168
|
+
longPressMs;
|
|
9169
|
+
slopPx;
|
|
9170
|
+
color;
|
|
9171
|
+
durationMs;
|
|
9172
|
+
radius;
|
|
9173
|
+
minIntervalMs;
|
|
9174
|
+
shouldPing;
|
|
9175
|
+
press = null;
|
|
9176
|
+
downPointers = /* @__PURE__ */ new Set();
|
|
9177
|
+
lastPointerScreen = null;
|
|
9178
|
+
lastEmitAt = -Infinity;
|
|
9179
|
+
disposed = false;
|
|
9180
|
+
optionListeners = /* @__PURE__ */ new Set();
|
|
9181
|
+
pingListeners = /* @__PURE__ */ new Set();
|
|
9182
|
+
handlePointerDown = (e) => this.onPointerDown(e);
|
|
9183
|
+
handlePointerMove = (e) => this.onPointerMove(e);
|
|
9184
|
+
handlePointerUp = (e) => this.onPointerEnd(e);
|
|
9185
|
+
handlePointerCancel = (e) => this.onPointerEnd(e);
|
|
9186
|
+
handlePointerLeave = (e) => this.onPointerEnd(e);
|
|
9187
|
+
constructor(element, host, options = {}) {
|
|
9188
|
+
this.element = element;
|
|
9189
|
+
this.host = host;
|
|
9190
|
+
this.longPressEnabled = options.longPressEnabled ?? false;
|
|
9191
|
+
this.longPressMs = options.longPressMs ?? DEFAULT_LONG_PRESS_MS;
|
|
9192
|
+
this.slopPx = options.slopPx ?? DEFAULT_SLOP_PX;
|
|
9193
|
+
this.color = options.color ?? DEFAULT_COLOR4;
|
|
9194
|
+
this.durationMs = options.durationMs ?? DEFAULT_DURATION_MS2;
|
|
9195
|
+
this.radius = options.radius ?? DEFAULT_RADIUS2;
|
|
9196
|
+
this.minIntervalMs = options.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS;
|
|
9197
|
+
this.shouldPing = options.shouldPing;
|
|
9198
|
+
const opts = { passive: true };
|
|
9199
|
+
element.addEventListener("pointerdown", this.handlePointerDown, opts);
|
|
9200
|
+
element.addEventListener("pointermove", this.handlePointerMove, opts);
|
|
9201
|
+
element.addEventListener("pointerup", this.handlePointerUp, opts);
|
|
9202
|
+
element.addEventListener("pointercancel", this.handlePointerCancel, opts);
|
|
9203
|
+
element.addEventListener("pointerleave", this.handlePointerLeave, opts);
|
|
9204
|
+
}
|
|
9205
|
+
now() {
|
|
9206
|
+
return performance.now();
|
|
9207
|
+
}
|
|
9208
|
+
getOptions() {
|
|
9209
|
+
return {
|
|
9210
|
+
longPressEnabled: this.longPressEnabled,
|
|
9211
|
+
longPressMs: this.longPressMs,
|
|
9212
|
+
slopPx: this.slopPx,
|
|
9213
|
+
color: this.color,
|
|
9214
|
+
durationMs: this.durationMs,
|
|
9215
|
+
radius: this.radius,
|
|
9216
|
+
minIntervalMs: this.minIntervalMs,
|
|
9217
|
+
...this.shouldPing ? { shouldPing: this.shouldPing } : {}
|
|
9218
|
+
};
|
|
9219
|
+
}
|
|
9220
|
+
setOptions(options) {
|
|
9221
|
+
if (options.longPressEnabled !== void 0) {
|
|
9222
|
+
this.longPressEnabled = options.longPressEnabled;
|
|
9223
|
+
if (!this.longPressEnabled) this.cancelPress();
|
|
9224
|
+
}
|
|
9225
|
+
if (options.longPressMs !== void 0) this.longPressMs = options.longPressMs;
|
|
9226
|
+
if (options.slopPx !== void 0) this.slopPx = options.slopPx;
|
|
9227
|
+
if (options.color !== void 0) this.color = options.color;
|
|
9228
|
+
if (options.durationMs !== void 0) this.durationMs = options.durationMs;
|
|
9229
|
+
if (options.radius !== void 0) this.radius = options.radius;
|
|
9230
|
+
if (options.minIntervalMs !== void 0) this.minIntervalMs = options.minIntervalMs;
|
|
9231
|
+
if (options.shouldPing !== void 0) this.shouldPing = options.shouldPing;
|
|
9232
|
+
for (const listener of this.optionListeners) listener();
|
|
9233
|
+
}
|
|
9234
|
+
onOptionsChange(listener) {
|
|
9235
|
+
this.optionListeners.add(listener);
|
|
9236
|
+
return () => this.optionListeners.delete(listener);
|
|
9237
|
+
}
|
|
9238
|
+
/**
|
|
9239
|
+
* Subscribes to emitted pings. Listeners must not throw; a throwing
|
|
9240
|
+
* listener is isolated so it cannot break input handling or other
|
|
9241
|
+
* listeners.
|
|
9242
|
+
*/
|
|
9243
|
+
onPing(listener) {
|
|
9244
|
+
this.pingListeners.add(listener);
|
|
9245
|
+
return () => this.pingListeners.delete(listener);
|
|
9246
|
+
}
|
|
9247
|
+
/**
|
|
9248
|
+
* Pings at the last tracked pointer position (hover moves and presses),
|
|
9249
|
+
* world-converted at call time. Returns `false` when no pointer has been
|
|
9250
|
+
* seen yet, the host vetoes, or the rate limit drops the ping.
|
|
9251
|
+
*/
|
|
9252
|
+
pingAtPointer() {
|
|
9253
|
+
if (this.disposed || this.lastPointerScreen === null) return false;
|
|
9254
|
+
return this.emit(this.host.screenToWorld(this.lastPointerScreen));
|
|
9255
|
+
}
|
|
9256
|
+
/** Pings a world position directly. Same veto and rate limit as every path. */
|
|
9257
|
+
pingAt(world) {
|
|
9258
|
+
if (this.disposed) return false;
|
|
9259
|
+
return this.emit(world);
|
|
9260
|
+
}
|
|
9261
|
+
/** Removes all DOM listeners and cancels any pending press. Idempotent. */
|
|
9262
|
+
dispose() {
|
|
9263
|
+
if (this.disposed) return;
|
|
9264
|
+
this.disposed = true;
|
|
9265
|
+
this.cancelPress();
|
|
9266
|
+
this.downPointers.clear();
|
|
9267
|
+
this.element.removeEventListener("pointerdown", this.handlePointerDown);
|
|
9268
|
+
this.element.removeEventListener("pointermove", this.handlePointerMove);
|
|
9269
|
+
this.element.removeEventListener("pointerup", this.handlePointerUp);
|
|
9270
|
+
this.element.removeEventListener("pointercancel", this.handlePointerCancel);
|
|
9271
|
+
this.element.removeEventListener("pointerleave", this.handlePointerLeave);
|
|
9272
|
+
this.optionListeners.clear();
|
|
9273
|
+
this.pingListeners.clear();
|
|
9274
|
+
}
|
|
9275
|
+
toLocal(e) {
|
|
9276
|
+
const rect = this.element.getBoundingClientRect();
|
|
9277
|
+
return { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
|
9278
|
+
}
|
|
9279
|
+
onPointerDown(e) {
|
|
9280
|
+
const local = this.toLocal(e);
|
|
9281
|
+
this.lastPointerScreen = local;
|
|
9282
|
+
const hadPointers = this.downPointers.size > 0;
|
|
9283
|
+
this.downPointers.add(e.pointerId);
|
|
9284
|
+
if (hadPointers) {
|
|
9285
|
+
this.cancelPress();
|
|
9286
|
+
return;
|
|
9287
|
+
}
|
|
9288
|
+
if (!this.longPressEnabled) return;
|
|
9289
|
+
if (e.pointerType === "mouse" && e.button !== 0) return;
|
|
9290
|
+
this.press = {
|
|
9291
|
+
pointerId: e.pointerId,
|
|
9292
|
+
x: local.x,
|
|
9293
|
+
y: local.y,
|
|
9294
|
+
timer: setTimeout(() => this.firePress(), this.longPressMs)
|
|
9295
|
+
};
|
|
9296
|
+
}
|
|
9297
|
+
onPointerMove(e) {
|
|
9298
|
+
const local = this.toLocal(e);
|
|
9299
|
+
this.lastPointerScreen = local;
|
|
9300
|
+
if (this.press === null || e.pointerId !== this.press.pointerId) return;
|
|
9301
|
+
const dx = local.x - this.press.x;
|
|
9302
|
+
const dy = local.y - this.press.y;
|
|
9303
|
+
if (Math.hypot(dx, dy) > this.slopPx) this.cancelPress();
|
|
9304
|
+
}
|
|
9305
|
+
onPointerEnd(e) {
|
|
9306
|
+
this.downPointers.delete(e.pointerId);
|
|
9307
|
+
if (this.press !== null && e.pointerId === this.press.pointerId) this.cancelPress();
|
|
9308
|
+
}
|
|
9309
|
+
cancelPress() {
|
|
9310
|
+
if (this.press === null) return;
|
|
9311
|
+
clearTimeout(this.press.timer);
|
|
9312
|
+
this.press = null;
|
|
9313
|
+
}
|
|
9314
|
+
firePress() {
|
|
9315
|
+
if (this.press === null) return;
|
|
9316
|
+
const screen = { x: this.press.x, y: this.press.y };
|
|
9317
|
+
this.press = null;
|
|
9318
|
+
this.emit(this.host.screenToWorld(screen));
|
|
9319
|
+
}
|
|
9320
|
+
emit(world) {
|
|
9321
|
+
if (this.shouldPing && !this.shouldPing()) return false;
|
|
9322
|
+
const t = this.now();
|
|
9323
|
+
if (t - this.lastEmitAt < this.minIntervalMs) return false;
|
|
9324
|
+
this.lastEmitAt = t;
|
|
9325
|
+
const emission = {
|
|
9326
|
+
x: world.x,
|
|
9327
|
+
y: world.y,
|
|
9328
|
+
color: this.color,
|
|
9329
|
+
durationMs: this.durationMs,
|
|
9330
|
+
radius: this.radius
|
|
9331
|
+
};
|
|
9332
|
+
for (const listener of this.pingListeners) {
|
|
9333
|
+
try {
|
|
9334
|
+
listener(emission);
|
|
9335
|
+
} catch {
|
|
9336
|
+
}
|
|
9337
|
+
}
|
|
9338
|
+
return true;
|
|
9339
|
+
}
|
|
9340
|
+
};
|
|
9341
|
+
|
|
8944
9342
|
// src/tools/hand-tool.ts
|
|
8945
9343
|
var HandTool = class {
|
|
8946
9344
|
name = "hand";
|
|
@@ -9202,7 +9600,7 @@ function erasePoints(points, eraser, radius) {
|
|
|
9202
9600
|
}
|
|
9203
9601
|
|
|
9204
9602
|
// src/tools/eraser-tool.ts
|
|
9205
|
-
var
|
|
9603
|
+
var DEFAULT_RADIUS3 = 20;
|
|
9206
9604
|
function makeEraserCursor(radius) {
|
|
9207
9605
|
const size = radius * 2;
|
|
9208
9606
|
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>`;
|
|
@@ -9215,7 +9613,7 @@ var EraserTool = class {
|
|
|
9215
9613
|
cursor;
|
|
9216
9614
|
mode;
|
|
9217
9615
|
constructor(options = {}) {
|
|
9218
|
-
this.radius = options.radius ??
|
|
9616
|
+
this.radius = options.radius ?? DEFAULT_RADIUS3;
|
|
9219
9617
|
this.cursor = makeEraserCursor(this.radius);
|
|
9220
9618
|
this.mode = options.mode ?? "partial";
|
|
9221
9619
|
}
|
|
@@ -11082,21 +11480,37 @@ var MeasureTool = class {
|
|
|
11082
11480
|
gridType;
|
|
11083
11481
|
hexOrientation;
|
|
11084
11482
|
feetPerCell;
|
|
11483
|
+
color;
|
|
11085
11484
|
optionListeners = /* @__PURE__ */ new Set();
|
|
11485
|
+
measurementListeners = /* @__PURE__ */ new Set();
|
|
11486
|
+
emissionRafId = null;
|
|
11086
11487
|
constructor(options = {}) {
|
|
11087
11488
|
this.feetPerCell = options.feetPerCell ?? 5;
|
|
11489
|
+
this.color = options.color ?? "#FF5722";
|
|
11088
11490
|
}
|
|
11089
11491
|
getOptions() {
|
|
11090
|
-
return { feetPerCell: this.feetPerCell };
|
|
11492
|
+
return { feetPerCell: this.feetPerCell, color: this.color };
|
|
11091
11493
|
}
|
|
11092
11494
|
setOptions(options) {
|
|
11093
11495
|
if (options.feetPerCell !== void 0) this.feetPerCell = options.feetPerCell;
|
|
11496
|
+
if (options.color !== void 0) this.color = options.color;
|
|
11094
11497
|
this.notifyOptionsChange();
|
|
11095
11498
|
}
|
|
11096
11499
|
onOptionsChange(listener) {
|
|
11097
11500
|
this.optionListeners.add(listener);
|
|
11098
11501
|
return () => this.optionListeners.delete(listener);
|
|
11099
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
|
+
}
|
|
11100
11514
|
onPointerDown(state, ctx) {
|
|
11101
11515
|
this.gridSize = ctx.gridSize ?? 1;
|
|
11102
11516
|
this.gridType = ctx.gridType;
|
|
@@ -11104,22 +11518,27 @@ var MeasureTool = class {
|
|
|
11104
11518
|
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
11105
11519
|
this.start = this.snapToGrid(world, ctx);
|
|
11106
11520
|
this.end = { ...this.start };
|
|
11521
|
+
this.scheduleEmission();
|
|
11107
11522
|
}
|
|
11108
11523
|
onPointerMove(state, ctx) {
|
|
11109
11524
|
if (!this.start) return;
|
|
11110
11525
|
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
11111
11526
|
this.end = this.snapToGrid(world, ctx);
|
|
11112
11527
|
ctx.requestRender();
|
|
11528
|
+
this.scheduleEmission();
|
|
11113
11529
|
}
|
|
11114
11530
|
onPointerUp(_state, ctx) {
|
|
11115
11531
|
if (!this.start) return;
|
|
11116
11532
|
this.start = null;
|
|
11117
11533
|
this.end = null;
|
|
11118
11534
|
ctx.requestRender();
|
|
11535
|
+
this.emitClear();
|
|
11119
11536
|
}
|
|
11120
11537
|
onDeactivate(_ctx) {
|
|
11538
|
+
const wasActive = this.start !== null;
|
|
11121
11539
|
this.start = null;
|
|
11122
11540
|
this.end = null;
|
|
11541
|
+
if (wasActive) this.emitClear();
|
|
11123
11542
|
}
|
|
11124
11543
|
getMeasurement() {
|
|
11125
11544
|
if (!this.start || !this.end) return null;
|
|
@@ -11145,46 +11564,7 @@ var MeasureTool = class {
|
|
|
11145
11564
|
renderOverlay(ctx) {
|
|
11146
11565
|
const m = this.getMeasurement();
|
|
11147
11566
|
if (!m) return;
|
|
11148
|
-
ctx.
|
|
11149
|
-
ctx.strokeStyle = "#FF5722";
|
|
11150
|
-
ctx.setLineDash([8, 4]);
|
|
11151
|
-
ctx.lineWidth = 2;
|
|
11152
|
-
ctx.beginPath();
|
|
11153
|
-
ctx.moveTo(m.start.x, m.start.y);
|
|
11154
|
-
ctx.lineTo(m.end.x, m.end.y);
|
|
11155
|
-
ctx.stroke();
|
|
11156
|
-
ctx.setLineDash([]);
|
|
11157
|
-
ctx.fillStyle = "#FF5722";
|
|
11158
|
-
const dotRadius = 4;
|
|
11159
|
-
ctx.beginPath();
|
|
11160
|
-
ctx.arc(m.start.x, m.start.y, dotRadius, 0, Math.PI * 2);
|
|
11161
|
-
ctx.fill();
|
|
11162
|
-
ctx.beginPath();
|
|
11163
|
-
ctx.arc(m.end.x, m.end.y, dotRadius, 0, Math.PI * 2);
|
|
11164
|
-
ctx.fill();
|
|
11165
|
-
const label = `${Math.round(m.feet)} ft`;
|
|
11166
|
-
const midX = (m.start.x + m.end.x) / 2;
|
|
11167
|
-
const midY = (m.start.y + m.end.y) / 2;
|
|
11168
|
-
ctx.font = "14px sans-serif";
|
|
11169
|
-
const metrics = ctx.measureText(label);
|
|
11170
|
-
const padX = 6;
|
|
11171
|
-
const padY = 4;
|
|
11172
|
-
const textH = 14;
|
|
11173
|
-
ctx.fillStyle = "rgba(0, 0, 0, 0.75)";
|
|
11174
|
-
ctx.beginPath();
|
|
11175
|
-
ctx.roundRect(
|
|
11176
|
-
midX - metrics.width / 2 - padX,
|
|
11177
|
-
midY - textH / 2 - padY,
|
|
11178
|
-
metrics.width + padX * 2,
|
|
11179
|
-
textH + padY * 2,
|
|
11180
|
-
4
|
|
11181
|
-
);
|
|
11182
|
-
ctx.fill();
|
|
11183
|
-
ctx.fillStyle = "#FFFFFF";
|
|
11184
|
-
ctx.textAlign = "center";
|
|
11185
|
-
ctx.textBaseline = "middle";
|
|
11186
|
-
ctx.fillText(label, midX, midY);
|
|
11187
|
-
ctx.restore();
|
|
11567
|
+
drawMeasurement(ctx, { start: m.start, end: m.end, feet: m.feet, color: this.color });
|
|
11188
11568
|
}
|
|
11189
11569
|
snapToGrid(point, ctx) {
|
|
11190
11570
|
if (!ctx.gridSize) return point;
|
|
@@ -11202,6 +11582,32 @@ var MeasureTool = class {
|
|
|
11202
11582
|
notifyOptionsChange() {
|
|
11203
11583
|
for (const listener of this.optionListeners) listener();
|
|
11204
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
|
+
}
|
|
11205
11611
|
};
|
|
11206
11612
|
|
|
11207
11613
|
// src/tools/template-tool.ts
|
|
@@ -11497,9 +11903,9 @@ var TemplateTool = class {
|
|
|
11497
11903
|
};
|
|
11498
11904
|
|
|
11499
11905
|
// src/tools/laser-tool.ts
|
|
11500
|
-
var
|
|
11906
|
+
var DEFAULT_COLOR5 = "#ff3b30";
|
|
11501
11907
|
var DEFAULT_WIDTH2 = 4;
|
|
11502
|
-
var
|
|
11908
|
+
var DEFAULT_FADE_MS3 = 1200;
|
|
11503
11909
|
var LaserTool = class {
|
|
11504
11910
|
name;
|
|
11505
11911
|
color;
|
|
@@ -11513,9 +11919,9 @@ var LaserTool = class {
|
|
|
11513
11919
|
pendingEmission = [];
|
|
11514
11920
|
constructor(options = {}) {
|
|
11515
11921
|
this.name = options.name ?? "laser";
|
|
11516
|
-
this.color = options.color ??
|
|
11922
|
+
this.color = options.color ?? DEFAULT_COLOR5;
|
|
11517
11923
|
this.width = options.width ?? DEFAULT_WIDTH2;
|
|
11518
|
-
this.fadeMs = options.fadeMs ??
|
|
11924
|
+
this.fadeMs = options.fadeMs ?? DEFAULT_FADE_MS3;
|
|
11519
11925
|
}
|
|
11520
11926
|
now() {
|
|
11521
11927
|
return performance.now();
|
|
@@ -11641,10 +12047,10 @@ var LaserTool = class {
|
|
|
11641
12047
|
};
|
|
11642
12048
|
|
|
11643
12049
|
// src/tools/ping-tool.ts
|
|
11644
|
-
var
|
|
11645
|
-
var
|
|
11646
|
-
var
|
|
11647
|
-
var
|
|
12050
|
+
var DEFAULT_COLOR6 = "#ff3b30";
|
|
12051
|
+
var DEFAULT_DURATION_MS3 = 1800;
|
|
12052
|
+
var DEFAULT_RADIUS4 = 48;
|
|
12053
|
+
var DEFAULT_MIN_INTERVAL_MS2 = 300;
|
|
11648
12054
|
var PingTool = class {
|
|
11649
12055
|
name;
|
|
11650
12056
|
color;
|
|
@@ -11658,10 +12064,10 @@ var PingTool = class {
|
|
|
11658
12064
|
pingListeners = /* @__PURE__ */ new Set();
|
|
11659
12065
|
constructor(options = {}) {
|
|
11660
12066
|
this.name = options.name ?? "ping";
|
|
11661
|
-
this.color = options.color ??
|
|
11662
|
-
this.durationMs = options.durationMs ??
|
|
11663
|
-
this.radius = options.radius ??
|
|
11664
|
-
this.minIntervalMs = options.minIntervalMs ??
|
|
12067
|
+
this.color = options.color ?? DEFAULT_COLOR6;
|
|
12068
|
+
this.durationMs = options.durationMs ?? DEFAULT_DURATION_MS3;
|
|
12069
|
+
this.radius = options.radius ?? DEFAULT_RADIUS4;
|
|
12070
|
+
this.minIntervalMs = options.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS2;
|
|
11665
12071
|
}
|
|
11666
12072
|
now() {
|
|
11667
12073
|
return performance.now();
|
|
@@ -11761,7 +12167,7 @@ var PingTool = class {
|
|
|
11761
12167
|
};
|
|
11762
12168
|
|
|
11763
12169
|
// src/index.ts
|
|
11764
|
-
var VERSION = "0.
|
|
12170
|
+
var VERSION = "0.57.0";
|
|
11765
12171
|
// Annotate the CommonJS export names for ESM import in node:
|
|
11766
12172
|
0 && (module.exports = {
|
|
11767
12173
|
ArrowTool,
|
|
@@ -11778,13 +12184,16 @@ var VERSION = "0.55.0";
|
|
|
11778
12184
|
LaserTool,
|
|
11779
12185
|
LayerManager,
|
|
11780
12186
|
LocalStorageAdapter,
|
|
12187
|
+
MEASURE_PRESENCE_KIND,
|
|
11781
12188
|
MeasureTool,
|
|
11782
12189
|
MemoryAdapter,
|
|
11783
12190
|
NoteTool,
|
|
11784
12191
|
PING_PRESENCE_KIND,
|
|
11785
12192
|
PencilTool,
|
|
12193
|
+
PingInput,
|
|
11786
12194
|
PingTool,
|
|
11787
12195
|
RemoteLaserOverlay,
|
|
12196
|
+
RemoteMeasureOverlay,
|
|
11788
12197
|
RemotePingOverlay,
|
|
11789
12198
|
SelectTool,
|
|
11790
12199
|
ShapeTool,
|
|
@@ -11822,6 +12231,7 @@ var VERSION = "0.55.0";
|
|
|
11822
12231
|
getHexCellsInSquare,
|
|
11823
12232
|
getHexDistance,
|
|
11824
12233
|
isLaserTrailPresence,
|
|
12234
|
+
isMeasurePresence,
|
|
11825
12235
|
isNearBezier,
|
|
11826
12236
|
isPingPresence,
|
|
11827
12237
|
setFontSize,
|
|
@@ -11830,6 +12240,7 @@ var VERSION = "0.55.0";
|
|
|
11830
12240
|
snapToHexCenter,
|
|
11831
12241
|
styleToPatch,
|
|
11832
12242
|
toLaserTrailPresence,
|
|
12243
|
+
toMeasurePresence,
|
|
11833
12244
|
toPingPresence,
|
|
11834
12245
|
toggleBold,
|
|
11835
12246
|
toggleItalic,
|