@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.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
|
|
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 ??
|
|
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.
|
|
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
|
|
11805
|
+
var DEFAULT_COLOR5 = "#ff3b30";
|
|
11405
11806
|
var DEFAULT_WIDTH2 = 4;
|
|
11406
|
-
var
|
|
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 ??
|
|
11821
|
+
this.color = options.color ?? DEFAULT_COLOR5;
|
|
11421
11822
|
this.width = options.width ?? DEFAULT_WIDTH2;
|
|
11422
|
-
this.fadeMs = options.fadeMs ??
|
|
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
|
|
11549
|
-
var
|
|
11550
|
-
var
|
|
11551
|
-
var
|
|
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 ??
|
|
11566
|
-
this.durationMs = options.durationMs ??
|
|
11567
|
-
this.radius = options.radius ??
|
|
11568
|
-
this.minIntervalMs = options.minIntervalMs ??
|
|
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.
|
|
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,
|