@fieldnotes/core 0.63.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 +1943 -421
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +775 -17
- package/dist/index.d.ts +775 -17
- package/dist/index.js +1922 -421
- 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,20 +40,29 @@ __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,
|
|
44
47
|
MemoryAdapter: () => MemoryAdapter,
|
|
45
48
|
MinimapController: () => MinimapController,
|
|
46
49
|
NoteTool: () => NoteTool,
|
|
50
|
+
PATH_PRESENCE_KIND: () => PATH_PRESENCE_KIND,
|
|
51
|
+
PATH_PRESENCE_MAX_POINTS: () => PATH_PRESENCE_MAX_POINTS,
|
|
52
|
+
PEER_COLORS: () => PEER_COLORS,
|
|
47
53
|
PING_PRESENCE_KIND: () => PING_PRESENCE_KIND,
|
|
54
|
+
PathTool: () => PathTool,
|
|
55
|
+
PeerRoster: () => PeerRoster,
|
|
48
56
|
PencilTool: () => PencilTool,
|
|
49
57
|
PingInput: () => PingInput,
|
|
50
58
|
PingTool: () => PingTool,
|
|
59
|
+
RemoteCursorOverlay: () => RemoteCursorOverlay,
|
|
51
60
|
RemoteFocusReceiver: () => RemoteFocusReceiver,
|
|
52
61
|
RemoteLaserOverlay: () => RemoteLaserOverlay,
|
|
53
62
|
RemoteMeasureOverlay: () => RemoteMeasureOverlay,
|
|
63
|
+
RemotePathOverlay: () => RemotePathOverlay,
|
|
54
64
|
RemotePingOverlay: () => RemotePingOverlay,
|
|
65
|
+
RemoteSelectionOverlay: () => RemoteSelectionOverlay,
|
|
55
66
|
SelectTool: () => SelectTool,
|
|
56
67
|
ShapeTool: () => ShapeTool,
|
|
57
68
|
TemplateTool: () => TemplateTool,
|
|
@@ -60,6 +71,7 @@ __export(index_exports, {
|
|
|
60
71
|
VERSION: () => VERSION,
|
|
61
72
|
Viewport: () => Viewport,
|
|
62
73
|
applyCameraView: () => applyCameraView,
|
|
74
|
+
attachAwareness: () => attachAwareness,
|
|
63
75
|
boundsIntersect: () => boundsIntersect,
|
|
64
76
|
cameraOriginForView: () => cameraOriginForView,
|
|
65
77
|
captureCameraView: () => captureCameraView,
|
|
@@ -73,11 +85,13 @@ __export(index_exports, {
|
|
|
73
85
|
createStroke: () => createStroke,
|
|
74
86
|
createTemplate: () => createTemplate,
|
|
75
87
|
createText: () => createText,
|
|
88
|
+
defaultPeerColor: () => defaultPeerColor,
|
|
76
89
|
drawHexPath: () => drawHexPath,
|
|
77
90
|
elementRectsEqual: () => elementRectsEqual,
|
|
78
91
|
exportImage: () => exportImage,
|
|
79
92
|
exportSvg: () => exportSvg,
|
|
80
93
|
fitZoomForView: () => fitZoomForView,
|
|
94
|
+
footprintFromSize: () => footprintFromSize,
|
|
81
95
|
getActiveFormats: () => getActiveFormats,
|
|
82
96
|
getArrowBounds: () => getArrowBounds,
|
|
83
97
|
getArrowControlPoint: () => getArrowControlPoint,
|
|
@@ -93,20 +107,27 @@ __export(index_exports, {
|
|
|
93
107
|
getHexCellsInRectangle: () => getHexCellsInRectangle,
|
|
94
108
|
getHexCellsInSquare: () => getHexCellsInSquare,
|
|
95
109
|
getHexDistance: () => getHexDistance,
|
|
110
|
+
gridDistanceCells: () => gridDistanceCells,
|
|
111
|
+
isAwarenessPresence: () => isAwarenessPresence,
|
|
96
112
|
isFocusPresence: () => isFocusPresence,
|
|
97
113
|
isLaserTrailPresence: () => isLaserTrailPresence,
|
|
98
114
|
isMeasurePresence: () => isMeasurePresence,
|
|
99
115
|
isNearBezier: () => isNearBezier,
|
|
116
|
+
isPathPresence: () => isPathPresence,
|
|
100
117
|
isPingPresence: () => isPingPresence,
|
|
118
|
+
pathDistanceCells: () => pathDistanceCells,
|
|
101
119
|
resolveHtmlRouting: () => resolveHtmlRouting,
|
|
102
120
|
setFontSize: () => setFontSize,
|
|
103
121
|
smartSnap: () => smartSnap,
|
|
122
|
+
snapFootprintCenter: () => snapFootprintCenter,
|
|
104
123
|
snapPoint: () => snapPoint,
|
|
124
|
+
snapToCellCenter: () => snapToCellCenter,
|
|
105
125
|
snapToHexCenter: () => snapToHexCenter,
|
|
106
126
|
styleToPatch: () => styleToPatch,
|
|
107
127
|
toFocusPresence: () => toFocusPresence,
|
|
108
128
|
toLaserTrailPresence: () => toLaserTrailPresence,
|
|
109
129
|
toMeasurePresence: () => toMeasurePresence,
|
|
130
|
+
toPathPresence: () => toPathPresence,
|
|
110
131
|
toPingPresence: () => toPingPresence,
|
|
111
132
|
toggleBold: () => toggleBold,
|
|
112
133
|
toggleItalic: () => toggleItalic,
|
|
@@ -146,6 +167,310 @@ function smartSnap(point, ctx) {
|
|
|
146
167
|
}
|
|
147
168
|
return snapPoint(point, ctx.gridSize);
|
|
148
169
|
}
|
|
170
|
+
function footprintOf(footprint) {
|
|
171
|
+
return typeof footprint === "number" ? { w: footprint, h: footprint } : footprint;
|
|
172
|
+
}
|
|
173
|
+
function footprintFromSize(size, gridSize) {
|
|
174
|
+
if (!(gridSize > 0)) return 1;
|
|
175
|
+
return {
|
|
176
|
+
w: Math.max(1, Math.round(size.w / gridSize)),
|
|
177
|
+
h: Math.max(1, Math.round(size.h / gridSize))
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function snapAxisToCell(value, gridSize, cells) {
|
|
181
|
+
const n2 = Math.max(1, Math.round(cells));
|
|
182
|
+
if (n2 % 2 === 0) return Math.round(value / gridSize) * gridSize || 0;
|
|
183
|
+
return (Math.round((value - gridSize / 2) / gridSize) + 0.5) * gridSize || 0;
|
|
184
|
+
}
|
|
185
|
+
function snapToCellCenter(point, gridSize, footprint = 1) {
|
|
186
|
+
const { w, h } = footprintOf(footprint);
|
|
187
|
+
return { x: snapAxisToCell(point.x, gridSize, w), y: snapAxisToCell(point.y, gridSize, h) };
|
|
188
|
+
}
|
|
189
|
+
function snapFootprintCenter(point, footprint, ctx) {
|
|
190
|
+
if (!ctx.snapToGrid || !ctx.gridSize) return point;
|
|
191
|
+
if (ctx.gridType === "hex" && ctx.hexOrientation) {
|
|
192
|
+
return snapToHexCenter(point, ctx.gridSize, ctx.hexOrientation);
|
|
193
|
+
}
|
|
194
|
+
return snapToCellCenter(point, ctx.gridSize, footprint);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/elements/hex-fill.ts
|
|
198
|
+
function offsetToCube(col, row, orientation) {
|
|
199
|
+
if (orientation === "pointy") {
|
|
200
|
+
return { q: col - (row - (row & 1)) / 2, r: row };
|
|
201
|
+
}
|
|
202
|
+
return { q: col, r: row - (col - (col & 1)) / 2 };
|
|
203
|
+
}
|
|
204
|
+
function cubeToOffset(q, r, orientation) {
|
|
205
|
+
if (orientation === "pointy") {
|
|
206
|
+
return { col: q + (r - (r & 1)) / 2, row: r };
|
|
207
|
+
}
|
|
208
|
+
return { col: q, row: r + (q - (q & 1)) / 2 };
|
|
209
|
+
}
|
|
210
|
+
function offsetToPixel(col, row, cellSize, orientation) {
|
|
211
|
+
if (orientation === "pointy") {
|
|
212
|
+
const hexW = Math.sqrt(3) * cellSize;
|
|
213
|
+
const rowH = 1.5 * cellSize;
|
|
214
|
+
const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
|
|
215
|
+
return { x: col * hexW + offsetX, y: row * rowH };
|
|
216
|
+
}
|
|
217
|
+
const hexH = Math.sqrt(3) * cellSize;
|
|
218
|
+
const colW = 1.5 * cellSize;
|
|
219
|
+
const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
|
|
220
|
+
return { x: col * colW, y: row * hexH + offsetY };
|
|
221
|
+
}
|
|
222
|
+
function pixelToOffset(x, y, cellSize, orientation) {
|
|
223
|
+
if (orientation === "pointy") {
|
|
224
|
+
const hexW = Math.sqrt(3) * cellSize;
|
|
225
|
+
const rowH = 1.5 * cellSize;
|
|
226
|
+
const row = Math.round(y / rowH);
|
|
227
|
+
const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
|
|
228
|
+
return { col: Math.round((x - offsetX) / hexW), row };
|
|
229
|
+
}
|
|
230
|
+
const hexH = Math.sqrt(3) * cellSize;
|
|
231
|
+
const colW = 1.5 * cellSize;
|
|
232
|
+
const col = Math.round(x / colW);
|
|
233
|
+
const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
|
|
234
|
+
return { col, row: Math.round((y - offsetY) / hexH) };
|
|
235
|
+
}
|
|
236
|
+
function enumerateHexRing(centerQ, centerR, n2, orientation, cellSize) {
|
|
237
|
+
const cells = [];
|
|
238
|
+
for (let dq = -n2; dq <= n2; dq++) {
|
|
239
|
+
const rMin = Math.max(-n2, -dq - n2);
|
|
240
|
+
const rMax = Math.min(n2, -dq + n2);
|
|
241
|
+
for (let dr = rMin; dr <= rMax; dr++) {
|
|
242
|
+
const absQ = centerQ + dq;
|
|
243
|
+
const absR = centerR + dr;
|
|
244
|
+
const off = cubeToOffset(absQ, absR, orientation);
|
|
245
|
+
cells.push(offsetToPixel(off.col, off.row, cellSize, orientation));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return cells;
|
|
249
|
+
}
|
|
250
|
+
function getHexDistance(a, b, cellSize, orientation) {
|
|
251
|
+
const offA = pixelToOffset(a.x, a.y, cellSize, orientation);
|
|
252
|
+
const offB = pixelToOffset(b.x, b.y, cellSize, orientation);
|
|
253
|
+
const cubeA = offsetToCube(offA.col, offA.row, orientation);
|
|
254
|
+
const cubeB = offsetToCube(offB.col, offB.row, orientation);
|
|
255
|
+
const dq = cubeA.q - cubeB.q;
|
|
256
|
+
const dr = cubeA.r - cubeB.r;
|
|
257
|
+
const ds = -dq - dr;
|
|
258
|
+
return Math.max(Math.abs(dq), Math.abs(dr), Math.abs(ds));
|
|
259
|
+
}
|
|
260
|
+
function getHexCellsInRadius(center2, radiusCells, cellSize, orientation) {
|
|
261
|
+
const n2 = Math.round(radiusCells);
|
|
262
|
+
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
263
|
+
const cube = offsetToCube(off.col, off.row, orientation);
|
|
264
|
+
if (n2 <= 0) {
|
|
265
|
+
return [offsetToPixel(off.col, off.row, cellSize, orientation)];
|
|
266
|
+
}
|
|
267
|
+
return enumerateHexRing(cube.q, cube.r, n2, orientation, cellSize);
|
|
268
|
+
}
|
|
269
|
+
function getHexCellsInCone(center2, angle, radiusCells, cellSize, orientation) {
|
|
270
|
+
const n2 = Math.round(radiusCells);
|
|
271
|
+
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
272
|
+
const cube = offsetToCube(off.col, off.row, orientation);
|
|
273
|
+
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
274
|
+
if (n2 <= 0) return [centerPixel];
|
|
275
|
+
const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
|
|
276
|
+
const step = Math.PI / 3;
|
|
277
|
+
const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
|
|
278
|
+
const halfAngle = Math.PI / 6 + 1e-6;
|
|
279
|
+
const cells = [centerPixel];
|
|
280
|
+
for (let dq = -n2; dq <= n2; dq++) {
|
|
281
|
+
const rMin = Math.max(-n2, -dq - n2);
|
|
282
|
+
const rMax = Math.min(n2, -dq + n2);
|
|
283
|
+
for (let dr = rMin; dr <= rMax; dr++) {
|
|
284
|
+
if (dq === 0 && dr === 0) continue;
|
|
285
|
+
const absQ = cube.q + dq;
|
|
286
|
+
const absR = cube.r + dr;
|
|
287
|
+
const pixel = offsetToPixel(
|
|
288
|
+
cubeToOffset(absQ, absR, orientation).col,
|
|
289
|
+
cubeToOffset(absQ, absR, orientation).row,
|
|
290
|
+
cellSize,
|
|
291
|
+
orientation
|
|
292
|
+
);
|
|
293
|
+
const dx = pixel.x - centerPixel.x;
|
|
294
|
+
const dy = pixel.y - centerPixel.y;
|
|
295
|
+
let diff = Math.atan2(dy, dx) - snappedAngle;
|
|
296
|
+
if (diff > Math.PI) diff -= 2 * Math.PI;
|
|
297
|
+
if (diff < -Math.PI) diff += 2 * Math.PI;
|
|
298
|
+
if (Math.abs(diff) <= halfAngle) {
|
|
299
|
+
cells.push(pixel);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return cells;
|
|
304
|
+
}
|
|
305
|
+
function getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation) {
|
|
306
|
+
const n2 = Math.round(radiusCells);
|
|
307
|
+
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
308
|
+
const cube = offsetToCube(off.col, off.row, orientation);
|
|
309
|
+
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
310
|
+
if (n2 <= 0) return [centerPixel];
|
|
311
|
+
const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
|
|
312
|
+
const step = Math.PI / 3;
|
|
313
|
+
const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
|
|
314
|
+
const cos = Math.cos(snappedAngle);
|
|
315
|
+
const sin = Math.sin(snappedAngle);
|
|
316
|
+
const snapUnit = Math.sqrt(3) * cellSize;
|
|
317
|
+
const lineLength = n2 * snapUnit;
|
|
318
|
+
const halfWidth = snapUnit * 0.5 + 1e-6;
|
|
319
|
+
const cells = [];
|
|
320
|
+
for (let dq = -n2; dq <= n2; dq++) {
|
|
321
|
+
const rMin = Math.max(-n2, -dq - n2);
|
|
322
|
+
const rMax = Math.min(n2, -dq + n2);
|
|
323
|
+
for (let dr = rMin; dr <= rMax; dr++) {
|
|
324
|
+
const absQ = cube.q + dq;
|
|
325
|
+
const absR = cube.r + dr;
|
|
326
|
+
const pixel = offsetToPixel(
|
|
327
|
+
cubeToOffset(absQ, absR, orientation).col,
|
|
328
|
+
cubeToOffset(absQ, absR, orientation).row,
|
|
329
|
+
cellSize,
|
|
330
|
+
orientation
|
|
331
|
+
);
|
|
332
|
+
const dx = pixel.x - centerPixel.x;
|
|
333
|
+
const dy = pixel.y - centerPixel.y;
|
|
334
|
+
const along = dx * cos + dy * sin;
|
|
335
|
+
const perp = Math.abs(-dx * sin + dy * cos);
|
|
336
|
+
if (along >= -snapUnit * 0.1 && along <= lineLength + snapUnit * 0.1 && perp <= halfWidth) {
|
|
337
|
+
cells.push(pixel);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return cells;
|
|
342
|
+
}
|
|
343
|
+
function getHexCellsInRectangle(center2, angle, lengthCells, widthCells, cellSize, orientation) {
|
|
344
|
+
const nLen = Math.round(lengthCells);
|
|
345
|
+
const wCells = Math.max(1, Math.round(widthCells));
|
|
346
|
+
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
347
|
+
const cube = offsetToCube(off.col, off.row, orientation);
|
|
348
|
+
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
349
|
+
if (nLen <= 0) return [centerPixel];
|
|
350
|
+
const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
|
|
351
|
+
const step = Math.PI / 3;
|
|
352
|
+
const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
|
|
353
|
+
const cos = Math.cos(snappedAngle);
|
|
354
|
+
const sin = Math.sin(snappedAngle);
|
|
355
|
+
const snapUnit = Math.sqrt(3) * cellSize;
|
|
356
|
+
const lineLength = nLen * snapUnit;
|
|
357
|
+
const halfWidth = wCells * snapUnit / 2 + 1e-6;
|
|
358
|
+
const iterM = nLen + Math.ceil(wCells / 2) + 1;
|
|
359
|
+
const cells = [];
|
|
360
|
+
for (let dq = -iterM; dq <= iterM; dq++) {
|
|
361
|
+
const rMin = Math.max(-iterM, -dq - iterM);
|
|
362
|
+
const rMax = Math.min(iterM, -dq + iterM);
|
|
363
|
+
for (let dr = rMin; dr <= rMax; dr++) {
|
|
364
|
+
const absQ = cube.q + dq;
|
|
365
|
+
const absR = cube.r + dr;
|
|
366
|
+
const pixel = offsetToPixel(
|
|
367
|
+
cubeToOffset(absQ, absR, orientation).col,
|
|
368
|
+
cubeToOffset(absQ, absR, orientation).row,
|
|
369
|
+
cellSize,
|
|
370
|
+
orientation
|
|
371
|
+
);
|
|
372
|
+
const dx = pixel.x - centerPixel.x;
|
|
373
|
+
const dy = pixel.y - centerPixel.y;
|
|
374
|
+
const along = dx * cos + dy * sin;
|
|
375
|
+
const perp = Math.abs(-dx * sin + dy * cos);
|
|
376
|
+
if (along >= -snapUnit * 0.1 && along <= lineLength + snapUnit * 0.1 && perp <= halfWidth) {
|
|
377
|
+
cells.push(pixel);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return cells;
|
|
382
|
+
}
|
|
383
|
+
function getHexCellsInSquare(center2, radiusCells, cellSize, orientation) {
|
|
384
|
+
const n2 = Math.round(radiusCells);
|
|
385
|
+
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
386
|
+
const cube = offsetToCube(off.col, off.row, orientation);
|
|
387
|
+
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
388
|
+
if (n2 <= 0) return [centerPixel];
|
|
389
|
+
const snapUnit = Math.sqrt(3) * cellSize;
|
|
390
|
+
const halfSide = n2 * snapUnit / 2;
|
|
391
|
+
const cells = [];
|
|
392
|
+
for (let dq = -n2; dq <= n2; dq++) {
|
|
393
|
+
const rMin = Math.max(-n2, -dq - n2);
|
|
394
|
+
const rMax = Math.min(n2, -dq + n2);
|
|
395
|
+
for (let dr = rMin; dr <= rMax; dr++) {
|
|
396
|
+
const absQ = cube.q + dq;
|
|
397
|
+
const absR = cube.r + dr;
|
|
398
|
+
const pixel = offsetToPixel(
|
|
399
|
+
cubeToOffset(absQ, absR, orientation).col,
|
|
400
|
+
cubeToOffset(absQ, absR, orientation).row,
|
|
401
|
+
cellSize,
|
|
402
|
+
orientation
|
|
403
|
+
);
|
|
404
|
+
if (Math.abs(pixel.x - centerPixel.x) <= halfSide && Math.abs(pixel.y - centerPixel.y) <= halfSide) {
|
|
405
|
+
cells.push(pixel);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return cells;
|
|
410
|
+
}
|
|
411
|
+
function drawHexPath(ctx, cx, cy, cellSize, orientation) {
|
|
412
|
+
const angleOffset = orientation === "pointy" ? Math.PI / 6 : 0;
|
|
413
|
+
ctx.moveTo(cx + cellSize * Math.cos(angleOffset), cy + cellSize * Math.sin(angleOffset));
|
|
414
|
+
for (let i = 1; i < 6; i++) {
|
|
415
|
+
const a = angleOffset + Math.PI / 3 * i;
|
|
416
|
+
ctx.lineTo(cx + cellSize * Math.cos(a), cy + cellSize * Math.sin(a));
|
|
417
|
+
}
|
|
418
|
+
ctx.closePath();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// src/core/grid-metric.ts
|
|
422
|
+
function squareCost(rule, straight, diag) {
|
|
423
|
+
switch (rule) {
|
|
424
|
+
case "alternate":
|
|
425
|
+
return straight + diag + Math.floor(diag / 2);
|
|
426
|
+
case "manhattan":
|
|
427
|
+
return straight + 2 * diag;
|
|
428
|
+
case "chebyshev":
|
|
429
|
+
case "euclidean":
|
|
430
|
+
default:
|
|
431
|
+
return straight + diag;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
function pathDistanceCells(points, grid) {
|
|
435
|
+
const cumulative = [];
|
|
436
|
+
const first = points[0];
|
|
437
|
+
if (first === void 0) return { total: 0, cumulative };
|
|
438
|
+
const gridSize = grid.gridSize > 0 ? grid.gridSize : 1;
|
|
439
|
+
const rule = grid.diagonalRule ?? "euclidean";
|
|
440
|
+
const orientation = grid.hexOrientation;
|
|
441
|
+
const hex = grid.gridType === "hex" && orientation !== void 0;
|
|
442
|
+
const squareRule = grid.gridType === "square" && rule !== "euclidean";
|
|
443
|
+
let straight = 0;
|
|
444
|
+
let diag = 0;
|
|
445
|
+
let euclid = 0;
|
|
446
|
+
let hexCells = 0;
|
|
447
|
+
cumulative.push(0);
|
|
448
|
+
let prev = first;
|
|
449
|
+
for (let i = 1; i < points.length; i++) {
|
|
450
|
+
const next = points[i];
|
|
451
|
+
if (next === void 0) break;
|
|
452
|
+
if (hex && orientation !== void 0) {
|
|
453
|
+
hexCells += getHexDistance(prev, next, gridSize, orientation);
|
|
454
|
+
cumulative.push(hexCells);
|
|
455
|
+
} else if (squareRule) {
|
|
456
|
+
const dx = Math.abs(Math.round((next.x - prev.x) / gridSize));
|
|
457
|
+
const dy = Math.abs(Math.round((next.y - prev.y) / gridSize));
|
|
458
|
+
const d = Math.min(dx, dy);
|
|
459
|
+
diag += d;
|
|
460
|
+
straight += Math.max(dx, dy) - d;
|
|
461
|
+
cumulative.push(squareCost(rule, straight, diag));
|
|
462
|
+
} else {
|
|
463
|
+
euclid += Math.hypot(next.x - prev.x, next.y - prev.y) / gridSize;
|
|
464
|
+
cumulative.push(euclid);
|
|
465
|
+
}
|
|
466
|
+
prev = next;
|
|
467
|
+
}
|
|
468
|
+
const total = cumulative[cumulative.length - 1] ?? 0;
|
|
469
|
+
return { total, cumulative };
|
|
470
|
+
}
|
|
471
|
+
function gridDistanceCells(a, b, grid) {
|
|
472
|
+
return pathDistanceCells([a, b], grid).total;
|
|
473
|
+
}
|
|
149
474
|
|
|
150
475
|
// src/elements/note-sanitizer.ts
|
|
151
476
|
var BOLD_TAGS = /* @__PURE__ */ new Set(["b", "strong"]);
|
|
@@ -1672,6 +1997,12 @@ var KeyboardHandler = class {
|
|
|
1672
1997
|
if (e.key === " ") {
|
|
1673
1998
|
this.deps.setSpaceHeld(true);
|
|
1674
1999
|
}
|
|
2000
|
+
const tool = this.deps.getActiveTool();
|
|
2001
|
+
const ctx = this.deps.getToolContext();
|
|
2002
|
+
if (tool?.onKeyDown && ctx && tool.onKeyDown(e, ctx)) {
|
|
2003
|
+
e.preventDefault();
|
|
2004
|
+
return;
|
|
2005
|
+
}
|
|
1675
2006
|
const action = this.shortcutMap.match(e);
|
|
1676
2007
|
if (action !== null) {
|
|
1677
2008
|
this.runAction(action, e);
|
|
@@ -1967,6 +2298,7 @@ var InputHandler = class {
|
|
|
1967
2298
|
abortSignal: this.abortController.signal,
|
|
1968
2299
|
getToolContext: () => this.toolContext,
|
|
1969
2300
|
getIsToolActive: () => this.isToolActive,
|
|
2301
|
+
getActiveTool: () => this.toolManager?.activeTool ?? null,
|
|
1970
2302
|
getLastPointerEvent: () => this.lastPointerEvent,
|
|
1971
2303
|
setSpaceHeld: (v) => {
|
|
1972
2304
|
this.spaceHeld = v;
|
|
@@ -2048,7 +2380,7 @@ var InputHandler = class {
|
|
|
2048
2380
|
this.element.addEventListener("pointermove", this.onPointerMove, opts);
|
|
2049
2381
|
this.element.addEventListener("pointerup", this.onPointerUp, opts);
|
|
2050
2382
|
this.element.addEventListener("pointerleave", this.onPointerLeave, opts);
|
|
2051
|
-
this.element.addEventListener("pointercancel", this.
|
|
2383
|
+
this.element.addEventListener("pointercancel", this.onPointerCancel, opts);
|
|
2052
2384
|
this.element.addEventListener("contextmenu", this.onContextMenu, opts);
|
|
2053
2385
|
window.addEventListener("blur", this.onCoastInterrupt, opts);
|
|
2054
2386
|
window.addEventListener("visibilitychange", this.onCoastInterrupt, opts);
|
|
@@ -2127,6 +2459,12 @@ var InputHandler = class {
|
|
|
2127
2459
|
}
|
|
2128
2460
|
};
|
|
2129
2461
|
onPointerUp = (e) => {
|
|
2462
|
+
this.finishPointer(e, false);
|
|
2463
|
+
};
|
|
2464
|
+
onPointerCancel = (e) => {
|
|
2465
|
+
this.finishPointer(e, true);
|
|
2466
|
+
};
|
|
2467
|
+
finishPointer(e, cancelled) {
|
|
2130
2468
|
this.cancelLongPress();
|
|
2131
2469
|
try {
|
|
2132
2470
|
this.element.releasePointerCapture(e.pointerId);
|
|
@@ -2149,16 +2487,18 @@ var InputHandler = class {
|
|
|
2149
2487
|
if (this.activePointers.size === 0) this.coastStoppedByPointer = false;
|
|
2150
2488
|
const upResult = this.inputFilter.filterUp(e);
|
|
2151
2489
|
if (this.isToolActive) {
|
|
2152
|
-
this.
|
|
2490
|
+
if (cancelled) this.dispatchToolCancel(e);
|
|
2491
|
+
else this.dispatchToolUp(e);
|
|
2153
2492
|
this.isToolActive = false;
|
|
2154
2493
|
} else if (this.deferredDown && upResult.pendingTap) {
|
|
2155
2494
|
this.dispatchToolDown(this.deferredDown);
|
|
2156
|
-
this.
|
|
2495
|
+
if (cancelled) this.dispatchToolCancel(e);
|
|
2496
|
+
else this.dispatchToolUp(e);
|
|
2157
2497
|
this.deferredDown = null;
|
|
2158
2498
|
} else {
|
|
2159
2499
|
this.deferredDown = null;
|
|
2160
2500
|
}
|
|
2161
|
-
}
|
|
2501
|
+
}
|
|
2162
2502
|
runAction(action, e) {
|
|
2163
2503
|
this.keyboard.runAction(action, e);
|
|
2164
2504
|
}
|
|
@@ -2265,6 +2605,11 @@ var InputHandler = class {
|
|
|
2265
2605
|
this.toolManager.handlePointerUp(this.toPointerState(e), this.toolContext);
|
|
2266
2606
|
this.historyRecorder?.commit();
|
|
2267
2607
|
}
|
|
2608
|
+
dispatchToolCancel(e) {
|
|
2609
|
+
if (!this.toolManager || !this.toolContext) return;
|
|
2610
|
+
this.toolManager.handlePointerCancel(this.toPointerState(e), this.toolContext);
|
|
2611
|
+
this.historyRecorder?.commit();
|
|
2612
|
+
}
|
|
2268
2613
|
isInScope() {
|
|
2269
2614
|
if (this.scope === "window") return true;
|
|
2270
2615
|
const active = document.activeElement;
|
|
@@ -2277,7 +2622,7 @@ var InputHandler = class {
|
|
|
2277
2622
|
cancelToolIfActive(e) {
|
|
2278
2623
|
this.cancelLongPress();
|
|
2279
2624
|
if (this.isToolActive) {
|
|
2280
|
-
this.
|
|
2625
|
+
this.dispatchToolCancel(e);
|
|
2281
2626
|
this.isToolActive = false;
|
|
2282
2627
|
}
|
|
2283
2628
|
this.deferredDown = null;
|
|
@@ -3375,230 +3720,6 @@ function getImage(src, imageCache, onImageLoad, onImageError) {
|
|
|
3375
3720
|
return null;
|
|
3376
3721
|
}
|
|
3377
3722
|
|
|
3378
|
-
// src/elements/hex-fill.ts
|
|
3379
|
-
function offsetToCube(col, row, orientation) {
|
|
3380
|
-
if (orientation === "pointy") {
|
|
3381
|
-
return { q: col - (row - (row & 1)) / 2, r: row };
|
|
3382
|
-
}
|
|
3383
|
-
return { q: col, r: row - (col - (col & 1)) / 2 };
|
|
3384
|
-
}
|
|
3385
|
-
function cubeToOffset(q, r, orientation) {
|
|
3386
|
-
if (orientation === "pointy") {
|
|
3387
|
-
return { col: q + (r - (r & 1)) / 2, row: r };
|
|
3388
|
-
}
|
|
3389
|
-
return { col: q, row: r + (q - (q & 1)) / 2 };
|
|
3390
|
-
}
|
|
3391
|
-
function offsetToPixel(col, row, cellSize, orientation) {
|
|
3392
|
-
if (orientation === "pointy") {
|
|
3393
|
-
const hexW = Math.sqrt(3) * cellSize;
|
|
3394
|
-
const rowH = 1.5 * cellSize;
|
|
3395
|
-
const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
|
|
3396
|
-
return { x: col * hexW + offsetX, y: row * rowH };
|
|
3397
|
-
}
|
|
3398
|
-
const hexH = Math.sqrt(3) * cellSize;
|
|
3399
|
-
const colW = 1.5 * cellSize;
|
|
3400
|
-
const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
|
|
3401
|
-
return { x: col * colW, y: row * hexH + offsetY };
|
|
3402
|
-
}
|
|
3403
|
-
function pixelToOffset(x, y, cellSize, orientation) {
|
|
3404
|
-
if (orientation === "pointy") {
|
|
3405
|
-
const hexW = Math.sqrt(3) * cellSize;
|
|
3406
|
-
const rowH = 1.5 * cellSize;
|
|
3407
|
-
const row = Math.round(y / rowH);
|
|
3408
|
-
const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
|
|
3409
|
-
return { col: Math.round((x - offsetX) / hexW), row };
|
|
3410
|
-
}
|
|
3411
|
-
const hexH = Math.sqrt(3) * cellSize;
|
|
3412
|
-
const colW = 1.5 * cellSize;
|
|
3413
|
-
const col = Math.round(x / colW);
|
|
3414
|
-
const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
|
|
3415
|
-
return { col, row: Math.round((y - offsetY) / hexH) };
|
|
3416
|
-
}
|
|
3417
|
-
function enumerateHexRing(centerQ, centerR, n2, orientation, cellSize) {
|
|
3418
|
-
const cells = [];
|
|
3419
|
-
for (let dq = -n2; dq <= n2; dq++) {
|
|
3420
|
-
const rMin = Math.max(-n2, -dq - n2);
|
|
3421
|
-
const rMax = Math.min(n2, -dq + n2);
|
|
3422
|
-
for (let dr = rMin; dr <= rMax; dr++) {
|
|
3423
|
-
const absQ = centerQ + dq;
|
|
3424
|
-
const absR = centerR + dr;
|
|
3425
|
-
const off = cubeToOffset(absQ, absR, orientation);
|
|
3426
|
-
cells.push(offsetToPixel(off.col, off.row, cellSize, orientation));
|
|
3427
|
-
}
|
|
3428
|
-
}
|
|
3429
|
-
return cells;
|
|
3430
|
-
}
|
|
3431
|
-
function getHexDistance(a, b, cellSize, orientation) {
|
|
3432
|
-
const offA = pixelToOffset(a.x, a.y, cellSize, orientation);
|
|
3433
|
-
const offB = pixelToOffset(b.x, b.y, cellSize, orientation);
|
|
3434
|
-
const cubeA = offsetToCube(offA.col, offA.row, orientation);
|
|
3435
|
-
const cubeB = offsetToCube(offB.col, offB.row, orientation);
|
|
3436
|
-
const dq = cubeA.q - cubeB.q;
|
|
3437
|
-
const dr = cubeA.r - cubeB.r;
|
|
3438
|
-
const ds = -dq - dr;
|
|
3439
|
-
return Math.max(Math.abs(dq), Math.abs(dr), Math.abs(ds));
|
|
3440
|
-
}
|
|
3441
|
-
function getHexCellsInRadius(center2, radiusCells, cellSize, orientation) {
|
|
3442
|
-
const n2 = Math.round(radiusCells);
|
|
3443
|
-
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
3444
|
-
const cube = offsetToCube(off.col, off.row, orientation);
|
|
3445
|
-
if (n2 <= 0) {
|
|
3446
|
-
return [offsetToPixel(off.col, off.row, cellSize, orientation)];
|
|
3447
|
-
}
|
|
3448
|
-
return enumerateHexRing(cube.q, cube.r, n2, orientation, cellSize);
|
|
3449
|
-
}
|
|
3450
|
-
function getHexCellsInCone(center2, angle, radiusCells, cellSize, orientation) {
|
|
3451
|
-
const n2 = Math.round(radiusCells);
|
|
3452
|
-
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
3453
|
-
const cube = offsetToCube(off.col, off.row, orientation);
|
|
3454
|
-
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
3455
|
-
if (n2 <= 0) return [centerPixel];
|
|
3456
|
-
const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
|
|
3457
|
-
const step = Math.PI / 3;
|
|
3458
|
-
const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
|
|
3459
|
-
const halfAngle = Math.PI / 6 + 1e-6;
|
|
3460
|
-
const cells = [centerPixel];
|
|
3461
|
-
for (let dq = -n2; dq <= n2; dq++) {
|
|
3462
|
-
const rMin = Math.max(-n2, -dq - n2);
|
|
3463
|
-
const rMax = Math.min(n2, -dq + n2);
|
|
3464
|
-
for (let dr = rMin; dr <= rMax; dr++) {
|
|
3465
|
-
if (dq === 0 && dr === 0) continue;
|
|
3466
|
-
const absQ = cube.q + dq;
|
|
3467
|
-
const absR = cube.r + dr;
|
|
3468
|
-
const pixel = offsetToPixel(
|
|
3469
|
-
cubeToOffset(absQ, absR, orientation).col,
|
|
3470
|
-
cubeToOffset(absQ, absR, orientation).row,
|
|
3471
|
-
cellSize,
|
|
3472
|
-
orientation
|
|
3473
|
-
);
|
|
3474
|
-
const dx = pixel.x - centerPixel.x;
|
|
3475
|
-
const dy = pixel.y - centerPixel.y;
|
|
3476
|
-
let diff = Math.atan2(dy, dx) - snappedAngle;
|
|
3477
|
-
if (diff > Math.PI) diff -= 2 * Math.PI;
|
|
3478
|
-
if (diff < -Math.PI) diff += 2 * Math.PI;
|
|
3479
|
-
if (Math.abs(diff) <= halfAngle) {
|
|
3480
|
-
cells.push(pixel);
|
|
3481
|
-
}
|
|
3482
|
-
}
|
|
3483
|
-
}
|
|
3484
|
-
return cells;
|
|
3485
|
-
}
|
|
3486
|
-
function getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation) {
|
|
3487
|
-
const n2 = Math.round(radiusCells);
|
|
3488
|
-
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
3489
|
-
const cube = offsetToCube(off.col, off.row, orientation);
|
|
3490
|
-
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
3491
|
-
if (n2 <= 0) return [centerPixel];
|
|
3492
|
-
const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
|
|
3493
|
-
const step = Math.PI / 3;
|
|
3494
|
-
const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
|
|
3495
|
-
const cos = Math.cos(snappedAngle);
|
|
3496
|
-
const sin = Math.sin(snappedAngle);
|
|
3497
|
-
const snapUnit = Math.sqrt(3) * cellSize;
|
|
3498
|
-
const lineLength = n2 * snapUnit;
|
|
3499
|
-
const halfWidth = snapUnit * 0.5 + 1e-6;
|
|
3500
|
-
const cells = [];
|
|
3501
|
-
for (let dq = -n2; dq <= n2; dq++) {
|
|
3502
|
-
const rMin = Math.max(-n2, -dq - n2);
|
|
3503
|
-
const rMax = Math.min(n2, -dq + n2);
|
|
3504
|
-
for (let dr = rMin; dr <= rMax; dr++) {
|
|
3505
|
-
const absQ = cube.q + dq;
|
|
3506
|
-
const absR = cube.r + dr;
|
|
3507
|
-
const pixel = offsetToPixel(
|
|
3508
|
-
cubeToOffset(absQ, absR, orientation).col,
|
|
3509
|
-
cubeToOffset(absQ, absR, orientation).row,
|
|
3510
|
-
cellSize,
|
|
3511
|
-
orientation
|
|
3512
|
-
);
|
|
3513
|
-
const dx = pixel.x - centerPixel.x;
|
|
3514
|
-
const dy = pixel.y - centerPixel.y;
|
|
3515
|
-
const along = dx * cos + dy * sin;
|
|
3516
|
-
const perp = Math.abs(-dx * sin + dy * cos);
|
|
3517
|
-
if (along >= -snapUnit * 0.1 && along <= lineLength + snapUnit * 0.1 && perp <= halfWidth) {
|
|
3518
|
-
cells.push(pixel);
|
|
3519
|
-
}
|
|
3520
|
-
}
|
|
3521
|
-
}
|
|
3522
|
-
return cells;
|
|
3523
|
-
}
|
|
3524
|
-
function getHexCellsInRectangle(center2, angle, lengthCells, widthCells, cellSize, orientation) {
|
|
3525
|
-
const nLen = Math.round(lengthCells);
|
|
3526
|
-
const wCells = Math.max(1, Math.round(widthCells));
|
|
3527
|
-
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
3528
|
-
const cube = offsetToCube(off.col, off.row, orientation);
|
|
3529
|
-
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
3530
|
-
if (nLen <= 0) return [centerPixel];
|
|
3531
|
-
const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
|
|
3532
|
-
const step = Math.PI / 3;
|
|
3533
|
-
const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
|
|
3534
|
-
const cos = Math.cos(snappedAngle);
|
|
3535
|
-
const sin = Math.sin(snappedAngle);
|
|
3536
|
-
const snapUnit = Math.sqrt(3) * cellSize;
|
|
3537
|
-
const lineLength = nLen * snapUnit;
|
|
3538
|
-
const halfWidth = wCells * snapUnit / 2 + 1e-6;
|
|
3539
|
-
const iterM = nLen + Math.ceil(wCells / 2) + 1;
|
|
3540
|
-
const cells = [];
|
|
3541
|
-
for (let dq = -iterM; dq <= iterM; dq++) {
|
|
3542
|
-
const rMin = Math.max(-iterM, -dq - iterM);
|
|
3543
|
-
const rMax = Math.min(iterM, -dq + iterM);
|
|
3544
|
-
for (let dr = rMin; dr <= rMax; dr++) {
|
|
3545
|
-
const absQ = cube.q + dq;
|
|
3546
|
-
const absR = cube.r + dr;
|
|
3547
|
-
const pixel = offsetToPixel(
|
|
3548
|
-
cubeToOffset(absQ, absR, orientation).col,
|
|
3549
|
-
cubeToOffset(absQ, absR, orientation).row,
|
|
3550
|
-
cellSize,
|
|
3551
|
-
orientation
|
|
3552
|
-
);
|
|
3553
|
-
const dx = pixel.x - centerPixel.x;
|
|
3554
|
-
const dy = pixel.y - centerPixel.y;
|
|
3555
|
-
const along = dx * cos + dy * sin;
|
|
3556
|
-
const perp = Math.abs(-dx * sin + dy * cos);
|
|
3557
|
-
if (along >= -snapUnit * 0.1 && along <= lineLength + snapUnit * 0.1 && perp <= halfWidth) {
|
|
3558
|
-
cells.push(pixel);
|
|
3559
|
-
}
|
|
3560
|
-
}
|
|
3561
|
-
}
|
|
3562
|
-
return cells;
|
|
3563
|
-
}
|
|
3564
|
-
function getHexCellsInSquare(center2, radiusCells, cellSize, orientation) {
|
|
3565
|
-
const n2 = Math.round(radiusCells);
|
|
3566
|
-
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
3567
|
-
const cube = offsetToCube(off.col, off.row, orientation);
|
|
3568
|
-
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
3569
|
-
if (n2 <= 0) return [centerPixel];
|
|
3570
|
-
const snapUnit = Math.sqrt(3) * cellSize;
|
|
3571
|
-
const halfSide = n2 * snapUnit / 2;
|
|
3572
|
-
const cells = [];
|
|
3573
|
-
for (let dq = -n2; dq <= n2; dq++) {
|
|
3574
|
-
const rMin = Math.max(-n2, -dq - n2);
|
|
3575
|
-
const rMax = Math.min(n2, -dq + n2);
|
|
3576
|
-
for (let dr = rMin; dr <= rMax; dr++) {
|
|
3577
|
-
const absQ = cube.q + dq;
|
|
3578
|
-
const absR = cube.r + dr;
|
|
3579
|
-
const pixel = offsetToPixel(
|
|
3580
|
-
cubeToOffset(absQ, absR, orientation).col,
|
|
3581
|
-
cubeToOffset(absQ, absR, orientation).row,
|
|
3582
|
-
cellSize,
|
|
3583
|
-
orientation
|
|
3584
|
-
);
|
|
3585
|
-
if (Math.abs(pixel.x - centerPixel.x) <= halfSide && Math.abs(pixel.y - centerPixel.y) <= halfSide) {
|
|
3586
|
-
cells.push(pixel);
|
|
3587
|
-
}
|
|
3588
|
-
}
|
|
3589
|
-
}
|
|
3590
|
-
return cells;
|
|
3591
|
-
}
|
|
3592
|
-
function drawHexPath(ctx, cx, cy, cellSize, orientation) {
|
|
3593
|
-
const angleOffset = orientation === "pointy" ? Math.PI / 6 : 0;
|
|
3594
|
-
ctx.moveTo(cx + cellSize * Math.cos(angleOffset), cy + cellSize * Math.sin(angleOffset));
|
|
3595
|
-
for (let i = 1; i < 6; i++) {
|
|
3596
|
-
const a = angleOffset + Math.PI / 3 * i;
|
|
3597
|
-
ctx.lineTo(cx + cellSize * Math.cos(a), cy + cellSize * Math.sin(a));
|
|
3598
|
-
}
|
|
3599
|
-
ctx.closePath();
|
|
3600
|
-
}
|
|
3601
|
-
|
|
3602
3723
|
// src/elements/renderers/template-measure.ts
|
|
3603
3724
|
function renderTemplateFeetLabel(ctx, p) {
|
|
3604
3725
|
if (p.feet <= 0) return;
|
|
@@ -5573,6 +5694,13 @@ var ToolManager = class {
|
|
|
5573
5694
|
handlePointerUp(state, ctx) {
|
|
5574
5695
|
this.current?.onPointerUp(state, ctx);
|
|
5575
5696
|
}
|
|
5697
|
+
/** Cancels the active gesture; falls back to `onPointerUp` for tools without `onPointerCancel`. */
|
|
5698
|
+
handlePointerCancel(state, ctx) {
|
|
5699
|
+
const tool = this.current;
|
|
5700
|
+
if (!tool) return;
|
|
5701
|
+
if (tool.onPointerCancel) tool.onPointerCancel(state, ctx);
|
|
5702
|
+
else tool.onPointerUp(state, ctx);
|
|
5703
|
+
}
|
|
5576
5704
|
onChange(listener) {
|
|
5577
5705
|
this.changeListeners.add(listener);
|
|
5578
5706
|
return () => this.changeListeners.delete(listener);
|
|
@@ -11182,145 +11310,119 @@ function drawMeasurement(ctx, m, opts = {}) {
|
|
|
11182
11310
|
ctx.restore();
|
|
11183
11311
|
}
|
|
11184
11312
|
|
|
11185
|
-
// src/canvas/
|
|
11186
|
-
var MEASURE_PRESENCE_KIND = "measure";
|
|
11187
|
-
function isFinitePoint2(value) {
|
|
11188
|
-
if (typeof value !== "object" || value === null) return false;
|
|
11189
|
-
const point = value;
|
|
11190
|
-
return typeof point.x === "number" && Number.isFinite(point.x) && typeof point.y === "number" && Number.isFinite(point.y);
|
|
11191
|
-
}
|
|
11192
|
-
function isMeasurePresence(data) {
|
|
11193
|
-
if (typeof data !== "object" || data === null) return false;
|
|
11194
|
-
const payload = data;
|
|
11195
|
-
if (payload.kind !== MEASURE_PRESENCE_KIND) return false;
|
|
11196
|
-
if ("cleared" in payload) return payload.cleared === true;
|
|
11197
|
-
if (!isFinitePoint2(payload.start) || !isFinitePoint2(payload.end)) return false;
|
|
11198
|
-
if (typeof payload.cells !== "number" || !Number.isFinite(payload.cells)) return false;
|
|
11199
|
-
if (typeof payload.feet !== "number" || !Number.isFinite(payload.feet)) return false;
|
|
11200
|
-
if (payload.color !== void 0 && typeof payload.color !== "string") return false;
|
|
11201
|
-
return true;
|
|
11202
|
-
}
|
|
11203
|
-
function toMeasurePresence(emission) {
|
|
11204
|
-
if (emission === null) return { kind: MEASURE_PRESENCE_KIND, cleared: true };
|
|
11205
|
-
return {
|
|
11206
|
-
kind: MEASURE_PRESENCE_KIND,
|
|
11207
|
-
start: emission.start,
|
|
11208
|
-
end: emission.end,
|
|
11209
|
-
cells: emission.cells,
|
|
11210
|
-
feet: emission.feet,
|
|
11211
|
-
color: emission.color
|
|
11212
|
-
};
|
|
11213
|
-
}
|
|
11214
|
-
var DEFAULT_COLOR3 = "#FF5722";
|
|
11313
|
+
// src/canvas/linger-overlay.ts
|
|
11215
11314
|
var DEFAULT_HOLD_MS = 1500;
|
|
11216
11315
|
var DEFAULT_FADE_MS2 = 400;
|
|
11217
11316
|
var DEFAULT_MAX_AGE_MS = 3e4;
|
|
11218
|
-
var
|
|
11317
|
+
var LingerOverlay = class {
|
|
11219
11318
|
host;
|
|
11220
|
-
|
|
11319
|
+
draw;
|
|
11320
|
+
clock;
|
|
11221
11321
|
holdMs;
|
|
11222
11322
|
fadeMs;
|
|
11223
11323
|
maxAgeMs;
|
|
11224
|
-
|
|
11324
|
+
entries = /* @__PURE__ */ new Map();
|
|
11225
11325
|
unregister;
|
|
11226
11326
|
rafId = null;
|
|
11227
|
-
|
|
11228
|
-
|
|
11327
|
+
isDisposed = false;
|
|
11328
|
+
/**
|
|
11329
|
+
* `clock` exists only as a test seam; production callers leave it at
|
|
11330
|
+
* `performance.now`. Owners that expose their own `now()` seam pass it
|
|
11331
|
+
* through so a spy on the owner still drives this overlay's timing.
|
|
11332
|
+
*/
|
|
11333
|
+
constructor(host, options, draw, clock = () => performance.now()) {
|
|
11229
11334
|
this.host = host;
|
|
11230
|
-
this.
|
|
11335
|
+
this.draw = draw;
|
|
11336
|
+
this.clock = clock;
|
|
11231
11337
|
this.holdMs = options.holdMs ?? DEFAULT_HOLD_MS;
|
|
11232
11338
|
this.fadeMs = options.fadeMs ?? DEFAULT_FADE_MS2;
|
|
11233
11339
|
this.maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
11234
|
-
this.unregister = host.registerOverlay((ctx) => this.
|
|
11340
|
+
this.unregister = host.registerOverlay((ctx) => this.render(ctx));
|
|
11235
11341
|
}
|
|
11236
11342
|
now() {
|
|
11237
|
-
return
|
|
11343
|
+
return this.clock();
|
|
11238
11344
|
}
|
|
11239
11345
|
/**
|
|
11240
|
-
*
|
|
11241
|
-
*
|
|
11242
|
-
* reported as `false`, so hosts can feed every presence frame through.
|
|
11346
|
+
* Stores `sender`'s current entry as active, replacing any previous one and
|
|
11347
|
+
* cancelling an in-flight linger, and restarts the `maxAgeMs` expiry timer.
|
|
11243
11348
|
*/
|
|
11244
|
-
|
|
11245
|
-
if (this.
|
|
11246
|
-
|
|
11247
|
-
this.beginLinger(sender);
|
|
11248
|
-
return true;
|
|
11249
|
-
}
|
|
11250
|
-
const existing = this.measurements.get(sender);
|
|
11349
|
+
set(sender, entry) {
|
|
11350
|
+
if (this.isDisposed) return;
|
|
11351
|
+
const existing = this.entries.get(sender);
|
|
11251
11352
|
if (existing?.expiryTimer != null) clearTimeout(existing.expiryTimer);
|
|
11252
|
-
this.
|
|
11253
|
-
|
|
11254
|
-
end: data.end,
|
|
11255
|
-
feet: data.feet,
|
|
11256
|
-
color: data.color ?? this.color,
|
|
11353
|
+
this.entries.set(sender, {
|
|
11354
|
+
entry,
|
|
11257
11355
|
clearedAt: null,
|
|
11258
|
-
expiryTimer: setTimeout(() => this.
|
|
11356
|
+
expiryTimer: setTimeout(() => this.linger(sender), this.maxAgeMs)
|
|
11259
11357
|
});
|
|
11260
11358
|
this.host.requestRender();
|
|
11261
|
-
return true;
|
|
11262
11359
|
}
|
|
11263
|
-
/**
|
|
11360
|
+
/** Starts the hold-then-fade lifetime for `sender`'s entry. */
|
|
11361
|
+
linger(sender) {
|
|
11362
|
+
const entry = this.entries.get(sender);
|
|
11363
|
+
if (!entry || entry.clearedAt !== null) return;
|
|
11364
|
+
if (entry.expiryTimer != null) {
|
|
11365
|
+
clearTimeout(entry.expiryTimer);
|
|
11366
|
+
entry.expiryTimer = null;
|
|
11367
|
+
}
|
|
11368
|
+
entry.clearedAt = this.now();
|
|
11369
|
+
this.ensureAnimating();
|
|
11370
|
+
this.host.requestRender();
|
|
11371
|
+
}
|
|
11372
|
+
/** Removes a sender's entry immediately (presence-leave/disconnect). */
|
|
11264
11373
|
remove(sender) {
|
|
11265
|
-
const entry = this.
|
|
11374
|
+
const entry = this.entries.get(sender);
|
|
11266
11375
|
if (!entry) return;
|
|
11267
11376
|
if (entry.expiryTimer != null) clearTimeout(entry.expiryTimer);
|
|
11268
|
-
this.
|
|
11377
|
+
this.entries.delete(sender);
|
|
11269
11378
|
this.host.requestRender();
|
|
11270
11379
|
}
|
|
11271
|
-
/** Removes every
|
|
11380
|
+
/** Removes every entry immediately. */
|
|
11272
11381
|
clear() {
|
|
11273
|
-
if (this.
|
|
11274
|
-
for (const entry of this.
|
|
11382
|
+
if (this.entries.size === 0) return;
|
|
11383
|
+
for (const entry of this.entries.values()) {
|
|
11275
11384
|
if (entry.expiryTimer != null) clearTimeout(entry.expiryTimer);
|
|
11276
11385
|
}
|
|
11277
|
-
this.
|
|
11386
|
+
this.entries.clear();
|
|
11278
11387
|
this.host.requestRender();
|
|
11279
11388
|
}
|
|
11280
|
-
/** Number of senders with a visible (active or lingering)
|
|
11389
|
+
/** Number of senders with a visible (active or lingering) entry. */
|
|
11281
11390
|
get activeSenderCount() {
|
|
11282
|
-
return this.
|
|
11391
|
+
return this.entries.size;
|
|
11392
|
+
}
|
|
11393
|
+
/** True once `dispose` has run; the overlay accepts no further entries. */
|
|
11394
|
+
get disposed() {
|
|
11395
|
+
return this.isDisposed;
|
|
11283
11396
|
}
|
|
11284
11397
|
/** Unregisters the overlay, cancels timers, stops animating. Idempotent. */
|
|
11285
11398
|
dispose() {
|
|
11286
|
-
if (this.
|
|
11287
|
-
this.
|
|
11399
|
+
if (this.isDisposed) return;
|
|
11400
|
+
this.isDisposed = true;
|
|
11288
11401
|
if (this.rafId !== null) {
|
|
11289
11402
|
cancelAnimationFrame(this.rafId);
|
|
11290
11403
|
this.rafId = null;
|
|
11291
11404
|
}
|
|
11292
|
-
for (const entry of this.
|
|
11405
|
+
for (const entry of this.entries.values()) {
|
|
11293
11406
|
if (entry.expiryTimer != null) clearTimeout(entry.expiryTimer);
|
|
11294
11407
|
}
|
|
11295
|
-
this.
|
|
11408
|
+
this.entries.clear();
|
|
11296
11409
|
this.unregister?.();
|
|
11297
11410
|
this.unregister = null;
|
|
11298
11411
|
this.host.requestRender();
|
|
11299
11412
|
}
|
|
11300
|
-
beginLinger(sender) {
|
|
11301
|
-
const entry = this.measurements.get(sender);
|
|
11302
|
-
if (!entry || entry.clearedAt !== null) return;
|
|
11303
|
-
if (entry.expiryTimer != null) {
|
|
11304
|
-
clearTimeout(entry.expiryTimer);
|
|
11305
|
-
entry.expiryTimer = null;
|
|
11306
|
-
}
|
|
11307
|
-
entry.clearedAt = this.now();
|
|
11308
|
-
this.ensureAnimating();
|
|
11309
|
-
this.host.requestRender();
|
|
11310
|
-
}
|
|
11311
11413
|
ensureAnimating() {
|
|
11312
11414
|
if (this.rafId === null) {
|
|
11313
11415
|
this.rafId = requestAnimationFrame(() => this.tick());
|
|
11314
11416
|
}
|
|
11315
11417
|
}
|
|
11316
11418
|
tick() {
|
|
11317
|
-
if (this.
|
|
11419
|
+
if (this.isDisposed) return;
|
|
11318
11420
|
const now = this.now();
|
|
11319
11421
|
let lingering = 0;
|
|
11320
|
-
for (const [sender, entry] of this.
|
|
11422
|
+
for (const [sender, entry] of this.entries) {
|
|
11321
11423
|
if (entry.clearedAt === null) continue;
|
|
11322
11424
|
if (now - entry.clearedAt >= this.holdMs + this.fadeMs) {
|
|
11323
|
-
this.
|
|
11425
|
+
this.entries.delete(sender);
|
|
11324
11426
|
} else {
|
|
11325
11427
|
lingering += 1;
|
|
11326
11428
|
}
|
|
@@ -11328,24 +11430,278 @@ var RemoteMeasureOverlay = class {
|
|
|
11328
11430
|
this.host.requestRender();
|
|
11329
11431
|
this.rafId = lingering > 0 ? requestAnimationFrame(() => this.tick()) : null;
|
|
11330
11432
|
}
|
|
11331
|
-
|
|
11332
|
-
if (this.
|
|
11433
|
+
render(ctx) {
|
|
11434
|
+
if (this.entries.size === 0) return;
|
|
11333
11435
|
const now = this.now();
|
|
11334
|
-
for (const entry of this.
|
|
11436
|
+
for (const entry of this.entries.values()) {
|
|
11335
11437
|
let alpha = 1;
|
|
11336
11438
|
if (entry.clearedAt !== null) {
|
|
11337
11439
|
const fadeAge = now - entry.clearedAt - this.holdMs;
|
|
11338
11440
|
if (fadeAge > 0) alpha = Math.max(0, 1 - fadeAge / this.fadeMs);
|
|
11339
11441
|
}
|
|
11340
|
-
|
|
11442
|
+
this.draw(ctx, entry.entry, alpha);
|
|
11443
|
+
}
|
|
11444
|
+
}
|
|
11445
|
+
};
|
|
11446
|
+
|
|
11447
|
+
// src/canvas/remote-measure-overlay.ts
|
|
11448
|
+
var MEASURE_PRESENCE_KIND = "measure";
|
|
11449
|
+
function isFinitePoint2(value) {
|
|
11450
|
+
if (typeof value !== "object" || value === null) return false;
|
|
11451
|
+
const point = value;
|
|
11452
|
+
return typeof point.x === "number" && Number.isFinite(point.x) && typeof point.y === "number" && Number.isFinite(point.y);
|
|
11453
|
+
}
|
|
11454
|
+
function isMeasurePresence(data) {
|
|
11455
|
+
if (typeof data !== "object" || data === null) return false;
|
|
11456
|
+
const payload = data;
|
|
11457
|
+
if (payload.kind !== MEASURE_PRESENCE_KIND) return false;
|
|
11458
|
+
if ("cleared" in payload) return payload.cleared === true;
|
|
11459
|
+
if (!isFinitePoint2(payload.start) || !isFinitePoint2(payload.end)) return false;
|
|
11460
|
+
if (typeof payload.cells !== "number" || !Number.isFinite(payload.cells)) return false;
|
|
11461
|
+
if (typeof payload.feet !== "number" || !Number.isFinite(payload.feet)) return false;
|
|
11462
|
+
if (payload.color !== void 0 && typeof payload.color !== "string") return false;
|
|
11463
|
+
return true;
|
|
11464
|
+
}
|
|
11465
|
+
function toMeasurePresence(emission) {
|
|
11466
|
+
if (emission === null) return { kind: MEASURE_PRESENCE_KIND, cleared: true };
|
|
11467
|
+
return {
|
|
11468
|
+
kind: MEASURE_PRESENCE_KIND,
|
|
11469
|
+
start: emission.start,
|
|
11470
|
+
end: emission.end,
|
|
11471
|
+
cells: emission.cells,
|
|
11472
|
+
feet: emission.feet,
|
|
11473
|
+
color: emission.color
|
|
11474
|
+
};
|
|
11475
|
+
}
|
|
11476
|
+
var DEFAULT_COLOR3 = "#FF5722";
|
|
11477
|
+
var RemoteMeasureOverlay = class {
|
|
11478
|
+
color;
|
|
11479
|
+
overlay;
|
|
11480
|
+
constructor(host, options = {}) {
|
|
11481
|
+
this.color = options.color ?? DEFAULT_COLOR3;
|
|
11482
|
+
this.overlay = new LingerOverlay(
|
|
11483
|
+
host,
|
|
11484
|
+
options,
|
|
11485
|
+
(ctx, entry, alpha) => drawMeasurement(ctx, entry, { alpha }),
|
|
11486
|
+
() => this.now()
|
|
11487
|
+
);
|
|
11488
|
+
}
|
|
11489
|
+
now() {
|
|
11490
|
+
return performance.now();
|
|
11491
|
+
}
|
|
11492
|
+
/**
|
|
11493
|
+
* Applies a presence payload from `sender` (any opaque per-sender key, e.g.
|
|
11494
|
+
* the envelope `from`). Non-measure or malformed payloads are ignored and
|
|
11495
|
+
* reported as `false`, so hosts can feed every presence frame through.
|
|
11496
|
+
*/
|
|
11497
|
+
apply(sender, data) {
|
|
11498
|
+
if (this.overlay.disposed || !isMeasurePresence(data)) return false;
|
|
11499
|
+
if ("cleared" in data) {
|
|
11500
|
+
this.overlay.linger(sender);
|
|
11501
|
+
return true;
|
|
11341
11502
|
}
|
|
11503
|
+
this.overlay.set(sender, {
|
|
11504
|
+
start: data.start,
|
|
11505
|
+
end: data.end,
|
|
11506
|
+
feet: data.feet,
|
|
11507
|
+
color: data.color ?? this.color
|
|
11508
|
+
});
|
|
11509
|
+
return true;
|
|
11510
|
+
}
|
|
11511
|
+
/** Removes a sender's ruler immediately (presence-leave/disconnect). */
|
|
11512
|
+
remove(sender) {
|
|
11513
|
+
this.overlay.remove(sender);
|
|
11514
|
+
}
|
|
11515
|
+
/** Removes every ruler immediately. */
|
|
11516
|
+
clear() {
|
|
11517
|
+
this.overlay.clear();
|
|
11518
|
+
}
|
|
11519
|
+
/** Number of senders with a visible (active or lingering) ruler. */
|
|
11520
|
+
get activeSenderCount() {
|
|
11521
|
+
return this.overlay.activeSenderCount;
|
|
11522
|
+
}
|
|
11523
|
+
/** Unregisters the overlay, cancels timers, stops animating. Idempotent. */
|
|
11524
|
+
dispose() {
|
|
11525
|
+
this.overlay.dispose();
|
|
11526
|
+
}
|
|
11527
|
+
};
|
|
11528
|
+
|
|
11529
|
+
// src/canvas/path-render.ts
|
|
11530
|
+
function resolveSegmentColors(cumulativeFeet, bands, color) {
|
|
11531
|
+
const sorted = [...bands].sort((a, b) => a.feet - b.feet);
|
|
11532
|
+
const out = [];
|
|
11533
|
+
for (let i = 1; i < cumulativeFeet.length; i++) {
|
|
11534
|
+
const end = cumulativeFeet[i] ?? 0;
|
|
11535
|
+
const band = sorted.find((b) => b.feet >= end);
|
|
11536
|
+
out.push(band ? band.color : color);
|
|
11537
|
+
}
|
|
11538
|
+
return out;
|
|
11539
|
+
}
|
|
11540
|
+
var LABEL_OFFSET_Y = 18;
|
|
11541
|
+
function drawPath(ctx, m, opts = {}) {
|
|
11542
|
+
const first = m.points[0];
|
|
11543
|
+
if (first === void 0) return;
|
|
11544
|
+
ctx.save();
|
|
11545
|
+
if (opts.alpha !== void 0) ctx.globalAlpha = opts.alpha;
|
|
11546
|
+
ctx.lineWidth = 2;
|
|
11547
|
+
let prev = first;
|
|
11548
|
+
for (let i = 1; i < m.points.length; i++) {
|
|
11549
|
+
const next = m.points[i];
|
|
11550
|
+
if (next === void 0) break;
|
|
11551
|
+
ctx.strokeStyle = m.segmentColors[i - 1] ?? m.color;
|
|
11552
|
+
ctx.setLineDash([8, 4]);
|
|
11553
|
+
ctx.beginPath();
|
|
11554
|
+
ctx.moveTo(prev.x, prev.y);
|
|
11555
|
+
ctx.lineTo(next.x, next.y);
|
|
11556
|
+
ctx.stroke();
|
|
11557
|
+
prev = next;
|
|
11558
|
+
}
|
|
11559
|
+
ctx.setLineDash([]);
|
|
11560
|
+
ctx.fillStyle = m.color;
|
|
11561
|
+
for (const p of m.points) {
|
|
11562
|
+
ctx.beginPath();
|
|
11563
|
+
ctx.arc(p.x, p.y, 4, 0, Math.PI * 2);
|
|
11564
|
+
ctx.fill();
|
|
11565
|
+
}
|
|
11566
|
+
if (m.points.length >= 2) {
|
|
11567
|
+
const last = prev;
|
|
11568
|
+
const label = formatMeasureLabel(m.feet);
|
|
11569
|
+
ctx.font = "14px sans-serif";
|
|
11570
|
+
const metrics = ctx.measureText(label);
|
|
11571
|
+
const padX = 6;
|
|
11572
|
+
const padY = 4;
|
|
11573
|
+
const textH = 14;
|
|
11574
|
+
const cx = last.x;
|
|
11575
|
+
const cy = last.y - LABEL_OFFSET_Y;
|
|
11576
|
+
ctx.fillStyle = "rgba(0, 0, 0, 0.75)";
|
|
11577
|
+
ctx.beginPath();
|
|
11578
|
+
ctx.roundRect(
|
|
11579
|
+
cx - metrics.width / 2 - padX,
|
|
11580
|
+
cy - textH / 2 - padY,
|
|
11581
|
+
metrics.width + padX * 2,
|
|
11582
|
+
textH + padY * 2,
|
|
11583
|
+
4
|
|
11584
|
+
);
|
|
11585
|
+
ctx.fill();
|
|
11586
|
+
ctx.fillStyle = "#FFFFFF";
|
|
11587
|
+
ctx.textAlign = "center";
|
|
11588
|
+
ctx.textBaseline = "middle";
|
|
11589
|
+
ctx.fillText(label, cx, cy);
|
|
11590
|
+
}
|
|
11591
|
+
ctx.restore();
|
|
11592
|
+
}
|
|
11593
|
+
|
|
11594
|
+
// src/canvas/remote-path-overlay.ts
|
|
11595
|
+
var PATH_PRESENCE_KIND = "path";
|
|
11596
|
+
var PATH_PRESENCE_MAX_POINTS = 256;
|
|
11597
|
+
var MAX_COLOR_LENGTH = 64;
|
|
11598
|
+
var EPS = 1e-6;
|
|
11599
|
+
function isFinitePoint3(value) {
|
|
11600
|
+
if (typeof value !== "object" || value === null) return false;
|
|
11601
|
+
const point = value;
|
|
11602
|
+
return typeof point.x === "number" && Number.isFinite(point.x) && typeof point.y === "number" && Number.isFinite(point.y);
|
|
11603
|
+
}
|
|
11604
|
+
function isPathPresence(data) {
|
|
11605
|
+
if (typeof data !== "object" || data === null) return false;
|
|
11606
|
+
const payload = data;
|
|
11607
|
+
if (payload.kind !== PATH_PRESENCE_KIND) return false;
|
|
11608
|
+
if ("cleared" in payload) return payload.cleared === true;
|
|
11609
|
+
if (!Array.isArray(payload.points)) return false;
|
|
11610
|
+
if (payload.points.length === 0 || payload.points.length > PATH_PRESENCE_MAX_POINTS) return false;
|
|
11611
|
+
for (const point of payload.points) {
|
|
11612
|
+
if (!isFinitePoint3(point)) return false;
|
|
11613
|
+
}
|
|
11614
|
+
if (!Array.isArray(payload.segmentColors)) return false;
|
|
11615
|
+
if (payload.segmentColors.length !== payload.points.length - 1) return false;
|
|
11616
|
+
for (const color of payload.segmentColors) {
|
|
11617
|
+
if (typeof color !== "string" || color.length > MAX_COLOR_LENGTH) return false;
|
|
11618
|
+
}
|
|
11619
|
+
if (typeof payload.feet !== "number" || !Number.isFinite(payload.feet)) return false;
|
|
11620
|
+
if (payload.color !== void 0) {
|
|
11621
|
+
if (typeof payload.color !== "string" || payload.color.length > MAX_COLOR_LENGTH) return false;
|
|
11622
|
+
}
|
|
11623
|
+
return true;
|
|
11624
|
+
}
|
|
11625
|
+
function samePoint(a, b) {
|
|
11626
|
+
return Math.abs(a.x - b.x) < EPS && Math.abs(a.y - b.y) < EPS;
|
|
11627
|
+
}
|
|
11628
|
+
function toPathPresence(emission) {
|
|
11629
|
+
if (emission === null) return { kind: PATH_PRESENCE_KIND, cleared: true };
|
|
11630
|
+
const points = emission.waypoints.map((point) => ({ x: point.x, y: point.y }));
|
|
11631
|
+
const last = points[points.length - 1];
|
|
11632
|
+
const cursor = emission.cursor;
|
|
11633
|
+
if (cursor && (!last || !samePoint(cursor, last))) points.push({ x: cursor.x, y: cursor.y });
|
|
11634
|
+
const cumulativeFeet = [0];
|
|
11635
|
+
let total = 0;
|
|
11636
|
+
for (let i = 1; i < points.length; i++) {
|
|
11637
|
+
total += emission.segments[i - 1]?.feet ?? 0;
|
|
11638
|
+
cumulativeFeet.push(total);
|
|
11639
|
+
}
|
|
11640
|
+
return {
|
|
11641
|
+
kind: PATH_PRESENCE_KIND,
|
|
11642
|
+
points,
|
|
11643
|
+
segmentColors: resolveSegmentColors(cumulativeFeet, emission.rangeBands, emission.color),
|
|
11644
|
+
feet: emission.totalFeet,
|
|
11645
|
+
color: emission.color
|
|
11646
|
+
};
|
|
11647
|
+
}
|
|
11648
|
+
var DEFAULT_COLOR4 = "#FF5722";
|
|
11649
|
+
var RemotePathOverlay = class {
|
|
11650
|
+
color;
|
|
11651
|
+
overlay;
|
|
11652
|
+
constructor(host, options = {}) {
|
|
11653
|
+
this.color = options.color ?? DEFAULT_COLOR4;
|
|
11654
|
+
this.overlay = new LingerOverlay(
|
|
11655
|
+
host,
|
|
11656
|
+
options,
|
|
11657
|
+
(ctx, entry, alpha) => drawPath(ctx, entry, { alpha }),
|
|
11658
|
+
() => this.now()
|
|
11659
|
+
);
|
|
11660
|
+
}
|
|
11661
|
+
now() {
|
|
11662
|
+
return performance.now();
|
|
11663
|
+
}
|
|
11664
|
+
/**
|
|
11665
|
+
* Applies a presence payload from `sender` (any opaque per-sender key, e.g.
|
|
11666
|
+
* the envelope `from`). Non-path or malformed payloads are ignored and
|
|
11667
|
+
* reported as `false`, so hosts can feed every presence frame through.
|
|
11668
|
+
*/
|
|
11669
|
+
apply(sender, data) {
|
|
11670
|
+
if (this.overlay.disposed || !isPathPresence(data)) return false;
|
|
11671
|
+
if ("cleared" in data) {
|
|
11672
|
+
this.overlay.linger(sender);
|
|
11673
|
+
return true;
|
|
11674
|
+
}
|
|
11675
|
+
this.overlay.set(sender, {
|
|
11676
|
+
points: data.points,
|
|
11677
|
+
segmentColors: data.segmentColors,
|
|
11678
|
+
feet: data.feet,
|
|
11679
|
+
color: data.color ?? this.color
|
|
11680
|
+
});
|
|
11681
|
+
return true;
|
|
11682
|
+
}
|
|
11683
|
+
/** Removes a sender's path immediately (presence-leave/disconnect). */
|
|
11684
|
+
remove(sender) {
|
|
11685
|
+
this.overlay.remove(sender);
|
|
11686
|
+
}
|
|
11687
|
+
/** Removes every path immediately. */
|
|
11688
|
+
clear() {
|
|
11689
|
+
this.overlay.clear();
|
|
11690
|
+
}
|
|
11691
|
+
/** Number of senders with a visible (active or lingering) path. */
|
|
11692
|
+
get activeSenderCount() {
|
|
11693
|
+
return this.overlay.activeSenderCount;
|
|
11694
|
+
}
|
|
11695
|
+
/** Unregisters the overlay, cancels timers, stops animating. Idempotent. */
|
|
11696
|
+
dispose() {
|
|
11697
|
+
this.overlay.dispose();
|
|
11342
11698
|
}
|
|
11343
11699
|
};
|
|
11344
11700
|
|
|
11345
11701
|
// src/canvas/ping-input.ts
|
|
11346
11702
|
var DEFAULT_LONG_PRESS_MS = 600;
|
|
11347
11703
|
var DEFAULT_SLOP_PX = 8;
|
|
11348
|
-
var
|
|
11704
|
+
var DEFAULT_COLOR5 = "#ff3b30";
|
|
11349
11705
|
var DEFAULT_DURATION_MS2 = 1800;
|
|
11350
11706
|
var DEFAULT_RADIUS2 = 48;
|
|
11351
11707
|
var DEFAULT_MIN_INTERVAL_MS = 300;
|
|
@@ -11378,7 +11734,7 @@ var PingInput = class {
|
|
|
11378
11734
|
this.longPressEnabled = options.longPressEnabled ?? false;
|
|
11379
11735
|
this.longPressMs = options.longPressMs ?? DEFAULT_LONG_PRESS_MS;
|
|
11380
11736
|
this.slopPx = options.slopPx ?? DEFAULT_SLOP_PX;
|
|
11381
|
-
this.color = options.color ??
|
|
11737
|
+
this.color = options.color ?? DEFAULT_COLOR5;
|
|
11382
11738
|
this.durationMs = options.durationMs ?? DEFAULT_DURATION_MS2;
|
|
11383
11739
|
this.radius = options.radius ?? DEFAULT_RADIUS2;
|
|
11384
11740
|
this.minIntervalMs = options.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS;
|
|
@@ -11783,105 +12139,899 @@ var CameraAnimator = class {
|
|
|
11783
12139
|
this.rafId = null;
|
|
11784
12140
|
}
|
|
11785
12141
|
}
|
|
11786
|
-
emit(reason) {
|
|
11787
|
-
for (const listener of [...this.endListeners]) {
|
|
12142
|
+
emit(reason) {
|
|
12143
|
+
for (const listener of [...this.endListeners]) {
|
|
12144
|
+
try {
|
|
12145
|
+
listener(reason);
|
|
12146
|
+
} catch {
|
|
12147
|
+
}
|
|
12148
|
+
}
|
|
12149
|
+
}
|
|
12150
|
+
};
|
|
12151
|
+
|
|
12152
|
+
// src/canvas/focus-presence.ts
|
|
12153
|
+
var FOCUS_PRESENCE_KIND = "focus";
|
|
12154
|
+
var AUDIENCES = ["all", "players", "display"];
|
|
12155
|
+
function isPositiveFinite(value) {
|
|
12156
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
12157
|
+
}
|
|
12158
|
+
function isFiniteNumber2(value) {
|
|
12159
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
12160
|
+
}
|
|
12161
|
+
function isFocusPresence(data) {
|
|
12162
|
+
if (typeof data !== "object" || data === null) return false;
|
|
12163
|
+
const payload = data;
|
|
12164
|
+
if (payload.kind !== FOCUS_PRESENCE_KIND) return false;
|
|
12165
|
+
if (!isFiniteNumber2(payload.x) || !isFiniteNumber2(payload.y)) return false;
|
|
12166
|
+
if (!isPositiveFinite(payload.w) || !isPositiveFinite(payload.h)) return false;
|
|
12167
|
+
if (typeof payload.audience !== "string" || !AUDIENCES.some((a) => a === payload.audience)) {
|
|
12168
|
+
return false;
|
|
12169
|
+
}
|
|
12170
|
+
if (payload.color !== void 0 && typeof payload.color !== "string") return false;
|
|
12171
|
+
return true;
|
|
12172
|
+
}
|
|
12173
|
+
function toFocusPresence(view, audience, color) {
|
|
12174
|
+
return {
|
|
12175
|
+
kind: FOCUS_PRESENCE_KIND,
|
|
12176
|
+
x: view.x,
|
|
12177
|
+
y: view.y,
|
|
12178
|
+
w: view.w,
|
|
12179
|
+
h: view.h,
|
|
12180
|
+
audience,
|
|
12181
|
+
...color === void 0 ? {} : { color }
|
|
12182
|
+
};
|
|
12183
|
+
}
|
|
12184
|
+
|
|
12185
|
+
// src/canvas/remote-focus-receiver.ts
|
|
12186
|
+
function audienceIncludes(audience, role) {
|
|
12187
|
+
if (role === "dm") return false;
|
|
12188
|
+
if (audience === "all") return true;
|
|
12189
|
+
if (audience === "players") return role === "player";
|
|
12190
|
+
return role === "display";
|
|
12191
|
+
}
|
|
12192
|
+
var RemoteFocusReceiver = class {
|
|
12193
|
+
role;
|
|
12194
|
+
animator;
|
|
12195
|
+
animate;
|
|
12196
|
+
pulseColor;
|
|
12197
|
+
overlay;
|
|
12198
|
+
disposed = false;
|
|
12199
|
+
constructor(host, options) {
|
|
12200
|
+
this.role = options.role;
|
|
12201
|
+
this.animator = options.animator;
|
|
12202
|
+
this.animate = options.animate ?? true;
|
|
12203
|
+
this.pulseColor = options.pulseColor;
|
|
12204
|
+
this.overlay = options.pulse ?? true ? new RemotePingOverlay(host, {
|
|
12205
|
+
...options.pulseColor === void 0 ? {} : { color: options.pulseColor },
|
|
12206
|
+
...options.pulseDurationMs === void 0 ? {} : { durationMs: options.pulseDurationMs },
|
|
12207
|
+
...options.pulseRadius === void 0 ? {} : { radius: options.pulseRadius },
|
|
12208
|
+
maxPingsPerSender: 1
|
|
12209
|
+
}) : null;
|
|
12210
|
+
}
|
|
12211
|
+
/**
|
|
12212
|
+
* Applies a presence payload from `sender`. Returns `false` for payloads
|
|
12213
|
+
* that are not focus frames, or are addressed to a different role, so hosts
|
|
12214
|
+
* can feed every presence frame through without disturbing other handlers.
|
|
12215
|
+
*/
|
|
12216
|
+
apply(from, data) {
|
|
12217
|
+
if (this.disposed || !isFocusPresence(data)) return false;
|
|
12218
|
+
if (!audienceIncludes(data.audience, this.role)) return false;
|
|
12219
|
+
const view = { x: data.x, y: data.y, w: data.w, h: data.h };
|
|
12220
|
+
if (this.animate) {
|
|
12221
|
+
this.animator.animateTo(view);
|
|
12222
|
+
} else {
|
|
12223
|
+
this.animator.jumpTo(view);
|
|
12224
|
+
}
|
|
12225
|
+
this.overlay?.apply(from, {
|
|
12226
|
+
kind: "ping",
|
|
12227
|
+
x: view.x + view.w / 2,
|
|
12228
|
+
y: view.y + view.h / 2,
|
|
12229
|
+
color: data.color ?? this.pulseColor
|
|
12230
|
+
});
|
|
12231
|
+
return true;
|
|
12232
|
+
}
|
|
12233
|
+
/** Idempotent. Does NOT dispose the animator — the host owns that. */
|
|
12234
|
+
dispose() {
|
|
12235
|
+
if (this.disposed) return;
|
|
12236
|
+
this.disposed = true;
|
|
12237
|
+
this.overlay?.dispose();
|
|
12238
|
+
}
|
|
12239
|
+
};
|
|
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;
|
|
11788
13008
|
try {
|
|
11789
|
-
|
|
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();
|
|
11790
13022
|
} catch {
|
|
11791
13023
|
}
|
|
13024
|
+
for (const unsub of unsubscribers) {
|
|
13025
|
+
try {
|
|
13026
|
+
unsub();
|
|
13027
|
+
} catch {
|
|
13028
|
+
}
|
|
13029
|
+
}
|
|
13030
|
+
unsubscribers.length = 0;
|
|
11792
13031
|
}
|
|
11793
|
-
}
|
|
11794
|
-
};
|
|
11795
|
-
|
|
11796
|
-
// src/canvas/focus-presence.ts
|
|
11797
|
-
var FOCUS_PRESENCE_KIND = "focus";
|
|
11798
|
-
var AUDIENCES = ["all", "players", "display"];
|
|
11799
|
-
function isPositiveFinite(value) {
|
|
11800
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
11801
|
-
}
|
|
11802
|
-
function isFiniteNumber2(value) {
|
|
11803
|
-
return typeof value === "number" && Number.isFinite(value);
|
|
11804
|
-
}
|
|
11805
|
-
function isFocusPresence(data) {
|
|
11806
|
-
if (typeof data !== "object" || data === null) return false;
|
|
11807
|
-
const payload = data;
|
|
11808
|
-
if (payload.kind !== FOCUS_PRESENCE_KIND) return false;
|
|
11809
|
-
if (!isFiniteNumber2(payload.x) || !isFiniteNumber2(payload.y)) return false;
|
|
11810
|
-
if (!isPositiveFinite(payload.w) || !isPositiveFinite(payload.h)) return false;
|
|
11811
|
-
if (typeof payload.audience !== "string" || !AUDIENCES.some((a) => a === payload.audience)) {
|
|
11812
|
-
return false;
|
|
11813
|
-
}
|
|
11814
|
-
if (payload.color !== void 0 && typeof payload.color !== "string") return false;
|
|
11815
|
-
return true;
|
|
11816
|
-
}
|
|
11817
|
-
function toFocusPresence(view, audience, color) {
|
|
11818
|
-
return {
|
|
11819
|
-
kind: FOCUS_PRESENCE_KIND,
|
|
11820
|
-
x: view.x,
|
|
11821
|
-
y: view.y,
|
|
11822
|
-
w: view.w,
|
|
11823
|
-
h: view.h,
|
|
11824
|
-
audience,
|
|
11825
|
-
...color === void 0 ? {} : { color }
|
|
11826
13032
|
};
|
|
11827
13033
|
}
|
|
11828
13034
|
|
|
11829
|
-
// src/canvas/remote-focus-receiver.ts
|
|
11830
|
-
function audienceIncludes(audience, role) {
|
|
11831
|
-
if (role === "dm") return false;
|
|
11832
|
-
if (audience === "all") return true;
|
|
11833
|
-
if (audience === "players") return role === "player";
|
|
11834
|
-
return role === "display";
|
|
11835
|
-
}
|
|
11836
|
-
var RemoteFocusReceiver = class {
|
|
11837
|
-
role;
|
|
11838
|
-
animator;
|
|
11839
|
-
animate;
|
|
11840
|
-
pulseColor;
|
|
11841
|
-
overlay;
|
|
11842
|
-
disposed = false;
|
|
11843
|
-
constructor(host, options) {
|
|
11844
|
-
this.role = options.role;
|
|
11845
|
-
this.animator = options.animator;
|
|
11846
|
-
this.animate = options.animate ?? true;
|
|
11847
|
-
this.pulseColor = options.pulseColor;
|
|
11848
|
-
this.overlay = options.pulse ?? true ? new RemotePingOverlay(host, {
|
|
11849
|
-
...options.pulseColor === void 0 ? {} : { color: options.pulseColor },
|
|
11850
|
-
...options.pulseDurationMs === void 0 ? {} : { durationMs: options.pulseDurationMs },
|
|
11851
|
-
...options.pulseRadius === void 0 ? {} : { radius: options.pulseRadius },
|
|
11852
|
-
maxPingsPerSender: 1
|
|
11853
|
-
}) : null;
|
|
11854
|
-
}
|
|
11855
|
-
/**
|
|
11856
|
-
* Applies a presence payload from `sender`. Returns `false` for payloads
|
|
11857
|
-
* that are not focus frames, or are addressed to a different role, so hosts
|
|
11858
|
-
* can feed every presence frame through without disturbing other handlers.
|
|
11859
|
-
*/
|
|
11860
|
-
apply(from, data) {
|
|
11861
|
-
if (this.disposed || !isFocusPresence(data)) return false;
|
|
11862
|
-
if (!audienceIncludes(data.audience, this.role)) return false;
|
|
11863
|
-
const view = { x: data.x, y: data.y, w: data.w, h: data.h };
|
|
11864
|
-
if (this.animate) {
|
|
11865
|
-
this.animator.animateTo(view);
|
|
11866
|
-
} else {
|
|
11867
|
-
this.animator.jumpTo(view);
|
|
11868
|
-
}
|
|
11869
|
-
this.overlay?.apply(from, {
|
|
11870
|
-
kind: "ping",
|
|
11871
|
-
x: view.x + view.w / 2,
|
|
11872
|
-
y: view.y + view.h / 2,
|
|
11873
|
-
color: data.color ?? this.pulseColor
|
|
11874
|
-
});
|
|
11875
|
-
return true;
|
|
11876
|
-
}
|
|
11877
|
-
/** Idempotent. Does NOT dispose the animator — the host owns that. */
|
|
11878
|
-
dispose() {
|
|
11879
|
-
if (this.disposed) return;
|
|
11880
|
-
this.disposed = true;
|
|
11881
|
-
this.overlay?.dispose();
|
|
11882
|
-
}
|
|
11883
|
-
};
|
|
11884
|
-
|
|
11885
13035
|
// src/tools/hand-tool.ts
|
|
11886
13036
|
var HandTool = class {
|
|
11887
13037
|
name = "hand";
|
|
@@ -12714,7 +13864,8 @@ var SelectTool = class {
|
|
|
12714
13864
|
} else if (!ctx.smartGuides && ctx.gridType && "size" in el) {
|
|
12715
13865
|
const centerX = el.position.x + el.size.w / 2 + adjDx;
|
|
12716
13866
|
const centerY = el.position.y + el.size.h / 2 + adjDy;
|
|
12717
|
-
const
|
|
13867
|
+
const footprint = footprintFromSize(el.size, ctx.gridSize ?? 0);
|
|
13868
|
+
const snappedCenter = snapFootprintCenter({ x: centerX, y: centerY }, footprint, ctx);
|
|
12718
13869
|
ctx.store.update(id, {
|
|
12719
13870
|
position: {
|
|
12720
13871
|
x: snappedCenter.x - el.size.w / 2,
|
|
@@ -13292,15 +14443,15 @@ var ShapeTool = class {
|
|
|
13292
14443
|
}
|
|
13293
14444
|
onActivate(_ctx) {
|
|
13294
14445
|
if (typeof window !== "undefined") {
|
|
13295
|
-
window.addEventListener("keydown", this.
|
|
13296
|
-
window.addEventListener("keyup", this.
|
|
14446
|
+
window.addEventListener("keydown", this.trackShiftKeyDown);
|
|
14447
|
+
window.addEventListener("keyup", this.trackShiftKeyUp);
|
|
13297
14448
|
}
|
|
13298
14449
|
}
|
|
13299
14450
|
onDeactivate(_ctx) {
|
|
13300
14451
|
this.shiftHeld = false;
|
|
13301
14452
|
if (typeof window !== "undefined") {
|
|
13302
|
-
window.removeEventListener("keydown", this.
|
|
13303
|
-
window.removeEventListener("keyup", this.
|
|
14453
|
+
window.removeEventListener("keydown", this.trackShiftKeyDown);
|
|
14454
|
+
window.removeEventListener("keyup", this.trackShiftKeyUp);
|
|
13304
14455
|
}
|
|
13305
14456
|
}
|
|
13306
14457
|
onPointerDown(state, ctx) {
|
|
@@ -13393,10 +14544,13 @@ var ShapeTool = class {
|
|
|
13393
14544
|
snap(point, ctx) {
|
|
13394
14545
|
return smartSnap(point, ctx);
|
|
13395
14546
|
}
|
|
13396
|
-
onKeyDown
|
|
14547
|
+
// Deliberately NOT migrated to `Tool.onKeyDown`: that hook is gated by
|
|
14548
|
+
// editable-target and keyboard-scope checks, and a Shift release must be
|
|
14549
|
+
// observed wherever it happens or the constraint sticks after the key is up.
|
|
14550
|
+
trackShiftKeyDown = (e) => {
|
|
13397
14551
|
if (e.key === "Shift") this.shiftHeld = true;
|
|
13398
14552
|
};
|
|
13399
|
-
|
|
14553
|
+
trackShiftKeyUp = (e) => {
|
|
13400
14554
|
if (e.key === "Shift") this.shiftHeld = false;
|
|
13401
14555
|
};
|
|
13402
14556
|
};
|
|
@@ -13540,6 +14694,353 @@ var MeasureTool = class {
|
|
|
13540
14694
|
}
|
|
13541
14695
|
};
|
|
13542
14696
|
|
|
14697
|
+
// src/tools/path-tool.ts
|
|
14698
|
+
var EPS2 = 1e-6;
|
|
14699
|
+
var DEFAULT_COMMIT_TAP_RADIUS_PX = 12;
|
|
14700
|
+
function samePoint3(a, b) {
|
|
14701
|
+
return Math.abs(a.x - b.x) < EPS2 && Math.abs(a.y - b.y) < EPS2;
|
|
14702
|
+
}
|
|
14703
|
+
var PathTool = class {
|
|
14704
|
+
name = "path";
|
|
14705
|
+
feetPerCell;
|
|
14706
|
+
color;
|
|
14707
|
+
diagonalRule;
|
|
14708
|
+
footprintOption;
|
|
14709
|
+
rangeBands;
|
|
14710
|
+
commitTapRadiusPx;
|
|
14711
|
+
resolveStart;
|
|
14712
|
+
waypoints = [];
|
|
14713
|
+
cursor = null;
|
|
14714
|
+
anchorKey;
|
|
14715
|
+
footprint = 1;
|
|
14716
|
+
pointerDown = false;
|
|
14717
|
+
commitOnUp = false;
|
|
14718
|
+
// Grid state is captured at the opening pointer-down so a path measured on
|
|
14719
|
+
// one grid keeps that metric even if the host reconfigures mid-gesture.
|
|
14720
|
+
gridSize = 0;
|
|
14721
|
+
gridType;
|
|
14722
|
+
hexOrientation;
|
|
14723
|
+
snapEnabled = false;
|
|
14724
|
+
optionListeners = /* @__PURE__ */ new Set();
|
|
14725
|
+
pathListeners = /* @__PURE__ */ new Set();
|
|
14726
|
+
commitListeners = /* @__PURE__ */ new Set();
|
|
14727
|
+
emissionRafId = null;
|
|
14728
|
+
constructor(options = {}) {
|
|
14729
|
+
this.feetPerCell = options.feetPerCell ?? 5;
|
|
14730
|
+
this.color = options.color ?? "#FF5722";
|
|
14731
|
+
this.diagonalRule = options.diagonalRule ?? "euclidean";
|
|
14732
|
+
this.footprintOption = options.footprint ?? 1;
|
|
14733
|
+
this.rangeBands = options.rangeBands ?? [];
|
|
14734
|
+
this.commitTapRadiusPx = options.commitTapRadiusPx ?? DEFAULT_COMMIT_TAP_RADIUS_PX;
|
|
14735
|
+
this.resolveStart = options.resolveStart;
|
|
14736
|
+
}
|
|
14737
|
+
getOptions() {
|
|
14738
|
+
return {
|
|
14739
|
+
feetPerCell: this.feetPerCell,
|
|
14740
|
+
color: this.color,
|
|
14741
|
+
diagonalRule: this.diagonalRule,
|
|
14742
|
+
footprint: this.footprintOption,
|
|
14743
|
+
rangeBands: this.rangeBands,
|
|
14744
|
+
commitTapRadiusPx: this.commitTapRadiusPx,
|
|
14745
|
+
resolveStart: this.resolveStart
|
|
14746
|
+
};
|
|
14747
|
+
}
|
|
14748
|
+
setOptions(options) {
|
|
14749
|
+
if (options.feetPerCell !== void 0) this.feetPerCell = options.feetPerCell;
|
|
14750
|
+
if (options.color !== void 0) this.color = options.color;
|
|
14751
|
+
if (options.diagonalRule !== void 0) this.diagonalRule = options.diagonalRule;
|
|
14752
|
+
if (options.footprint !== void 0) this.footprintOption = options.footprint;
|
|
14753
|
+
if (options.rangeBands !== void 0) this.rangeBands = options.rangeBands;
|
|
14754
|
+
if (options.commitTapRadiusPx !== void 0) {
|
|
14755
|
+
this.commitTapRadiusPx = options.commitTapRadiusPx;
|
|
14756
|
+
}
|
|
14757
|
+
if (options.resolveStart !== void 0) this.resolveStart = options.resolveStart;
|
|
14758
|
+
this.notifyOptionsChange();
|
|
14759
|
+
}
|
|
14760
|
+
onOptionsChange(listener) {
|
|
14761
|
+
this.optionListeners.add(listener);
|
|
14762
|
+
return () => this.optionListeners.delete(listener);
|
|
14763
|
+
}
|
|
14764
|
+
/**
|
|
14765
|
+
* Subscribes to raf-coalesced path snapshots. While a path is open,
|
|
14766
|
+
* listeners receive at most one snapshot per animation frame carrying the
|
|
14767
|
+
* latest state; `null` is delivered synchronously when the path closes
|
|
14768
|
+
* (commit, cancel, or deactivate).
|
|
14769
|
+
*/
|
|
14770
|
+
onPath(listener) {
|
|
14771
|
+
this.pathListeners.add(listener);
|
|
14772
|
+
return () => this.pathListeners.delete(listener);
|
|
14773
|
+
}
|
|
14774
|
+
/**
|
|
14775
|
+
* Subscribes to finished paths. The emission carries `cursor: null` and the
|
|
14776
|
+
* final waypoints; applying the move (and recording history for it) is the
|
|
14777
|
+
* host's job — the tool has already forgotten the path by then.
|
|
14778
|
+
*/
|
|
14779
|
+
onCommit(listener) {
|
|
14780
|
+
this.commitListeners.add(listener);
|
|
14781
|
+
return () => this.commitListeners.delete(listener);
|
|
14782
|
+
}
|
|
14783
|
+
get isOpen() {
|
|
14784
|
+
return this.waypoints.length > 0;
|
|
14785
|
+
}
|
|
14786
|
+
/** Synchronous snapshot of the open path; `null` when no path is open. */
|
|
14787
|
+
getEmission() {
|
|
14788
|
+
if (!this.isOpen) return null;
|
|
14789
|
+
return this.buildEmission(this.cursor);
|
|
14790
|
+
}
|
|
14791
|
+
onPointerDown(state, ctx) {
|
|
14792
|
+
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
14793
|
+
if (!this.isOpen) {
|
|
14794
|
+
const anchor = this.resolveStart ? this.resolveStart(world, ctx) : { origin: world };
|
|
14795
|
+
if (!anchor) return;
|
|
14796
|
+
this.gridSize = ctx.gridSize ?? 0;
|
|
14797
|
+
this.gridType = ctx.gridType;
|
|
14798
|
+
this.hexOrientation = ctx.hexOrientation;
|
|
14799
|
+
this.snapEnabled = ctx.snapToGrid === true;
|
|
14800
|
+
this.footprint = anchor.footprint ?? this.footprintOption;
|
|
14801
|
+
const origin = this.snap(anchor.origin);
|
|
14802
|
+
this.waypoints = [origin];
|
|
14803
|
+
this.cursor = { ...origin };
|
|
14804
|
+
this.anchorKey = anchor.anchorKey;
|
|
14805
|
+
this.pointerDown = true;
|
|
14806
|
+
this.scheduleEmission();
|
|
14807
|
+
ctx.requestRender();
|
|
14808
|
+
return;
|
|
14809
|
+
}
|
|
14810
|
+
const point = this.snap(world);
|
|
14811
|
+
const last = this.lastWaypoint();
|
|
14812
|
+
if (last && this.withinCommitRadius(point, last, ctx)) {
|
|
14813
|
+
this.commitOnUp = true;
|
|
14814
|
+
this.cursor = { ...last };
|
|
14815
|
+
} else {
|
|
14816
|
+
this.cursor = point;
|
|
14817
|
+
}
|
|
14818
|
+
this.pointerDown = true;
|
|
14819
|
+
this.scheduleEmission();
|
|
14820
|
+
ctx.requestRender();
|
|
14821
|
+
}
|
|
14822
|
+
onPointerMove(state, ctx) {
|
|
14823
|
+
if (!this.isOpen) return;
|
|
14824
|
+
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
14825
|
+
const point = this.snap(world);
|
|
14826
|
+
const last = this.lastWaypoint();
|
|
14827
|
+
if (this.commitOnUp && last && this.withinCommitRadius(point, last, ctx)) {
|
|
14828
|
+
this.cursor = { ...last };
|
|
14829
|
+
} else {
|
|
14830
|
+
this.commitOnUp = false;
|
|
14831
|
+
this.cursor = point;
|
|
14832
|
+
}
|
|
14833
|
+
this.scheduleEmission();
|
|
14834
|
+
ctx.requestRender();
|
|
14835
|
+
}
|
|
14836
|
+
onPointerUp(_state, ctx) {
|
|
14837
|
+
if (!this.isOpen) return;
|
|
14838
|
+
this.pointerDown = false;
|
|
14839
|
+
if (this.commitOnUp) {
|
|
14840
|
+
this.commit(ctx);
|
|
14841
|
+
return;
|
|
14842
|
+
}
|
|
14843
|
+
const last = this.lastWaypoint();
|
|
14844
|
+
if (this.cursor && last && !samePoint3(this.cursor, last)) {
|
|
14845
|
+
this.waypoints.push({ ...this.cursor });
|
|
14846
|
+
}
|
|
14847
|
+
this.scheduleEmission();
|
|
14848
|
+
ctx.requestRender();
|
|
14849
|
+
}
|
|
14850
|
+
onHover(state, ctx) {
|
|
14851
|
+
if (!this.isOpen || this.pointerDown) return;
|
|
14852
|
+
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
14853
|
+
this.cursor = this.snap(world);
|
|
14854
|
+
this.scheduleEmission();
|
|
14855
|
+
ctx.requestRender();
|
|
14856
|
+
}
|
|
14857
|
+
/** A takeover (second pointer, platform cancel) abandons the path outright. */
|
|
14858
|
+
onPointerCancel(_state, ctx) {
|
|
14859
|
+
this.cancel(ctx);
|
|
14860
|
+
}
|
|
14861
|
+
onDeactivate(ctx) {
|
|
14862
|
+
this.cancel(ctx);
|
|
14863
|
+
}
|
|
14864
|
+
onKeyDown(event, ctx) {
|
|
14865
|
+
if (!this.isOpen) return false;
|
|
14866
|
+
if (event.key === "Enter") {
|
|
14867
|
+
this.commit(ctx);
|
|
14868
|
+
return true;
|
|
14869
|
+
}
|
|
14870
|
+
if (event.key === "Escape") {
|
|
14871
|
+
this.cancel(ctx);
|
|
14872
|
+
return true;
|
|
14873
|
+
}
|
|
14874
|
+
return false;
|
|
14875
|
+
}
|
|
14876
|
+
renderOverlay(ctx) {
|
|
14877
|
+
if (!this.isOpen) return;
|
|
14878
|
+
const { points, cumulative, total } = this.measure(this.cursor);
|
|
14879
|
+
const cumulativeFeet = cumulative.map((cells) => cells * this.feetPerCell);
|
|
14880
|
+
drawPath(ctx, {
|
|
14881
|
+
points,
|
|
14882
|
+
segmentColors: resolveSegmentColors(cumulativeFeet, this.rangeBands, this.color),
|
|
14883
|
+
color: this.color,
|
|
14884
|
+
feet: total * this.feetPerCell
|
|
14885
|
+
});
|
|
14886
|
+
}
|
|
14887
|
+
lastWaypoint() {
|
|
14888
|
+
return this.waypoints[this.waypoints.length - 1];
|
|
14889
|
+
}
|
|
14890
|
+
/**
|
|
14891
|
+
* Is `point` close enough to `last` to mean "finish here"? EXACT match always
|
|
14892
|
+
* qualifies. The screen-space tolerance is added ONLY when this path has no
|
|
14893
|
+
* active snapping grid: on a snapping grid (`gridType === 'square'`, or
|
|
14894
|
+
* `'hex'` with a captured orientation, and `gridSize > 0`) the adjacent
|
|
14895
|
+
* cell/hex centre can be as little as one grid cell away, and a radius
|
|
14896
|
+
* computed from CSS pixels has no relationship to that distance — at a
|
|
14897
|
+
* sufficiently zoomed-out camera the radius would swallow a whole neighbour
|
|
14898
|
+
* cell and turn a deliberate next-waypoint tap into an unwanted commit.
|
|
14899
|
+
* Snapping already makes a fingertip tap land exactly on the last waypoint,
|
|
14900
|
+
* so the tolerance is unnecessary there; it exists for the gridless and
|
|
14901
|
+
* `snapPoint`/identity paths, where a fingertip can never land on an exact
|
|
14902
|
+
* world point.
|
|
14903
|
+
*/
|
|
14904
|
+
withinCommitRadius(point, last, ctx) {
|
|
14905
|
+
if (samePoint3(point, last)) return true;
|
|
14906
|
+
if (this.hasSnappingGrid()) return false;
|
|
14907
|
+
const zoom = ctx.camera.zoom;
|
|
14908
|
+
if (!(zoom > 0)) return false;
|
|
14909
|
+
const radius = this.commitTapRadiusPx / zoom;
|
|
14910
|
+
if (radius <= 0) return false;
|
|
14911
|
+
const dx = point.x - last.x;
|
|
14912
|
+
const dy = point.y - last.y;
|
|
14913
|
+
return dx * dx + dy * dy <= radius * radius;
|
|
14914
|
+
}
|
|
14915
|
+
/**
|
|
14916
|
+
* True when this path's captured grid state snaps every waypoint onto a
|
|
14917
|
+
* cell/hex centre: a `square` grid, or a `hex` grid with a captured
|
|
14918
|
+
* orientation, both with a usable `gridSize`. Mirrors the unconditional
|
|
14919
|
+
* branches of `snap` below — NOT the `snapToGrid`-gated fallback, which
|
|
14920
|
+
* leaves waypoints unsnapped when the user turns snapping off.
|
|
14921
|
+
*/
|
|
14922
|
+
hasSnappingGrid() {
|
|
14923
|
+
if (!(this.gridSize > 0)) return false;
|
|
14924
|
+
if (this.gridType === "square") return true;
|
|
14925
|
+
return this.gridType === "hex" && this.hexOrientation !== void 0;
|
|
14926
|
+
}
|
|
14927
|
+
/**
|
|
14928
|
+
* Snapping to cell or hex centres is UNCONDITIONAL for a `square` grid, or a
|
|
14929
|
+
* `hex` grid WITH a captured orientation: a movement path measures in cells,
|
|
14930
|
+
* so an unsnapped waypoint would report a distance the grid does not agree
|
|
14931
|
+
* with. A `hex` grid WITHOUT an orientation falls through to the
|
|
14932
|
+
* `snapToGrid`-gated `snapPoint` (intersection) branch below, same as the
|
|
14933
|
+
* gridType-less case — unlike `smartSnap`, which is off entirely when the
|
|
14934
|
+
* user turns snapping off.
|
|
14935
|
+
*/
|
|
14936
|
+
snap(point) {
|
|
14937
|
+
if (this.gridSize <= 0) return point;
|
|
14938
|
+
if (this.gridType === "hex" && this.hexOrientation) {
|
|
14939
|
+
return snapToHexCenter(point, this.gridSize, this.hexOrientation);
|
|
14940
|
+
}
|
|
14941
|
+
if (this.gridType === "square") {
|
|
14942
|
+
return snapToCellCenter(point, this.gridSize, this.footprint);
|
|
14943
|
+
}
|
|
14944
|
+
if (this.snapEnabled) {
|
|
14945
|
+
return snapPoint(point, this.gridSize);
|
|
14946
|
+
}
|
|
14947
|
+
return point;
|
|
14948
|
+
}
|
|
14949
|
+
/** The measured polyline: waypoints, plus the cursor when it adds a leg. */
|
|
14950
|
+
measure(cursor) {
|
|
14951
|
+
const points = this.waypoints.map((p) => ({ ...p }));
|
|
14952
|
+
const last = points[points.length - 1];
|
|
14953
|
+
if (cursor && (!last || !samePoint3(cursor, last))) points.push({ ...cursor });
|
|
14954
|
+
const { total, cumulative } = pathDistanceCells(points, {
|
|
14955
|
+
gridSize: this.gridSize,
|
|
14956
|
+
gridType: this.gridType,
|
|
14957
|
+
hexOrientation: this.hexOrientation,
|
|
14958
|
+
diagonalRule: this.diagonalRule
|
|
14959
|
+
});
|
|
14960
|
+
return { points, cumulative, total };
|
|
14961
|
+
}
|
|
14962
|
+
buildEmission(cursor) {
|
|
14963
|
+
const { cumulative, total } = this.measure(cursor);
|
|
14964
|
+
const segments = [];
|
|
14965
|
+
for (let i = 1; i < cumulative.length; i++) {
|
|
14966
|
+
const cells = (cumulative[i] ?? 0) - (cumulative[i - 1] ?? 0);
|
|
14967
|
+
segments.push({ cells, feet: cells * this.feetPerCell });
|
|
14968
|
+
}
|
|
14969
|
+
return {
|
|
14970
|
+
anchorKey: this.anchorKey,
|
|
14971
|
+
waypoints: this.waypoints.map((p) => ({ ...p })),
|
|
14972
|
+
cursor: cursor ? { ...cursor } : null,
|
|
14973
|
+
segments,
|
|
14974
|
+
totalCells: total,
|
|
14975
|
+
totalFeet: total * this.feetPerCell,
|
|
14976
|
+
color: this.color,
|
|
14977
|
+
rangeBands: this.rangeBands
|
|
14978
|
+
};
|
|
14979
|
+
}
|
|
14980
|
+
commit(ctx) {
|
|
14981
|
+
if (this.waypoints.length < 2) {
|
|
14982
|
+
this.cancel(ctx);
|
|
14983
|
+
return;
|
|
14984
|
+
}
|
|
14985
|
+
const emission = this.buildEmission(null);
|
|
14986
|
+
this.reset();
|
|
14987
|
+
this.emitCommit(emission);
|
|
14988
|
+
this.emitClear();
|
|
14989
|
+
ctx.requestRender();
|
|
14990
|
+
}
|
|
14991
|
+
cancel(ctx) {
|
|
14992
|
+
if (!this.isOpen) return;
|
|
14993
|
+
this.reset();
|
|
14994
|
+
this.emitClear();
|
|
14995
|
+
ctx.requestRender();
|
|
14996
|
+
}
|
|
14997
|
+
reset() {
|
|
14998
|
+
this.waypoints = [];
|
|
14999
|
+
this.cursor = null;
|
|
15000
|
+
this.anchorKey = void 0;
|
|
15001
|
+
this.footprint = this.footprintOption;
|
|
15002
|
+
this.pointerDown = false;
|
|
15003
|
+
this.commitOnUp = false;
|
|
15004
|
+
}
|
|
15005
|
+
notifyOptionsChange() {
|
|
15006
|
+
for (const listener of this.optionListeners) listener();
|
|
15007
|
+
}
|
|
15008
|
+
scheduleEmission() {
|
|
15009
|
+
if (this.pathListeners.size === 0) return;
|
|
15010
|
+
if (this.emissionRafId !== null) return;
|
|
15011
|
+
this.emissionRafId = requestAnimationFrame(() => {
|
|
15012
|
+
this.emissionRafId = null;
|
|
15013
|
+
const emission = this.getEmission();
|
|
15014
|
+
if (!emission) return;
|
|
15015
|
+
this.emitPath(emission);
|
|
15016
|
+
});
|
|
15017
|
+
}
|
|
15018
|
+
emitClear() {
|
|
15019
|
+
if (this.emissionRafId !== null) {
|
|
15020
|
+
cancelAnimationFrame(this.emissionRafId);
|
|
15021
|
+
this.emissionRafId = null;
|
|
15022
|
+
}
|
|
15023
|
+
if (this.pathListeners.size === 0) return;
|
|
15024
|
+
this.emitPath(null);
|
|
15025
|
+
}
|
|
15026
|
+
emitPath(emission) {
|
|
15027
|
+
for (const listener of this.pathListeners) {
|
|
15028
|
+
try {
|
|
15029
|
+
listener(emission);
|
|
15030
|
+
} catch {
|
|
15031
|
+
}
|
|
15032
|
+
}
|
|
15033
|
+
}
|
|
15034
|
+
emitCommit(emission) {
|
|
15035
|
+
for (const listener of this.commitListeners) {
|
|
15036
|
+
try {
|
|
15037
|
+
listener(emission);
|
|
15038
|
+
} catch {
|
|
15039
|
+
}
|
|
15040
|
+
}
|
|
15041
|
+
}
|
|
15042
|
+
};
|
|
15043
|
+
|
|
13543
15044
|
// src/tools/template-tool.ts
|
|
13544
15045
|
var MIN_RECT_WIDTH = 20;
|
|
13545
15046
|
function defaultRectWidth(radius, scaleUnit) {
|
|
@@ -13833,7 +15334,7 @@ var TemplateTool = class {
|
|
|
13833
15334
|
};
|
|
13834
15335
|
|
|
13835
15336
|
// src/tools/laser-tool.ts
|
|
13836
|
-
var
|
|
15337
|
+
var DEFAULT_COLOR6 = "#ff3b30";
|
|
13837
15338
|
var DEFAULT_WIDTH3 = 4;
|
|
13838
15339
|
var DEFAULT_FADE_MS3 = 1200;
|
|
13839
15340
|
var LaserTool = class {
|
|
@@ -13849,7 +15350,7 @@ var LaserTool = class {
|
|
|
13849
15350
|
pendingEmission = [];
|
|
13850
15351
|
constructor(options = {}) {
|
|
13851
15352
|
this.name = options.name ?? "laser";
|
|
13852
|
-
this.color = options.color ??
|
|
15353
|
+
this.color = options.color ?? DEFAULT_COLOR6;
|
|
13853
15354
|
this.width = options.width ?? DEFAULT_WIDTH3;
|
|
13854
15355
|
this.fadeMs = options.fadeMs ?? DEFAULT_FADE_MS3;
|
|
13855
15356
|
}
|
|
@@ -13977,7 +15478,7 @@ var LaserTool = class {
|
|
|
13977
15478
|
};
|
|
13978
15479
|
|
|
13979
15480
|
// src/tools/ping-tool.ts
|
|
13980
|
-
var
|
|
15481
|
+
var DEFAULT_COLOR7 = "#ff3b30";
|
|
13981
15482
|
var DEFAULT_DURATION_MS4 = 1800;
|
|
13982
15483
|
var DEFAULT_RADIUS4 = 48;
|
|
13983
15484
|
var DEFAULT_MIN_INTERVAL_MS2 = 300;
|
|
@@ -13994,7 +15495,7 @@ var PingTool = class {
|
|
|
13994
15495
|
pingListeners = /* @__PURE__ */ new Set();
|
|
13995
15496
|
constructor(options = {}) {
|
|
13996
15497
|
this.name = options.name ?? "ping";
|
|
13997
|
-
this.color = options.color ??
|
|
15498
|
+
this.color = options.color ?? DEFAULT_COLOR7;
|
|
13998
15499
|
this.durationMs = options.durationMs ?? DEFAULT_DURATION_MS4;
|
|
13999
15500
|
this.radius = options.radius ?? DEFAULT_RADIUS4;
|
|
14000
15501
|
this.minIntervalMs = options.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS2;
|
|
@@ -14097,9 +15598,11 @@ var PingTool = class {
|
|
|
14097
15598
|
};
|
|
14098
15599
|
|
|
14099
15600
|
// src/index.ts
|
|
14100
|
-
var VERSION = "0.
|
|
15601
|
+
var VERSION = "0.65.0";
|
|
14101
15602
|
// Annotate the CommonJS export names for ESM import in node:
|
|
14102
15603
|
0 && (module.exports = {
|
|
15604
|
+
AWARENESS_MAX_SELECTION,
|
|
15605
|
+
AWARENESS_PRESENCE_KIND,
|
|
14103
15606
|
ArrowTool,
|
|
14104
15607
|
AutoSave,
|
|
14105
15608
|
Camera,
|
|
@@ -14118,20 +15621,29 @@ var VERSION = "0.63.0";
|
|
|
14118
15621
|
LASER_TRAIL_PRESENCE_KIND,
|
|
14119
15622
|
LaserTool,
|
|
14120
15623
|
LayerManager,
|
|
15624
|
+
LocalAwareness,
|
|
14121
15625
|
LocalStorageAdapter,
|
|
14122
15626
|
MEASURE_PRESENCE_KIND,
|
|
14123
15627
|
MeasureTool,
|
|
14124
15628
|
MemoryAdapter,
|
|
14125
15629
|
MinimapController,
|
|
14126
15630
|
NoteTool,
|
|
15631
|
+
PATH_PRESENCE_KIND,
|
|
15632
|
+
PATH_PRESENCE_MAX_POINTS,
|
|
15633
|
+
PEER_COLORS,
|
|
14127
15634
|
PING_PRESENCE_KIND,
|
|
15635
|
+
PathTool,
|
|
15636
|
+
PeerRoster,
|
|
14128
15637
|
PencilTool,
|
|
14129
15638
|
PingInput,
|
|
14130
15639
|
PingTool,
|
|
15640
|
+
RemoteCursorOverlay,
|
|
14131
15641
|
RemoteFocusReceiver,
|
|
14132
15642
|
RemoteLaserOverlay,
|
|
14133
15643
|
RemoteMeasureOverlay,
|
|
15644
|
+
RemotePathOverlay,
|
|
14134
15645
|
RemotePingOverlay,
|
|
15646
|
+
RemoteSelectionOverlay,
|
|
14135
15647
|
SelectTool,
|
|
14136
15648
|
ShapeTool,
|
|
14137
15649
|
TemplateTool,
|
|
@@ -14140,6 +15652,7 @@ var VERSION = "0.63.0";
|
|
|
14140
15652
|
VERSION,
|
|
14141
15653
|
Viewport,
|
|
14142
15654
|
applyCameraView,
|
|
15655
|
+
attachAwareness,
|
|
14143
15656
|
boundsIntersect,
|
|
14144
15657
|
cameraOriginForView,
|
|
14145
15658
|
captureCameraView,
|
|
@@ -14153,11 +15666,13 @@ var VERSION = "0.63.0";
|
|
|
14153
15666
|
createStroke,
|
|
14154
15667
|
createTemplate,
|
|
14155
15668
|
createText,
|
|
15669
|
+
defaultPeerColor,
|
|
14156
15670
|
drawHexPath,
|
|
14157
15671
|
elementRectsEqual,
|
|
14158
15672
|
exportImage,
|
|
14159
15673
|
exportSvg,
|
|
14160
15674
|
fitZoomForView,
|
|
15675
|
+
footprintFromSize,
|
|
14161
15676
|
getActiveFormats,
|
|
14162
15677
|
getArrowBounds,
|
|
14163
15678
|
getArrowControlPoint,
|
|
@@ -14173,20 +15688,27 @@ var VERSION = "0.63.0";
|
|
|
14173
15688
|
getHexCellsInRectangle,
|
|
14174
15689
|
getHexCellsInSquare,
|
|
14175
15690
|
getHexDistance,
|
|
15691
|
+
gridDistanceCells,
|
|
15692
|
+
isAwarenessPresence,
|
|
14176
15693
|
isFocusPresence,
|
|
14177
15694
|
isLaserTrailPresence,
|
|
14178
15695
|
isMeasurePresence,
|
|
14179
15696
|
isNearBezier,
|
|
15697
|
+
isPathPresence,
|
|
14180
15698
|
isPingPresence,
|
|
15699
|
+
pathDistanceCells,
|
|
14181
15700
|
resolveHtmlRouting,
|
|
14182
15701
|
setFontSize,
|
|
14183
15702
|
smartSnap,
|
|
15703
|
+
snapFootprintCenter,
|
|
14184
15704
|
snapPoint,
|
|
15705
|
+
snapToCellCenter,
|
|
14185
15706
|
snapToHexCenter,
|
|
14186
15707
|
styleToPatch,
|
|
14187
15708
|
toFocusPresence,
|
|
14188
15709
|
toLaserTrailPresence,
|
|
14189
15710
|
toMeasurePresence,
|
|
15711
|
+
toPathPresence,
|
|
14190
15712
|
toPingPresence,
|
|
14191
15713
|
toggleBold,
|
|
14192
15714
|
toggleItalic,
|