@fieldnotes/core 0.64.0 → 0.65.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 +819 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +430 -2
- package/dist/index.d.ts +430 -2
- package/dist/index.js +809 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -20,6 +20,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
AWARENESS_MAX_SELECTION: () => AWARENESS_MAX_SELECTION,
|
|
24
|
+
AWARENESS_PRESENCE_KIND: () => AWARENESS_PRESENCE_KIND,
|
|
23
25
|
ArrowTool: () => ArrowTool,
|
|
24
26
|
AutoSave: () => AutoSave,
|
|
25
27
|
Camera: () => Camera,
|
|
@@ -38,6 +40,7 @@ __export(index_exports, {
|
|
|
38
40
|
LASER_TRAIL_PRESENCE_KIND: () => LASER_TRAIL_PRESENCE_KIND,
|
|
39
41
|
LaserTool: () => LaserTool,
|
|
40
42
|
LayerManager: () => LayerManager,
|
|
43
|
+
LocalAwareness: () => LocalAwareness,
|
|
41
44
|
LocalStorageAdapter: () => LocalStorageAdapter,
|
|
42
45
|
MEASURE_PRESENCE_KIND: () => MEASURE_PRESENCE_KIND,
|
|
43
46
|
MeasureTool: () => MeasureTool,
|
|
@@ -46,16 +49,20 @@ __export(index_exports, {
|
|
|
46
49
|
NoteTool: () => NoteTool,
|
|
47
50
|
PATH_PRESENCE_KIND: () => PATH_PRESENCE_KIND,
|
|
48
51
|
PATH_PRESENCE_MAX_POINTS: () => PATH_PRESENCE_MAX_POINTS,
|
|
52
|
+
PEER_COLORS: () => PEER_COLORS,
|
|
49
53
|
PING_PRESENCE_KIND: () => PING_PRESENCE_KIND,
|
|
50
54
|
PathTool: () => PathTool,
|
|
55
|
+
PeerRoster: () => PeerRoster,
|
|
51
56
|
PencilTool: () => PencilTool,
|
|
52
57
|
PingInput: () => PingInput,
|
|
53
58
|
PingTool: () => PingTool,
|
|
59
|
+
RemoteCursorOverlay: () => RemoteCursorOverlay,
|
|
54
60
|
RemoteFocusReceiver: () => RemoteFocusReceiver,
|
|
55
61
|
RemoteLaserOverlay: () => RemoteLaserOverlay,
|
|
56
62
|
RemoteMeasureOverlay: () => RemoteMeasureOverlay,
|
|
57
63
|
RemotePathOverlay: () => RemotePathOverlay,
|
|
58
64
|
RemotePingOverlay: () => RemotePingOverlay,
|
|
65
|
+
RemoteSelectionOverlay: () => RemoteSelectionOverlay,
|
|
59
66
|
SelectTool: () => SelectTool,
|
|
60
67
|
ShapeTool: () => ShapeTool,
|
|
61
68
|
TemplateTool: () => TemplateTool,
|
|
@@ -64,6 +71,7 @@ __export(index_exports, {
|
|
|
64
71
|
VERSION: () => VERSION,
|
|
65
72
|
Viewport: () => Viewport,
|
|
66
73
|
applyCameraView: () => applyCameraView,
|
|
74
|
+
attachAwareness: () => attachAwareness,
|
|
67
75
|
boundsIntersect: () => boundsIntersect,
|
|
68
76
|
cameraOriginForView: () => cameraOriginForView,
|
|
69
77
|
captureCameraView: () => captureCameraView,
|
|
@@ -77,6 +85,7 @@ __export(index_exports, {
|
|
|
77
85
|
createStroke: () => createStroke,
|
|
78
86
|
createTemplate: () => createTemplate,
|
|
79
87
|
createText: () => createText,
|
|
88
|
+
defaultPeerColor: () => defaultPeerColor,
|
|
80
89
|
drawHexPath: () => drawHexPath,
|
|
81
90
|
elementRectsEqual: () => elementRectsEqual,
|
|
82
91
|
exportImage: () => exportImage,
|
|
@@ -99,6 +108,7 @@ __export(index_exports, {
|
|
|
99
108
|
getHexCellsInSquare: () => getHexCellsInSquare,
|
|
100
109
|
getHexDistance: () => getHexDistance,
|
|
101
110
|
gridDistanceCells: () => gridDistanceCells,
|
|
111
|
+
isAwarenessPresence: () => isAwarenessPresence,
|
|
102
112
|
isFocusPresence: () => isFocusPresence,
|
|
103
113
|
isLaserTrailPresence: () => isLaserTrailPresence,
|
|
104
114
|
isMeasurePresence: () => isMeasurePresence,
|
|
@@ -12228,6 +12238,800 @@ var RemoteFocusReceiver = class {
|
|
|
12228
12238
|
}
|
|
12229
12239
|
};
|
|
12230
12240
|
|
|
12241
|
+
// src/canvas/awareness-presence.ts
|
|
12242
|
+
var AWARENESS_PRESENCE_KIND = "awareness";
|
|
12243
|
+
var AWARENESS_MAX_SELECTION = 256;
|
|
12244
|
+
var MAX_ID_LENGTH = 128;
|
|
12245
|
+
var MAX_NAME_LENGTH = 64;
|
|
12246
|
+
var MAX_COLOR_LENGTH2 = 64;
|
|
12247
|
+
var MAX_ROLE_LENGTH = 32;
|
|
12248
|
+
var MAX_TOOL_LENGTH = 64;
|
|
12249
|
+
function isBoundedString(value, max) {
|
|
12250
|
+
return typeof value === "string" && value.length <= max;
|
|
12251
|
+
}
|
|
12252
|
+
function isOptionalBoundedString(value, max) {
|
|
12253
|
+
return value === void 0 || isBoundedString(value, max);
|
|
12254
|
+
}
|
|
12255
|
+
function isFinitePoint4(value) {
|
|
12256
|
+
if (typeof value !== "object" || value === null) return false;
|
|
12257
|
+
const point = value;
|
|
12258
|
+
return typeof point.x === "number" && Number.isFinite(point.x) && typeof point.y === "number" && Number.isFinite(point.y);
|
|
12259
|
+
}
|
|
12260
|
+
function isAwarenessPresence(data) {
|
|
12261
|
+
if (typeof data !== "object" || data === null) return false;
|
|
12262
|
+
const payload = data;
|
|
12263
|
+
if (payload.kind !== AWARENESS_PRESENCE_KIND) return false;
|
|
12264
|
+
if (!isBoundedString(payload.id, MAX_ID_LENGTH) || payload.id.length === 0) return false;
|
|
12265
|
+
if ("cleared" in payload) return payload.cleared === true;
|
|
12266
|
+
if (!isOptionalBoundedString(payload.name, MAX_NAME_LENGTH)) return false;
|
|
12267
|
+
if (!isOptionalBoundedString(payload.color, MAX_COLOR_LENGTH2)) return false;
|
|
12268
|
+
if (!isOptionalBoundedString(payload.role, MAX_ROLE_LENGTH)) return false;
|
|
12269
|
+
if (!isOptionalBoundedString(payload.tool, MAX_TOOL_LENGTH)) return false;
|
|
12270
|
+
if (payload.cursor !== void 0 && !isFinitePoint4(payload.cursor)) return false;
|
|
12271
|
+
if (payload.selection !== void 0) {
|
|
12272
|
+
if (!Array.isArray(payload.selection)) return false;
|
|
12273
|
+
if (payload.selection.length > AWARENESS_MAX_SELECTION) return false;
|
|
12274
|
+
for (const id of payload.selection) {
|
|
12275
|
+
if (!isBoundedString(id, MAX_ID_LENGTH) || id.length === 0) return false;
|
|
12276
|
+
}
|
|
12277
|
+
}
|
|
12278
|
+
return true;
|
|
12279
|
+
}
|
|
12280
|
+
|
|
12281
|
+
// src/canvas/awareness-roster.ts
|
|
12282
|
+
var DEFAULT_STALE_MS = 45e3;
|
|
12283
|
+
var EMPTY_PEERS = Object.freeze([]);
|
|
12284
|
+
var EMPTY_SELECTION = Object.freeze([]);
|
|
12285
|
+
function sameSelection(a, b) {
|
|
12286
|
+
if (a.length !== b.length) return false;
|
|
12287
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
12288
|
+
return true;
|
|
12289
|
+
}
|
|
12290
|
+
function samePoint2(a, b) {
|
|
12291
|
+
if (a === null || b === null) return a === b;
|
|
12292
|
+
return a.x === b.x && a.y === b.y;
|
|
12293
|
+
}
|
|
12294
|
+
function toPeer(from, data, prev) {
|
|
12295
|
+
const cursor = data.cursor ? { x: data.cursor.x, y: data.cursor.y } : null;
|
|
12296
|
+
const incoming = data.selection ?? EMPTY_SELECTION;
|
|
12297
|
+
const selection = prev && sameSelection(prev.selection, incoming) ? prev.selection : incoming.length === 0 ? EMPTY_SELECTION : Object.freeze([...incoming]);
|
|
12298
|
+
const tool = data.tool ?? null;
|
|
12299
|
+
if (prev && prev.id === data.id && prev.name === data.name && prev.color === data.color && prev.role === data.role && prev.tool === tool && prev.selection === selection && samePoint2(prev.cursor, cursor)) {
|
|
12300
|
+
return prev;
|
|
12301
|
+
}
|
|
12302
|
+
const peer = {
|
|
12303
|
+
from,
|
|
12304
|
+
id: data.id,
|
|
12305
|
+
...data.name === void 0 ? {} : { name: data.name },
|
|
12306
|
+
...data.color === void 0 ? {} : { color: data.color },
|
|
12307
|
+
...data.role === void 0 ? {} : { role: data.role },
|
|
12308
|
+
cursor,
|
|
12309
|
+
selection,
|
|
12310
|
+
tool
|
|
12311
|
+
};
|
|
12312
|
+
return peer;
|
|
12313
|
+
}
|
|
12314
|
+
var PeerRoster = class {
|
|
12315
|
+
staleMs;
|
|
12316
|
+
now;
|
|
12317
|
+
rows = /* @__PURE__ */ new Map();
|
|
12318
|
+
discovered = /* @__PURE__ */ new Map();
|
|
12319
|
+
changeListeners = /* @__PURE__ */ new Set();
|
|
12320
|
+
discoverListeners = /* @__PURE__ */ new Set();
|
|
12321
|
+
leaveListeners = /* @__PURE__ */ new Set();
|
|
12322
|
+
snapshot = EMPTY_PEERS;
|
|
12323
|
+
snapshotDirty = false;
|
|
12324
|
+
staleTimer = null;
|
|
12325
|
+
isDisposed = false;
|
|
12326
|
+
constructor(options = {}) {
|
|
12327
|
+
this.staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
12328
|
+
this.now = options.now ?? (() => Date.now());
|
|
12329
|
+
}
|
|
12330
|
+
get disposed() {
|
|
12331
|
+
return this.isDisposed;
|
|
12332
|
+
}
|
|
12333
|
+
/**
|
|
12334
|
+
* Applies a presence payload from `from`. Non-awareness or malformed payloads
|
|
12335
|
+
* return `false` untouched, so hosts can feed every presence frame through.
|
|
12336
|
+
*/
|
|
12337
|
+
apply(from, data) {
|
|
12338
|
+
if (this.isDisposed || !isAwarenessPresence(data)) return false;
|
|
12339
|
+
const isNew = !this.discovered.has(from);
|
|
12340
|
+
this.discovered.set(from, this.now());
|
|
12341
|
+
if ("cleared" in data) {
|
|
12342
|
+
this.dropRow(from, "cleared");
|
|
12343
|
+
} else {
|
|
12344
|
+
const prev = this.rows.get(from);
|
|
12345
|
+
const next = toPeer(from, data, prev);
|
|
12346
|
+
if (next !== prev) {
|
|
12347
|
+
this.rows.set(from, next);
|
|
12348
|
+
this.changed();
|
|
12349
|
+
}
|
|
12350
|
+
}
|
|
12351
|
+
this.armStaleTimer();
|
|
12352
|
+
if (isNew) this.emit(this.discoverListeners, (l) => l(from));
|
|
12353
|
+
return true;
|
|
12354
|
+
}
|
|
12355
|
+
/** Server-authored presence-leave: drops the row AND the discovery entry. */
|
|
12356
|
+
remove(from) {
|
|
12357
|
+
if (this.isDisposed) return;
|
|
12358
|
+
const hadEntry = this.discovered.delete(from);
|
|
12359
|
+
this.dropRow(from, "left");
|
|
12360
|
+
if (hadEntry) this.armStaleTimer();
|
|
12361
|
+
}
|
|
12362
|
+
getPeers() {
|
|
12363
|
+
if (this.snapshotDirty) {
|
|
12364
|
+
this.snapshot = this.rows.size === 0 ? EMPTY_PEERS : Object.freeze([...this.rows.values()]);
|
|
12365
|
+
this.snapshotDirty = false;
|
|
12366
|
+
}
|
|
12367
|
+
return this.snapshot;
|
|
12368
|
+
}
|
|
12369
|
+
getPeer(from) {
|
|
12370
|
+
return this.rows.get(from);
|
|
12371
|
+
}
|
|
12372
|
+
/** Fires only when `getPeers()` would return a new reference. */
|
|
12373
|
+
onChange(listener) {
|
|
12374
|
+
this.changeListeners.add(listener);
|
|
12375
|
+
return () => this.changeListeners.delete(listener);
|
|
12376
|
+
}
|
|
12377
|
+
/** First valid frame from a sender since its discovery entry was last dropped. */
|
|
12378
|
+
onDiscover(listener) {
|
|
12379
|
+
this.discoverListeners.add(listener);
|
|
12380
|
+
return () => this.discoverListeners.delete(listener);
|
|
12381
|
+
}
|
|
12382
|
+
onLeave(listener) {
|
|
12383
|
+
this.leaveListeners.add(listener);
|
|
12384
|
+
return () => this.leaveListeners.delete(listener);
|
|
12385
|
+
}
|
|
12386
|
+
dispose() {
|
|
12387
|
+
if (this.isDisposed) return;
|
|
12388
|
+
this.isDisposed = true;
|
|
12389
|
+
if (this.staleTimer !== null) clearTimeout(this.staleTimer);
|
|
12390
|
+
this.staleTimer = null;
|
|
12391
|
+
this.rows.clear();
|
|
12392
|
+
this.discovered.clear();
|
|
12393
|
+
this.snapshot = EMPTY_PEERS;
|
|
12394
|
+
this.snapshotDirty = false;
|
|
12395
|
+
this.changeListeners.clear();
|
|
12396
|
+
this.discoverListeners.clear();
|
|
12397
|
+
this.leaveListeners.clear();
|
|
12398
|
+
}
|
|
12399
|
+
dropRow(from, reason) {
|
|
12400
|
+
const row = this.rows.get(from);
|
|
12401
|
+
if (!row) return;
|
|
12402
|
+
this.rows.delete(from);
|
|
12403
|
+
this.changed();
|
|
12404
|
+
this.emit(this.leaveListeners, (l) => l(row, reason));
|
|
12405
|
+
}
|
|
12406
|
+
changed() {
|
|
12407
|
+
this.snapshotDirty = true;
|
|
12408
|
+
this.emit(this.changeListeners, (l) => l());
|
|
12409
|
+
}
|
|
12410
|
+
emit(listeners, call) {
|
|
12411
|
+
for (const listener of [...listeners]) {
|
|
12412
|
+
try {
|
|
12413
|
+
call(listener);
|
|
12414
|
+
} catch {
|
|
12415
|
+
}
|
|
12416
|
+
}
|
|
12417
|
+
}
|
|
12418
|
+
armStaleTimer() {
|
|
12419
|
+
if (this.staleTimer !== null) clearTimeout(this.staleTimer);
|
|
12420
|
+
this.staleTimer = null;
|
|
12421
|
+
if (!Number.isFinite(this.staleMs) || this.staleMs <= 0 || this.isDisposed || this.discovered.size === 0) {
|
|
12422
|
+
return;
|
|
12423
|
+
}
|
|
12424
|
+
let earliest = Infinity;
|
|
12425
|
+
for (const seen of this.discovered.values()) if (seen < earliest) earliest = seen;
|
|
12426
|
+
const delay = Math.min(Math.max(0, earliest + this.staleMs - this.now()), 2 ** 31 - 1);
|
|
12427
|
+
this.staleTimer = setTimeout(() => {
|
|
12428
|
+
this.staleTimer = null;
|
|
12429
|
+
this.expireStale();
|
|
12430
|
+
}, delay);
|
|
12431
|
+
}
|
|
12432
|
+
expireStale() {
|
|
12433
|
+
const t = this.now();
|
|
12434
|
+
for (const from of [...this.discovered.keys()]) {
|
|
12435
|
+
const seen = this.discovered.get(from);
|
|
12436
|
+
if (seen === void 0 || t - seen < this.staleMs) continue;
|
|
12437
|
+
this.discovered.delete(from);
|
|
12438
|
+
this.dropRow(from, "stale");
|
|
12439
|
+
}
|
|
12440
|
+
this.armStaleTimer();
|
|
12441
|
+
}
|
|
12442
|
+
};
|
|
12443
|
+
|
|
12444
|
+
// src/canvas/awareness-publisher.ts
|
|
12445
|
+
var DEFAULT_FIELDS = Object.freeze({
|
|
12446
|
+
cursor: true,
|
|
12447
|
+
selection: false,
|
|
12448
|
+
tool: true
|
|
12449
|
+
});
|
|
12450
|
+
var DEFAULT_INTERVAL_MS = 50;
|
|
12451
|
+
var DEFAULT_HEARTBEAT_MS = 15e3;
|
|
12452
|
+
var MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
|
|
12453
|
+
var MAX_IDENTITY_ID_LENGTH = 128;
|
|
12454
|
+
var MAX_IDENTITY_NAME_LENGTH = 64;
|
|
12455
|
+
var MAX_IDENTITY_COLOR_LENGTH = 64;
|
|
12456
|
+
var MAX_IDENTITY_ROLE_LENGTH = 32;
|
|
12457
|
+
var MAX_TOOL_LENGTH2 = 64;
|
|
12458
|
+
var MAX_SELECTION_ID_LENGTH = 128;
|
|
12459
|
+
function normalizeIntervalMs(value) {
|
|
12460
|
+
return Number.isFinite(value) && value >= 0 ? value : 0;
|
|
12461
|
+
}
|
|
12462
|
+
function normalizeHeartbeatMs(value) {
|
|
12463
|
+
return Number.isFinite(value) && value > 0 ? value : 0;
|
|
12464
|
+
}
|
|
12465
|
+
var LocalAwareness = class {
|
|
12466
|
+
host;
|
|
12467
|
+
element;
|
|
12468
|
+
send;
|
|
12469
|
+
selectionFilter;
|
|
12470
|
+
onError;
|
|
12471
|
+
intervalMs;
|
|
12472
|
+
heartbeatMs;
|
|
12473
|
+
identity;
|
|
12474
|
+
fields;
|
|
12475
|
+
lastPointer = null;
|
|
12476
|
+
selection = [];
|
|
12477
|
+
selectionFailed = false;
|
|
12478
|
+
tool;
|
|
12479
|
+
dirty = false;
|
|
12480
|
+
lastSentAt = null;
|
|
12481
|
+
throttleTimer = null;
|
|
12482
|
+
heartbeatTimer = null;
|
|
12483
|
+
unsubscribers = [];
|
|
12484
|
+
isDisposed = false;
|
|
12485
|
+
handlePointerMove = (e) => this.onPointerMove(e);
|
|
12486
|
+
handlePointerEnd = (e) => this.onPointerEnd(e);
|
|
12487
|
+
constructor(host, options) {
|
|
12488
|
+
const element = options.element ?? host.domLayer.parentElement;
|
|
12489
|
+
if (!element) throw new Error("LocalAwareness: the viewport wrapper is not mounted");
|
|
12490
|
+
this.host = host;
|
|
12491
|
+
this.element = element;
|
|
12492
|
+
this.send = options.send;
|
|
12493
|
+
this.selectionFilter = options.selectionFilter;
|
|
12494
|
+
this.onError = options.onError;
|
|
12495
|
+
this.intervalMs = normalizeIntervalMs(options.intervalMs ?? DEFAULT_INTERVAL_MS);
|
|
12496
|
+
this.heartbeatMs = normalizeHeartbeatMs(options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS);
|
|
12497
|
+
this.identity = this.normalizeIdentity(options.identity);
|
|
12498
|
+
this.fields = mergeFields(DEFAULT_FIELDS, options.fields ?? {});
|
|
12499
|
+
this.tool = host.toolManager.activeTool?.name ?? null;
|
|
12500
|
+
if (this.fields.selection) this.refreshSelection();
|
|
12501
|
+
const opts = { passive: true };
|
|
12502
|
+
element.addEventListener("pointermove", this.handlePointerMove, opts);
|
|
12503
|
+
element.addEventListener("pointerleave", this.handlePointerEnd, opts);
|
|
12504
|
+
element.addEventListener("pointercancel", this.handlePointerEnd, opts);
|
|
12505
|
+
this.unsubscribers.push(
|
|
12506
|
+
host.onSelectionChange(() => {
|
|
12507
|
+
if (this.fields.selection) this.schedule();
|
|
12508
|
+
}),
|
|
12509
|
+
host.toolManager.onChange((name) => {
|
|
12510
|
+
this.tool = name;
|
|
12511
|
+
if (this.fields.tool) this.schedule();
|
|
12512
|
+
})
|
|
12513
|
+
);
|
|
12514
|
+
this.armHeartbeat();
|
|
12515
|
+
}
|
|
12516
|
+
get disposed() {
|
|
12517
|
+
return this.isDisposed;
|
|
12518
|
+
}
|
|
12519
|
+
getFields() {
|
|
12520
|
+
return this.fields;
|
|
12521
|
+
}
|
|
12522
|
+
setIdentity(identity) {
|
|
12523
|
+
this.identity = this.normalizeIdentity(identity);
|
|
12524
|
+
this.schedule();
|
|
12525
|
+
}
|
|
12526
|
+
/** Merges the given flags into the current policy; `undefined` keys are ignored. */
|
|
12527
|
+
setFields(fields) {
|
|
12528
|
+
this.fields = mergeFields(this.fields, fields);
|
|
12529
|
+
this.schedule();
|
|
12530
|
+
}
|
|
12531
|
+
/**
|
|
12532
|
+
* Requests a full frame: immediate when idle, otherwise folded into the
|
|
12533
|
+
* pending trailing frame (so N simultaneous requests cost one frame). Hosts
|
|
12534
|
+
* call it when the connection becomes live or reconnects.
|
|
12535
|
+
*/
|
|
12536
|
+
announce() {
|
|
12537
|
+
this.schedule();
|
|
12538
|
+
}
|
|
12539
|
+
/**
|
|
12540
|
+
* The complete state a frame carries right now. Side-effecting when
|
|
12541
|
+
* selection publishing is on: re-reads `getSelectedIds()`, re-runs
|
|
12542
|
+
* `selectionFilter`, updates the fail-closed selection state, and may call
|
|
12543
|
+
* `onError`. A no-op with respect to selection while publishing is off.
|
|
12544
|
+
*/
|
|
12545
|
+
getState() {
|
|
12546
|
+
const frame = { kind: AWARENESS_PRESENCE_KIND, id: this.identity.id };
|
|
12547
|
+
if (this.identity.name !== void 0) frame.name = this.identity.name;
|
|
12548
|
+
if (this.identity.color !== void 0) frame.color = this.identity.color;
|
|
12549
|
+
if (this.identity.role !== void 0) frame.role = this.identity.role;
|
|
12550
|
+
if (this.fields.cursor && this.lastPointer !== null) {
|
|
12551
|
+
frame.cursor = { x: this.lastPointer.x, y: this.lastPointer.y };
|
|
12552
|
+
}
|
|
12553
|
+
if (this.fields.selection) {
|
|
12554
|
+
this.refreshSelection();
|
|
12555
|
+
if (!this.selectionFailed && this.selection.length > 0) {
|
|
12556
|
+
frame.selection = [...this.selection];
|
|
12557
|
+
}
|
|
12558
|
+
}
|
|
12559
|
+
if (this.fields.tool && this.tool !== null && this.tool.length <= MAX_TOOL_LENGTH2) {
|
|
12560
|
+
frame.tool = this.tool;
|
|
12561
|
+
}
|
|
12562
|
+
return frame;
|
|
12563
|
+
}
|
|
12564
|
+
dispose() {
|
|
12565
|
+
if (this.isDisposed) return;
|
|
12566
|
+
this.isDisposed = true;
|
|
12567
|
+
if (this.throttleTimer !== null) clearTimeout(this.throttleTimer);
|
|
12568
|
+
this.throttleTimer = null;
|
|
12569
|
+
if (this.heartbeatTimer !== null) clearTimeout(this.heartbeatTimer);
|
|
12570
|
+
this.heartbeatTimer = null;
|
|
12571
|
+
this.element.removeEventListener("pointermove", this.handlePointerMove);
|
|
12572
|
+
this.element.removeEventListener("pointerleave", this.handlePointerEnd);
|
|
12573
|
+
this.element.removeEventListener("pointercancel", this.handlePointerEnd);
|
|
12574
|
+
for (const unsub of this.unsubscribers) unsub();
|
|
12575
|
+
this.unsubscribers.length = 0;
|
|
12576
|
+
this.safeSend({ kind: AWARENESS_PRESENCE_KIND, id: this.identity.id, cleared: true });
|
|
12577
|
+
}
|
|
12578
|
+
now() {
|
|
12579
|
+
return Date.now();
|
|
12580
|
+
}
|
|
12581
|
+
onPointerMove(e) {
|
|
12582
|
+
if (!e.isPrimary) return;
|
|
12583
|
+
const rect = this.element.getBoundingClientRect();
|
|
12584
|
+
const world = this.host.camera.screenToWorld({
|
|
12585
|
+
x: e.clientX - rect.left,
|
|
12586
|
+
y: e.clientY - rect.top
|
|
12587
|
+
});
|
|
12588
|
+
this.lastPointer = Number.isFinite(world.x) && Number.isFinite(world.y) ? { x: world.x, y: world.y } : null;
|
|
12589
|
+
if (this.fields.cursor) this.schedule();
|
|
12590
|
+
}
|
|
12591
|
+
onPointerEnd(e) {
|
|
12592
|
+
if (!e.isPrimary || this.lastPointer === null) return;
|
|
12593
|
+
this.lastPointer = null;
|
|
12594
|
+
if (this.fields.cursor) this.schedule();
|
|
12595
|
+
}
|
|
12596
|
+
refreshSelection() {
|
|
12597
|
+
try {
|
|
12598
|
+
const raw = this.host.getSelectedIds();
|
|
12599
|
+
const ids = this.selectionFilter ? this.selectionFilter(raw) : raw;
|
|
12600
|
+
if (!Array.isArray(ids)) throw new TypeError("selectionFilter must return an array");
|
|
12601
|
+
for (const id of ids) {
|
|
12602
|
+
if (typeof id !== "string") throw new TypeError("selectionFilter must return strings");
|
|
12603
|
+
if (id.length === 0 || id.length > MAX_SELECTION_ID_LENGTH) {
|
|
12604
|
+
throw new TypeError("selectionFilter must return ids of 1..128 characters");
|
|
12605
|
+
}
|
|
12606
|
+
}
|
|
12607
|
+
this.selection = ids.slice(0, AWARENESS_MAX_SELECTION);
|
|
12608
|
+
this.selectionFailed = false;
|
|
12609
|
+
} catch (error) {
|
|
12610
|
+
this.selection = [];
|
|
12611
|
+
this.selectionFailed = true;
|
|
12612
|
+
this.report(error);
|
|
12613
|
+
}
|
|
12614
|
+
}
|
|
12615
|
+
schedule() {
|
|
12616
|
+
if (this.isDisposed) return;
|
|
12617
|
+
this.dirty = true;
|
|
12618
|
+
if (this.throttleTimer !== null) return;
|
|
12619
|
+
const elapsed = this.lastSentAt === null ? Infinity : this.now() - this.lastSentAt;
|
|
12620
|
+
if (elapsed >= this.intervalMs) {
|
|
12621
|
+
this.flush();
|
|
12622
|
+
return;
|
|
12623
|
+
}
|
|
12624
|
+
this.throttleTimer = setTimeout(
|
|
12625
|
+
() => {
|
|
12626
|
+
this.throttleTimer = null;
|
|
12627
|
+
if (this.dirty) this.flush();
|
|
12628
|
+
},
|
|
12629
|
+
Math.min(this.intervalMs - elapsed, MAX_TIMER_DELAY_MS)
|
|
12630
|
+
);
|
|
12631
|
+
}
|
|
12632
|
+
flush() {
|
|
12633
|
+
this.dirty = false;
|
|
12634
|
+
this.lastSentAt = this.now();
|
|
12635
|
+
this.safeSend(this.getState());
|
|
12636
|
+
this.armHeartbeat();
|
|
12637
|
+
}
|
|
12638
|
+
armHeartbeat() {
|
|
12639
|
+
if (this.heartbeatTimer !== null) clearTimeout(this.heartbeatTimer);
|
|
12640
|
+
this.heartbeatTimer = null;
|
|
12641
|
+
if (this.heartbeatMs <= 0 || this.isDisposed) return;
|
|
12642
|
+
this.heartbeatTimer = setTimeout(
|
|
12643
|
+
() => {
|
|
12644
|
+
this.heartbeatTimer = null;
|
|
12645
|
+
this.flush();
|
|
12646
|
+
},
|
|
12647
|
+
Math.min(this.heartbeatMs, MAX_TIMER_DELAY_MS)
|
|
12648
|
+
);
|
|
12649
|
+
}
|
|
12650
|
+
safeSend(frame) {
|
|
12651
|
+
try {
|
|
12652
|
+
this.send(frame);
|
|
12653
|
+
} catch (error) {
|
|
12654
|
+
this.report(error);
|
|
12655
|
+
}
|
|
12656
|
+
}
|
|
12657
|
+
report(error) {
|
|
12658
|
+
try {
|
|
12659
|
+
this.onError?.(error);
|
|
12660
|
+
} catch {
|
|
12661
|
+
}
|
|
12662
|
+
}
|
|
12663
|
+
/**
|
|
12664
|
+
* Truncates identity strings to the wire caps and rejects an invalid id, so a
|
|
12665
|
+
* sender can never publish a frame that the wire guard would drop outright.
|
|
12666
|
+
* A truncated field is reported through `onError` (a `RangeError`) rather
|
|
12667
|
+
* than silently shortened, so a caller passing an over-long name finds out.
|
|
12668
|
+
*/
|
|
12669
|
+
normalizeIdentity(identity) {
|
|
12670
|
+
if (identity.id.length === 0 || identity.id.length > MAX_IDENTITY_ID_LENGTH) {
|
|
12671
|
+
throw new RangeError("LocalAwareness: identity.id must be 1..128 characters");
|
|
12672
|
+
}
|
|
12673
|
+
const normalized = {
|
|
12674
|
+
id: identity.id
|
|
12675
|
+
};
|
|
12676
|
+
if (identity.name !== void 0) {
|
|
12677
|
+
normalized.name = identity.name.slice(0, MAX_IDENTITY_NAME_LENGTH);
|
|
12678
|
+
if (identity.name.length > MAX_IDENTITY_NAME_LENGTH) {
|
|
12679
|
+
this.report(
|
|
12680
|
+
new RangeError(
|
|
12681
|
+
`LocalAwareness: identity.name truncated to ${MAX_IDENTITY_NAME_LENGTH} characters`
|
|
12682
|
+
)
|
|
12683
|
+
);
|
|
12684
|
+
}
|
|
12685
|
+
}
|
|
12686
|
+
if (identity.color !== void 0) {
|
|
12687
|
+
normalized.color = identity.color.slice(0, MAX_IDENTITY_COLOR_LENGTH);
|
|
12688
|
+
if (identity.color.length > MAX_IDENTITY_COLOR_LENGTH) {
|
|
12689
|
+
this.report(
|
|
12690
|
+
new RangeError(
|
|
12691
|
+
`LocalAwareness: identity.color truncated to ${MAX_IDENTITY_COLOR_LENGTH} characters`
|
|
12692
|
+
)
|
|
12693
|
+
);
|
|
12694
|
+
}
|
|
12695
|
+
}
|
|
12696
|
+
if (identity.role !== void 0) {
|
|
12697
|
+
normalized.role = identity.role.slice(0, MAX_IDENTITY_ROLE_LENGTH);
|
|
12698
|
+
if (identity.role.length > MAX_IDENTITY_ROLE_LENGTH) {
|
|
12699
|
+
this.report(
|
|
12700
|
+
new RangeError(
|
|
12701
|
+
`LocalAwareness: identity.role truncated to ${MAX_IDENTITY_ROLE_LENGTH} characters`
|
|
12702
|
+
)
|
|
12703
|
+
);
|
|
12704
|
+
}
|
|
12705
|
+
}
|
|
12706
|
+
return normalized;
|
|
12707
|
+
}
|
|
12708
|
+
};
|
|
12709
|
+
function mergeFields(current, patch) {
|
|
12710
|
+
return Object.freeze({
|
|
12711
|
+
cursor: patch.cursor ?? current.cursor,
|
|
12712
|
+
selection: patch.selection ?? current.selection,
|
|
12713
|
+
tool: patch.tool ?? current.tool
|
|
12714
|
+
});
|
|
12715
|
+
}
|
|
12716
|
+
|
|
12717
|
+
// src/canvas/remote-cursor-overlay.ts
|
|
12718
|
+
var PEER_COLORS = Object.freeze([
|
|
12719
|
+
"#e11d48",
|
|
12720
|
+
"#ea580c",
|
|
12721
|
+
"#ca8a04",
|
|
12722
|
+
"#16a34a",
|
|
12723
|
+
"#0d9488",
|
|
12724
|
+
"#0284c7",
|
|
12725
|
+
"#2563eb",
|
|
12726
|
+
"#7c3aed",
|
|
12727
|
+
"#c026d3",
|
|
12728
|
+
"#db2777",
|
|
12729
|
+
"#4d7c0f",
|
|
12730
|
+
"#b45309"
|
|
12731
|
+
]);
|
|
12732
|
+
function defaultPeerColor(seed) {
|
|
12733
|
+
if (seed.length === 0) return PEER_COLORS[0] ?? "#2563eb";
|
|
12734
|
+
let hash = 2166136261;
|
|
12735
|
+
for (let i = 0; i < seed.length; i++) {
|
|
12736
|
+
hash ^= seed.charCodeAt(i);
|
|
12737
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
12738
|
+
}
|
|
12739
|
+
return PEER_COLORS[hash % PEER_COLORS.length] ?? "#2563eb";
|
|
12740
|
+
}
|
|
12741
|
+
var DEFAULT_LABEL_FONT = "12px sans-serif";
|
|
12742
|
+
var LABEL_PAD_X = 6;
|
|
12743
|
+
var LABEL_PAD_Y = 3;
|
|
12744
|
+
var LABEL_HEIGHT = 16;
|
|
12745
|
+
var LABEL_OFFSET = 14;
|
|
12746
|
+
var MAX_LABEL_WIDTH_CACHE = 64;
|
|
12747
|
+
var RemoteCursorOverlay = class {
|
|
12748
|
+
host;
|
|
12749
|
+
roster;
|
|
12750
|
+
colorFor;
|
|
12751
|
+
showLabels;
|
|
12752
|
+
labelFont;
|
|
12753
|
+
labelWidths = /* @__PURE__ */ new Map();
|
|
12754
|
+
unregister;
|
|
12755
|
+
unsubscribe;
|
|
12756
|
+
isDisposed = false;
|
|
12757
|
+
constructor(host, roster, options = {}) {
|
|
12758
|
+
this.host = host;
|
|
12759
|
+
this.roster = roster;
|
|
12760
|
+
this.colorFor = options.colorFor;
|
|
12761
|
+
this.showLabels = options.showLabels ?? true;
|
|
12762
|
+
this.labelFont = options.labelFont ?? DEFAULT_LABEL_FONT;
|
|
12763
|
+
this.unregister = host.registerOverlay((ctx) => this.render(ctx));
|
|
12764
|
+
this.unsubscribe = roster.onChange(() => {
|
|
12765
|
+
if (this.labelWidths.size > MAX_LABEL_WIDTH_CACHE) this.labelWidths.clear();
|
|
12766
|
+
host.requestRender();
|
|
12767
|
+
});
|
|
12768
|
+
}
|
|
12769
|
+
get disposed() {
|
|
12770
|
+
return this.isDisposed;
|
|
12771
|
+
}
|
|
12772
|
+
resolveColor(peer) {
|
|
12773
|
+
return this.colorFor?.(peer) ?? peer.color ?? defaultPeerColor(peer.id);
|
|
12774
|
+
}
|
|
12775
|
+
dispose() {
|
|
12776
|
+
if (this.isDisposed) return;
|
|
12777
|
+
this.isDisposed = true;
|
|
12778
|
+
this.unsubscribe?.();
|
|
12779
|
+
this.unsubscribe = null;
|
|
12780
|
+
this.unregister?.();
|
|
12781
|
+
this.unregister = null;
|
|
12782
|
+
this.labelWidths.clear();
|
|
12783
|
+
this.host.requestRender();
|
|
12784
|
+
}
|
|
12785
|
+
render(ctx) {
|
|
12786
|
+
if (this.isDisposed) return;
|
|
12787
|
+
const zoom = this.host.camera.zoom;
|
|
12788
|
+
const inv = zoom > 0 && Number.isFinite(zoom) ? 1 / zoom : 1;
|
|
12789
|
+
for (const peer of this.roster.getPeers()) {
|
|
12790
|
+
if (peer.cursor === null) continue;
|
|
12791
|
+
const color = this.resolveColor(peer);
|
|
12792
|
+
ctx.save();
|
|
12793
|
+
ctx.translate(peer.cursor.x, peer.cursor.y);
|
|
12794
|
+
ctx.scale(inv, inv);
|
|
12795
|
+
ctx.beginPath();
|
|
12796
|
+
ctx.moveTo(0, 0);
|
|
12797
|
+
ctx.lineTo(0, 16);
|
|
12798
|
+
ctx.lineTo(4.5, 12.5);
|
|
12799
|
+
ctx.lineTo(11, 12.5);
|
|
12800
|
+
ctx.closePath();
|
|
12801
|
+
ctx.fillStyle = color;
|
|
12802
|
+
ctx.fill();
|
|
12803
|
+
ctx.strokeStyle = "#ffffff";
|
|
12804
|
+
ctx.lineWidth = 1;
|
|
12805
|
+
ctx.stroke();
|
|
12806
|
+
if (this.showLabels && peer.name !== void 0 && peer.name.length > 0) {
|
|
12807
|
+
this.drawLabel(ctx, peer.name, color);
|
|
12808
|
+
}
|
|
12809
|
+
ctx.restore();
|
|
12810
|
+
}
|
|
12811
|
+
}
|
|
12812
|
+
drawLabel(ctx, name, color) {
|
|
12813
|
+
ctx.font = this.labelFont;
|
|
12814
|
+
const key = `${this.labelFont} ${name}`;
|
|
12815
|
+
let width = this.labelWidths.get(key);
|
|
12816
|
+
if (width === void 0) {
|
|
12817
|
+
width = ctx.measureText(name).width;
|
|
12818
|
+
this.labelWidths.set(key, width);
|
|
12819
|
+
}
|
|
12820
|
+
const w = width + LABEL_PAD_X * 2;
|
|
12821
|
+
ctx.fillStyle = color;
|
|
12822
|
+
ctx.beginPath();
|
|
12823
|
+
ctx.roundRect(LABEL_OFFSET, LABEL_OFFSET, w, LABEL_HEIGHT + LABEL_PAD_Y, 4);
|
|
12824
|
+
ctx.fill();
|
|
12825
|
+
ctx.fillStyle = "#ffffff";
|
|
12826
|
+
ctx.textAlign = "left";
|
|
12827
|
+
ctx.textBaseline = "middle";
|
|
12828
|
+
ctx.fillText(name, LABEL_OFFSET + LABEL_PAD_X, LABEL_OFFSET + (LABEL_HEIGHT + LABEL_PAD_Y) / 2);
|
|
12829
|
+
}
|
|
12830
|
+
};
|
|
12831
|
+
|
|
12832
|
+
// src/canvas/remote-selection-overlay.ts
|
|
12833
|
+
var DEFAULT_ALPHA = 0.6;
|
|
12834
|
+
var DEFAULT_LINE_WIDTH_PX = 2;
|
|
12835
|
+
var RemoteSelectionOverlay = class {
|
|
12836
|
+
host;
|
|
12837
|
+
roster;
|
|
12838
|
+
colorFor;
|
|
12839
|
+
alpha;
|
|
12840
|
+
lineWidthPx;
|
|
12841
|
+
signatures = [];
|
|
12842
|
+
outlines = [];
|
|
12843
|
+
storeDirty = true;
|
|
12844
|
+
unregister;
|
|
12845
|
+
unsubscribers = [];
|
|
12846
|
+
isDisposed = false;
|
|
12847
|
+
constructor(host, roster, options = {}) {
|
|
12848
|
+
this.host = host;
|
|
12849
|
+
this.roster = roster;
|
|
12850
|
+
this.colorFor = options.colorFor;
|
|
12851
|
+
this.alpha = options.alpha ?? DEFAULT_ALPHA;
|
|
12852
|
+
this.lineWidthPx = options.lineWidthPx ?? DEFAULT_LINE_WIDTH_PX;
|
|
12853
|
+
this.unregister = host.registerOverlay((ctx) => this.render(ctx));
|
|
12854
|
+
const invalidate = () => {
|
|
12855
|
+
this.storeDirty = true;
|
|
12856
|
+
host.requestRender();
|
|
12857
|
+
};
|
|
12858
|
+
this.unsubscribers.push(
|
|
12859
|
+
roster.onChange(() => host.requestRender()),
|
|
12860
|
+
host.store.onChange(invalidate),
|
|
12861
|
+
host.layerManager.on("change", invalidate)
|
|
12862
|
+
);
|
|
12863
|
+
}
|
|
12864
|
+
get disposed() {
|
|
12865
|
+
return this.isDisposed;
|
|
12866
|
+
}
|
|
12867
|
+
dispose() {
|
|
12868
|
+
if (this.isDisposed) return;
|
|
12869
|
+
this.isDisposed = true;
|
|
12870
|
+
for (const unsub of this.unsubscribers) unsub();
|
|
12871
|
+
this.unsubscribers.length = 0;
|
|
12872
|
+
this.unregister?.();
|
|
12873
|
+
this.unregister = null;
|
|
12874
|
+
this.signatures = [];
|
|
12875
|
+
this.outlines = [];
|
|
12876
|
+
this.host.requestRender();
|
|
12877
|
+
}
|
|
12878
|
+
resolveColor(peer) {
|
|
12879
|
+
return this.colorFor?.(peer) ?? peer.color ?? defaultPeerColor(peer.id);
|
|
12880
|
+
}
|
|
12881
|
+
/** Recomputes outlines only when the selection signature or the store/layers changed. */
|
|
12882
|
+
rebuild() {
|
|
12883
|
+
const peers = this.roster.getPeers();
|
|
12884
|
+
const next = [];
|
|
12885
|
+
for (const peer of peers) {
|
|
12886
|
+
if (peer.selection.length === 0) continue;
|
|
12887
|
+
next.push({ from: peer.from, selection: peer.selection, color: this.resolveColor(peer) });
|
|
12888
|
+
}
|
|
12889
|
+
let changed = this.storeDirty || next.length !== this.signatures.length;
|
|
12890
|
+
if (!changed) {
|
|
12891
|
+
for (let i = 0; i < next.length; i++) {
|
|
12892
|
+
const a = next[i];
|
|
12893
|
+
const b = this.signatures[i];
|
|
12894
|
+
if (!a || !b || a.from !== b.from || a.selection !== b.selection || a.color !== b.color) {
|
|
12895
|
+
changed = true;
|
|
12896
|
+
break;
|
|
12897
|
+
}
|
|
12898
|
+
}
|
|
12899
|
+
}
|
|
12900
|
+
if (!changed) return;
|
|
12901
|
+
this.storeDirty = false;
|
|
12902
|
+
this.signatures = next;
|
|
12903
|
+
if (next.length === 0) {
|
|
12904
|
+
this.outlines = [];
|
|
12905
|
+
return;
|
|
12906
|
+
}
|
|
12907
|
+
const colorById = /* @__PURE__ */ new Map();
|
|
12908
|
+
for (const sig of next) {
|
|
12909
|
+
for (const id of sig.selection) if (!colorById.has(id)) colorById.set(id, sig.color);
|
|
12910
|
+
}
|
|
12911
|
+
const layers = this.host.layerManager;
|
|
12912
|
+
const rects = computeElementRects(
|
|
12913
|
+
this.host.store,
|
|
12914
|
+
(element) => colorById.has(element.id) && layers.isLayerVisible(element.layerId) ? element.id : null
|
|
12915
|
+
);
|
|
12916
|
+
this.outlines = rects.map((rect) => ({ rect, color: colorById.get(rect.id) ?? "#2563eb" }));
|
|
12917
|
+
}
|
|
12918
|
+
render(ctx) {
|
|
12919
|
+
if (this.isDisposed) return;
|
|
12920
|
+
this.rebuild();
|
|
12921
|
+
if (this.outlines.length === 0) return;
|
|
12922
|
+
const zoom = this.host.camera.zoom;
|
|
12923
|
+
const inv = zoom > 0 && Number.isFinite(zoom) ? 1 / zoom : 1;
|
|
12924
|
+
ctx.save();
|
|
12925
|
+
ctx.globalAlpha = this.alpha;
|
|
12926
|
+
ctx.lineWidth = this.lineWidthPx * inv;
|
|
12927
|
+
for (const { rect, color } of this.outlines) {
|
|
12928
|
+
ctx.save();
|
|
12929
|
+
ctx.strokeStyle = color;
|
|
12930
|
+
ctx.translate(rect.x + rect.w / 2, rect.y + rect.h / 2);
|
|
12931
|
+
if (rect.rotation !== 0) ctx.rotate(rect.rotation);
|
|
12932
|
+
ctx.strokeRect(-rect.w / 2, -rect.h / 2, rect.w, rect.h);
|
|
12933
|
+
ctx.restore();
|
|
12934
|
+
}
|
|
12935
|
+
ctx.restore();
|
|
12936
|
+
}
|
|
12937
|
+
};
|
|
12938
|
+
|
|
12939
|
+
// src/canvas/attach-awareness.ts
|
|
12940
|
+
function attachAwareness(viewport, channel, options) {
|
|
12941
|
+
const {
|
|
12942
|
+
roster: rosterOptions,
|
|
12943
|
+
cursors: cursorOptions,
|
|
12944
|
+
selections: selectionOptions,
|
|
12945
|
+
publish,
|
|
12946
|
+
...localOptions
|
|
12947
|
+
} = options;
|
|
12948
|
+
const roster = new PeerRoster(rosterOptions);
|
|
12949
|
+
let local = null;
|
|
12950
|
+
let cursors = null;
|
|
12951
|
+
let selections = null;
|
|
12952
|
+
const unsubscribers = [];
|
|
12953
|
+
try {
|
|
12954
|
+
local = publish === false ? null : new LocalAwareness(viewport, {
|
|
12955
|
+
...localOptions,
|
|
12956
|
+
send: (data) => channel.sendPresence(data)
|
|
12957
|
+
});
|
|
12958
|
+
cursors = cursorOptions === false ? null : new RemoteCursorOverlay(viewport, roster, cursorOptions ?? {});
|
|
12959
|
+
selections = selectionOptions === void 0 || selectionOptions === false ? null : new RemoteSelectionOverlay(
|
|
12960
|
+
viewport,
|
|
12961
|
+
roster,
|
|
12962
|
+
selectionOptions === true ? {} : selectionOptions
|
|
12963
|
+
);
|
|
12964
|
+
if (publish !== false) unsubscribers.push(roster.onDiscover(() => local?.announce()));
|
|
12965
|
+
unsubscribers.push(
|
|
12966
|
+
channel.onPresence((from, data) => {
|
|
12967
|
+
roster.apply(from, data);
|
|
12968
|
+
})
|
|
12969
|
+
);
|
|
12970
|
+
unsubscribers.push(channel.onPresenceLeave((from) => roster.remove(from)));
|
|
12971
|
+
} catch (error) {
|
|
12972
|
+
for (let i = unsubscribers.length - 1; i >= 0; i--) {
|
|
12973
|
+
try {
|
|
12974
|
+
unsubscribers[i]?.();
|
|
12975
|
+
} catch {
|
|
12976
|
+
}
|
|
12977
|
+
}
|
|
12978
|
+
unsubscribers.length = 0;
|
|
12979
|
+
try {
|
|
12980
|
+
selections?.dispose();
|
|
12981
|
+
} catch {
|
|
12982
|
+
}
|
|
12983
|
+
try {
|
|
12984
|
+
cursors?.dispose();
|
|
12985
|
+
} catch {
|
|
12986
|
+
}
|
|
12987
|
+
try {
|
|
12988
|
+
local?.dispose();
|
|
12989
|
+
} catch {
|
|
12990
|
+
}
|
|
12991
|
+
try {
|
|
12992
|
+
roster.dispose();
|
|
12993
|
+
} catch {
|
|
12994
|
+
}
|
|
12995
|
+
throw error;
|
|
12996
|
+
}
|
|
12997
|
+
let disposed = false;
|
|
12998
|
+
return {
|
|
12999
|
+
roster,
|
|
13000
|
+
local,
|
|
13001
|
+
cursors,
|
|
13002
|
+
selections,
|
|
13003
|
+
announce: () => local?.announce(),
|
|
13004
|
+
setFields: (fields) => local?.setFields(fields),
|
|
13005
|
+
dispose: () => {
|
|
13006
|
+
if (disposed) return;
|
|
13007
|
+
disposed = true;
|
|
13008
|
+
try {
|
|
13009
|
+
local?.dispose();
|
|
13010
|
+
} catch {
|
|
13011
|
+
}
|
|
13012
|
+
try {
|
|
13013
|
+
cursors?.dispose();
|
|
13014
|
+
} catch {
|
|
13015
|
+
}
|
|
13016
|
+
try {
|
|
13017
|
+
selections?.dispose();
|
|
13018
|
+
} catch {
|
|
13019
|
+
}
|
|
13020
|
+
try {
|
|
13021
|
+
roster.dispose();
|
|
13022
|
+
} catch {
|
|
13023
|
+
}
|
|
13024
|
+
for (const unsub of unsubscribers) {
|
|
13025
|
+
try {
|
|
13026
|
+
unsub();
|
|
13027
|
+
} catch {
|
|
13028
|
+
}
|
|
13029
|
+
}
|
|
13030
|
+
unsubscribers.length = 0;
|
|
13031
|
+
}
|
|
13032
|
+
};
|
|
13033
|
+
}
|
|
13034
|
+
|
|
12231
13035
|
// src/tools/hand-tool.ts
|
|
12232
13036
|
var HandTool = class {
|
|
12233
13037
|
name = "hand";
|
|
@@ -13893,7 +14697,7 @@ var MeasureTool = class {
|
|
|
13893
14697
|
// src/tools/path-tool.ts
|
|
13894
14698
|
var EPS2 = 1e-6;
|
|
13895
14699
|
var DEFAULT_COMMIT_TAP_RADIUS_PX = 12;
|
|
13896
|
-
function
|
|
14700
|
+
function samePoint3(a, b) {
|
|
13897
14701
|
return Math.abs(a.x - b.x) < EPS2 && Math.abs(a.y - b.y) < EPS2;
|
|
13898
14702
|
}
|
|
13899
14703
|
var PathTool = class {
|
|
@@ -14037,7 +14841,7 @@ var PathTool = class {
|
|
|
14037
14841
|
return;
|
|
14038
14842
|
}
|
|
14039
14843
|
const last = this.lastWaypoint();
|
|
14040
|
-
if (this.cursor && last && !
|
|
14844
|
+
if (this.cursor && last && !samePoint3(this.cursor, last)) {
|
|
14041
14845
|
this.waypoints.push({ ...this.cursor });
|
|
14042
14846
|
}
|
|
14043
14847
|
this.scheduleEmission();
|
|
@@ -14098,7 +14902,7 @@ var PathTool = class {
|
|
|
14098
14902
|
* world point.
|
|
14099
14903
|
*/
|
|
14100
14904
|
withinCommitRadius(point, last, ctx) {
|
|
14101
|
-
if (
|
|
14905
|
+
if (samePoint3(point, last)) return true;
|
|
14102
14906
|
if (this.hasSnappingGrid()) return false;
|
|
14103
14907
|
const zoom = ctx.camera.zoom;
|
|
14104
14908
|
if (!(zoom > 0)) return false;
|
|
@@ -14146,7 +14950,7 @@ var PathTool = class {
|
|
|
14146
14950
|
measure(cursor) {
|
|
14147
14951
|
const points = this.waypoints.map((p) => ({ ...p }));
|
|
14148
14952
|
const last = points[points.length - 1];
|
|
14149
|
-
if (cursor && (!last || !
|
|
14953
|
+
if (cursor && (!last || !samePoint3(cursor, last))) points.push({ ...cursor });
|
|
14150
14954
|
const { total, cumulative } = pathDistanceCells(points, {
|
|
14151
14955
|
gridSize: this.gridSize,
|
|
14152
14956
|
gridType: this.gridType,
|
|
@@ -14794,9 +15598,11 @@ var PingTool = class {
|
|
|
14794
15598
|
};
|
|
14795
15599
|
|
|
14796
15600
|
// src/index.ts
|
|
14797
|
-
var VERSION = "0.
|
|
15601
|
+
var VERSION = "0.65.0";
|
|
14798
15602
|
// Annotate the CommonJS export names for ESM import in node:
|
|
14799
15603
|
0 && (module.exports = {
|
|
15604
|
+
AWARENESS_MAX_SELECTION,
|
|
15605
|
+
AWARENESS_PRESENCE_KIND,
|
|
14800
15606
|
ArrowTool,
|
|
14801
15607
|
AutoSave,
|
|
14802
15608
|
Camera,
|
|
@@ -14815,6 +15621,7 @@ var VERSION = "0.64.0";
|
|
|
14815
15621
|
LASER_TRAIL_PRESENCE_KIND,
|
|
14816
15622
|
LaserTool,
|
|
14817
15623
|
LayerManager,
|
|
15624
|
+
LocalAwareness,
|
|
14818
15625
|
LocalStorageAdapter,
|
|
14819
15626
|
MEASURE_PRESENCE_KIND,
|
|
14820
15627
|
MeasureTool,
|
|
@@ -14823,16 +15630,20 @@ var VERSION = "0.64.0";
|
|
|
14823
15630
|
NoteTool,
|
|
14824
15631
|
PATH_PRESENCE_KIND,
|
|
14825
15632
|
PATH_PRESENCE_MAX_POINTS,
|
|
15633
|
+
PEER_COLORS,
|
|
14826
15634
|
PING_PRESENCE_KIND,
|
|
14827
15635
|
PathTool,
|
|
15636
|
+
PeerRoster,
|
|
14828
15637
|
PencilTool,
|
|
14829
15638
|
PingInput,
|
|
14830
15639
|
PingTool,
|
|
15640
|
+
RemoteCursorOverlay,
|
|
14831
15641
|
RemoteFocusReceiver,
|
|
14832
15642
|
RemoteLaserOverlay,
|
|
14833
15643
|
RemoteMeasureOverlay,
|
|
14834
15644
|
RemotePathOverlay,
|
|
14835
15645
|
RemotePingOverlay,
|
|
15646
|
+
RemoteSelectionOverlay,
|
|
14836
15647
|
SelectTool,
|
|
14837
15648
|
ShapeTool,
|
|
14838
15649
|
TemplateTool,
|
|
@@ -14841,6 +15652,7 @@ var VERSION = "0.64.0";
|
|
|
14841
15652
|
VERSION,
|
|
14842
15653
|
Viewport,
|
|
14843
15654
|
applyCameraView,
|
|
15655
|
+
attachAwareness,
|
|
14844
15656
|
boundsIntersect,
|
|
14845
15657
|
cameraOriginForView,
|
|
14846
15658
|
captureCameraView,
|
|
@@ -14854,6 +15666,7 @@ var VERSION = "0.64.0";
|
|
|
14854
15666
|
createStroke,
|
|
14855
15667
|
createTemplate,
|
|
14856
15668
|
createText,
|
|
15669
|
+
defaultPeerColor,
|
|
14857
15670
|
drawHexPath,
|
|
14858
15671
|
elementRectsEqual,
|
|
14859
15672
|
exportImage,
|
|
@@ -14876,6 +15689,7 @@ var VERSION = "0.64.0";
|
|
|
14876
15689
|
getHexCellsInSquare,
|
|
14877
15690
|
getHexDistance,
|
|
14878
15691
|
gridDistanceCells,
|
|
15692
|
+
isAwarenessPresence,
|
|
14879
15693
|
isFocusPresence,
|
|
14880
15694
|
isLaserTrailPresence,
|
|
14881
15695
|
isMeasurePresence,
|