@fieldnotes/core 0.63.0 → 0.64.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 +1037 -329
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +347 -17
- package/dist/index.d.ts +347 -17
- package/dist/index.js +1026 -329
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -44,13 +44,17 @@ __export(index_exports, {
|
|
|
44
44
|
MemoryAdapter: () => MemoryAdapter,
|
|
45
45
|
MinimapController: () => MinimapController,
|
|
46
46
|
NoteTool: () => NoteTool,
|
|
47
|
+
PATH_PRESENCE_KIND: () => PATH_PRESENCE_KIND,
|
|
48
|
+
PATH_PRESENCE_MAX_POINTS: () => PATH_PRESENCE_MAX_POINTS,
|
|
47
49
|
PING_PRESENCE_KIND: () => PING_PRESENCE_KIND,
|
|
50
|
+
PathTool: () => PathTool,
|
|
48
51
|
PencilTool: () => PencilTool,
|
|
49
52
|
PingInput: () => PingInput,
|
|
50
53
|
PingTool: () => PingTool,
|
|
51
54
|
RemoteFocusReceiver: () => RemoteFocusReceiver,
|
|
52
55
|
RemoteLaserOverlay: () => RemoteLaserOverlay,
|
|
53
56
|
RemoteMeasureOverlay: () => RemoteMeasureOverlay,
|
|
57
|
+
RemotePathOverlay: () => RemotePathOverlay,
|
|
54
58
|
RemotePingOverlay: () => RemotePingOverlay,
|
|
55
59
|
SelectTool: () => SelectTool,
|
|
56
60
|
ShapeTool: () => ShapeTool,
|
|
@@ -78,6 +82,7 @@ __export(index_exports, {
|
|
|
78
82
|
exportImage: () => exportImage,
|
|
79
83
|
exportSvg: () => exportSvg,
|
|
80
84
|
fitZoomForView: () => fitZoomForView,
|
|
85
|
+
footprintFromSize: () => footprintFromSize,
|
|
81
86
|
getActiveFormats: () => getActiveFormats,
|
|
82
87
|
getArrowBounds: () => getArrowBounds,
|
|
83
88
|
getArrowControlPoint: () => getArrowControlPoint,
|
|
@@ -93,20 +98,26 @@ __export(index_exports, {
|
|
|
93
98
|
getHexCellsInRectangle: () => getHexCellsInRectangle,
|
|
94
99
|
getHexCellsInSquare: () => getHexCellsInSquare,
|
|
95
100
|
getHexDistance: () => getHexDistance,
|
|
101
|
+
gridDistanceCells: () => gridDistanceCells,
|
|
96
102
|
isFocusPresence: () => isFocusPresence,
|
|
97
103
|
isLaserTrailPresence: () => isLaserTrailPresence,
|
|
98
104
|
isMeasurePresence: () => isMeasurePresence,
|
|
99
105
|
isNearBezier: () => isNearBezier,
|
|
106
|
+
isPathPresence: () => isPathPresence,
|
|
100
107
|
isPingPresence: () => isPingPresence,
|
|
108
|
+
pathDistanceCells: () => pathDistanceCells,
|
|
101
109
|
resolveHtmlRouting: () => resolveHtmlRouting,
|
|
102
110
|
setFontSize: () => setFontSize,
|
|
103
111
|
smartSnap: () => smartSnap,
|
|
112
|
+
snapFootprintCenter: () => snapFootprintCenter,
|
|
104
113
|
snapPoint: () => snapPoint,
|
|
114
|
+
snapToCellCenter: () => snapToCellCenter,
|
|
105
115
|
snapToHexCenter: () => snapToHexCenter,
|
|
106
116
|
styleToPatch: () => styleToPatch,
|
|
107
117
|
toFocusPresence: () => toFocusPresence,
|
|
108
118
|
toLaserTrailPresence: () => toLaserTrailPresence,
|
|
109
119
|
toMeasurePresence: () => toMeasurePresence,
|
|
120
|
+
toPathPresence: () => toPathPresence,
|
|
110
121
|
toPingPresence: () => toPingPresence,
|
|
111
122
|
toggleBold: () => toggleBold,
|
|
112
123
|
toggleItalic: () => toggleItalic,
|
|
@@ -146,6 +157,310 @@ function smartSnap(point, ctx) {
|
|
|
146
157
|
}
|
|
147
158
|
return snapPoint(point, ctx.gridSize);
|
|
148
159
|
}
|
|
160
|
+
function footprintOf(footprint) {
|
|
161
|
+
return typeof footprint === "number" ? { w: footprint, h: footprint } : footprint;
|
|
162
|
+
}
|
|
163
|
+
function footprintFromSize(size, gridSize) {
|
|
164
|
+
if (!(gridSize > 0)) return 1;
|
|
165
|
+
return {
|
|
166
|
+
w: Math.max(1, Math.round(size.w / gridSize)),
|
|
167
|
+
h: Math.max(1, Math.round(size.h / gridSize))
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function snapAxisToCell(value, gridSize, cells) {
|
|
171
|
+
const n2 = Math.max(1, Math.round(cells));
|
|
172
|
+
if (n2 % 2 === 0) return Math.round(value / gridSize) * gridSize || 0;
|
|
173
|
+
return (Math.round((value - gridSize / 2) / gridSize) + 0.5) * gridSize || 0;
|
|
174
|
+
}
|
|
175
|
+
function snapToCellCenter(point, gridSize, footprint = 1) {
|
|
176
|
+
const { w, h } = footprintOf(footprint);
|
|
177
|
+
return { x: snapAxisToCell(point.x, gridSize, w), y: snapAxisToCell(point.y, gridSize, h) };
|
|
178
|
+
}
|
|
179
|
+
function snapFootprintCenter(point, footprint, ctx) {
|
|
180
|
+
if (!ctx.snapToGrid || !ctx.gridSize) return point;
|
|
181
|
+
if (ctx.gridType === "hex" && ctx.hexOrientation) {
|
|
182
|
+
return snapToHexCenter(point, ctx.gridSize, ctx.hexOrientation);
|
|
183
|
+
}
|
|
184
|
+
return snapToCellCenter(point, ctx.gridSize, footprint);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/elements/hex-fill.ts
|
|
188
|
+
function offsetToCube(col, row, orientation) {
|
|
189
|
+
if (orientation === "pointy") {
|
|
190
|
+
return { q: col - (row - (row & 1)) / 2, r: row };
|
|
191
|
+
}
|
|
192
|
+
return { q: col, r: row - (col - (col & 1)) / 2 };
|
|
193
|
+
}
|
|
194
|
+
function cubeToOffset(q, r, orientation) {
|
|
195
|
+
if (orientation === "pointy") {
|
|
196
|
+
return { col: q + (r - (r & 1)) / 2, row: r };
|
|
197
|
+
}
|
|
198
|
+
return { col: q, row: r + (q - (q & 1)) / 2 };
|
|
199
|
+
}
|
|
200
|
+
function offsetToPixel(col, row, cellSize, orientation) {
|
|
201
|
+
if (orientation === "pointy") {
|
|
202
|
+
const hexW = Math.sqrt(3) * cellSize;
|
|
203
|
+
const rowH = 1.5 * cellSize;
|
|
204
|
+
const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
|
|
205
|
+
return { x: col * hexW + offsetX, y: row * rowH };
|
|
206
|
+
}
|
|
207
|
+
const hexH = Math.sqrt(3) * cellSize;
|
|
208
|
+
const colW = 1.5 * cellSize;
|
|
209
|
+
const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
|
|
210
|
+
return { x: col * colW, y: row * hexH + offsetY };
|
|
211
|
+
}
|
|
212
|
+
function pixelToOffset(x, y, cellSize, orientation) {
|
|
213
|
+
if (orientation === "pointy") {
|
|
214
|
+
const hexW = Math.sqrt(3) * cellSize;
|
|
215
|
+
const rowH = 1.5 * cellSize;
|
|
216
|
+
const row = Math.round(y / rowH);
|
|
217
|
+
const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
|
|
218
|
+
return { col: Math.round((x - offsetX) / hexW), row };
|
|
219
|
+
}
|
|
220
|
+
const hexH = Math.sqrt(3) * cellSize;
|
|
221
|
+
const colW = 1.5 * cellSize;
|
|
222
|
+
const col = Math.round(x / colW);
|
|
223
|
+
const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
|
|
224
|
+
return { col, row: Math.round((y - offsetY) / hexH) };
|
|
225
|
+
}
|
|
226
|
+
function enumerateHexRing(centerQ, centerR, n2, orientation, cellSize) {
|
|
227
|
+
const cells = [];
|
|
228
|
+
for (let dq = -n2; dq <= n2; dq++) {
|
|
229
|
+
const rMin = Math.max(-n2, -dq - n2);
|
|
230
|
+
const rMax = Math.min(n2, -dq + n2);
|
|
231
|
+
for (let dr = rMin; dr <= rMax; dr++) {
|
|
232
|
+
const absQ = centerQ + dq;
|
|
233
|
+
const absR = centerR + dr;
|
|
234
|
+
const off = cubeToOffset(absQ, absR, orientation);
|
|
235
|
+
cells.push(offsetToPixel(off.col, off.row, cellSize, orientation));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return cells;
|
|
239
|
+
}
|
|
240
|
+
function getHexDistance(a, b, cellSize, orientation) {
|
|
241
|
+
const offA = pixelToOffset(a.x, a.y, cellSize, orientation);
|
|
242
|
+
const offB = pixelToOffset(b.x, b.y, cellSize, orientation);
|
|
243
|
+
const cubeA = offsetToCube(offA.col, offA.row, orientation);
|
|
244
|
+
const cubeB = offsetToCube(offB.col, offB.row, orientation);
|
|
245
|
+
const dq = cubeA.q - cubeB.q;
|
|
246
|
+
const dr = cubeA.r - cubeB.r;
|
|
247
|
+
const ds = -dq - dr;
|
|
248
|
+
return Math.max(Math.abs(dq), Math.abs(dr), Math.abs(ds));
|
|
249
|
+
}
|
|
250
|
+
function getHexCellsInRadius(center2, radiusCells, cellSize, orientation) {
|
|
251
|
+
const n2 = Math.round(radiusCells);
|
|
252
|
+
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
253
|
+
const cube = offsetToCube(off.col, off.row, orientation);
|
|
254
|
+
if (n2 <= 0) {
|
|
255
|
+
return [offsetToPixel(off.col, off.row, cellSize, orientation)];
|
|
256
|
+
}
|
|
257
|
+
return enumerateHexRing(cube.q, cube.r, n2, orientation, cellSize);
|
|
258
|
+
}
|
|
259
|
+
function getHexCellsInCone(center2, angle, radiusCells, cellSize, orientation) {
|
|
260
|
+
const n2 = Math.round(radiusCells);
|
|
261
|
+
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
262
|
+
const cube = offsetToCube(off.col, off.row, orientation);
|
|
263
|
+
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
264
|
+
if (n2 <= 0) return [centerPixel];
|
|
265
|
+
const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
|
|
266
|
+
const step = Math.PI / 3;
|
|
267
|
+
const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
|
|
268
|
+
const halfAngle = Math.PI / 6 + 1e-6;
|
|
269
|
+
const cells = [centerPixel];
|
|
270
|
+
for (let dq = -n2; dq <= n2; dq++) {
|
|
271
|
+
const rMin = Math.max(-n2, -dq - n2);
|
|
272
|
+
const rMax = Math.min(n2, -dq + n2);
|
|
273
|
+
for (let dr = rMin; dr <= rMax; dr++) {
|
|
274
|
+
if (dq === 0 && dr === 0) continue;
|
|
275
|
+
const absQ = cube.q + dq;
|
|
276
|
+
const absR = cube.r + dr;
|
|
277
|
+
const pixel = offsetToPixel(
|
|
278
|
+
cubeToOffset(absQ, absR, orientation).col,
|
|
279
|
+
cubeToOffset(absQ, absR, orientation).row,
|
|
280
|
+
cellSize,
|
|
281
|
+
orientation
|
|
282
|
+
);
|
|
283
|
+
const dx = pixel.x - centerPixel.x;
|
|
284
|
+
const dy = pixel.y - centerPixel.y;
|
|
285
|
+
let diff = Math.atan2(dy, dx) - snappedAngle;
|
|
286
|
+
if (diff > Math.PI) diff -= 2 * Math.PI;
|
|
287
|
+
if (diff < -Math.PI) diff += 2 * Math.PI;
|
|
288
|
+
if (Math.abs(diff) <= halfAngle) {
|
|
289
|
+
cells.push(pixel);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return cells;
|
|
294
|
+
}
|
|
295
|
+
function getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation) {
|
|
296
|
+
const n2 = Math.round(radiusCells);
|
|
297
|
+
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
298
|
+
const cube = offsetToCube(off.col, off.row, orientation);
|
|
299
|
+
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
300
|
+
if (n2 <= 0) return [centerPixel];
|
|
301
|
+
const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
|
|
302
|
+
const step = Math.PI / 3;
|
|
303
|
+
const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
|
|
304
|
+
const cos = Math.cos(snappedAngle);
|
|
305
|
+
const sin = Math.sin(snappedAngle);
|
|
306
|
+
const snapUnit = Math.sqrt(3) * cellSize;
|
|
307
|
+
const lineLength = n2 * snapUnit;
|
|
308
|
+
const halfWidth = snapUnit * 0.5 + 1e-6;
|
|
309
|
+
const cells = [];
|
|
310
|
+
for (let dq = -n2; dq <= n2; dq++) {
|
|
311
|
+
const rMin = Math.max(-n2, -dq - n2);
|
|
312
|
+
const rMax = Math.min(n2, -dq + n2);
|
|
313
|
+
for (let dr = rMin; dr <= rMax; dr++) {
|
|
314
|
+
const absQ = cube.q + dq;
|
|
315
|
+
const absR = cube.r + dr;
|
|
316
|
+
const pixel = offsetToPixel(
|
|
317
|
+
cubeToOffset(absQ, absR, orientation).col,
|
|
318
|
+
cubeToOffset(absQ, absR, orientation).row,
|
|
319
|
+
cellSize,
|
|
320
|
+
orientation
|
|
321
|
+
);
|
|
322
|
+
const dx = pixel.x - centerPixel.x;
|
|
323
|
+
const dy = pixel.y - centerPixel.y;
|
|
324
|
+
const along = dx * cos + dy * sin;
|
|
325
|
+
const perp = Math.abs(-dx * sin + dy * cos);
|
|
326
|
+
if (along >= -snapUnit * 0.1 && along <= lineLength + snapUnit * 0.1 && perp <= halfWidth) {
|
|
327
|
+
cells.push(pixel);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return cells;
|
|
332
|
+
}
|
|
333
|
+
function getHexCellsInRectangle(center2, angle, lengthCells, widthCells, cellSize, orientation) {
|
|
334
|
+
const nLen = Math.round(lengthCells);
|
|
335
|
+
const wCells = Math.max(1, Math.round(widthCells));
|
|
336
|
+
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
337
|
+
const cube = offsetToCube(off.col, off.row, orientation);
|
|
338
|
+
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
339
|
+
if (nLen <= 0) return [centerPixel];
|
|
340
|
+
const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
|
|
341
|
+
const step = Math.PI / 3;
|
|
342
|
+
const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
|
|
343
|
+
const cos = Math.cos(snappedAngle);
|
|
344
|
+
const sin = Math.sin(snappedAngle);
|
|
345
|
+
const snapUnit = Math.sqrt(3) * cellSize;
|
|
346
|
+
const lineLength = nLen * snapUnit;
|
|
347
|
+
const halfWidth = wCells * snapUnit / 2 + 1e-6;
|
|
348
|
+
const iterM = nLen + Math.ceil(wCells / 2) + 1;
|
|
349
|
+
const cells = [];
|
|
350
|
+
for (let dq = -iterM; dq <= iterM; dq++) {
|
|
351
|
+
const rMin = Math.max(-iterM, -dq - iterM);
|
|
352
|
+
const rMax = Math.min(iterM, -dq + iterM);
|
|
353
|
+
for (let dr = rMin; dr <= rMax; dr++) {
|
|
354
|
+
const absQ = cube.q + dq;
|
|
355
|
+
const absR = cube.r + dr;
|
|
356
|
+
const pixel = offsetToPixel(
|
|
357
|
+
cubeToOffset(absQ, absR, orientation).col,
|
|
358
|
+
cubeToOffset(absQ, absR, orientation).row,
|
|
359
|
+
cellSize,
|
|
360
|
+
orientation
|
|
361
|
+
);
|
|
362
|
+
const dx = pixel.x - centerPixel.x;
|
|
363
|
+
const dy = pixel.y - centerPixel.y;
|
|
364
|
+
const along = dx * cos + dy * sin;
|
|
365
|
+
const perp = Math.abs(-dx * sin + dy * cos);
|
|
366
|
+
if (along >= -snapUnit * 0.1 && along <= lineLength + snapUnit * 0.1 && perp <= halfWidth) {
|
|
367
|
+
cells.push(pixel);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return cells;
|
|
372
|
+
}
|
|
373
|
+
function getHexCellsInSquare(center2, radiusCells, cellSize, orientation) {
|
|
374
|
+
const n2 = Math.round(radiusCells);
|
|
375
|
+
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
376
|
+
const cube = offsetToCube(off.col, off.row, orientation);
|
|
377
|
+
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
378
|
+
if (n2 <= 0) return [centerPixel];
|
|
379
|
+
const snapUnit = Math.sqrt(3) * cellSize;
|
|
380
|
+
const halfSide = n2 * snapUnit / 2;
|
|
381
|
+
const cells = [];
|
|
382
|
+
for (let dq = -n2; dq <= n2; dq++) {
|
|
383
|
+
const rMin = Math.max(-n2, -dq - n2);
|
|
384
|
+
const rMax = Math.min(n2, -dq + n2);
|
|
385
|
+
for (let dr = rMin; dr <= rMax; dr++) {
|
|
386
|
+
const absQ = cube.q + dq;
|
|
387
|
+
const absR = cube.r + dr;
|
|
388
|
+
const pixel = offsetToPixel(
|
|
389
|
+
cubeToOffset(absQ, absR, orientation).col,
|
|
390
|
+
cubeToOffset(absQ, absR, orientation).row,
|
|
391
|
+
cellSize,
|
|
392
|
+
orientation
|
|
393
|
+
);
|
|
394
|
+
if (Math.abs(pixel.x - centerPixel.x) <= halfSide && Math.abs(pixel.y - centerPixel.y) <= halfSide) {
|
|
395
|
+
cells.push(pixel);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return cells;
|
|
400
|
+
}
|
|
401
|
+
function drawHexPath(ctx, cx, cy, cellSize, orientation) {
|
|
402
|
+
const angleOffset = orientation === "pointy" ? Math.PI / 6 : 0;
|
|
403
|
+
ctx.moveTo(cx + cellSize * Math.cos(angleOffset), cy + cellSize * Math.sin(angleOffset));
|
|
404
|
+
for (let i = 1; i < 6; i++) {
|
|
405
|
+
const a = angleOffset + Math.PI / 3 * i;
|
|
406
|
+
ctx.lineTo(cx + cellSize * Math.cos(a), cy + cellSize * Math.sin(a));
|
|
407
|
+
}
|
|
408
|
+
ctx.closePath();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// src/core/grid-metric.ts
|
|
412
|
+
function squareCost(rule, straight, diag) {
|
|
413
|
+
switch (rule) {
|
|
414
|
+
case "alternate":
|
|
415
|
+
return straight + diag + Math.floor(diag / 2);
|
|
416
|
+
case "manhattan":
|
|
417
|
+
return straight + 2 * diag;
|
|
418
|
+
case "chebyshev":
|
|
419
|
+
case "euclidean":
|
|
420
|
+
default:
|
|
421
|
+
return straight + diag;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
function pathDistanceCells(points, grid) {
|
|
425
|
+
const cumulative = [];
|
|
426
|
+
const first = points[0];
|
|
427
|
+
if (first === void 0) return { total: 0, cumulative };
|
|
428
|
+
const gridSize = grid.gridSize > 0 ? grid.gridSize : 1;
|
|
429
|
+
const rule = grid.diagonalRule ?? "euclidean";
|
|
430
|
+
const orientation = grid.hexOrientation;
|
|
431
|
+
const hex = grid.gridType === "hex" && orientation !== void 0;
|
|
432
|
+
const squareRule = grid.gridType === "square" && rule !== "euclidean";
|
|
433
|
+
let straight = 0;
|
|
434
|
+
let diag = 0;
|
|
435
|
+
let euclid = 0;
|
|
436
|
+
let hexCells = 0;
|
|
437
|
+
cumulative.push(0);
|
|
438
|
+
let prev = first;
|
|
439
|
+
for (let i = 1; i < points.length; i++) {
|
|
440
|
+
const next = points[i];
|
|
441
|
+
if (next === void 0) break;
|
|
442
|
+
if (hex && orientation !== void 0) {
|
|
443
|
+
hexCells += getHexDistance(prev, next, gridSize, orientation);
|
|
444
|
+
cumulative.push(hexCells);
|
|
445
|
+
} else if (squareRule) {
|
|
446
|
+
const dx = Math.abs(Math.round((next.x - prev.x) / gridSize));
|
|
447
|
+
const dy = Math.abs(Math.round((next.y - prev.y) / gridSize));
|
|
448
|
+
const d = Math.min(dx, dy);
|
|
449
|
+
diag += d;
|
|
450
|
+
straight += Math.max(dx, dy) - d;
|
|
451
|
+
cumulative.push(squareCost(rule, straight, diag));
|
|
452
|
+
} else {
|
|
453
|
+
euclid += Math.hypot(next.x - prev.x, next.y - prev.y) / gridSize;
|
|
454
|
+
cumulative.push(euclid);
|
|
455
|
+
}
|
|
456
|
+
prev = next;
|
|
457
|
+
}
|
|
458
|
+
const total = cumulative[cumulative.length - 1] ?? 0;
|
|
459
|
+
return { total, cumulative };
|
|
460
|
+
}
|
|
461
|
+
function gridDistanceCells(a, b, grid) {
|
|
462
|
+
return pathDistanceCells([a, b], grid).total;
|
|
463
|
+
}
|
|
149
464
|
|
|
150
465
|
// src/elements/note-sanitizer.ts
|
|
151
466
|
var BOLD_TAGS = /* @__PURE__ */ new Set(["b", "strong"]);
|
|
@@ -1672,6 +1987,12 @@ var KeyboardHandler = class {
|
|
|
1672
1987
|
if (e.key === " ") {
|
|
1673
1988
|
this.deps.setSpaceHeld(true);
|
|
1674
1989
|
}
|
|
1990
|
+
const tool = this.deps.getActiveTool();
|
|
1991
|
+
const ctx = this.deps.getToolContext();
|
|
1992
|
+
if (tool?.onKeyDown && ctx && tool.onKeyDown(e, ctx)) {
|
|
1993
|
+
e.preventDefault();
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1675
1996
|
const action = this.shortcutMap.match(e);
|
|
1676
1997
|
if (action !== null) {
|
|
1677
1998
|
this.runAction(action, e);
|
|
@@ -1967,6 +2288,7 @@ var InputHandler = class {
|
|
|
1967
2288
|
abortSignal: this.abortController.signal,
|
|
1968
2289
|
getToolContext: () => this.toolContext,
|
|
1969
2290
|
getIsToolActive: () => this.isToolActive,
|
|
2291
|
+
getActiveTool: () => this.toolManager?.activeTool ?? null,
|
|
1970
2292
|
getLastPointerEvent: () => this.lastPointerEvent,
|
|
1971
2293
|
setSpaceHeld: (v) => {
|
|
1972
2294
|
this.spaceHeld = v;
|
|
@@ -2048,7 +2370,7 @@ var InputHandler = class {
|
|
|
2048
2370
|
this.element.addEventListener("pointermove", this.onPointerMove, opts);
|
|
2049
2371
|
this.element.addEventListener("pointerup", this.onPointerUp, opts);
|
|
2050
2372
|
this.element.addEventListener("pointerleave", this.onPointerLeave, opts);
|
|
2051
|
-
this.element.addEventListener("pointercancel", this.
|
|
2373
|
+
this.element.addEventListener("pointercancel", this.onPointerCancel, opts);
|
|
2052
2374
|
this.element.addEventListener("contextmenu", this.onContextMenu, opts);
|
|
2053
2375
|
window.addEventListener("blur", this.onCoastInterrupt, opts);
|
|
2054
2376
|
window.addEventListener("visibilitychange", this.onCoastInterrupt, opts);
|
|
@@ -2127,6 +2449,12 @@ var InputHandler = class {
|
|
|
2127
2449
|
}
|
|
2128
2450
|
};
|
|
2129
2451
|
onPointerUp = (e) => {
|
|
2452
|
+
this.finishPointer(e, false);
|
|
2453
|
+
};
|
|
2454
|
+
onPointerCancel = (e) => {
|
|
2455
|
+
this.finishPointer(e, true);
|
|
2456
|
+
};
|
|
2457
|
+
finishPointer(e, cancelled) {
|
|
2130
2458
|
this.cancelLongPress();
|
|
2131
2459
|
try {
|
|
2132
2460
|
this.element.releasePointerCapture(e.pointerId);
|
|
@@ -2149,16 +2477,18 @@ var InputHandler = class {
|
|
|
2149
2477
|
if (this.activePointers.size === 0) this.coastStoppedByPointer = false;
|
|
2150
2478
|
const upResult = this.inputFilter.filterUp(e);
|
|
2151
2479
|
if (this.isToolActive) {
|
|
2152
|
-
this.
|
|
2480
|
+
if (cancelled) this.dispatchToolCancel(e);
|
|
2481
|
+
else this.dispatchToolUp(e);
|
|
2153
2482
|
this.isToolActive = false;
|
|
2154
2483
|
} else if (this.deferredDown && upResult.pendingTap) {
|
|
2155
2484
|
this.dispatchToolDown(this.deferredDown);
|
|
2156
|
-
this.
|
|
2485
|
+
if (cancelled) this.dispatchToolCancel(e);
|
|
2486
|
+
else this.dispatchToolUp(e);
|
|
2157
2487
|
this.deferredDown = null;
|
|
2158
2488
|
} else {
|
|
2159
2489
|
this.deferredDown = null;
|
|
2160
2490
|
}
|
|
2161
|
-
}
|
|
2491
|
+
}
|
|
2162
2492
|
runAction(action, e) {
|
|
2163
2493
|
this.keyboard.runAction(action, e);
|
|
2164
2494
|
}
|
|
@@ -2265,6 +2595,11 @@ var InputHandler = class {
|
|
|
2265
2595
|
this.toolManager.handlePointerUp(this.toPointerState(e), this.toolContext);
|
|
2266
2596
|
this.historyRecorder?.commit();
|
|
2267
2597
|
}
|
|
2598
|
+
dispatchToolCancel(e) {
|
|
2599
|
+
if (!this.toolManager || !this.toolContext) return;
|
|
2600
|
+
this.toolManager.handlePointerCancel(this.toPointerState(e), this.toolContext);
|
|
2601
|
+
this.historyRecorder?.commit();
|
|
2602
|
+
}
|
|
2268
2603
|
isInScope() {
|
|
2269
2604
|
if (this.scope === "window") return true;
|
|
2270
2605
|
const active = document.activeElement;
|
|
@@ -2277,7 +2612,7 @@ var InputHandler = class {
|
|
|
2277
2612
|
cancelToolIfActive(e) {
|
|
2278
2613
|
this.cancelLongPress();
|
|
2279
2614
|
if (this.isToolActive) {
|
|
2280
|
-
this.
|
|
2615
|
+
this.dispatchToolCancel(e);
|
|
2281
2616
|
this.isToolActive = false;
|
|
2282
2617
|
}
|
|
2283
2618
|
this.deferredDown = null;
|
|
@@ -3375,230 +3710,6 @@ function getImage(src, imageCache, onImageLoad, onImageError) {
|
|
|
3375
3710
|
return null;
|
|
3376
3711
|
}
|
|
3377
3712
|
|
|
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
3713
|
// src/elements/renderers/template-measure.ts
|
|
3603
3714
|
function renderTemplateFeetLabel(ctx, p) {
|
|
3604
3715
|
if (p.feet <= 0) return;
|
|
@@ -5573,6 +5684,13 @@ var ToolManager = class {
|
|
|
5573
5684
|
handlePointerUp(state, ctx) {
|
|
5574
5685
|
this.current?.onPointerUp(state, ctx);
|
|
5575
5686
|
}
|
|
5687
|
+
/** Cancels the active gesture; falls back to `onPointerUp` for tools without `onPointerCancel`. */
|
|
5688
|
+
handlePointerCancel(state, ctx) {
|
|
5689
|
+
const tool = this.current;
|
|
5690
|
+
if (!tool) return;
|
|
5691
|
+
if (tool.onPointerCancel) tool.onPointerCancel(state, ctx);
|
|
5692
|
+
else tool.onPointerUp(state, ctx);
|
|
5693
|
+
}
|
|
5576
5694
|
onChange(listener) {
|
|
5577
5695
|
this.changeListeners.add(listener);
|
|
5578
5696
|
return () => this.changeListeners.delete(listener);
|
|
@@ -11182,145 +11300,119 @@ function drawMeasurement(ctx, m, opts = {}) {
|
|
|
11182
11300
|
ctx.restore();
|
|
11183
11301
|
}
|
|
11184
11302
|
|
|
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";
|
|
11303
|
+
// src/canvas/linger-overlay.ts
|
|
11215
11304
|
var DEFAULT_HOLD_MS = 1500;
|
|
11216
11305
|
var DEFAULT_FADE_MS2 = 400;
|
|
11217
11306
|
var DEFAULT_MAX_AGE_MS = 3e4;
|
|
11218
|
-
var
|
|
11307
|
+
var LingerOverlay = class {
|
|
11219
11308
|
host;
|
|
11220
|
-
|
|
11309
|
+
draw;
|
|
11310
|
+
clock;
|
|
11221
11311
|
holdMs;
|
|
11222
11312
|
fadeMs;
|
|
11223
11313
|
maxAgeMs;
|
|
11224
|
-
|
|
11314
|
+
entries = /* @__PURE__ */ new Map();
|
|
11225
11315
|
unregister;
|
|
11226
11316
|
rafId = null;
|
|
11227
|
-
|
|
11228
|
-
|
|
11317
|
+
isDisposed = false;
|
|
11318
|
+
/**
|
|
11319
|
+
* `clock` exists only as a test seam; production callers leave it at
|
|
11320
|
+
* `performance.now`. Owners that expose their own `now()` seam pass it
|
|
11321
|
+
* through so a spy on the owner still drives this overlay's timing.
|
|
11322
|
+
*/
|
|
11323
|
+
constructor(host, options, draw, clock = () => performance.now()) {
|
|
11229
11324
|
this.host = host;
|
|
11230
|
-
this.
|
|
11325
|
+
this.draw = draw;
|
|
11326
|
+
this.clock = clock;
|
|
11231
11327
|
this.holdMs = options.holdMs ?? DEFAULT_HOLD_MS;
|
|
11232
11328
|
this.fadeMs = options.fadeMs ?? DEFAULT_FADE_MS2;
|
|
11233
11329
|
this.maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
11234
|
-
this.unregister = host.registerOverlay((ctx) => this.
|
|
11330
|
+
this.unregister = host.registerOverlay((ctx) => this.render(ctx));
|
|
11235
11331
|
}
|
|
11236
11332
|
now() {
|
|
11237
|
-
return
|
|
11333
|
+
return this.clock();
|
|
11238
11334
|
}
|
|
11239
11335
|
/**
|
|
11240
|
-
*
|
|
11241
|
-
*
|
|
11242
|
-
* reported as `false`, so hosts can feed every presence frame through.
|
|
11336
|
+
* Stores `sender`'s current entry as active, replacing any previous one and
|
|
11337
|
+
* cancelling an in-flight linger, and restarts the `maxAgeMs` expiry timer.
|
|
11243
11338
|
*/
|
|
11244
|
-
|
|
11245
|
-
if (this.
|
|
11246
|
-
|
|
11247
|
-
this.beginLinger(sender);
|
|
11248
|
-
return true;
|
|
11249
|
-
}
|
|
11250
|
-
const existing = this.measurements.get(sender);
|
|
11339
|
+
set(sender, entry) {
|
|
11340
|
+
if (this.isDisposed) return;
|
|
11341
|
+
const existing = this.entries.get(sender);
|
|
11251
11342
|
if (existing?.expiryTimer != null) clearTimeout(existing.expiryTimer);
|
|
11252
|
-
this.
|
|
11253
|
-
|
|
11254
|
-
end: data.end,
|
|
11255
|
-
feet: data.feet,
|
|
11256
|
-
color: data.color ?? this.color,
|
|
11343
|
+
this.entries.set(sender, {
|
|
11344
|
+
entry,
|
|
11257
11345
|
clearedAt: null,
|
|
11258
|
-
expiryTimer: setTimeout(() => this.
|
|
11346
|
+
expiryTimer: setTimeout(() => this.linger(sender), this.maxAgeMs)
|
|
11259
11347
|
});
|
|
11260
11348
|
this.host.requestRender();
|
|
11261
|
-
return true;
|
|
11262
11349
|
}
|
|
11263
|
-
/**
|
|
11350
|
+
/** Starts the hold-then-fade lifetime for `sender`'s entry. */
|
|
11351
|
+
linger(sender) {
|
|
11352
|
+
const entry = this.entries.get(sender);
|
|
11353
|
+
if (!entry || entry.clearedAt !== null) return;
|
|
11354
|
+
if (entry.expiryTimer != null) {
|
|
11355
|
+
clearTimeout(entry.expiryTimer);
|
|
11356
|
+
entry.expiryTimer = null;
|
|
11357
|
+
}
|
|
11358
|
+
entry.clearedAt = this.now();
|
|
11359
|
+
this.ensureAnimating();
|
|
11360
|
+
this.host.requestRender();
|
|
11361
|
+
}
|
|
11362
|
+
/** Removes a sender's entry immediately (presence-leave/disconnect). */
|
|
11264
11363
|
remove(sender) {
|
|
11265
|
-
const entry = this.
|
|
11364
|
+
const entry = this.entries.get(sender);
|
|
11266
11365
|
if (!entry) return;
|
|
11267
11366
|
if (entry.expiryTimer != null) clearTimeout(entry.expiryTimer);
|
|
11268
|
-
this.
|
|
11367
|
+
this.entries.delete(sender);
|
|
11269
11368
|
this.host.requestRender();
|
|
11270
11369
|
}
|
|
11271
|
-
/** Removes every
|
|
11370
|
+
/** Removes every entry immediately. */
|
|
11272
11371
|
clear() {
|
|
11273
|
-
if (this.
|
|
11274
|
-
for (const entry of this.
|
|
11372
|
+
if (this.entries.size === 0) return;
|
|
11373
|
+
for (const entry of this.entries.values()) {
|
|
11275
11374
|
if (entry.expiryTimer != null) clearTimeout(entry.expiryTimer);
|
|
11276
11375
|
}
|
|
11277
|
-
this.
|
|
11376
|
+
this.entries.clear();
|
|
11278
11377
|
this.host.requestRender();
|
|
11279
11378
|
}
|
|
11280
|
-
/** Number of senders with a visible (active or lingering)
|
|
11379
|
+
/** Number of senders with a visible (active or lingering) entry. */
|
|
11281
11380
|
get activeSenderCount() {
|
|
11282
|
-
return this.
|
|
11381
|
+
return this.entries.size;
|
|
11382
|
+
}
|
|
11383
|
+
/** True once `dispose` has run; the overlay accepts no further entries. */
|
|
11384
|
+
get disposed() {
|
|
11385
|
+
return this.isDisposed;
|
|
11283
11386
|
}
|
|
11284
11387
|
/** Unregisters the overlay, cancels timers, stops animating. Idempotent. */
|
|
11285
11388
|
dispose() {
|
|
11286
|
-
if (this.
|
|
11287
|
-
this.
|
|
11389
|
+
if (this.isDisposed) return;
|
|
11390
|
+
this.isDisposed = true;
|
|
11288
11391
|
if (this.rafId !== null) {
|
|
11289
11392
|
cancelAnimationFrame(this.rafId);
|
|
11290
11393
|
this.rafId = null;
|
|
11291
11394
|
}
|
|
11292
|
-
for (const entry of this.
|
|
11395
|
+
for (const entry of this.entries.values()) {
|
|
11293
11396
|
if (entry.expiryTimer != null) clearTimeout(entry.expiryTimer);
|
|
11294
11397
|
}
|
|
11295
|
-
this.
|
|
11398
|
+
this.entries.clear();
|
|
11296
11399
|
this.unregister?.();
|
|
11297
11400
|
this.unregister = null;
|
|
11298
11401
|
this.host.requestRender();
|
|
11299
11402
|
}
|
|
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
11403
|
ensureAnimating() {
|
|
11312
11404
|
if (this.rafId === null) {
|
|
11313
11405
|
this.rafId = requestAnimationFrame(() => this.tick());
|
|
11314
11406
|
}
|
|
11315
11407
|
}
|
|
11316
11408
|
tick() {
|
|
11317
|
-
if (this.
|
|
11409
|
+
if (this.isDisposed) return;
|
|
11318
11410
|
const now = this.now();
|
|
11319
11411
|
let lingering = 0;
|
|
11320
|
-
for (const [sender, entry] of this.
|
|
11412
|
+
for (const [sender, entry] of this.entries) {
|
|
11321
11413
|
if (entry.clearedAt === null) continue;
|
|
11322
11414
|
if (now - entry.clearedAt >= this.holdMs + this.fadeMs) {
|
|
11323
|
-
this.
|
|
11415
|
+
this.entries.delete(sender);
|
|
11324
11416
|
} else {
|
|
11325
11417
|
lingering += 1;
|
|
11326
11418
|
}
|
|
@@ -11328,24 +11420,278 @@ var RemoteMeasureOverlay = class {
|
|
|
11328
11420
|
this.host.requestRender();
|
|
11329
11421
|
this.rafId = lingering > 0 ? requestAnimationFrame(() => this.tick()) : null;
|
|
11330
11422
|
}
|
|
11331
|
-
|
|
11332
|
-
if (this.
|
|
11423
|
+
render(ctx) {
|
|
11424
|
+
if (this.entries.size === 0) return;
|
|
11333
11425
|
const now = this.now();
|
|
11334
|
-
for (const entry of this.
|
|
11426
|
+
for (const entry of this.entries.values()) {
|
|
11335
11427
|
let alpha = 1;
|
|
11336
11428
|
if (entry.clearedAt !== null) {
|
|
11337
11429
|
const fadeAge = now - entry.clearedAt - this.holdMs;
|
|
11338
11430
|
if (fadeAge > 0) alpha = Math.max(0, 1 - fadeAge / this.fadeMs);
|
|
11339
11431
|
}
|
|
11340
|
-
|
|
11432
|
+
this.draw(ctx, entry.entry, alpha);
|
|
11433
|
+
}
|
|
11434
|
+
}
|
|
11435
|
+
};
|
|
11436
|
+
|
|
11437
|
+
// src/canvas/remote-measure-overlay.ts
|
|
11438
|
+
var MEASURE_PRESENCE_KIND = "measure";
|
|
11439
|
+
function isFinitePoint2(value) {
|
|
11440
|
+
if (typeof value !== "object" || value === null) return false;
|
|
11441
|
+
const point = value;
|
|
11442
|
+
return typeof point.x === "number" && Number.isFinite(point.x) && typeof point.y === "number" && Number.isFinite(point.y);
|
|
11443
|
+
}
|
|
11444
|
+
function isMeasurePresence(data) {
|
|
11445
|
+
if (typeof data !== "object" || data === null) return false;
|
|
11446
|
+
const payload = data;
|
|
11447
|
+
if (payload.kind !== MEASURE_PRESENCE_KIND) return false;
|
|
11448
|
+
if ("cleared" in payload) return payload.cleared === true;
|
|
11449
|
+
if (!isFinitePoint2(payload.start) || !isFinitePoint2(payload.end)) return false;
|
|
11450
|
+
if (typeof payload.cells !== "number" || !Number.isFinite(payload.cells)) return false;
|
|
11451
|
+
if (typeof payload.feet !== "number" || !Number.isFinite(payload.feet)) return false;
|
|
11452
|
+
if (payload.color !== void 0 && typeof payload.color !== "string") return false;
|
|
11453
|
+
return true;
|
|
11454
|
+
}
|
|
11455
|
+
function toMeasurePresence(emission) {
|
|
11456
|
+
if (emission === null) return { kind: MEASURE_PRESENCE_KIND, cleared: true };
|
|
11457
|
+
return {
|
|
11458
|
+
kind: MEASURE_PRESENCE_KIND,
|
|
11459
|
+
start: emission.start,
|
|
11460
|
+
end: emission.end,
|
|
11461
|
+
cells: emission.cells,
|
|
11462
|
+
feet: emission.feet,
|
|
11463
|
+
color: emission.color
|
|
11464
|
+
};
|
|
11465
|
+
}
|
|
11466
|
+
var DEFAULT_COLOR3 = "#FF5722";
|
|
11467
|
+
var RemoteMeasureOverlay = class {
|
|
11468
|
+
color;
|
|
11469
|
+
overlay;
|
|
11470
|
+
constructor(host, options = {}) {
|
|
11471
|
+
this.color = options.color ?? DEFAULT_COLOR3;
|
|
11472
|
+
this.overlay = new LingerOverlay(
|
|
11473
|
+
host,
|
|
11474
|
+
options,
|
|
11475
|
+
(ctx, entry, alpha) => drawMeasurement(ctx, entry, { alpha }),
|
|
11476
|
+
() => this.now()
|
|
11477
|
+
);
|
|
11478
|
+
}
|
|
11479
|
+
now() {
|
|
11480
|
+
return performance.now();
|
|
11481
|
+
}
|
|
11482
|
+
/**
|
|
11483
|
+
* Applies a presence payload from `sender` (any opaque per-sender key, e.g.
|
|
11484
|
+
* the envelope `from`). Non-measure or malformed payloads are ignored and
|
|
11485
|
+
* reported as `false`, so hosts can feed every presence frame through.
|
|
11486
|
+
*/
|
|
11487
|
+
apply(sender, data) {
|
|
11488
|
+
if (this.overlay.disposed || !isMeasurePresence(data)) return false;
|
|
11489
|
+
if ("cleared" in data) {
|
|
11490
|
+
this.overlay.linger(sender);
|
|
11491
|
+
return true;
|
|
11492
|
+
}
|
|
11493
|
+
this.overlay.set(sender, {
|
|
11494
|
+
start: data.start,
|
|
11495
|
+
end: data.end,
|
|
11496
|
+
feet: data.feet,
|
|
11497
|
+
color: data.color ?? this.color
|
|
11498
|
+
});
|
|
11499
|
+
return true;
|
|
11500
|
+
}
|
|
11501
|
+
/** Removes a sender's ruler immediately (presence-leave/disconnect). */
|
|
11502
|
+
remove(sender) {
|
|
11503
|
+
this.overlay.remove(sender);
|
|
11504
|
+
}
|
|
11505
|
+
/** Removes every ruler immediately. */
|
|
11506
|
+
clear() {
|
|
11507
|
+
this.overlay.clear();
|
|
11508
|
+
}
|
|
11509
|
+
/** Number of senders with a visible (active or lingering) ruler. */
|
|
11510
|
+
get activeSenderCount() {
|
|
11511
|
+
return this.overlay.activeSenderCount;
|
|
11512
|
+
}
|
|
11513
|
+
/** Unregisters the overlay, cancels timers, stops animating. Idempotent. */
|
|
11514
|
+
dispose() {
|
|
11515
|
+
this.overlay.dispose();
|
|
11516
|
+
}
|
|
11517
|
+
};
|
|
11518
|
+
|
|
11519
|
+
// src/canvas/path-render.ts
|
|
11520
|
+
function resolveSegmentColors(cumulativeFeet, bands, color) {
|
|
11521
|
+
const sorted = [...bands].sort((a, b) => a.feet - b.feet);
|
|
11522
|
+
const out = [];
|
|
11523
|
+
for (let i = 1; i < cumulativeFeet.length; i++) {
|
|
11524
|
+
const end = cumulativeFeet[i] ?? 0;
|
|
11525
|
+
const band = sorted.find((b) => b.feet >= end);
|
|
11526
|
+
out.push(band ? band.color : color);
|
|
11527
|
+
}
|
|
11528
|
+
return out;
|
|
11529
|
+
}
|
|
11530
|
+
var LABEL_OFFSET_Y = 18;
|
|
11531
|
+
function drawPath(ctx, m, opts = {}) {
|
|
11532
|
+
const first = m.points[0];
|
|
11533
|
+
if (first === void 0) return;
|
|
11534
|
+
ctx.save();
|
|
11535
|
+
if (opts.alpha !== void 0) ctx.globalAlpha = opts.alpha;
|
|
11536
|
+
ctx.lineWidth = 2;
|
|
11537
|
+
let prev = first;
|
|
11538
|
+
for (let i = 1; i < m.points.length; i++) {
|
|
11539
|
+
const next = m.points[i];
|
|
11540
|
+
if (next === void 0) break;
|
|
11541
|
+
ctx.strokeStyle = m.segmentColors[i - 1] ?? m.color;
|
|
11542
|
+
ctx.setLineDash([8, 4]);
|
|
11543
|
+
ctx.beginPath();
|
|
11544
|
+
ctx.moveTo(prev.x, prev.y);
|
|
11545
|
+
ctx.lineTo(next.x, next.y);
|
|
11546
|
+
ctx.stroke();
|
|
11547
|
+
prev = next;
|
|
11548
|
+
}
|
|
11549
|
+
ctx.setLineDash([]);
|
|
11550
|
+
ctx.fillStyle = m.color;
|
|
11551
|
+
for (const p of m.points) {
|
|
11552
|
+
ctx.beginPath();
|
|
11553
|
+
ctx.arc(p.x, p.y, 4, 0, Math.PI * 2);
|
|
11554
|
+
ctx.fill();
|
|
11555
|
+
}
|
|
11556
|
+
if (m.points.length >= 2) {
|
|
11557
|
+
const last = prev;
|
|
11558
|
+
const label = formatMeasureLabel(m.feet);
|
|
11559
|
+
ctx.font = "14px sans-serif";
|
|
11560
|
+
const metrics = ctx.measureText(label);
|
|
11561
|
+
const padX = 6;
|
|
11562
|
+
const padY = 4;
|
|
11563
|
+
const textH = 14;
|
|
11564
|
+
const cx = last.x;
|
|
11565
|
+
const cy = last.y - LABEL_OFFSET_Y;
|
|
11566
|
+
ctx.fillStyle = "rgba(0, 0, 0, 0.75)";
|
|
11567
|
+
ctx.beginPath();
|
|
11568
|
+
ctx.roundRect(
|
|
11569
|
+
cx - metrics.width / 2 - padX,
|
|
11570
|
+
cy - textH / 2 - padY,
|
|
11571
|
+
metrics.width + padX * 2,
|
|
11572
|
+
textH + padY * 2,
|
|
11573
|
+
4
|
|
11574
|
+
);
|
|
11575
|
+
ctx.fill();
|
|
11576
|
+
ctx.fillStyle = "#FFFFFF";
|
|
11577
|
+
ctx.textAlign = "center";
|
|
11578
|
+
ctx.textBaseline = "middle";
|
|
11579
|
+
ctx.fillText(label, cx, cy);
|
|
11580
|
+
}
|
|
11581
|
+
ctx.restore();
|
|
11582
|
+
}
|
|
11583
|
+
|
|
11584
|
+
// src/canvas/remote-path-overlay.ts
|
|
11585
|
+
var PATH_PRESENCE_KIND = "path";
|
|
11586
|
+
var PATH_PRESENCE_MAX_POINTS = 256;
|
|
11587
|
+
var MAX_COLOR_LENGTH = 64;
|
|
11588
|
+
var EPS = 1e-6;
|
|
11589
|
+
function isFinitePoint3(value) {
|
|
11590
|
+
if (typeof value !== "object" || value === null) return false;
|
|
11591
|
+
const point = value;
|
|
11592
|
+
return typeof point.x === "number" && Number.isFinite(point.x) && typeof point.y === "number" && Number.isFinite(point.y);
|
|
11593
|
+
}
|
|
11594
|
+
function isPathPresence(data) {
|
|
11595
|
+
if (typeof data !== "object" || data === null) return false;
|
|
11596
|
+
const payload = data;
|
|
11597
|
+
if (payload.kind !== PATH_PRESENCE_KIND) return false;
|
|
11598
|
+
if ("cleared" in payload) return payload.cleared === true;
|
|
11599
|
+
if (!Array.isArray(payload.points)) return false;
|
|
11600
|
+
if (payload.points.length === 0 || payload.points.length > PATH_PRESENCE_MAX_POINTS) return false;
|
|
11601
|
+
for (const point of payload.points) {
|
|
11602
|
+
if (!isFinitePoint3(point)) return false;
|
|
11603
|
+
}
|
|
11604
|
+
if (!Array.isArray(payload.segmentColors)) return false;
|
|
11605
|
+
if (payload.segmentColors.length !== payload.points.length - 1) return false;
|
|
11606
|
+
for (const color of payload.segmentColors) {
|
|
11607
|
+
if (typeof color !== "string" || color.length > MAX_COLOR_LENGTH) return false;
|
|
11608
|
+
}
|
|
11609
|
+
if (typeof payload.feet !== "number" || !Number.isFinite(payload.feet)) return false;
|
|
11610
|
+
if (payload.color !== void 0) {
|
|
11611
|
+
if (typeof payload.color !== "string" || payload.color.length > MAX_COLOR_LENGTH) return false;
|
|
11612
|
+
}
|
|
11613
|
+
return true;
|
|
11614
|
+
}
|
|
11615
|
+
function samePoint(a, b) {
|
|
11616
|
+
return Math.abs(a.x - b.x) < EPS && Math.abs(a.y - b.y) < EPS;
|
|
11617
|
+
}
|
|
11618
|
+
function toPathPresence(emission) {
|
|
11619
|
+
if (emission === null) return { kind: PATH_PRESENCE_KIND, cleared: true };
|
|
11620
|
+
const points = emission.waypoints.map((point) => ({ x: point.x, y: point.y }));
|
|
11621
|
+
const last = points[points.length - 1];
|
|
11622
|
+
const cursor = emission.cursor;
|
|
11623
|
+
if (cursor && (!last || !samePoint(cursor, last))) points.push({ x: cursor.x, y: cursor.y });
|
|
11624
|
+
const cumulativeFeet = [0];
|
|
11625
|
+
let total = 0;
|
|
11626
|
+
for (let i = 1; i < points.length; i++) {
|
|
11627
|
+
total += emission.segments[i - 1]?.feet ?? 0;
|
|
11628
|
+
cumulativeFeet.push(total);
|
|
11629
|
+
}
|
|
11630
|
+
return {
|
|
11631
|
+
kind: PATH_PRESENCE_KIND,
|
|
11632
|
+
points,
|
|
11633
|
+
segmentColors: resolveSegmentColors(cumulativeFeet, emission.rangeBands, emission.color),
|
|
11634
|
+
feet: emission.totalFeet,
|
|
11635
|
+
color: emission.color
|
|
11636
|
+
};
|
|
11637
|
+
}
|
|
11638
|
+
var DEFAULT_COLOR4 = "#FF5722";
|
|
11639
|
+
var RemotePathOverlay = class {
|
|
11640
|
+
color;
|
|
11641
|
+
overlay;
|
|
11642
|
+
constructor(host, options = {}) {
|
|
11643
|
+
this.color = options.color ?? DEFAULT_COLOR4;
|
|
11644
|
+
this.overlay = new LingerOverlay(
|
|
11645
|
+
host,
|
|
11646
|
+
options,
|
|
11647
|
+
(ctx, entry, alpha) => drawPath(ctx, entry, { alpha }),
|
|
11648
|
+
() => this.now()
|
|
11649
|
+
);
|
|
11650
|
+
}
|
|
11651
|
+
now() {
|
|
11652
|
+
return performance.now();
|
|
11653
|
+
}
|
|
11654
|
+
/**
|
|
11655
|
+
* Applies a presence payload from `sender` (any opaque per-sender key, e.g.
|
|
11656
|
+
* the envelope `from`). Non-path or malformed payloads are ignored and
|
|
11657
|
+
* reported as `false`, so hosts can feed every presence frame through.
|
|
11658
|
+
*/
|
|
11659
|
+
apply(sender, data) {
|
|
11660
|
+
if (this.overlay.disposed || !isPathPresence(data)) return false;
|
|
11661
|
+
if ("cleared" in data) {
|
|
11662
|
+
this.overlay.linger(sender);
|
|
11663
|
+
return true;
|
|
11341
11664
|
}
|
|
11665
|
+
this.overlay.set(sender, {
|
|
11666
|
+
points: data.points,
|
|
11667
|
+
segmentColors: data.segmentColors,
|
|
11668
|
+
feet: data.feet,
|
|
11669
|
+
color: data.color ?? this.color
|
|
11670
|
+
});
|
|
11671
|
+
return true;
|
|
11672
|
+
}
|
|
11673
|
+
/** Removes a sender's path immediately (presence-leave/disconnect). */
|
|
11674
|
+
remove(sender) {
|
|
11675
|
+
this.overlay.remove(sender);
|
|
11676
|
+
}
|
|
11677
|
+
/** Removes every path immediately. */
|
|
11678
|
+
clear() {
|
|
11679
|
+
this.overlay.clear();
|
|
11680
|
+
}
|
|
11681
|
+
/** Number of senders with a visible (active or lingering) path. */
|
|
11682
|
+
get activeSenderCount() {
|
|
11683
|
+
return this.overlay.activeSenderCount;
|
|
11684
|
+
}
|
|
11685
|
+
/** Unregisters the overlay, cancels timers, stops animating. Idempotent. */
|
|
11686
|
+
dispose() {
|
|
11687
|
+
this.overlay.dispose();
|
|
11342
11688
|
}
|
|
11343
11689
|
};
|
|
11344
11690
|
|
|
11345
11691
|
// src/canvas/ping-input.ts
|
|
11346
11692
|
var DEFAULT_LONG_PRESS_MS = 600;
|
|
11347
11693
|
var DEFAULT_SLOP_PX = 8;
|
|
11348
|
-
var
|
|
11694
|
+
var DEFAULT_COLOR5 = "#ff3b30";
|
|
11349
11695
|
var DEFAULT_DURATION_MS2 = 1800;
|
|
11350
11696
|
var DEFAULT_RADIUS2 = 48;
|
|
11351
11697
|
var DEFAULT_MIN_INTERVAL_MS = 300;
|
|
@@ -11378,7 +11724,7 @@ var PingInput = class {
|
|
|
11378
11724
|
this.longPressEnabled = options.longPressEnabled ?? false;
|
|
11379
11725
|
this.longPressMs = options.longPressMs ?? DEFAULT_LONG_PRESS_MS;
|
|
11380
11726
|
this.slopPx = options.slopPx ?? DEFAULT_SLOP_PX;
|
|
11381
|
-
this.color = options.color ??
|
|
11727
|
+
this.color = options.color ?? DEFAULT_COLOR5;
|
|
11382
11728
|
this.durationMs = options.durationMs ?? DEFAULT_DURATION_MS2;
|
|
11383
11729
|
this.radius = options.radius ?? DEFAULT_RADIUS2;
|
|
11384
11730
|
this.minIntervalMs = options.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS;
|
|
@@ -12714,7 +13060,8 @@ var SelectTool = class {
|
|
|
12714
13060
|
} else if (!ctx.smartGuides && ctx.gridType && "size" in el) {
|
|
12715
13061
|
const centerX = el.position.x + el.size.w / 2 + adjDx;
|
|
12716
13062
|
const centerY = el.position.y + el.size.h / 2 + adjDy;
|
|
12717
|
-
const
|
|
13063
|
+
const footprint = footprintFromSize(el.size, ctx.gridSize ?? 0);
|
|
13064
|
+
const snappedCenter = snapFootprintCenter({ x: centerX, y: centerY }, footprint, ctx);
|
|
12718
13065
|
ctx.store.update(id, {
|
|
12719
13066
|
position: {
|
|
12720
13067
|
x: snappedCenter.x - el.size.w / 2,
|
|
@@ -13292,15 +13639,15 @@ var ShapeTool = class {
|
|
|
13292
13639
|
}
|
|
13293
13640
|
onActivate(_ctx) {
|
|
13294
13641
|
if (typeof window !== "undefined") {
|
|
13295
|
-
window.addEventListener("keydown", this.
|
|
13296
|
-
window.addEventListener("keyup", this.
|
|
13642
|
+
window.addEventListener("keydown", this.trackShiftKeyDown);
|
|
13643
|
+
window.addEventListener("keyup", this.trackShiftKeyUp);
|
|
13297
13644
|
}
|
|
13298
13645
|
}
|
|
13299
13646
|
onDeactivate(_ctx) {
|
|
13300
13647
|
this.shiftHeld = false;
|
|
13301
13648
|
if (typeof window !== "undefined") {
|
|
13302
|
-
window.removeEventListener("keydown", this.
|
|
13303
|
-
window.removeEventListener("keyup", this.
|
|
13649
|
+
window.removeEventListener("keydown", this.trackShiftKeyDown);
|
|
13650
|
+
window.removeEventListener("keyup", this.trackShiftKeyUp);
|
|
13304
13651
|
}
|
|
13305
13652
|
}
|
|
13306
13653
|
onPointerDown(state, ctx) {
|
|
@@ -13393,10 +13740,13 @@ var ShapeTool = class {
|
|
|
13393
13740
|
snap(point, ctx) {
|
|
13394
13741
|
return smartSnap(point, ctx);
|
|
13395
13742
|
}
|
|
13396
|
-
onKeyDown
|
|
13743
|
+
// Deliberately NOT migrated to `Tool.onKeyDown`: that hook is gated by
|
|
13744
|
+
// editable-target and keyboard-scope checks, and a Shift release must be
|
|
13745
|
+
// observed wherever it happens or the constraint sticks after the key is up.
|
|
13746
|
+
trackShiftKeyDown = (e) => {
|
|
13397
13747
|
if (e.key === "Shift") this.shiftHeld = true;
|
|
13398
13748
|
};
|
|
13399
|
-
|
|
13749
|
+
trackShiftKeyUp = (e) => {
|
|
13400
13750
|
if (e.key === "Shift") this.shiftHeld = false;
|
|
13401
13751
|
};
|
|
13402
13752
|
};
|
|
@@ -13540,6 +13890,353 @@ var MeasureTool = class {
|
|
|
13540
13890
|
}
|
|
13541
13891
|
};
|
|
13542
13892
|
|
|
13893
|
+
// src/tools/path-tool.ts
|
|
13894
|
+
var EPS2 = 1e-6;
|
|
13895
|
+
var DEFAULT_COMMIT_TAP_RADIUS_PX = 12;
|
|
13896
|
+
function samePoint2(a, b) {
|
|
13897
|
+
return Math.abs(a.x - b.x) < EPS2 && Math.abs(a.y - b.y) < EPS2;
|
|
13898
|
+
}
|
|
13899
|
+
var PathTool = class {
|
|
13900
|
+
name = "path";
|
|
13901
|
+
feetPerCell;
|
|
13902
|
+
color;
|
|
13903
|
+
diagonalRule;
|
|
13904
|
+
footprintOption;
|
|
13905
|
+
rangeBands;
|
|
13906
|
+
commitTapRadiusPx;
|
|
13907
|
+
resolveStart;
|
|
13908
|
+
waypoints = [];
|
|
13909
|
+
cursor = null;
|
|
13910
|
+
anchorKey;
|
|
13911
|
+
footprint = 1;
|
|
13912
|
+
pointerDown = false;
|
|
13913
|
+
commitOnUp = false;
|
|
13914
|
+
// Grid state is captured at the opening pointer-down so a path measured on
|
|
13915
|
+
// one grid keeps that metric even if the host reconfigures mid-gesture.
|
|
13916
|
+
gridSize = 0;
|
|
13917
|
+
gridType;
|
|
13918
|
+
hexOrientation;
|
|
13919
|
+
snapEnabled = false;
|
|
13920
|
+
optionListeners = /* @__PURE__ */ new Set();
|
|
13921
|
+
pathListeners = /* @__PURE__ */ new Set();
|
|
13922
|
+
commitListeners = /* @__PURE__ */ new Set();
|
|
13923
|
+
emissionRafId = null;
|
|
13924
|
+
constructor(options = {}) {
|
|
13925
|
+
this.feetPerCell = options.feetPerCell ?? 5;
|
|
13926
|
+
this.color = options.color ?? "#FF5722";
|
|
13927
|
+
this.diagonalRule = options.diagonalRule ?? "euclidean";
|
|
13928
|
+
this.footprintOption = options.footprint ?? 1;
|
|
13929
|
+
this.rangeBands = options.rangeBands ?? [];
|
|
13930
|
+
this.commitTapRadiusPx = options.commitTapRadiusPx ?? DEFAULT_COMMIT_TAP_RADIUS_PX;
|
|
13931
|
+
this.resolveStart = options.resolveStart;
|
|
13932
|
+
}
|
|
13933
|
+
getOptions() {
|
|
13934
|
+
return {
|
|
13935
|
+
feetPerCell: this.feetPerCell,
|
|
13936
|
+
color: this.color,
|
|
13937
|
+
diagonalRule: this.diagonalRule,
|
|
13938
|
+
footprint: this.footprintOption,
|
|
13939
|
+
rangeBands: this.rangeBands,
|
|
13940
|
+
commitTapRadiusPx: this.commitTapRadiusPx,
|
|
13941
|
+
resolveStart: this.resolveStart
|
|
13942
|
+
};
|
|
13943
|
+
}
|
|
13944
|
+
setOptions(options) {
|
|
13945
|
+
if (options.feetPerCell !== void 0) this.feetPerCell = options.feetPerCell;
|
|
13946
|
+
if (options.color !== void 0) this.color = options.color;
|
|
13947
|
+
if (options.diagonalRule !== void 0) this.diagonalRule = options.diagonalRule;
|
|
13948
|
+
if (options.footprint !== void 0) this.footprintOption = options.footprint;
|
|
13949
|
+
if (options.rangeBands !== void 0) this.rangeBands = options.rangeBands;
|
|
13950
|
+
if (options.commitTapRadiusPx !== void 0) {
|
|
13951
|
+
this.commitTapRadiusPx = options.commitTapRadiusPx;
|
|
13952
|
+
}
|
|
13953
|
+
if (options.resolveStart !== void 0) this.resolveStart = options.resolveStart;
|
|
13954
|
+
this.notifyOptionsChange();
|
|
13955
|
+
}
|
|
13956
|
+
onOptionsChange(listener) {
|
|
13957
|
+
this.optionListeners.add(listener);
|
|
13958
|
+
return () => this.optionListeners.delete(listener);
|
|
13959
|
+
}
|
|
13960
|
+
/**
|
|
13961
|
+
* Subscribes to raf-coalesced path snapshots. While a path is open,
|
|
13962
|
+
* listeners receive at most one snapshot per animation frame carrying the
|
|
13963
|
+
* latest state; `null` is delivered synchronously when the path closes
|
|
13964
|
+
* (commit, cancel, or deactivate).
|
|
13965
|
+
*/
|
|
13966
|
+
onPath(listener) {
|
|
13967
|
+
this.pathListeners.add(listener);
|
|
13968
|
+
return () => this.pathListeners.delete(listener);
|
|
13969
|
+
}
|
|
13970
|
+
/**
|
|
13971
|
+
* Subscribes to finished paths. The emission carries `cursor: null` and the
|
|
13972
|
+
* final waypoints; applying the move (and recording history for it) is the
|
|
13973
|
+
* host's job — the tool has already forgotten the path by then.
|
|
13974
|
+
*/
|
|
13975
|
+
onCommit(listener) {
|
|
13976
|
+
this.commitListeners.add(listener);
|
|
13977
|
+
return () => this.commitListeners.delete(listener);
|
|
13978
|
+
}
|
|
13979
|
+
get isOpen() {
|
|
13980
|
+
return this.waypoints.length > 0;
|
|
13981
|
+
}
|
|
13982
|
+
/** Synchronous snapshot of the open path; `null` when no path is open. */
|
|
13983
|
+
getEmission() {
|
|
13984
|
+
if (!this.isOpen) return null;
|
|
13985
|
+
return this.buildEmission(this.cursor);
|
|
13986
|
+
}
|
|
13987
|
+
onPointerDown(state, ctx) {
|
|
13988
|
+
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
13989
|
+
if (!this.isOpen) {
|
|
13990
|
+
const anchor = this.resolveStart ? this.resolveStart(world, ctx) : { origin: world };
|
|
13991
|
+
if (!anchor) return;
|
|
13992
|
+
this.gridSize = ctx.gridSize ?? 0;
|
|
13993
|
+
this.gridType = ctx.gridType;
|
|
13994
|
+
this.hexOrientation = ctx.hexOrientation;
|
|
13995
|
+
this.snapEnabled = ctx.snapToGrid === true;
|
|
13996
|
+
this.footprint = anchor.footprint ?? this.footprintOption;
|
|
13997
|
+
const origin = this.snap(anchor.origin);
|
|
13998
|
+
this.waypoints = [origin];
|
|
13999
|
+
this.cursor = { ...origin };
|
|
14000
|
+
this.anchorKey = anchor.anchorKey;
|
|
14001
|
+
this.pointerDown = true;
|
|
14002
|
+
this.scheduleEmission();
|
|
14003
|
+
ctx.requestRender();
|
|
14004
|
+
return;
|
|
14005
|
+
}
|
|
14006
|
+
const point = this.snap(world);
|
|
14007
|
+
const last = this.lastWaypoint();
|
|
14008
|
+
if (last && this.withinCommitRadius(point, last, ctx)) {
|
|
14009
|
+
this.commitOnUp = true;
|
|
14010
|
+
this.cursor = { ...last };
|
|
14011
|
+
} else {
|
|
14012
|
+
this.cursor = point;
|
|
14013
|
+
}
|
|
14014
|
+
this.pointerDown = true;
|
|
14015
|
+
this.scheduleEmission();
|
|
14016
|
+
ctx.requestRender();
|
|
14017
|
+
}
|
|
14018
|
+
onPointerMove(state, ctx) {
|
|
14019
|
+
if (!this.isOpen) return;
|
|
14020
|
+
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
14021
|
+
const point = this.snap(world);
|
|
14022
|
+
const last = this.lastWaypoint();
|
|
14023
|
+
if (this.commitOnUp && last && this.withinCommitRadius(point, last, ctx)) {
|
|
14024
|
+
this.cursor = { ...last };
|
|
14025
|
+
} else {
|
|
14026
|
+
this.commitOnUp = false;
|
|
14027
|
+
this.cursor = point;
|
|
14028
|
+
}
|
|
14029
|
+
this.scheduleEmission();
|
|
14030
|
+
ctx.requestRender();
|
|
14031
|
+
}
|
|
14032
|
+
onPointerUp(_state, ctx) {
|
|
14033
|
+
if (!this.isOpen) return;
|
|
14034
|
+
this.pointerDown = false;
|
|
14035
|
+
if (this.commitOnUp) {
|
|
14036
|
+
this.commit(ctx);
|
|
14037
|
+
return;
|
|
14038
|
+
}
|
|
14039
|
+
const last = this.lastWaypoint();
|
|
14040
|
+
if (this.cursor && last && !samePoint2(this.cursor, last)) {
|
|
14041
|
+
this.waypoints.push({ ...this.cursor });
|
|
14042
|
+
}
|
|
14043
|
+
this.scheduleEmission();
|
|
14044
|
+
ctx.requestRender();
|
|
14045
|
+
}
|
|
14046
|
+
onHover(state, ctx) {
|
|
14047
|
+
if (!this.isOpen || this.pointerDown) return;
|
|
14048
|
+
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
14049
|
+
this.cursor = this.snap(world);
|
|
14050
|
+
this.scheduleEmission();
|
|
14051
|
+
ctx.requestRender();
|
|
14052
|
+
}
|
|
14053
|
+
/** A takeover (second pointer, platform cancel) abandons the path outright. */
|
|
14054
|
+
onPointerCancel(_state, ctx) {
|
|
14055
|
+
this.cancel(ctx);
|
|
14056
|
+
}
|
|
14057
|
+
onDeactivate(ctx) {
|
|
14058
|
+
this.cancel(ctx);
|
|
14059
|
+
}
|
|
14060
|
+
onKeyDown(event, ctx) {
|
|
14061
|
+
if (!this.isOpen) return false;
|
|
14062
|
+
if (event.key === "Enter") {
|
|
14063
|
+
this.commit(ctx);
|
|
14064
|
+
return true;
|
|
14065
|
+
}
|
|
14066
|
+
if (event.key === "Escape") {
|
|
14067
|
+
this.cancel(ctx);
|
|
14068
|
+
return true;
|
|
14069
|
+
}
|
|
14070
|
+
return false;
|
|
14071
|
+
}
|
|
14072
|
+
renderOverlay(ctx) {
|
|
14073
|
+
if (!this.isOpen) return;
|
|
14074
|
+
const { points, cumulative, total } = this.measure(this.cursor);
|
|
14075
|
+
const cumulativeFeet = cumulative.map((cells) => cells * this.feetPerCell);
|
|
14076
|
+
drawPath(ctx, {
|
|
14077
|
+
points,
|
|
14078
|
+
segmentColors: resolveSegmentColors(cumulativeFeet, this.rangeBands, this.color),
|
|
14079
|
+
color: this.color,
|
|
14080
|
+
feet: total * this.feetPerCell
|
|
14081
|
+
});
|
|
14082
|
+
}
|
|
14083
|
+
lastWaypoint() {
|
|
14084
|
+
return this.waypoints[this.waypoints.length - 1];
|
|
14085
|
+
}
|
|
14086
|
+
/**
|
|
14087
|
+
* Is `point` close enough to `last` to mean "finish here"? EXACT match always
|
|
14088
|
+
* qualifies. The screen-space tolerance is added ONLY when this path has no
|
|
14089
|
+
* active snapping grid: on a snapping grid (`gridType === 'square'`, or
|
|
14090
|
+
* `'hex'` with a captured orientation, and `gridSize > 0`) the adjacent
|
|
14091
|
+
* cell/hex centre can be as little as one grid cell away, and a radius
|
|
14092
|
+
* computed from CSS pixels has no relationship to that distance — at a
|
|
14093
|
+
* sufficiently zoomed-out camera the radius would swallow a whole neighbour
|
|
14094
|
+
* cell and turn a deliberate next-waypoint tap into an unwanted commit.
|
|
14095
|
+
* Snapping already makes a fingertip tap land exactly on the last waypoint,
|
|
14096
|
+
* so the tolerance is unnecessary there; it exists for the gridless and
|
|
14097
|
+
* `snapPoint`/identity paths, where a fingertip can never land on an exact
|
|
14098
|
+
* world point.
|
|
14099
|
+
*/
|
|
14100
|
+
withinCommitRadius(point, last, ctx) {
|
|
14101
|
+
if (samePoint2(point, last)) return true;
|
|
14102
|
+
if (this.hasSnappingGrid()) return false;
|
|
14103
|
+
const zoom = ctx.camera.zoom;
|
|
14104
|
+
if (!(zoom > 0)) return false;
|
|
14105
|
+
const radius = this.commitTapRadiusPx / zoom;
|
|
14106
|
+
if (radius <= 0) return false;
|
|
14107
|
+
const dx = point.x - last.x;
|
|
14108
|
+
const dy = point.y - last.y;
|
|
14109
|
+
return dx * dx + dy * dy <= radius * radius;
|
|
14110
|
+
}
|
|
14111
|
+
/**
|
|
14112
|
+
* True when this path's captured grid state snaps every waypoint onto a
|
|
14113
|
+
* cell/hex centre: a `square` grid, or a `hex` grid with a captured
|
|
14114
|
+
* orientation, both with a usable `gridSize`. Mirrors the unconditional
|
|
14115
|
+
* branches of `snap` below — NOT the `snapToGrid`-gated fallback, which
|
|
14116
|
+
* leaves waypoints unsnapped when the user turns snapping off.
|
|
14117
|
+
*/
|
|
14118
|
+
hasSnappingGrid() {
|
|
14119
|
+
if (!(this.gridSize > 0)) return false;
|
|
14120
|
+
if (this.gridType === "square") return true;
|
|
14121
|
+
return this.gridType === "hex" && this.hexOrientation !== void 0;
|
|
14122
|
+
}
|
|
14123
|
+
/**
|
|
14124
|
+
* Snapping to cell or hex centres is UNCONDITIONAL for a `square` grid, or a
|
|
14125
|
+
* `hex` grid WITH a captured orientation: a movement path measures in cells,
|
|
14126
|
+
* so an unsnapped waypoint would report a distance the grid does not agree
|
|
14127
|
+
* with. A `hex` grid WITHOUT an orientation falls through to the
|
|
14128
|
+
* `snapToGrid`-gated `snapPoint` (intersection) branch below, same as the
|
|
14129
|
+
* gridType-less case — unlike `smartSnap`, which is off entirely when the
|
|
14130
|
+
* user turns snapping off.
|
|
14131
|
+
*/
|
|
14132
|
+
snap(point) {
|
|
14133
|
+
if (this.gridSize <= 0) return point;
|
|
14134
|
+
if (this.gridType === "hex" && this.hexOrientation) {
|
|
14135
|
+
return snapToHexCenter(point, this.gridSize, this.hexOrientation);
|
|
14136
|
+
}
|
|
14137
|
+
if (this.gridType === "square") {
|
|
14138
|
+
return snapToCellCenter(point, this.gridSize, this.footprint);
|
|
14139
|
+
}
|
|
14140
|
+
if (this.snapEnabled) {
|
|
14141
|
+
return snapPoint(point, this.gridSize);
|
|
14142
|
+
}
|
|
14143
|
+
return point;
|
|
14144
|
+
}
|
|
14145
|
+
/** The measured polyline: waypoints, plus the cursor when it adds a leg. */
|
|
14146
|
+
measure(cursor) {
|
|
14147
|
+
const points = this.waypoints.map((p) => ({ ...p }));
|
|
14148
|
+
const last = points[points.length - 1];
|
|
14149
|
+
if (cursor && (!last || !samePoint2(cursor, last))) points.push({ ...cursor });
|
|
14150
|
+
const { total, cumulative } = pathDistanceCells(points, {
|
|
14151
|
+
gridSize: this.gridSize,
|
|
14152
|
+
gridType: this.gridType,
|
|
14153
|
+
hexOrientation: this.hexOrientation,
|
|
14154
|
+
diagonalRule: this.diagonalRule
|
|
14155
|
+
});
|
|
14156
|
+
return { points, cumulative, total };
|
|
14157
|
+
}
|
|
14158
|
+
buildEmission(cursor) {
|
|
14159
|
+
const { cumulative, total } = this.measure(cursor);
|
|
14160
|
+
const segments = [];
|
|
14161
|
+
for (let i = 1; i < cumulative.length; i++) {
|
|
14162
|
+
const cells = (cumulative[i] ?? 0) - (cumulative[i - 1] ?? 0);
|
|
14163
|
+
segments.push({ cells, feet: cells * this.feetPerCell });
|
|
14164
|
+
}
|
|
14165
|
+
return {
|
|
14166
|
+
anchorKey: this.anchorKey,
|
|
14167
|
+
waypoints: this.waypoints.map((p) => ({ ...p })),
|
|
14168
|
+
cursor: cursor ? { ...cursor } : null,
|
|
14169
|
+
segments,
|
|
14170
|
+
totalCells: total,
|
|
14171
|
+
totalFeet: total * this.feetPerCell,
|
|
14172
|
+
color: this.color,
|
|
14173
|
+
rangeBands: this.rangeBands
|
|
14174
|
+
};
|
|
14175
|
+
}
|
|
14176
|
+
commit(ctx) {
|
|
14177
|
+
if (this.waypoints.length < 2) {
|
|
14178
|
+
this.cancel(ctx);
|
|
14179
|
+
return;
|
|
14180
|
+
}
|
|
14181
|
+
const emission = this.buildEmission(null);
|
|
14182
|
+
this.reset();
|
|
14183
|
+
this.emitCommit(emission);
|
|
14184
|
+
this.emitClear();
|
|
14185
|
+
ctx.requestRender();
|
|
14186
|
+
}
|
|
14187
|
+
cancel(ctx) {
|
|
14188
|
+
if (!this.isOpen) return;
|
|
14189
|
+
this.reset();
|
|
14190
|
+
this.emitClear();
|
|
14191
|
+
ctx.requestRender();
|
|
14192
|
+
}
|
|
14193
|
+
reset() {
|
|
14194
|
+
this.waypoints = [];
|
|
14195
|
+
this.cursor = null;
|
|
14196
|
+
this.anchorKey = void 0;
|
|
14197
|
+
this.footprint = this.footprintOption;
|
|
14198
|
+
this.pointerDown = false;
|
|
14199
|
+
this.commitOnUp = false;
|
|
14200
|
+
}
|
|
14201
|
+
notifyOptionsChange() {
|
|
14202
|
+
for (const listener of this.optionListeners) listener();
|
|
14203
|
+
}
|
|
14204
|
+
scheduleEmission() {
|
|
14205
|
+
if (this.pathListeners.size === 0) return;
|
|
14206
|
+
if (this.emissionRafId !== null) return;
|
|
14207
|
+
this.emissionRafId = requestAnimationFrame(() => {
|
|
14208
|
+
this.emissionRafId = null;
|
|
14209
|
+
const emission = this.getEmission();
|
|
14210
|
+
if (!emission) return;
|
|
14211
|
+
this.emitPath(emission);
|
|
14212
|
+
});
|
|
14213
|
+
}
|
|
14214
|
+
emitClear() {
|
|
14215
|
+
if (this.emissionRafId !== null) {
|
|
14216
|
+
cancelAnimationFrame(this.emissionRafId);
|
|
14217
|
+
this.emissionRafId = null;
|
|
14218
|
+
}
|
|
14219
|
+
if (this.pathListeners.size === 0) return;
|
|
14220
|
+
this.emitPath(null);
|
|
14221
|
+
}
|
|
14222
|
+
emitPath(emission) {
|
|
14223
|
+
for (const listener of this.pathListeners) {
|
|
14224
|
+
try {
|
|
14225
|
+
listener(emission);
|
|
14226
|
+
} catch {
|
|
14227
|
+
}
|
|
14228
|
+
}
|
|
14229
|
+
}
|
|
14230
|
+
emitCommit(emission) {
|
|
14231
|
+
for (const listener of this.commitListeners) {
|
|
14232
|
+
try {
|
|
14233
|
+
listener(emission);
|
|
14234
|
+
} catch {
|
|
14235
|
+
}
|
|
14236
|
+
}
|
|
14237
|
+
}
|
|
14238
|
+
};
|
|
14239
|
+
|
|
13543
14240
|
// src/tools/template-tool.ts
|
|
13544
14241
|
var MIN_RECT_WIDTH = 20;
|
|
13545
14242
|
function defaultRectWidth(radius, scaleUnit) {
|
|
@@ -13833,7 +14530,7 @@ var TemplateTool = class {
|
|
|
13833
14530
|
};
|
|
13834
14531
|
|
|
13835
14532
|
// src/tools/laser-tool.ts
|
|
13836
|
-
var
|
|
14533
|
+
var DEFAULT_COLOR6 = "#ff3b30";
|
|
13837
14534
|
var DEFAULT_WIDTH3 = 4;
|
|
13838
14535
|
var DEFAULT_FADE_MS3 = 1200;
|
|
13839
14536
|
var LaserTool = class {
|
|
@@ -13849,7 +14546,7 @@ var LaserTool = class {
|
|
|
13849
14546
|
pendingEmission = [];
|
|
13850
14547
|
constructor(options = {}) {
|
|
13851
14548
|
this.name = options.name ?? "laser";
|
|
13852
|
-
this.color = options.color ??
|
|
14549
|
+
this.color = options.color ?? DEFAULT_COLOR6;
|
|
13853
14550
|
this.width = options.width ?? DEFAULT_WIDTH3;
|
|
13854
14551
|
this.fadeMs = options.fadeMs ?? DEFAULT_FADE_MS3;
|
|
13855
14552
|
}
|
|
@@ -13977,7 +14674,7 @@ var LaserTool = class {
|
|
|
13977
14674
|
};
|
|
13978
14675
|
|
|
13979
14676
|
// src/tools/ping-tool.ts
|
|
13980
|
-
var
|
|
14677
|
+
var DEFAULT_COLOR7 = "#ff3b30";
|
|
13981
14678
|
var DEFAULT_DURATION_MS4 = 1800;
|
|
13982
14679
|
var DEFAULT_RADIUS4 = 48;
|
|
13983
14680
|
var DEFAULT_MIN_INTERVAL_MS2 = 300;
|
|
@@ -13994,7 +14691,7 @@ var PingTool = class {
|
|
|
13994
14691
|
pingListeners = /* @__PURE__ */ new Set();
|
|
13995
14692
|
constructor(options = {}) {
|
|
13996
14693
|
this.name = options.name ?? "ping";
|
|
13997
|
-
this.color = options.color ??
|
|
14694
|
+
this.color = options.color ?? DEFAULT_COLOR7;
|
|
13998
14695
|
this.durationMs = options.durationMs ?? DEFAULT_DURATION_MS4;
|
|
13999
14696
|
this.radius = options.radius ?? DEFAULT_RADIUS4;
|
|
14000
14697
|
this.minIntervalMs = options.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS2;
|
|
@@ -14097,7 +14794,7 @@ var PingTool = class {
|
|
|
14097
14794
|
};
|
|
14098
14795
|
|
|
14099
14796
|
// src/index.ts
|
|
14100
|
-
var VERSION = "0.
|
|
14797
|
+
var VERSION = "0.64.0";
|
|
14101
14798
|
// Annotate the CommonJS export names for ESM import in node:
|
|
14102
14799
|
0 && (module.exports = {
|
|
14103
14800
|
ArrowTool,
|
|
@@ -14124,13 +14821,17 @@ var VERSION = "0.63.0";
|
|
|
14124
14821
|
MemoryAdapter,
|
|
14125
14822
|
MinimapController,
|
|
14126
14823
|
NoteTool,
|
|
14824
|
+
PATH_PRESENCE_KIND,
|
|
14825
|
+
PATH_PRESENCE_MAX_POINTS,
|
|
14127
14826
|
PING_PRESENCE_KIND,
|
|
14827
|
+
PathTool,
|
|
14128
14828
|
PencilTool,
|
|
14129
14829
|
PingInput,
|
|
14130
14830
|
PingTool,
|
|
14131
14831
|
RemoteFocusReceiver,
|
|
14132
14832
|
RemoteLaserOverlay,
|
|
14133
14833
|
RemoteMeasureOverlay,
|
|
14834
|
+
RemotePathOverlay,
|
|
14134
14835
|
RemotePingOverlay,
|
|
14135
14836
|
SelectTool,
|
|
14136
14837
|
ShapeTool,
|
|
@@ -14158,6 +14859,7 @@ var VERSION = "0.63.0";
|
|
|
14158
14859
|
exportImage,
|
|
14159
14860
|
exportSvg,
|
|
14160
14861
|
fitZoomForView,
|
|
14862
|
+
footprintFromSize,
|
|
14161
14863
|
getActiveFormats,
|
|
14162
14864
|
getArrowBounds,
|
|
14163
14865
|
getArrowControlPoint,
|
|
@@ -14173,20 +14875,26 @@ var VERSION = "0.63.0";
|
|
|
14173
14875
|
getHexCellsInRectangle,
|
|
14174
14876
|
getHexCellsInSquare,
|
|
14175
14877
|
getHexDistance,
|
|
14878
|
+
gridDistanceCells,
|
|
14176
14879
|
isFocusPresence,
|
|
14177
14880
|
isLaserTrailPresence,
|
|
14178
14881
|
isMeasurePresence,
|
|
14179
14882
|
isNearBezier,
|
|
14883
|
+
isPathPresence,
|
|
14180
14884
|
isPingPresence,
|
|
14885
|
+
pathDistanceCells,
|
|
14181
14886
|
resolveHtmlRouting,
|
|
14182
14887
|
setFontSize,
|
|
14183
14888
|
smartSnap,
|
|
14889
|
+
snapFootprintCenter,
|
|
14184
14890
|
snapPoint,
|
|
14891
|
+
snapToCellCenter,
|
|
14185
14892
|
snapToHexCenter,
|
|
14186
14893
|
styleToPatch,
|
|
14187
14894
|
toFocusPresence,
|
|
14188
14895
|
toLaserTrailPresence,
|
|
14189
14896
|
toMeasurePresence,
|
|
14897
|
+
toPathPresence,
|
|
14190
14898
|
toPingPresence,
|
|
14191
14899
|
toggleBold,
|
|
14192
14900
|
toggleItalic,
|