@fieldnotes/core 0.64.0 → 0.66.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 +2105 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +623 -19
- package/dist/index.d.ts +623 -19
- package/dist/index.js +2081 -37
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -454,8 +454,464 @@ function sanitizeAttributes(el, tag) {
|
|
|
454
454
|
}
|
|
455
455
|
}
|
|
456
456
|
|
|
457
|
+
// src/fog/types.ts
|
|
458
|
+
var FOG_STATE_VERSION = 1;
|
|
459
|
+
var FOG_TILE_CELLS = 128;
|
|
460
|
+
var FOG_MAX_TILES = 256;
|
|
461
|
+
|
|
462
|
+
// src/fog/tile-codec.ts
|
|
463
|
+
var TILE_BYTES = FOG_TILE_CELLS * FOG_TILE_CELLS / 8;
|
|
464
|
+
var CANONICAL_B64_LENGTH = Math.ceil(TILE_BYTES / 3) * 4;
|
|
465
|
+
var B64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
466
|
+
var B64_LOOKUP = new Uint8Array(128);
|
|
467
|
+
for (let i = 0; i < B64_CHARS.length; i++) B64_LOOKUP[B64_CHARS.charCodeAt(i)] = i;
|
|
468
|
+
function encodeBase64(bytes) {
|
|
469
|
+
let result = "";
|
|
470
|
+
const len = bytes.length;
|
|
471
|
+
for (let i = 0; i < len; i += 3) {
|
|
472
|
+
const a = bytes[i];
|
|
473
|
+
const b = i + 1 < len ? bytes[i + 1] : 0;
|
|
474
|
+
const c = i + 2 < len ? bytes[i + 2] : 0;
|
|
475
|
+
result += B64_CHARS[a >> 2 & 63];
|
|
476
|
+
result += B64_CHARS[(a << 4 | b >> 4) & 63];
|
|
477
|
+
result += i + 1 < len ? B64_CHARS[(b << 2 | c >> 6) & 63] : "=";
|
|
478
|
+
result += i + 2 < len ? B64_CHARS[c & 63] : "=";
|
|
479
|
+
}
|
|
480
|
+
return result;
|
|
481
|
+
}
|
|
482
|
+
function decodeBase64(str) {
|
|
483
|
+
if (str.length % 4 !== 0) throw new Error("Invalid base64 length");
|
|
484
|
+
let padCount = 0;
|
|
485
|
+
if (str.length >= 2 && str[str.length - 1] === "=") {
|
|
486
|
+
padCount++;
|
|
487
|
+
if (str[str.length - 2] === "=") padCount++;
|
|
488
|
+
}
|
|
489
|
+
const byteLen = str.length / 4 * 3 - padCount;
|
|
490
|
+
const bytes = new Uint8Array(byteLen);
|
|
491
|
+
let j = 0;
|
|
492
|
+
for (let i = 0; i < str.length; i += 4) {
|
|
493
|
+
const a = B64_LOOKUP[str.charCodeAt(i)];
|
|
494
|
+
const b = B64_LOOKUP[str.charCodeAt(i + 1)];
|
|
495
|
+
const c = str[i + 2] === "=" ? 0 : B64_LOOKUP[str.charCodeAt(i + 2)];
|
|
496
|
+
const d = str[i + 3] === "=" ? 0 : B64_LOOKUP[str.charCodeAt(i + 3)];
|
|
497
|
+
bytes[j++] = a << 2 | b >> 4;
|
|
498
|
+
if (j < byteLen) bytes[j++] = (b << 4 | c >> 2) & 255;
|
|
499
|
+
if (j < byteLen) bytes[j++] = (c << 6 | d) & 255;
|
|
500
|
+
}
|
|
501
|
+
return bytes;
|
|
502
|
+
}
|
|
503
|
+
function createTileBytes(fill) {
|
|
504
|
+
const bytes = new Uint8Array(TILE_BYTES);
|
|
505
|
+
if (fill) bytes.fill(255);
|
|
506
|
+
return bytes;
|
|
507
|
+
}
|
|
508
|
+
function setBit(bytes, col, row, value) {
|
|
509
|
+
const index = row * FOG_TILE_CELLS + col;
|
|
510
|
+
const byteIndex = index >> 3;
|
|
511
|
+
const bitIndex = 7 - (index & 7);
|
|
512
|
+
if (value) {
|
|
513
|
+
bytes[byteIndex] = bytes[byteIndex] | 1 << bitIndex;
|
|
514
|
+
} else {
|
|
515
|
+
bytes[byteIndex] = bytes[byteIndex] & ~(1 << bitIndex);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
function isTileAllValue(bytes, value) {
|
|
519
|
+
const expected = value ? 255 : 0;
|
|
520
|
+
for (let i = 0; i < TILE_BYTES; i++) {
|
|
521
|
+
if (bytes[i] !== expected) return false;
|
|
522
|
+
}
|
|
523
|
+
return true;
|
|
524
|
+
}
|
|
525
|
+
function isBaseValue(base) {
|
|
526
|
+
return base === "revealed";
|
|
527
|
+
}
|
|
528
|
+
function isTileBase(bytes, base) {
|
|
529
|
+
return isTileAllValue(bytes, isBaseValue(base));
|
|
530
|
+
}
|
|
531
|
+
function canonicalizeEdgePadding(bytes, def, tileX, tileY) {
|
|
532
|
+
const baseVal = isBaseValue(def.base);
|
|
533
|
+
const worldX = tileX * FOG_TILE_CELLS * def.cellSize;
|
|
534
|
+
const worldY = tileY * FOG_TILE_CELLS * def.cellSize;
|
|
535
|
+
const boundsRight = def.bounds.x + def.bounds.w;
|
|
536
|
+
const boundsBottom = def.bounds.y + def.bounds.h;
|
|
537
|
+
for (let row = 0; row < FOG_TILE_CELLS; row++) {
|
|
538
|
+
for (let col = 0; col < FOG_TILE_CELLS; col++) {
|
|
539
|
+
const cellWorldX = worldX + col * def.cellSize;
|
|
540
|
+
const cellWorldY = worldY + row * def.cellSize;
|
|
541
|
+
const outside = cellWorldX < def.bounds.x || cellWorldY < def.bounds.y || cellWorldX >= boundsRight || cellWorldY >= boundsBottom;
|
|
542
|
+
if (outside) {
|
|
543
|
+
setBit(bytes, col, row, baseVal);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function canonicalizeFogTile(tile, def) {
|
|
549
|
+
const bytes = decodeBase64(tile.data);
|
|
550
|
+
canonicalizeEdgePadding(bytes, def, tile.x, tile.y);
|
|
551
|
+
if (isTileBase(bytes, def.base)) return null;
|
|
552
|
+
return { x: tile.x, y: tile.y, data: encodeBase64(bytes) };
|
|
553
|
+
}
|
|
554
|
+
function isCanonicalBase64(str) {
|
|
555
|
+
if (str.length !== CANONICAL_B64_LENGTH) return false;
|
|
556
|
+
if (str[str.length - 1] !== "=" || str[str.length - 2] === "=") return false;
|
|
557
|
+
for (let i = 0; i < str.length - 1; i++) {
|
|
558
|
+
if (!B64_CHARS.includes(str[i])) return false;
|
|
559
|
+
}
|
|
560
|
+
const finalSextet = B64_CHARS.indexOf(str[str.length - 2]);
|
|
561
|
+
return finalSextet >= 0 && (finalSextet & 3) === 0;
|
|
562
|
+
}
|
|
563
|
+
function isSafeInteger(n2) {
|
|
564
|
+
return typeof n2 === "number" && Number.isSafeInteger(n2);
|
|
565
|
+
}
|
|
566
|
+
function isFinitePositive(n2) {
|
|
567
|
+
return typeof n2 === "number" && Number.isFinite(n2) && n2 > 0;
|
|
568
|
+
}
|
|
569
|
+
function isFiniteBounds(b) {
|
|
570
|
+
if (typeof b !== "object" || b === null) return false;
|
|
571
|
+
const r = b;
|
|
572
|
+
return typeof r["x"] === "number" && Number.isFinite(r["x"]) && typeof r["y"] === "number" && Number.isFinite(r["y"]) && isFinitePositive(r["w"]) && isFinitePositive(r["h"]);
|
|
573
|
+
}
|
|
574
|
+
function tileIntersectsBounds(x, y, def) {
|
|
575
|
+
const tileWorldX = x * FOG_TILE_CELLS * def.cellSize;
|
|
576
|
+
const tileWorldY = y * FOG_TILE_CELLS * def.cellSize;
|
|
577
|
+
const tileWorldW = FOG_TILE_CELLS * def.cellSize;
|
|
578
|
+
const tileWorldH = FOG_TILE_CELLS * def.cellSize;
|
|
579
|
+
return !(tileWorldX + tileWorldW <= def.bounds.x || tileWorldY + tileWorldH <= def.bounds.y || tileWorldX >= def.bounds.x + def.bounds.w || tileWorldY >= def.bounds.y + def.bounds.h);
|
|
580
|
+
}
|
|
581
|
+
function validateFogDefinition(def) {
|
|
582
|
+
if (typeof def !== "object" || def === null) {
|
|
583
|
+
throw new Error("Invalid fog definition: expected an object");
|
|
584
|
+
}
|
|
585
|
+
const d = def;
|
|
586
|
+
if (d["version"] !== 1) throw new Error("Invalid fog definition: unsupported version");
|
|
587
|
+
if (typeof d["generation"] !== "string" || d["generation"].length === 0 || d["generation"].length > 128 || !/^[\x20-\x7e]+$/.test(d["generation"])) {
|
|
588
|
+
throw new Error("Invalid fog definition: invalid generation");
|
|
589
|
+
}
|
|
590
|
+
if (!isFiniteBounds(d["bounds"])) {
|
|
591
|
+
throw new Error("Invalid fog definition: invalid bounds");
|
|
592
|
+
}
|
|
593
|
+
if (!isFinitePositive(d["cellSize"])) {
|
|
594
|
+
throw new Error("Invalid fog definition: invalid cellSize");
|
|
595
|
+
}
|
|
596
|
+
if (d["tileCells"] !== 128) {
|
|
597
|
+
throw new Error("Invalid fog definition: tileCells must be 128");
|
|
598
|
+
}
|
|
599
|
+
if (d["base"] !== "covered" && d["base"] !== "revealed") {
|
|
600
|
+
throw new Error("Invalid fog definition: invalid base");
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
function validateFogTile(tile, def) {
|
|
604
|
+
if (typeof tile !== "object" || tile === null) {
|
|
605
|
+
throw new Error("Invalid fog tile: expected an object");
|
|
606
|
+
}
|
|
607
|
+
const t = tile;
|
|
608
|
+
if (!isSafeInteger(t["x"]) || !isSafeInteger(t["y"])) {
|
|
609
|
+
throw new Error("Invalid fog tile: coordinates must be safe integers");
|
|
610
|
+
}
|
|
611
|
+
if (!tileIntersectsBounds(t["x"], t["y"], def)) {
|
|
612
|
+
throw new Error("Invalid fog tile: coordinates outside bounds");
|
|
613
|
+
}
|
|
614
|
+
if (typeof t["data"] !== "string" || !isCanonicalBase64(t["data"])) {
|
|
615
|
+
throw new Error("Invalid fog tile: invalid data");
|
|
616
|
+
}
|
|
617
|
+
const decoded = decodeBase64(t["data"]);
|
|
618
|
+
if (decoded.length !== TILE_BYTES) {
|
|
619
|
+
throw new Error("Invalid fog tile: decoded data wrong length");
|
|
620
|
+
}
|
|
621
|
+
if (isTileBase(decoded, def.base)) {
|
|
622
|
+
throw new Error("Invalid fog tile: base-value tiles must be omitted");
|
|
623
|
+
}
|
|
624
|
+
const canonical = new Uint8Array(decoded);
|
|
625
|
+
canonicalizeEdgePadding(canonical, def, t["x"], t["y"]);
|
|
626
|
+
for (let i = 0; i < decoded.length; i++) {
|
|
627
|
+
if (decoded[i] !== canonical[i]) {
|
|
628
|
+
throw new Error("Invalid fog tile: non-canonical edge padding");
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
function validateFogState(state) {
|
|
633
|
+
if (typeof state !== "object" || state === null) {
|
|
634
|
+
throw new Error("Invalid fog state: expected an object");
|
|
635
|
+
}
|
|
636
|
+
const s = state;
|
|
637
|
+
validateFogDefinition(s["definition"]);
|
|
638
|
+
const def = s["definition"];
|
|
639
|
+
if (!Array.isArray(s["tiles"])) {
|
|
640
|
+
throw new Error("Invalid fog state: tiles must be an array");
|
|
641
|
+
}
|
|
642
|
+
const tiles = s["tiles"];
|
|
643
|
+
if (tiles.length > FOG_MAX_TILES) {
|
|
644
|
+
throw new Error(`Invalid fog state: too many tiles (${tiles.length} > ${FOG_MAX_TILES})`);
|
|
645
|
+
}
|
|
646
|
+
const seen = /* @__PURE__ */ new Set();
|
|
647
|
+
for (const tile of tiles) {
|
|
648
|
+
validateFogTile(tile, def);
|
|
649
|
+
const t = tile;
|
|
650
|
+
const key = `${t.x},${t.y}`;
|
|
651
|
+
if (seen.has(key)) {
|
|
652
|
+
throw new Error(`Invalid fog state: duplicate tile at (${t.x}, ${t.y})`);
|
|
653
|
+
}
|
|
654
|
+
seen.add(key);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
function recommendedFogCellSize(bounds) {
|
|
658
|
+
let cellSize = 1;
|
|
659
|
+
while (true) {
|
|
660
|
+
const tileWorldSize = FOG_TILE_CELLS * cellSize;
|
|
661
|
+
const minTX = Math.floor(bounds.x / tileWorldSize);
|
|
662
|
+
const minTY = Math.floor(bounds.y / tileWorldSize);
|
|
663
|
+
const maxTX = Math.ceil((bounds.x + bounds.w) / tileWorldSize) - 1;
|
|
664
|
+
const maxTY = Math.ceil((bounds.y + bounds.h) / tileWorldSize) - 1;
|
|
665
|
+
const tw = maxTX - minTX + 1;
|
|
666
|
+
const th = maxTY - minTY + 1;
|
|
667
|
+
if (tw * th <= FOG_MAX_TILES) return cellSize;
|
|
668
|
+
cellSize++;
|
|
669
|
+
if (cellSize > 1e4) return cellSize;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
function tileKey(x, y) {
|
|
673
|
+
return `${x},${y}`;
|
|
674
|
+
}
|
|
675
|
+
function worldToCell(worldX, worldY, cellSize) {
|
|
676
|
+
const cellX = Math.floor(worldX / cellSize);
|
|
677
|
+
const cellY = Math.floor(worldY / cellSize);
|
|
678
|
+
const tx = Math.floor(cellX / FOG_TILE_CELLS);
|
|
679
|
+
const ty = Math.floor(cellY / FOG_TILE_CELLS);
|
|
680
|
+
let col = cellX - tx * FOG_TILE_CELLS;
|
|
681
|
+
let row = cellY - ty * FOG_TILE_CELLS;
|
|
682
|
+
if (col < 0) col += FOG_TILE_CELLS;
|
|
683
|
+
if (row < 0) row += FOG_TILE_CELLS;
|
|
684
|
+
return { tx, ty, col, row };
|
|
685
|
+
}
|
|
686
|
+
function rasterizeRegion(state, region, operation) {
|
|
687
|
+
const { definition } = state;
|
|
688
|
+
const bitValue = operation === "reveal";
|
|
689
|
+
const tileMap = /* @__PURE__ */ new Map();
|
|
690
|
+
for (const tile of state.tiles) {
|
|
691
|
+
tileMap.set(tileKey(tile.x, tile.y), decodeBase64(tile.data));
|
|
692
|
+
}
|
|
693
|
+
const baseFill = isBaseValue(definition.base);
|
|
694
|
+
const affectedTiles = /* @__PURE__ */ new Set();
|
|
695
|
+
const setCellIfInBounds = (worldX, worldY) => {
|
|
696
|
+
if (worldX < definition.bounds.x || worldY < definition.bounds.y || worldX >= definition.bounds.x + definition.bounds.w || worldY >= definition.bounds.y + definition.bounds.h) {
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
const { tx, ty, col, row } = worldToCell(worldX, worldY, definition.cellSize);
|
|
700
|
+
if (!tileIntersectsBounds(tx, ty, definition)) return;
|
|
701
|
+
const key = tileKey(tx, ty);
|
|
702
|
+
let bytes = tileMap.get(key);
|
|
703
|
+
if (!bytes) {
|
|
704
|
+
bytes = createTileBytes(baseFill);
|
|
705
|
+
tileMap.set(key, bytes);
|
|
706
|
+
} else if (!affectedTiles.has(key)) {
|
|
707
|
+
const clone = new Uint8Array(bytes);
|
|
708
|
+
tileMap.set(key, clone);
|
|
709
|
+
bytes = clone;
|
|
710
|
+
}
|
|
711
|
+
affectedTiles.add(key);
|
|
712
|
+
setBit(bytes, col, row, bitValue);
|
|
713
|
+
};
|
|
714
|
+
switch (region.kind) {
|
|
715
|
+
case "brush":
|
|
716
|
+
rasterizeBrush(region.points, region.radius, definition, setCellIfInBounds);
|
|
717
|
+
break;
|
|
718
|
+
case "rectangle":
|
|
719
|
+
rasterizeRectangle(region.from, region.to, definition, setCellIfInBounds);
|
|
720
|
+
break;
|
|
721
|
+
case "polygon":
|
|
722
|
+
rasterizePolygon(region.points, definition, setCellIfInBounds);
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
725
|
+
if (affectedTiles.size === 0) return { changed: [], noop: true };
|
|
726
|
+
const changedTiles = [];
|
|
727
|
+
let hasChange = false;
|
|
728
|
+
for (const key of affectedTiles) {
|
|
729
|
+
const bytes = tileMap.get(key);
|
|
730
|
+
const [txStr, tyStr] = key.split(",");
|
|
731
|
+
const tx = Number(txStr);
|
|
732
|
+
const ty = Number(tyStr);
|
|
733
|
+
canonicalizeEdgePadding(bytes, definition, tx, ty);
|
|
734
|
+
const newData = encodeBase64(bytes);
|
|
735
|
+
const originalTile = state.tiles.find((t) => t.x === tx && t.y === ty);
|
|
736
|
+
if (originalTile) {
|
|
737
|
+
if (originalTile.data !== newData) {
|
|
738
|
+
hasChange = true;
|
|
739
|
+
changedTiles.push({ x: tx, y: ty, data: newData });
|
|
740
|
+
}
|
|
741
|
+
} else if (!isTileBase(bytes, definition.base)) {
|
|
742
|
+
hasChange = true;
|
|
743
|
+
changedTiles.push({ x: tx, y: ty, data: newData });
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
if (!hasChange) return { changed: [], noop: true };
|
|
747
|
+
return { changed: changedTiles, noop: false };
|
|
748
|
+
}
|
|
749
|
+
function applyRasterResult(state, result) {
|
|
750
|
+
if (result.noop) return state;
|
|
751
|
+
const changedMap = /* @__PURE__ */ new Map();
|
|
752
|
+
for (const t of result.changed) changedMap.set(tileKey(t.x, t.y), t);
|
|
753
|
+
const tiles = [];
|
|
754
|
+
const seen = /* @__PURE__ */ new Set();
|
|
755
|
+
for (const tile of state.tiles) {
|
|
756
|
+
const key = tileKey(tile.x, tile.y);
|
|
757
|
+
seen.add(key);
|
|
758
|
+
const changed = changedMap.get(key);
|
|
759
|
+
if (changed) {
|
|
760
|
+
const bytes = decodeBase64(changed.data);
|
|
761
|
+
if (!isTileBase(bytes, state.definition.base)) {
|
|
762
|
+
tiles.push(changed);
|
|
763
|
+
}
|
|
764
|
+
} else {
|
|
765
|
+
tiles.push(tile);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
for (const t of result.changed) {
|
|
769
|
+
const key = tileKey(t.x, t.y);
|
|
770
|
+
if (seen.has(key)) continue;
|
|
771
|
+
const bytes = decodeBase64(t.data);
|
|
772
|
+
if (!isTileBase(bytes, state.definition.base)) {
|
|
773
|
+
tiles.push(t);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
if (tiles.length > FOG_MAX_TILES) {
|
|
777
|
+
throw new Error(`Fog tile cap exceeded: ${tiles.length} > ${FOG_MAX_TILES}`);
|
|
778
|
+
}
|
|
779
|
+
return { definition: state.definition, tiles };
|
|
780
|
+
}
|
|
781
|
+
function sampleAndSimplify(points, tolerance) {
|
|
782
|
+
if (points.length <= 2) return [...points];
|
|
783
|
+
const sampled = [points[0]];
|
|
784
|
+
let lastSampled = points[0];
|
|
785
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
786
|
+
const p = points[i];
|
|
787
|
+
const dx = p.x - lastSampled.x;
|
|
788
|
+
const dy = p.y - lastSampled.y;
|
|
789
|
+
if (dx * dx + dy * dy >= tolerance * tolerance) {
|
|
790
|
+
sampled.push(p);
|
|
791
|
+
lastSampled = p;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
sampled.push(points[points.length - 1]);
|
|
795
|
+
return sampled;
|
|
796
|
+
}
|
|
797
|
+
function rasterizeBrush(points, radius, def, setCell) {
|
|
798
|
+
if (points.length === 0) return;
|
|
799
|
+
const simplified = sampleAndSimplify(points, def.cellSize * 0.5);
|
|
800
|
+
for (let i = 0; i < simplified.length; i++) {
|
|
801
|
+
const p = simplified[i];
|
|
802
|
+
rasterizeDisc(p.x, p.y, radius, def, setCell);
|
|
803
|
+
if (i > 0) {
|
|
804
|
+
const prev = simplified[i - 1];
|
|
805
|
+
rasterizeCapsuleSegment(prev.x, prev.y, p.x, p.y, radius, def, setCell);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
function rasterizeDisc(cx, cy, radius, def, setCell) {
|
|
810
|
+
const minX = Math.max(def.bounds.x, cx - radius);
|
|
811
|
+
const maxX = Math.min(def.bounds.x + def.bounds.w - 1, cx + radius);
|
|
812
|
+
const minY = Math.max(def.bounds.y, cy - radius);
|
|
813
|
+
const maxY = Math.min(def.bounds.y + def.bounds.h - 1, cy + radius);
|
|
814
|
+
const startCol = Math.floor(minX / def.cellSize) * def.cellSize;
|
|
815
|
+
const startRow = Math.floor(minY / def.cellSize) * def.cellSize;
|
|
816
|
+
const r2 = radius * radius;
|
|
817
|
+
for (let wy = startRow; wy <= maxY; wy += def.cellSize) {
|
|
818
|
+
for (let wx = startCol; wx <= maxX; wx += def.cellSize) {
|
|
819
|
+
const cellCenterX = wx + def.cellSize * 0.5;
|
|
820
|
+
const cellCenterY = wy + def.cellSize * 0.5;
|
|
821
|
+
const dx = cellCenterX - cx;
|
|
822
|
+
const dy = cellCenterY - cy;
|
|
823
|
+
if (dx * dx + dy * dy <= r2) {
|
|
824
|
+
setCell(wx, wy);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
function rasterizeCapsuleSegment(x1, y1, x2, y2, radius, def, setCell) {
|
|
830
|
+
const segDx = x2 - x1;
|
|
831
|
+
const segDy = y2 - y1;
|
|
832
|
+
const segLen2 = segDx * segDx + segDy * segDy;
|
|
833
|
+
if (segLen2 === 0) return;
|
|
834
|
+
const minX = Math.max(def.bounds.x, Math.min(x1, x2) - radius);
|
|
835
|
+
const maxX = Math.min(def.bounds.x + def.bounds.w - 1, Math.max(x1, x2) + radius);
|
|
836
|
+
const minY = Math.max(def.bounds.y, Math.min(y1, y2) - radius);
|
|
837
|
+
const maxY = Math.min(def.bounds.y + def.bounds.h - 1, Math.max(y1, y2) + radius);
|
|
838
|
+
const startCol = Math.floor(minX / def.cellSize) * def.cellSize;
|
|
839
|
+
const startRow = Math.floor(minY / def.cellSize) * def.cellSize;
|
|
840
|
+
const r2 = radius * radius;
|
|
841
|
+
for (let wy = startRow; wy <= maxY; wy += def.cellSize) {
|
|
842
|
+
for (let wx = startCol; wx <= maxX; wx += def.cellSize) {
|
|
843
|
+
const cellCenterX = wx + def.cellSize * 0.5;
|
|
844
|
+
const cellCenterY = wy + def.cellSize * 0.5;
|
|
845
|
+
const t = Math.max(
|
|
846
|
+
0,
|
|
847
|
+
Math.min(1, ((cellCenterX - x1) * segDx + (cellCenterY - y1) * segDy) / segLen2)
|
|
848
|
+
);
|
|
849
|
+
const projX = x1 + t * segDx;
|
|
850
|
+
const projY = y1 + t * segDy;
|
|
851
|
+
const dx = cellCenterX - projX;
|
|
852
|
+
const dy = cellCenterY - projY;
|
|
853
|
+
if (dx * dx + dy * dy <= r2) {
|
|
854
|
+
setCell(wx, wy);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
function rasterizeRectangle(from, to, def, setCell) {
|
|
860
|
+
const minX = Math.max(def.bounds.x, Math.min(from.x, to.x));
|
|
861
|
+
const maxX = Math.min(def.bounds.x + def.bounds.w - 1, Math.max(from.x, to.x));
|
|
862
|
+
const minY = Math.max(def.bounds.y, Math.min(from.y, to.y));
|
|
863
|
+
const maxY = Math.min(def.bounds.y + def.bounds.h - 1, Math.max(from.y, to.y));
|
|
864
|
+
const startCol = Math.floor(minX / def.cellSize) * def.cellSize;
|
|
865
|
+
const startRow = Math.floor(minY / def.cellSize) * def.cellSize;
|
|
866
|
+
for (let wy = startRow; wy <= maxY; wy += def.cellSize) {
|
|
867
|
+
for (let wx = startCol; wx <= maxX; wx += def.cellSize) {
|
|
868
|
+
setCell(wx, wy);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
function rasterizePolygon(points, def, setCell) {
|
|
873
|
+
if (points.length < 3) return;
|
|
874
|
+
let minX = Infinity;
|
|
875
|
+
let maxX = -Infinity;
|
|
876
|
+
let minY = Infinity;
|
|
877
|
+
let maxY = -Infinity;
|
|
878
|
+
for (const p of points) {
|
|
879
|
+
if (p.x < minX) minX = p.x;
|
|
880
|
+
if (p.x > maxX) maxX = p.x;
|
|
881
|
+
if (p.y < minY) minY = p.y;
|
|
882
|
+
if (p.y > maxY) maxY = p.y;
|
|
883
|
+
}
|
|
884
|
+
minX = Math.max(def.bounds.x, minX);
|
|
885
|
+
maxX = Math.min(def.bounds.x + def.bounds.w - 1, maxX);
|
|
886
|
+
minY = Math.max(def.bounds.y, minY);
|
|
887
|
+
maxY = Math.min(def.bounds.y + def.bounds.h - 1, maxY);
|
|
888
|
+
const startCol = Math.floor(minX / def.cellSize) * def.cellSize;
|
|
889
|
+
const startRow = Math.floor(minY / def.cellSize) * def.cellSize;
|
|
890
|
+
for (let wy = startRow; wy <= maxY; wy += def.cellSize) {
|
|
891
|
+
for (let wx = startCol; wx <= maxX; wx += def.cellSize) {
|
|
892
|
+
const cx = wx + def.cellSize * 0.5;
|
|
893
|
+
const cy = wy + def.cellSize * 0.5;
|
|
894
|
+
if (pointInPolygon(cx, cy, points)) {
|
|
895
|
+
setCell(wx, wy);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
function pointInPolygon(x, y, polygon) {
|
|
901
|
+
let inside = false;
|
|
902
|
+
const n2 = polygon.length;
|
|
903
|
+
for (let i = 0, j = n2 - 1; i < n2; j = i++) {
|
|
904
|
+
const pi = polygon[i];
|
|
905
|
+
const pj = polygon[j];
|
|
906
|
+
if (pi.y > y !== pj.y > y && x < (pj.x - pi.x) * (y - pi.y) / (pj.y - pi.y) + pi.x) {
|
|
907
|
+
inside = !inside;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
return inside;
|
|
911
|
+
}
|
|
912
|
+
|
|
457
913
|
// src/core/state-serializer.ts
|
|
458
|
-
var CURRENT_VERSION =
|
|
914
|
+
var CURRENT_VERSION = 3;
|
|
459
915
|
var ELEMENT_TYPES = [
|
|
460
916
|
"stroke",
|
|
461
917
|
"note",
|
|
@@ -467,7 +923,7 @@ var ELEMENT_TYPES = [
|
|
|
467
923
|
"grid",
|
|
468
924
|
"template"
|
|
469
925
|
];
|
|
470
|
-
function exportState(elements, camera, layers = [], activeLayerId) {
|
|
926
|
+
function exportState(elements, camera, layers = [], activeLayerId, fog) {
|
|
471
927
|
const state = {
|
|
472
928
|
version: CURRENT_VERSION,
|
|
473
929
|
camera: {
|
|
@@ -484,6 +940,7 @@ function exportState(elements, camera, layers = [], activeLayerId) {
|
|
|
484
940
|
layers: layers.map((l) => ({ ...l }))
|
|
485
941
|
};
|
|
486
942
|
if (activeLayerId) state.activeLayerId = activeLayerId;
|
|
943
|
+
if (fog) state.fog = structuredClone(fog);
|
|
487
944
|
return state;
|
|
488
945
|
}
|
|
489
946
|
function parseState(json) {
|
|
@@ -561,6 +1018,9 @@ function validateState(data) {
|
|
|
561
1018
|
}
|
|
562
1019
|
}
|
|
563
1020
|
cleanBindings(elements);
|
|
1021
|
+
if (obj["fog"] !== void 0 && obj["fog"] !== null) {
|
|
1022
|
+
validateFogState(obj["fog"]);
|
|
1023
|
+
}
|
|
564
1024
|
}
|
|
565
1025
|
function validateElement(el) {
|
|
566
1026
|
if (!isRecord(el)) {
|
|
@@ -706,12 +1166,14 @@ var AutoSave = class {
|
|
|
706
1166
|
this.key = options.key ?? DEFAULT_KEY;
|
|
707
1167
|
this.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
708
1168
|
this.layerManager = options.layerManager;
|
|
1169
|
+
this.fogManager = options.fogManager;
|
|
709
1170
|
this.adapter = options.adapter ?? new LocalStorageAdapter();
|
|
710
1171
|
this.onError = options.onError;
|
|
711
1172
|
}
|
|
712
1173
|
key;
|
|
713
1174
|
debounceMs;
|
|
714
1175
|
layerManager;
|
|
1176
|
+
fogManager;
|
|
715
1177
|
adapter;
|
|
716
1178
|
timerId = null;
|
|
717
1179
|
unsubscribers = [];
|
|
@@ -729,6 +1191,9 @@ var AutoSave = class {
|
|
|
729
1191
|
if (this.layerManager) {
|
|
730
1192
|
this.unsubscribers.push(this.layerManager.on("change", schedule));
|
|
731
1193
|
}
|
|
1194
|
+
if (this.fogManager) {
|
|
1195
|
+
this.unsubscribers.push(this.fogManager.on("change", schedule));
|
|
1196
|
+
}
|
|
732
1197
|
}
|
|
733
1198
|
stop() {
|
|
734
1199
|
this.cancelPending();
|
|
@@ -765,7 +1230,8 @@ var AutoSave = class {
|
|
|
765
1230
|
this.saving = true;
|
|
766
1231
|
try {
|
|
767
1232
|
const layers = this.layerManager?.snapshot() ?? [];
|
|
768
|
-
const
|
|
1233
|
+
const fog = this.fogManager?.getState() ?? void 0;
|
|
1234
|
+
const state = exportState(this.store.snapshot(), this.camera, layers, void 0, fog);
|
|
769
1235
|
await this.adapter.save(this.key, JSON.stringify(state));
|
|
770
1236
|
} catch (e) {
|
|
771
1237
|
this.onError?.(e instanceof Error ? e : new Error(String(e)));
|
|
@@ -5097,6 +5563,8 @@ var MinimapController = class {
|
|
|
5097
5563
|
// read the `viewport` parameter. Assigned in the constructor body instead.
|
|
5098
5564
|
htmlPainters;
|
|
5099
5565
|
scene = null;
|
|
5566
|
+
fogRenderer = null;
|
|
5567
|
+
fogUnsub = null;
|
|
5100
5568
|
frameId = null;
|
|
5101
5569
|
debounceTimer = null;
|
|
5102
5570
|
dragging = false;
|
|
@@ -5111,6 +5579,15 @@ var MinimapController = class {
|
|
|
5111
5579
|
this.renderScene();
|
|
5112
5580
|
this.requestDraw();
|
|
5113
5581
|
}
|
|
5582
|
+
setFogRenderer(renderer) {
|
|
5583
|
+
if (this.disposed) return;
|
|
5584
|
+
if (this.fogUnsub) {
|
|
5585
|
+
this.fogUnsub();
|
|
5586
|
+
this.fogUnsub = null;
|
|
5587
|
+
}
|
|
5588
|
+
this.fogRenderer = renderer;
|
|
5589
|
+
this.invalidateScene();
|
|
5590
|
+
}
|
|
5114
5591
|
requestDraw() {
|
|
5115
5592
|
if (this.disposed || this.frameId !== null) return;
|
|
5116
5593
|
this.frameId = this.requestFrame(this.draw);
|
|
@@ -5172,8 +5649,15 @@ var MinimapController = class {
|
|
|
5172
5649
|
}
|
|
5173
5650
|
currentMapping() {
|
|
5174
5651
|
const viewportRect = this.viewport.getVisibleRect();
|
|
5175
|
-
|
|
5176
|
-
|
|
5652
|
+
let mapping = getElementsBoundingBox(this.sceneElements());
|
|
5653
|
+
mapping = mapping ? unionBounds(mapping, viewportRect) : viewportRect;
|
|
5654
|
+
if (this.fogRenderer?.isVisible()) {
|
|
5655
|
+
const fogState = this.fogRenderer.getState();
|
|
5656
|
+
if (fogState) {
|
|
5657
|
+
mapping = unionBounds(mapping, fogState.definition.bounds);
|
|
5658
|
+
}
|
|
5659
|
+
}
|
|
5660
|
+
return mapping;
|
|
5177
5661
|
}
|
|
5178
5662
|
onViewChanged() {
|
|
5179
5663
|
if (this.disposed) return;
|
|
@@ -5231,6 +5715,23 @@ var MinimapController = class {
|
|
|
5231
5715
|
ctx.drawImage(layerCanvas, 0, 0);
|
|
5232
5716
|
ctx.restore();
|
|
5233
5717
|
}
|
|
5718
|
+
if (this.fogRenderer?.isVisible()) {
|
|
5719
|
+
const fogState = this.fogRenderer.getState();
|
|
5720
|
+
const fogMode = this.fogRenderer.getViewMode();
|
|
5721
|
+
if (fogState && (fogMode === "editor" || fogMode === "player")) {
|
|
5722
|
+
ctx.save();
|
|
5723
|
+
ctx.setTransform(
|
|
5724
|
+
dpr * transform.scale,
|
|
5725
|
+
0,
|
|
5726
|
+
0,
|
|
5727
|
+
dpr * transform.scale,
|
|
5728
|
+
dpr * transform.offsetX,
|
|
5729
|
+
dpr * transform.offsetY
|
|
5730
|
+
);
|
|
5731
|
+
this.fogRenderer.renderForExport(ctx, fogState, fogMode);
|
|
5732
|
+
ctx.restore();
|
|
5733
|
+
}
|
|
5734
|
+
}
|
|
5234
5735
|
this.scene = { canvas: sceneCanvas, transform, mapping };
|
|
5235
5736
|
}
|
|
5236
5737
|
renderLayerElements(ctx, elements, t, dpr) {
|
|
@@ -5343,9 +5844,15 @@ var Minimap = class {
|
|
|
5343
5844
|
this.canvas = canvas;
|
|
5344
5845
|
this.controller = new MinimapController(viewport, canvas, { width: WIDTH, height: HEIGHT });
|
|
5345
5846
|
}
|
|
5847
|
+
setFogRenderer(renderer) {
|
|
5848
|
+
this.controller.setFogRenderer(renderer);
|
|
5849
|
+
}
|
|
5346
5850
|
scheduleDraw() {
|
|
5347
5851
|
this.controller.requestDraw();
|
|
5348
5852
|
}
|
|
5853
|
+
invalidateScene() {
|
|
5854
|
+
this.controller.invalidateScene();
|
|
5855
|
+
}
|
|
5349
5856
|
destroy() {
|
|
5350
5857
|
this.controller.dispose();
|
|
5351
5858
|
this.canvas.remove();
|
|
@@ -6667,6 +7174,220 @@ async function renderHtmlElements(elements, options) {
|
|
|
6667
7174
|
return sources;
|
|
6668
7175
|
}
|
|
6669
7176
|
|
|
7177
|
+
// src/fog/fog-renderer.ts
|
|
7178
|
+
var DEFAULT_EDITOR_COLOR = "rgba(30, 40, 60, 0.45)";
|
|
7179
|
+
var DEFAULT_PLAYER_COLOR = "#0b1020";
|
|
7180
|
+
var FogRenderer = class {
|
|
7181
|
+
tileCache = /* @__PURE__ */ new Map();
|
|
7182
|
+
state = null;
|
|
7183
|
+
viewMode = "off";
|
|
7184
|
+
dirty = true;
|
|
7185
|
+
editorColor;
|
|
7186
|
+
playerColor;
|
|
7187
|
+
constructor(options = {}) {
|
|
7188
|
+
this.editorColor = options.editorColor ?? DEFAULT_EDITOR_COLOR;
|
|
7189
|
+
this.playerColor = options.playerColor ?? DEFAULT_PLAYER_COLOR;
|
|
7190
|
+
}
|
|
7191
|
+
setState(state) {
|
|
7192
|
+
this.state = state;
|
|
7193
|
+
this.dirty = true;
|
|
7194
|
+
}
|
|
7195
|
+
setViewMode(mode) {
|
|
7196
|
+
if (mode === this.viewMode) return;
|
|
7197
|
+
this.viewMode = mode;
|
|
7198
|
+
this.dirty = true;
|
|
7199
|
+
}
|
|
7200
|
+
getState() {
|
|
7201
|
+
return this.state;
|
|
7202
|
+
}
|
|
7203
|
+
getViewMode() {
|
|
7204
|
+
return this.viewMode;
|
|
7205
|
+
}
|
|
7206
|
+
markDirty() {
|
|
7207
|
+
this.dirty = true;
|
|
7208
|
+
}
|
|
7209
|
+
isDirty() {
|
|
7210
|
+
return this.dirty;
|
|
7211
|
+
}
|
|
7212
|
+
isVisible() {
|
|
7213
|
+
return this.viewMode !== "off" && this.state !== null;
|
|
7214
|
+
}
|
|
7215
|
+
render(ctx, camera, viewportWidth, viewportHeight, _dpr) {
|
|
7216
|
+
if (!this.state || this.viewMode === "off") return;
|
|
7217
|
+
const def = this.state.definition;
|
|
7218
|
+
const cellSize = def.cellSize;
|
|
7219
|
+
const tileWorldSize = FOG_TILE_CELLS * cellSize;
|
|
7220
|
+
const color = this.viewMode === "editor" ? this.editorColor : this.playerColor;
|
|
7221
|
+
const worldBounds = getVisibleWorld(camera, viewportWidth, viewportHeight);
|
|
7222
|
+
const minTX = Math.floor(Math.max(def.bounds.x, worldBounds.x) / tileWorldSize);
|
|
7223
|
+
const minTY = Math.floor(Math.max(def.bounds.y, worldBounds.y) / tileWorldSize);
|
|
7224
|
+
const maxTX = Math.floor(
|
|
7225
|
+
Math.min(def.bounds.x + def.bounds.w - 1, worldBounds.x + worldBounds.w) / tileWorldSize
|
|
7226
|
+
);
|
|
7227
|
+
const maxTY = Math.floor(
|
|
7228
|
+
Math.min(def.bounds.y + def.bounds.h - 1, worldBounds.y + worldBounds.h) / tileWorldSize
|
|
7229
|
+
);
|
|
7230
|
+
ctx.save();
|
|
7231
|
+
ctx.translate(camera.position.x, camera.position.y);
|
|
7232
|
+
ctx.scale(camera.zoom, camera.zoom);
|
|
7233
|
+
const tileMap = /* @__PURE__ */ new Map();
|
|
7234
|
+
for (const tile of this.state.tiles) {
|
|
7235
|
+
tileMap.set(`${tile.x},${tile.y}`, tile.data);
|
|
7236
|
+
}
|
|
7237
|
+
const baseCovered = def.base === "covered";
|
|
7238
|
+
for (let ty = minTY; ty <= maxTY; ty++) {
|
|
7239
|
+
for (let tx = minTX; tx <= maxTX; tx++) {
|
|
7240
|
+
const key = `${tx},${ty}`;
|
|
7241
|
+
const data = tileMap.get(key);
|
|
7242
|
+
const tileWorldX = tx * tileWorldSize;
|
|
7243
|
+
const tileWorldY = ty * tileWorldSize;
|
|
7244
|
+
if (!data && baseCovered) {
|
|
7245
|
+
ctx.fillStyle = color;
|
|
7246
|
+
const clipX = Math.max(tileWorldX, def.bounds.x);
|
|
7247
|
+
const clipY = Math.max(tileWorldY, def.bounds.y);
|
|
7248
|
+
const clipR = Math.min(tileWorldX + tileWorldSize, def.bounds.x + def.bounds.w);
|
|
7249
|
+
const clipB = Math.min(tileWorldY + tileWorldSize, def.bounds.y + def.bounds.h);
|
|
7250
|
+
if (clipR > clipX && clipB > clipY) {
|
|
7251
|
+
ctx.fillRect(clipX, clipY, clipR - clipX, clipB - clipY);
|
|
7252
|
+
}
|
|
7253
|
+
continue;
|
|
7254
|
+
}
|
|
7255
|
+
if (!data && !baseCovered) {
|
|
7256
|
+
continue;
|
|
7257
|
+
}
|
|
7258
|
+
if (data) {
|
|
7259
|
+
this.renderTile(ctx, data, tx, ty, def, color);
|
|
7260
|
+
}
|
|
7261
|
+
}
|
|
7262
|
+
}
|
|
7263
|
+
ctx.restore();
|
|
7264
|
+
this.dirty = false;
|
|
7265
|
+
}
|
|
7266
|
+
renderForExport(ctx, state, mode, color) {
|
|
7267
|
+
const def = state.definition;
|
|
7268
|
+
const cellSize = def.cellSize;
|
|
7269
|
+
const tileWorldSize = FOG_TILE_CELLS * cellSize;
|
|
7270
|
+
const fogColor = color ?? (mode === "editor" ? this.editorColor : this.playerColor);
|
|
7271
|
+
const baseCovered = def.base === "covered";
|
|
7272
|
+
const tileMap = /* @__PURE__ */ new Map();
|
|
7273
|
+
for (const tile of state.tiles) {
|
|
7274
|
+
tileMap.set(`${tile.x},${tile.y}`, tile.data);
|
|
7275
|
+
}
|
|
7276
|
+
const minTX = Math.floor(def.bounds.x / tileWorldSize);
|
|
7277
|
+
const minTY = Math.floor(def.bounds.y / tileWorldSize);
|
|
7278
|
+
const maxTX = Math.floor((def.bounds.x + def.bounds.w - 1) / tileWorldSize);
|
|
7279
|
+
const maxTY = Math.floor((def.bounds.y + def.bounds.h - 1) / tileWorldSize);
|
|
7280
|
+
for (let ty = minTY; ty <= maxTY; ty++) {
|
|
7281
|
+
for (let tx = minTX; tx <= maxTX; tx++) {
|
|
7282
|
+
const key = `${tx},${ty}`;
|
|
7283
|
+
const data = tileMap.get(key);
|
|
7284
|
+
const tileWorldX = tx * tileWorldSize;
|
|
7285
|
+
const tileWorldY = ty * tileWorldSize;
|
|
7286
|
+
if (!data && baseCovered) {
|
|
7287
|
+
ctx.fillStyle = fogColor;
|
|
7288
|
+
const clipX = Math.max(tileWorldX, def.bounds.x);
|
|
7289
|
+
const clipY = Math.max(tileWorldY, def.bounds.y);
|
|
7290
|
+
const clipR = Math.min(tileWorldX + tileWorldSize, def.bounds.x + def.bounds.w);
|
|
7291
|
+
const clipB = Math.min(tileWorldY + tileWorldSize, def.bounds.y + def.bounds.h);
|
|
7292
|
+
if (clipR > clipX && clipB > clipY) {
|
|
7293
|
+
ctx.fillRect(clipX, clipY, clipR - clipX, clipB - clipY);
|
|
7294
|
+
}
|
|
7295
|
+
continue;
|
|
7296
|
+
}
|
|
7297
|
+
if (data) {
|
|
7298
|
+
this.renderTileForExport(ctx, data, tx, ty, def, fogColor);
|
|
7299
|
+
}
|
|
7300
|
+
}
|
|
7301
|
+
}
|
|
7302
|
+
}
|
|
7303
|
+
dispose() {
|
|
7304
|
+
this.tileCache.clear();
|
|
7305
|
+
this.state = null;
|
|
7306
|
+
}
|
|
7307
|
+
tileRaster(data, color) {
|
|
7308
|
+
const key = `${color}\0${data}`;
|
|
7309
|
+
const cached = this.tileCache.get(key);
|
|
7310
|
+
if (cached) return cached;
|
|
7311
|
+
if (typeof document === "undefined") return null;
|
|
7312
|
+
const canvas = document.createElement("canvas");
|
|
7313
|
+
canvas.width = FOG_TILE_CELLS;
|
|
7314
|
+
canvas.height = FOG_TILE_CELLS;
|
|
7315
|
+
const ctx = canvas.getContext("2d");
|
|
7316
|
+
if (!ctx) return null;
|
|
7317
|
+
const bytes = decodeBase64(data);
|
|
7318
|
+
ctx.fillStyle = color;
|
|
7319
|
+
for (let row = 0; row < FOG_TILE_CELLS; row++) {
|
|
7320
|
+
for (let col = 0; col < FOG_TILE_CELLS; col++) {
|
|
7321
|
+
const index = row * FOG_TILE_CELLS + col;
|
|
7322
|
+
const byteIndex = index >> 3;
|
|
7323
|
+
const bitIndex = 7 - (index & 7);
|
|
7324
|
+
const revealed = (bytes[byteIndex] >> bitIndex & 1) === 1;
|
|
7325
|
+
if (!revealed) ctx.fillRect(col, row, 1, 1);
|
|
7326
|
+
}
|
|
7327
|
+
}
|
|
7328
|
+
if (this.tileCache.size >= 256) {
|
|
7329
|
+
const oldest = this.tileCache.keys().next().value;
|
|
7330
|
+
if (oldest !== void 0) this.tileCache.delete(oldest);
|
|
7331
|
+
}
|
|
7332
|
+
this.tileCache.set(key, canvas);
|
|
7333
|
+
return canvas;
|
|
7334
|
+
}
|
|
7335
|
+
renderTile(ctx, data, tx, ty, def, color) {
|
|
7336
|
+
const cellSize = def.cellSize;
|
|
7337
|
+
const tileWorldX = tx * FOG_TILE_CELLS * cellSize;
|
|
7338
|
+
const tileWorldY = ty * FOG_TILE_CELLS * cellSize;
|
|
7339
|
+
const raster = this.tileRaster(data, color);
|
|
7340
|
+
if (raster) {
|
|
7341
|
+
ctx.save();
|
|
7342
|
+
ctx.beginPath();
|
|
7343
|
+
ctx.rect(def.bounds.x, def.bounds.y, def.bounds.w, def.bounds.h);
|
|
7344
|
+
ctx.clip();
|
|
7345
|
+
ctx.imageSmoothingEnabled = false;
|
|
7346
|
+
ctx.drawImage(
|
|
7347
|
+
raster,
|
|
7348
|
+
tileWorldX,
|
|
7349
|
+
tileWorldY,
|
|
7350
|
+
FOG_TILE_CELLS * cellSize,
|
|
7351
|
+
FOG_TILE_CELLS * cellSize
|
|
7352
|
+
);
|
|
7353
|
+
ctx.restore();
|
|
7354
|
+
return;
|
|
7355
|
+
}
|
|
7356
|
+
const bytes = decodeBase64(data);
|
|
7357
|
+
ctx.fillStyle = color;
|
|
7358
|
+
for (let row = 0; row < FOG_TILE_CELLS; row++) {
|
|
7359
|
+
for (let col = 0; col < FOG_TILE_CELLS; col++) {
|
|
7360
|
+
const cellWorldX = tileWorldX + col * cellSize;
|
|
7361
|
+
const cellWorldY = tileWorldY + row * cellSize;
|
|
7362
|
+
if (cellWorldX < def.bounds.x || cellWorldY < def.bounds.y || cellWorldX >= def.bounds.x + def.bounds.w || cellWorldY >= def.bounds.y + def.bounds.h) {
|
|
7363
|
+
continue;
|
|
7364
|
+
}
|
|
7365
|
+
const index = row * FOG_TILE_CELLS + col;
|
|
7366
|
+
const byteIndex = index >> 3;
|
|
7367
|
+
const bitIndex = 7 - (index & 7);
|
|
7368
|
+
const revealed = (bytes[byteIndex] >> bitIndex & 1) === 1;
|
|
7369
|
+
const covered = !revealed;
|
|
7370
|
+
if (covered) {
|
|
7371
|
+
ctx.fillRect(cellWorldX, cellWorldY, cellSize, cellSize);
|
|
7372
|
+
}
|
|
7373
|
+
}
|
|
7374
|
+
}
|
|
7375
|
+
}
|
|
7376
|
+
renderTileForExport(ctx, data, tx, ty, def, color) {
|
|
7377
|
+
this.renderTile(ctx, data, tx, ty, def, color);
|
|
7378
|
+
}
|
|
7379
|
+
};
|
|
7380
|
+
function getVisibleWorld(camera, viewportWidth, viewportHeight) {
|
|
7381
|
+
const topLeft = camera.screenToWorld({ x: 0, y: 0 });
|
|
7382
|
+
const bottomRight = camera.screenToWorld({ x: viewportWidth, y: viewportHeight });
|
|
7383
|
+
return {
|
|
7384
|
+
x: topLeft.x,
|
|
7385
|
+
y: topLeft.y,
|
|
7386
|
+
w: bottomRight.x - topLeft.x,
|
|
7387
|
+
h: bottomRight.y - topLeft.y
|
|
7388
|
+
};
|
|
7389
|
+
}
|
|
7390
|
+
|
|
6670
7391
|
// src/canvas/export-image.ts
|
|
6671
7392
|
var DEFAULT_IMAGE_TIMEOUT_MS = 1e4;
|
|
6672
7393
|
var DEFAULT_MAX_DIMENSION = 16384;
|
|
@@ -7079,6 +7800,10 @@ async function exportImage(store, options = {}, layerManager) {
|
|
|
7079
7800
|
renderGridForBounds(ctx, grid, bounds);
|
|
7080
7801
|
ctx.restore();
|
|
7081
7802
|
}
|
|
7803
|
+
if (options.fog) {
|
|
7804
|
+
const fogRenderer = new FogRenderer();
|
|
7805
|
+
fogRenderer.renderForExport(ctx, options.fog.state, options.fog.mode, options.fog.color);
|
|
7806
|
+
}
|
|
7082
7807
|
const mimeType = format === "jpeg" ? "image/jpeg" : "image/png";
|
|
7083
7808
|
return new Promise((resolve) => {
|
|
7084
7809
|
canvas.toBlob((blob) => resolve(blob), mimeType, options.quality);
|
|
@@ -7460,6 +8185,27 @@ async function exportSvg(store, options = {}, layerManager) {
|
|
|
7460
8185
|
const opacity = layerManager?.getLayer?.(grid.layerId)?.opacity ?? 1;
|
|
7461
8186
|
body += opacity === 1 ? emitted : `<g opacity="${n(opacity)}">${emitted}</g>`;
|
|
7462
8187
|
}
|
|
8188
|
+
if (options.fog && typeof document !== "undefined") {
|
|
8189
|
+
const fogState = options.fog.state;
|
|
8190
|
+
const fogW = Math.max(1, Math.ceil(bounds.w));
|
|
8191
|
+
const fogH = Math.max(1, Math.ceil(bounds.h));
|
|
8192
|
+
const fogCanvas = document.createElement("canvas");
|
|
8193
|
+
fogCanvas.width = fogW;
|
|
8194
|
+
fogCanvas.height = fogH;
|
|
8195
|
+
const fogCtx = fogCanvas.getContext("2d");
|
|
8196
|
+
if (fogCtx) {
|
|
8197
|
+
fogCtx.translate(-bounds.x, -bounds.y);
|
|
8198
|
+
const fogRenderer = new FogRenderer();
|
|
8199
|
+
fogRenderer.renderForExport(fogCtx, fogState, options.fog.mode, options.fog.color);
|
|
8200
|
+
try {
|
|
8201
|
+
const fogDataUri = fogCanvas.toDataURL("image/png");
|
|
8202
|
+
if (fogDataUri.startsWith("data:")) {
|
|
8203
|
+
body += `<image href="${esc(fogDataUri)}" x="${n(bounds.x)}" y="${n(bounds.y)}" width="${n(bounds.w)}" height="${n(bounds.h)}" />`;
|
|
8204
|
+
}
|
|
8205
|
+
} catch {
|
|
8206
|
+
}
|
|
8207
|
+
}
|
|
8208
|
+
}
|
|
7463
8209
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${n(bounds.w)}" height="${n(bounds.h)}" viewBox="${n(bounds.x)} ${n(bounds.y)} ${n(bounds.w)} ${n(bounds.h)}">${body}</svg>`;
|
|
7464
8210
|
}
|
|
7465
8211
|
function emitElement(el, imageDataUris, htmlDataUris, rasterScale, firstGrid, store, resourceOptions) {
|
|
@@ -8297,6 +9043,7 @@ var RenderLoop = class {
|
|
|
8297
9043
|
layerCache;
|
|
8298
9044
|
marginViewport;
|
|
8299
9045
|
hybridSurface;
|
|
9046
|
+
fogRenderer;
|
|
8300
9047
|
activeDrawingLayerId = null;
|
|
8301
9048
|
gridCacheDirty = true;
|
|
8302
9049
|
// set on recenter/viewport-change; consumed by the grid block
|
|
@@ -8320,6 +9067,7 @@ var RenderLoop = class {
|
|
|
8320
9067
|
this.layerCache = deps.layerCache;
|
|
8321
9068
|
this.marginViewport = deps.marginViewport;
|
|
8322
9069
|
this.hybridSurface = deps.hybridSurface;
|
|
9070
|
+
this.fogRenderer = deps.fogRenderer;
|
|
8323
9071
|
}
|
|
8324
9072
|
requestRender() {
|
|
8325
9073
|
this.needsRender = true;
|
|
@@ -8629,9 +9377,12 @@ var RenderLoop = class {
|
|
|
8629
9377
|
group.push(element);
|
|
8630
9378
|
}
|
|
8631
9379
|
const activeTool = this.toolManager.activeTool;
|
|
8632
|
-
const
|
|
9380
|
+
const fogVisible = this.fogRenderer?.isVisible() ?? false;
|
|
9381
|
+
const fogOrder = visibleElements.length + 1;
|
|
9382
|
+
const overlayOrder = fogVisible ? fogOrder + 1 : visibleElements.length + 1;
|
|
8633
9383
|
const hasOverlay = activeTool?.renderOverlay !== void 0 || this.overlays.size > 0;
|
|
8634
|
-
if (
|
|
9384
|
+
if (fogVisible) hybridOrders.add(fogOrder);
|
|
9385
|
+
if (hasOverlay && (hybridActive || fogVisible)) hybridOrders.add(overlayOrder);
|
|
8635
9386
|
this.hybridSurface.beginFrame(hybridOrders, this.canvasEl.width, this.canvasEl.height);
|
|
8636
9387
|
for (const [layerId, elements] of this.layerGroups) {
|
|
8637
9388
|
const isActiveDrawingLayer = layerId === this.activeDrawingLayerId;
|
|
@@ -8745,8 +9496,18 @@ var RenderLoop = class {
|
|
|
8745
9496
|
}
|
|
8746
9497
|
hybridCtx.restore();
|
|
8747
9498
|
}
|
|
9499
|
+
if (fogVisible && this.fogRenderer) {
|
|
9500
|
+
const fogCtx = this.hybridSurface.getContext(fogOrder);
|
|
9501
|
+
if (fogCtx) {
|
|
9502
|
+
fogCtx.clearRect(0, 0, this.canvasEl.width, this.canvasEl.height);
|
|
9503
|
+
fogCtx.save();
|
|
9504
|
+
fogCtx.scale(dpr, dpr);
|
|
9505
|
+
this.fogRenderer.render(fogCtx, this.camera, cssWidth, cssHeight, dpr);
|
|
9506
|
+
fogCtx.restore();
|
|
9507
|
+
}
|
|
9508
|
+
}
|
|
8748
9509
|
const overlayT0 = performance.now();
|
|
8749
|
-
if (hybridActive && hasOverlay) {
|
|
9510
|
+
if ((hybridActive || fogVisible) && hasOverlay) {
|
|
8750
9511
|
const overlayCtx = this.hybridSurface.getContext(overlayOrder);
|
|
8751
9512
|
if (overlayCtx) {
|
|
8752
9513
|
overlayCtx.clearRect(0, 0, this.canvasEl.width, this.canvasEl.height);
|
|
@@ -9757,6 +10518,248 @@ var ElementActivation = class {
|
|
|
9757
10518
|
}
|
|
9758
10519
|
};
|
|
9759
10520
|
|
|
10521
|
+
// src/fog/fog-command.ts
|
|
10522
|
+
var FogRegionCommand = class {
|
|
10523
|
+
constructor(manager, before, after) {
|
|
10524
|
+
this.manager = manager;
|
|
10525
|
+
this.before = before;
|
|
10526
|
+
this.after = after;
|
|
10527
|
+
}
|
|
10528
|
+
execute(_store) {
|
|
10529
|
+
this.manager.applyTilesDirect(this.after);
|
|
10530
|
+
}
|
|
10531
|
+
undo(_store) {
|
|
10532
|
+
this.manager.applyTilesDirect(this.before);
|
|
10533
|
+
}
|
|
10534
|
+
};
|
|
10535
|
+
var FogResetCommand = class {
|
|
10536
|
+
constructor(manager, before, after) {
|
|
10537
|
+
this.manager = manager;
|
|
10538
|
+
this.before = before;
|
|
10539
|
+
this.after = after;
|
|
10540
|
+
}
|
|
10541
|
+
execute(_store) {
|
|
10542
|
+
this.manager.restoreHistoryState(this.after);
|
|
10543
|
+
}
|
|
10544
|
+
undo(_store) {
|
|
10545
|
+
this.manager.restoreHistoryState(this.before);
|
|
10546
|
+
}
|
|
10547
|
+
};
|
|
10548
|
+
|
|
10549
|
+
// src/fog/fog-manager.ts
|
|
10550
|
+
function defaultIdFactory() {
|
|
10551
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
10552
|
+
return crypto.randomUUID();
|
|
10553
|
+
}
|
|
10554
|
+
return `fog-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
10555
|
+
}
|
|
10556
|
+
var FogManager = class {
|
|
10557
|
+
state = null;
|
|
10558
|
+
viewMode = "off";
|
|
10559
|
+
idFactory;
|
|
10560
|
+
onCommand;
|
|
10561
|
+
changeListeners = /* @__PURE__ */ new Set();
|
|
10562
|
+
viewListeners = /* @__PURE__ */ new Set();
|
|
10563
|
+
constructor(options = {}) {
|
|
10564
|
+
this.idFactory = options.idFactory ?? defaultIdFactory;
|
|
10565
|
+
this.onCommand = options.onCommand;
|
|
10566
|
+
}
|
|
10567
|
+
getState() {
|
|
10568
|
+
if (!this.state) return null;
|
|
10569
|
+
return {
|
|
10570
|
+
definition: { ...this.state.definition, bounds: { ...this.state.definition.bounds } },
|
|
10571
|
+
tiles: this.state.tiles.map((t) => ({ ...t }))
|
|
10572
|
+
};
|
|
10573
|
+
}
|
|
10574
|
+
getViewMode() {
|
|
10575
|
+
return this.viewMode;
|
|
10576
|
+
}
|
|
10577
|
+
initialize(options) {
|
|
10578
|
+
const base = options.base ?? "covered";
|
|
10579
|
+
const cellSize = options.cellSize ?? recommendedFogCellSize(options.bounds);
|
|
10580
|
+
const generation = this.idFactory();
|
|
10581
|
+
const newState = {
|
|
10582
|
+
definition: {
|
|
10583
|
+
version: 1,
|
|
10584
|
+
generation,
|
|
10585
|
+
bounds: { ...options.bounds },
|
|
10586
|
+
cellSize,
|
|
10587
|
+
tileCells: FOG_TILE_CELLS,
|
|
10588
|
+
base
|
|
10589
|
+
},
|
|
10590
|
+
tiles: []
|
|
10591
|
+
};
|
|
10592
|
+
validateFogState(newState);
|
|
10593
|
+
const before = this.state;
|
|
10594
|
+
this.state = newState;
|
|
10595
|
+
const command = new FogResetCommand(this, before, newState);
|
|
10596
|
+
this.onCommand?.(command);
|
|
10597
|
+
this.notifyChange({ kind: "definition" });
|
|
10598
|
+
return structuredClone(newState);
|
|
10599
|
+
}
|
|
10600
|
+
loadState(state, meta) {
|
|
10601
|
+
if (state !== null) {
|
|
10602
|
+
validateFogState(state);
|
|
10603
|
+
this.state = structuredClone(state);
|
|
10604
|
+
} else {
|
|
10605
|
+
this.state = null;
|
|
10606
|
+
}
|
|
10607
|
+
this.notifyChange({
|
|
10608
|
+
kind: state === null ? "disable" : "definition",
|
|
10609
|
+
origin: meta?.origin
|
|
10610
|
+
});
|
|
10611
|
+
}
|
|
10612
|
+
/** Restores a historical visual state without reusing its causal generation id. */
|
|
10613
|
+
restoreHistoryState(state) {
|
|
10614
|
+
if (state === null) {
|
|
10615
|
+
this.loadState(null);
|
|
10616
|
+
return;
|
|
10617
|
+
}
|
|
10618
|
+
this.loadState({
|
|
10619
|
+
definition: { ...state.definition, generation: this.idFactory() },
|
|
10620
|
+
tiles: state.tiles
|
|
10621
|
+
});
|
|
10622
|
+
}
|
|
10623
|
+
setBounds(bounds) {
|
|
10624
|
+
if (!this.state) return;
|
|
10625
|
+
const def = this.state.definition;
|
|
10626
|
+
validateFogState({
|
|
10627
|
+
definition: { ...def, bounds: { ...bounds } },
|
|
10628
|
+
tiles: []
|
|
10629
|
+
});
|
|
10630
|
+
const shrinks = bounds.x > def.bounds.x || bounds.y > def.bounds.y || bounds.x + bounds.w < def.bounds.x + def.bounds.w || bounds.y + bounds.h < def.bounds.y + def.bounds.h;
|
|
10631
|
+
const nextDefinition = {
|
|
10632
|
+
...def,
|
|
10633
|
+
bounds: { ...bounds },
|
|
10634
|
+
generation: shrinks ? this.idFactory() : def.generation
|
|
10635
|
+
};
|
|
10636
|
+
const tiles = this.state.tiles.flatMap((tile) => {
|
|
10637
|
+
const tileWorldX = tile.x * FOG_TILE_CELLS * def.cellSize;
|
|
10638
|
+
const tileWorldY = tile.y * FOG_TILE_CELLS * def.cellSize;
|
|
10639
|
+
const tileWorldW = FOG_TILE_CELLS * def.cellSize;
|
|
10640
|
+
const tileWorldH = FOG_TILE_CELLS * def.cellSize;
|
|
10641
|
+
const intersects2 = !(tileWorldX + tileWorldW <= bounds.x || tileWorldY + tileWorldH <= bounds.y || tileWorldX >= bounds.x + bounds.w || tileWorldY >= bounds.y + bounds.h);
|
|
10642
|
+
if (!intersects2) return [];
|
|
10643
|
+
const canonical = canonicalizeFogTile(tile, nextDefinition);
|
|
10644
|
+
return canonical ? [canonical] : [];
|
|
10645
|
+
});
|
|
10646
|
+
const before = this.state;
|
|
10647
|
+
this.state = {
|
|
10648
|
+
definition: nextDefinition,
|
|
10649
|
+
tiles
|
|
10650
|
+
};
|
|
10651
|
+
const command = new FogResetCommand(this, before, this.state);
|
|
10652
|
+
this.onCommand?.(command);
|
|
10653
|
+
this.notifyChange({ kind: "definition" });
|
|
10654
|
+
}
|
|
10655
|
+
reset(base) {
|
|
10656
|
+
if (!this.state) return;
|
|
10657
|
+
const before = this.state;
|
|
10658
|
+
const generation = this.idFactory();
|
|
10659
|
+
this.state = {
|
|
10660
|
+
definition: { ...this.state.definition, base, generation },
|
|
10661
|
+
tiles: []
|
|
10662
|
+
};
|
|
10663
|
+
const command = new FogResetCommand(this, before, this.state);
|
|
10664
|
+
this.onCommand?.(command);
|
|
10665
|
+
this.notifyChange({ kind: "reset" });
|
|
10666
|
+
}
|
|
10667
|
+
disable() {
|
|
10668
|
+
if (!this.state) return;
|
|
10669
|
+
const before = this.state;
|
|
10670
|
+
this.state = null;
|
|
10671
|
+
const command = new FogResetCommand(this, before, null);
|
|
10672
|
+
this.onCommand?.(command);
|
|
10673
|
+
this.notifyChange({ kind: "disable" });
|
|
10674
|
+
}
|
|
10675
|
+
setViewMode(mode) {
|
|
10676
|
+
if (mode === this.viewMode) return;
|
|
10677
|
+
this.viewMode = mode;
|
|
10678
|
+
this.notifyView({ mode });
|
|
10679
|
+
}
|
|
10680
|
+
applyRegion(region, operation) {
|
|
10681
|
+
if (!this.state) return;
|
|
10682
|
+
const result = rasterizeRegion(this.state, region, operation);
|
|
10683
|
+
if (result.noop) return;
|
|
10684
|
+
const before = this.collectTiles(result.changed);
|
|
10685
|
+
const newState = applyRasterResult(this.state, result);
|
|
10686
|
+
this.state = newState;
|
|
10687
|
+
const command = new FogRegionCommand(this, before, result.changed);
|
|
10688
|
+
this.onCommand?.(command);
|
|
10689
|
+
this.notifyChange({
|
|
10690
|
+
kind: "tiles",
|
|
10691
|
+
tiles: result.changed.map((t) => ({ x: t.x, y: t.y }))
|
|
10692
|
+
});
|
|
10693
|
+
}
|
|
10694
|
+
applyPatchDirect(patch, meta) {
|
|
10695
|
+
if (!this.state) return;
|
|
10696
|
+
const result = applyRasterResult(this.state, { changed: patch.tiles, noop: false });
|
|
10697
|
+
this.state = result;
|
|
10698
|
+
this.notifyChange({
|
|
10699
|
+
kind: "tiles",
|
|
10700
|
+
tiles: patch.tiles.map((t) => ({ x: t.x, y: t.y })),
|
|
10701
|
+
origin: meta?.origin
|
|
10702
|
+
});
|
|
10703
|
+
}
|
|
10704
|
+
applyTilesDirect(tiles) {
|
|
10705
|
+
if (!this.state) return;
|
|
10706
|
+
const result = applyRasterResult(this.state, { changed: tiles, noop: false });
|
|
10707
|
+
this.state = result;
|
|
10708
|
+
this.notifyChange({
|
|
10709
|
+
kind: "tiles",
|
|
10710
|
+
tiles: tiles.map((t) => ({ x: t.x, y: t.y }))
|
|
10711
|
+
});
|
|
10712
|
+
}
|
|
10713
|
+
on(event, listener) {
|
|
10714
|
+
if (event === "change") {
|
|
10715
|
+
const l2 = listener;
|
|
10716
|
+
this.changeListeners.add(l2);
|
|
10717
|
+
return () => this.changeListeners.delete(l2);
|
|
10718
|
+
}
|
|
10719
|
+
const l = listener;
|
|
10720
|
+
this.viewListeners.add(l);
|
|
10721
|
+
return () => this.viewListeners.delete(l);
|
|
10722
|
+
}
|
|
10723
|
+
dispose() {
|
|
10724
|
+
this.changeListeners.clear();
|
|
10725
|
+
this.viewListeners.clear();
|
|
10726
|
+
}
|
|
10727
|
+
collectTiles(changed) {
|
|
10728
|
+
if (!this.state) return [];
|
|
10729
|
+
const result = [];
|
|
10730
|
+
for (const c of changed) {
|
|
10731
|
+
const existing = this.state.tiles.find((t) => t.x === c.x && t.y === c.y);
|
|
10732
|
+
if (existing) {
|
|
10733
|
+
result.push(existing);
|
|
10734
|
+
} else {
|
|
10735
|
+
const baseVal = this.state.definition.base === "revealed";
|
|
10736
|
+
result.push({
|
|
10737
|
+
x: c.x,
|
|
10738
|
+
y: c.y,
|
|
10739
|
+
data: encodeBase64(createTileBytes(baseVal))
|
|
10740
|
+
});
|
|
10741
|
+
}
|
|
10742
|
+
}
|
|
10743
|
+
return result;
|
|
10744
|
+
}
|
|
10745
|
+
notifyChange(event) {
|
|
10746
|
+
for (const listener of this.changeListeners) {
|
|
10747
|
+
try {
|
|
10748
|
+
listener(event);
|
|
10749
|
+
} catch {
|
|
10750
|
+
}
|
|
10751
|
+
}
|
|
10752
|
+
}
|
|
10753
|
+
notifyView(event) {
|
|
10754
|
+
for (const listener of this.viewListeners) {
|
|
10755
|
+
try {
|
|
10756
|
+
listener(event);
|
|
10757
|
+
} catch {
|
|
10758
|
+
}
|
|
10759
|
+
}
|
|
10760
|
+
}
|
|
10761
|
+
};
|
|
10762
|
+
|
|
9760
10763
|
// src/canvas/viewport.ts
|
|
9761
10764
|
var EMPTY_IDS = [];
|
|
9762
10765
|
function noop2() {
|
|
@@ -9883,8 +10886,13 @@ var Viewport = class _Viewport {
|
|
|
9883
10886
|
});
|
|
9884
10887
|
}
|
|
9885
10888
|
this.unsubToolChange = this.toolManager.onChange(() => this.contextMenu?.close());
|
|
10889
|
+
this.fogManager = new FogManager({
|
|
10890
|
+
onCommand: (cmd) => this.history.push(cmd)
|
|
10891
|
+
});
|
|
10892
|
+
this.fogRenderer = new FogRenderer(options.fog);
|
|
9886
10893
|
if (options.minimap) {
|
|
9887
10894
|
this.minimap = new Minimap(this.wrapper, this);
|
|
10895
|
+
this.minimap.setFogRenderer(this.fogRenderer);
|
|
9888
10896
|
}
|
|
9889
10897
|
this.domNodeManager = new DomNodeManager({
|
|
9890
10898
|
domLayer: this.paintStack,
|
|
@@ -9914,7 +10922,18 @@ var Viewport = class _Viewport {
|
|
|
9914
10922
|
domNodeManager: this.domNodeManager,
|
|
9915
10923
|
layerCache,
|
|
9916
10924
|
marginViewport: this.marginViewport,
|
|
9917
|
-
hybridSurface: new HybridRenderSurface(this.paintStack)
|
|
10925
|
+
hybridSurface: new HybridRenderSurface(this.paintStack),
|
|
10926
|
+
fogRenderer: this.fogRenderer
|
|
10927
|
+
});
|
|
10928
|
+
this.fogManager.on("change", () => {
|
|
10929
|
+
this.fogRenderer.setState(this.fogManager.getState());
|
|
10930
|
+
this.renderLoop.requestRender();
|
|
10931
|
+
this.minimap?.invalidateScene();
|
|
10932
|
+
});
|
|
10933
|
+
this.fogManager.on("view", () => {
|
|
10934
|
+
this.fogRenderer.setViewMode(this.fogManager.getViewMode());
|
|
10935
|
+
this.renderLoop.requestRender();
|
|
10936
|
+
this.minimap?.invalidateScene();
|
|
9918
10937
|
});
|
|
9919
10938
|
this.unsubHtmlPainters = this.htmlPainters.onChange(() => this.onHtmlRegistryChanged());
|
|
9920
10939
|
this.unsubCamera = this.camera.onChange(() => {
|
|
@@ -10026,6 +11045,8 @@ var Viewport = class _Viewport {
|
|
|
10026
11045
|
_smartGuides = false;
|
|
10027
11046
|
_gridSize;
|
|
10028
11047
|
renderLoop;
|
|
11048
|
+
fogManager;
|
|
11049
|
+
fogRenderer;
|
|
10029
11050
|
domNodeManager;
|
|
10030
11051
|
interactMode;
|
|
10031
11052
|
onHtmlElementMount;
|
|
@@ -10061,6 +11082,9 @@ var Viewport = class _Viewport {
|
|
|
10061
11082
|
get ctx() {
|
|
10062
11083
|
return this.canvasEl.getContext("2d");
|
|
10063
11084
|
}
|
|
11085
|
+
get fog() {
|
|
11086
|
+
return this.fogManager;
|
|
11087
|
+
}
|
|
10064
11088
|
get snapToGrid() {
|
|
10065
11089
|
return this._snapToGrid;
|
|
10066
11090
|
}
|
|
@@ -10145,7 +11169,8 @@ var Viewport = class _Viewport {
|
|
|
10145
11169
|
this.store.snapshot(),
|
|
10146
11170
|
this.camera,
|
|
10147
11171
|
this.layerManager.snapshot(),
|
|
10148
|
-
this.layerManager.activeLayerId
|
|
11172
|
+
this.layerManager.activeLayerId,
|
|
11173
|
+
this.fogManager.getState()
|
|
10149
11174
|
);
|
|
10150
11175
|
}
|
|
10151
11176
|
exportJSON() {
|
|
@@ -10167,12 +11192,31 @@ var Viewport = class _Viewport {
|
|
|
10167
11192
|
return { ...base, htmlPainters: registry, expectedCanvasTypes: expected };
|
|
10168
11193
|
}
|
|
10169
11194
|
async exportImage(options) {
|
|
10170
|
-
|
|
11195
|
+
const opts = this.withHtmlDefaults(options);
|
|
11196
|
+
if (opts.fog === void 0 && this.fogRenderer.isVisible()) {
|
|
11197
|
+
const state = this.fogManager.getState();
|
|
11198
|
+
if (state) {
|
|
11199
|
+
const mode = this.fogRenderer.getViewMode();
|
|
11200
|
+
opts.fog = { state, mode };
|
|
11201
|
+
}
|
|
11202
|
+
}
|
|
11203
|
+
return exportImage(this.store, opts, this.layerManager);
|
|
10171
11204
|
}
|
|
10172
11205
|
async exportSVG(options) {
|
|
10173
|
-
|
|
11206
|
+
const opts = this.withHtmlDefaults(options);
|
|
11207
|
+
if (opts.fog === void 0 && this.fogRenderer.isVisible()) {
|
|
11208
|
+
const state = this.fogManager.getState();
|
|
11209
|
+
if (state) {
|
|
11210
|
+
const mode = this.fogRenderer.getViewMode();
|
|
11211
|
+
opts.fog = { state, mode };
|
|
11212
|
+
}
|
|
11213
|
+
}
|
|
11214
|
+
return exportSvg(this.store, opts, this.layerManager);
|
|
10174
11215
|
}
|
|
10175
11216
|
loadState(state) {
|
|
11217
|
+
if (state.fog != null) {
|
|
11218
|
+
validateFogState(state.fog);
|
|
11219
|
+
}
|
|
10176
11220
|
this.inputHandler.flushPendingHistory();
|
|
10177
11221
|
this.historyRecorder.pause();
|
|
10178
11222
|
this.noteEditor.destroy(this.store);
|
|
@@ -10207,6 +11251,7 @@ var Viewport = class _Viewport {
|
|
|
10207
11251
|
}
|
|
10208
11252
|
}
|
|
10209
11253
|
}
|
|
11254
|
+
this.fogManager.loadState(state.fog ?? null);
|
|
10210
11255
|
this.history.clear();
|
|
10211
11256
|
this.historyRecorder.resume();
|
|
10212
11257
|
this.camera.moveTo(state.camera.position.x, state.camera.position.y);
|
|
@@ -10618,6 +11663,8 @@ var Viewport = class _Viewport {
|
|
|
10618
11663
|
this.unsubToolRegister();
|
|
10619
11664
|
this.unsubRecorderEnd();
|
|
10620
11665
|
this.unsubHtmlPainters();
|
|
11666
|
+
this.fogManager.dispose();
|
|
11667
|
+
this.fogRenderer.dispose();
|
|
10621
11668
|
this.activation?.dispose();
|
|
10622
11669
|
this.activation = null;
|
|
10623
11670
|
this.activationListeners.clear();
|
|
@@ -12100,26 +13147,820 @@ var RemoteFocusReceiver = class {
|
|
|
12100
13147
|
}
|
|
12101
13148
|
};
|
|
12102
13149
|
|
|
12103
|
-
// src/
|
|
12104
|
-
var
|
|
12105
|
-
|
|
12106
|
-
|
|
12107
|
-
|
|
12108
|
-
|
|
12109
|
-
|
|
12110
|
-
|
|
12111
|
-
|
|
12112
|
-
|
|
12113
|
-
|
|
12114
|
-
|
|
12115
|
-
|
|
12116
|
-
|
|
12117
|
-
|
|
13150
|
+
// src/canvas/awareness-presence.ts
|
|
13151
|
+
var AWARENESS_PRESENCE_KIND = "awareness";
|
|
13152
|
+
var AWARENESS_MAX_SELECTION = 256;
|
|
13153
|
+
var MAX_ID_LENGTH = 128;
|
|
13154
|
+
var MAX_NAME_LENGTH = 64;
|
|
13155
|
+
var MAX_COLOR_LENGTH2 = 64;
|
|
13156
|
+
var MAX_ROLE_LENGTH = 32;
|
|
13157
|
+
var MAX_TOOL_LENGTH = 64;
|
|
13158
|
+
function isBoundedString(value, max) {
|
|
13159
|
+
return typeof value === "string" && value.length <= max;
|
|
13160
|
+
}
|
|
13161
|
+
function isOptionalBoundedString(value, max) {
|
|
13162
|
+
return value === void 0 || isBoundedString(value, max);
|
|
13163
|
+
}
|
|
13164
|
+
function isFinitePoint4(value) {
|
|
13165
|
+
if (typeof value !== "object" || value === null) return false;
|
|
13166
|
+
const point = value;
|
|
13167
|
+
return typeof point.x === "number" && Number.isFinite(point.x) && typeof point.y === "number" && Number.isFinite(point.y);
|
|
13168
|
+
}
|
|
13169
|
+
function isAwarenessPresence(data) {
|
|
13170
|
+
if (typeof data !== "object" || data === null) return false;
|
|
13171
|
+
const payload = data;
|
|
13172
|
+
if (payload.kind !== AWARENESS_PRESENCE_KIND) return false;
|
|
13173
|
+
if (!isBoundedString(payload.id, MAX_ID_LENGTH) || payload.id.length === 0) return false;
|
|
13174
|
+
if ("cleared" in payload) return payload.cleared === true;
|
|
13175
|
+
if (!isOptionalBoundedString(payload.name, MAX_NAME_LENGTH)) return false;
|
|
13176
|
+
if (!isOptionalBoundedString(payload.color, MAX_COLOR_LENGTH2)) return false;
|
|
13177
|
+
if (!isOptionalBoundedString(payload.role, MAX_ROLE_LENGTH)) return false;
|
|
13178
|
+
if (!isOptionalBoundedString(payload.tool, MAX_TOOL_LENGTH)) return false;
|
|
13179
|
+
if (payload.cursor !== void 0 && !isFinitePoint4(payload.cursor)) return false;
|
|
13180
|
+
if (payload.selection !== void 0) {
|
|
13181
|
+
if (!Array.isArray(payload.selection)) return false;
|
|
13182
|
+
if (payload.selection.length > AWARENESS_MAX_SELECTION) return false;
|
|
13183
|
+
for (const id of payload.selection) {
|
|
13184
|
+
if (!isBoundedString(id, MAX_ID_LENGTH) || id.length === 0) return false;
|
|
13185
|
+
}
|
|
12118
13186
|
}
|
|
12119
|
-
|
|
12120
|
-
|
|
12121
|
-
|
|
12122
|
-
|
|
13187
|
+
return true;
|
|
13188
|
+
}
|
|
13189
|
+
|
|
13190
|
+
// src/canvas/awareness-roster.ts
|
|
13191
|
+
var DEFAULT_STALE_MS = 45e3;
|
|
13192
|
+
var EMPTY_PEERS = Object.freeze([]);
|
|
13193
|
+
var EMPTY_SELECTION = Object.freeze([]);
|
|
13194
|
+
function sameSelection(a, b) {
|
|
13195
|
+
if (a.length !== b.length) return false;
|
|
13196
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
13197
|
+
return true;
|
|
13198
|
+
}
|
|
13199
|
+
function samePoint2(a, b) {
|
|
13200
|
+
if (a === null || b === null) return a === b;
|
|
13201
|
+
return a.x === b.x && a.y === b.y;
|
|
13202
|
+
}
|
|
13203
|
+
function toPeer(from, data, prev) {
|
|
13204
|
+
const cursor = data.cursor ? { x: data.cursor.x, y: data.cursor.y } : null;
|
|
13205
|
+
const incoming = data.selection ?? EMPTY_SELECTION;
|
|
13206
|
+
const selection = prev && sameSelection(prev.selection, incoming) ? prev.selection : incoming.length === 0 ? EMPTY_SELECTION : Object.freeze([...incoming]);
|
|
13207
|
+
const tool = data.tool ?? null;
|
|
13208
|
+
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)) {
|
|
13209
|
+
return prev;
|
|
13210
|
+
}
|
|
13211
|
+
const peer = {
|
|
13212
|
+
from,
|
|
13213
|
+
id: data.id,
|
|
13214
|
+
...data.name === void 0 ? {} : { name: data.name },
|
|
13215
|
+
...data.color === void 0 ? {} : { color: data.color },
|
|
13216
|
+
...data.role === void 0 ? {} : { role: data.role },
|
|
13217
|
+
cursor,
|
|
13218
|
+
selection,
|
|
13219
|
+
tool
|
|
13220
|
+
};
|
|
13221
|
+
return peer;
|
|
13222
|
+
}
|
|
13223
|
+
var PeerRoster = class {
|
|
13224
|
+
staleMs;
|
|
13225
|
+
now;
|
|
13226
|
+
rows = /* @__PURE__ */ new Map();
|
|
13227
|
+
discovered = /* @__PURE__ */ new Map();
|
|
13228
|
+
changeListeners = /* @__PURE__ */ new Set();
|
|
13229
|
+
discoverListeners = /* @__PURE__ */ new Set();
|
|
13230
|
+
leaveListeners = /* @__PURE__ */ new Set();
|
|
13231
|
+
snapshot = EMPTY_PEERS;
|
|
13232
|
+
snapshotDirty = false;
|
|
13233
|
+
staleTimer = null;
|
|
13234
|
+
isDisposed = false;
|
|
13235
|
+
constructor(options = {}) {
|
|
13236
|
+
this.staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
13237
|
+
this.now = options.now ?? (() => Date.now());
|
|
13238
|
+
}
|
|
13239
|
+
get disposed() {
|
|
13240
|
+
return this.isDisposed;
|
|
13241
|
+
}
|
|
13242
|
+
/**
|
|
13243
|
+
* Applies a presence payload from `from`. Non-awareness or malformed payloads
|
|
13244
|
+
* return `false` untouched, so hosts can feed every presence frame through.
|
|
13245
|
+
*/
|
|
13246
|
+
apply(from, data) {
|
|
13247
|
+
if (this.isDisposed || !isAwarenessPresence(data)) return false;
|
|
13248
|
+
const isNew = !this.discovered.has(from);
|
|
13249
|
+
this.discovered.set(from, this.now());
|
|
13250
|
+
if ("cleared" in data) {
|
|
13251
|
+
this.dropRow(from, "cleared");
|
|
13252
|
+
} else {
|
|
13253
|
+
const prev = this.rows.get(from);
|
|
13254
|
+
const next = toPeer(from, data, prev);
|
|
13255
|
+
if (next !== prev) {
|
|
13256
|
+
this.rows.set(from, next);
|
|
13257
|
+
this.changed();
|
|
13258
|
+
}
|
|
13259
|
+
}
|
|
13260
|
+
this.armStaleTimer();
|
|
13261
|
+
if (isNew) this.emit(this.discoverListeners, (l) => l(from));
|
|
13262
|
+
return true;
|
|
13263
|
+
}
|
|
13264
|
+
/** Server-authored presence-leave: drops the row AND the discovery entry. */
|
|
13265
|
+
remove(from) {
|
|
13266
|
+
if (this.isDisposed) return;
|
|
13267
|
+
const hadEntry = this.discovered.delete(from);
|
|
13268
|
+
this.dropRow(from, "left");
|
|
13269
|
+
if (hadEntry) this.armStaleTimer();
|
|
13270
|
+
}
|
|
13271
|
+
getPeers() {
|
|
13272
|
+
if (this.snapshotDirty) {
|
|
13273
|
+
this.snapshot = this.rows.size === 0 ? EMPTY_PEERS : Object.freeze([...this.rows.values()]);
|
|
13274
|
+
this.snapshotDirty = false;
|
|
13275
|
+
}
|
|
13276
|
+
return this.snapshot;
|
|
13277
|
+
}
|
|
13278
|
+
getPeer(from) {
|
|
13279
|
+
return this.rows.get(from);
|
|
13280
|
+
}
|
|
13281
|
+
/** Fires only when `getPeers()` would return a new reference. */
|
|
13282
|
+
onChange(listener) {
|
|
13283
|
+
this.changeListeners.add(listener);
|
|
13284
|
+
return () => this.changeListeners.delete(listener);
|
|
13285
|
+
}
|
|
13286
|
+
/** First valid frame from a sender since its discovery entry was last dropped. */
|
|
13287
|
+
onDiscover(listener) {
|
|
13288
|
+
this.discoverListeners.add(listener);
|
|
13289
|
+
return () => this.discoverListeners.delete(listener);
|
|
13290
|
+
}
|
|
13291
|
+
onLeave(listener) {
|
|
13292
|
+
this.leaveListeners.add(listener);
|
|
13293
|
+
return () => this.leaveListeners.delete(listener);
|
|
13294
|
+
}
|
|
13295
|
+
dispose() {
|
|
13296
|
+
if (this.isDisposed) return;
|
|
13297
|
+
this.isDisposed = true;
|
|
13298
|
+
if (this.staleTimer !== null) clearTimeout(this.staleTimer);
|
|
13299
|
+
this.staleTimer = null;
|
|
13300
|
+
this.rows.clear();
|
|
13301
|
+
this.discovered.clear();
|
|
13302
|
+
this.snapshot = EMPTY_PEERS;
|
|
13303
|
+
this.snapshotDirty = false;
|
|
13304
|
+
this.changeListeners.clear();
|
|
13305
|
+
this.discoverListeners.clear();
|
|
13306
|
+
this.leaveListeners.clear();
|
|
13307
|
+
}
|
|
13308
|
+
dropRow(from, reason) {
|
|
13309
|
+
const row = this.rows.get(from);
|
|
13310
|
+
if (!row) return;
|
|
13311
|
+
this.rows.delete(from);
|
|
13312
|
+
this.changed();
|
|
13313
|
+
this.emit(this.leaveListeners, (l) => l(row, reason));
|
|
13314
|
+
}
|
|
13315
|
+
changed() {
|
|
13316
|
+
this.snapshotDirty = true;
|
|
13317
|
+
this.emit(this.changeListeners, (l) => l());
|
|
13318
|
+
}
|
|
13319
|
+
emit(listeners, call) {
|
|
13320
|
+
for (const listener of [...listeners]) {
|
|
13321
|
+
try {
|
|
13322
|
+
call(listener);
|
|
13323
|
+
} catch {
|
|
13324
|
+
}
|
|
13325
|
+
}
|
|
13326
|
+
}
|
|
13327
|
+
armStaleTimer() {
|
|
13328
|
+
if (this.staleTimer !== null) clearTimeout(this.staleTimer);
|
|
13329
|
+
this.staleTimer = null;
|
|
13330
|
+
if (!Number.isFinite(this.staleMs) || this.staleMs <= 0 || this.isDisposed || this.discovered.size === 0) {
|
|
13331
|
+
return;
|
|
13332
|
+
}
|
|
13333
|
+
let earliest = Infinity;
|
|
13334
|
+
for (const seen of this.discovered.values()) if (seen < earliest) earliest = seen;
|
|
13335
|
+
const delay = Math.min(Math.max(0, earliest + this.staleMs - this.now()), 2 ** 31 - 1);
|
|
13336
|
+
this.staleTimer = setTimeout(() => {
|
|
13337
|
+
this.staleTimer = null;
|
|
13338
|
+
this.expireStale();
|
|
13339
|
+
}, delay);
|
|
13340
|
+
}
|
|
13341
|
+
expireStale() {
|
|
13342
|
+
const t = this.now();
|
|
13343
|
+
for (const from of [...this.discovered.keys()]) {
|
|
13344
|
+
const seen = this.discovered.get(from);
|
|
13345
|
+
if (seen === void 0 || t - seen < this.staleMs) continue;
|
|
13346
|
+
this.discovered.delete(from);
|
|
13347
|
+
this.dropRow(from, "stale");
|
|
13348
|
+
}
|
|
13349
|
+
this.armStaleTimer();
|
|
13350
|
+
}
|
|
13351
|
+
};
|
|
13352
|
+
|
|
13353
|
+
// src/canvas/awareness-publisher.ts
|
|
13354
|
+
var DEFAULT_FIELDS = Object.freeze({
|
|
13355
|
+
cursor: true,
|
|
13356
|
+
selection: false,
|
|
13357
|
+
tool: true
|
|
13358
|
+
});
|
|
13359
|
+
var DEFAULT_INTERVAL_MS = 50;
|
|
13360
|
+
var DEFAULT_HEARTBEAT_MS = 15e3;
|
|
13361
|
+
var MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
|
|
13362
|
+
var MAX_IDENTITY_ID_LENGTH = 128;
|
|
13363
|
+
var MAX_IDENTITY_NAME_LENGTH = 64;
|
|
13364
|
+
var MAX_IDENTITY_COLOR_LENGTH = 64;
|
|
13365
|
+
var MAX_IDENTITY_ROLE_LENGTH = 32;
|
|
13366
|
+
var MAX_TOOL_LENGTH2 = 64;
|
|
13367
|
+
var MAX_SELECTION_ID_LENGTH = 128;
|
|
13368
|
+
function normalizeIntervalMs(value) {
|
|
13369
|
+
return Number.isFinite(value) && value >= 0 ? value : 0;
|
|
13370
|
+
}
|
|
13371
|
+
function normalizeHeartbeatMs(value) {
|
|
13372
|
+
return Number.isFinite(value) && value > 0 ? value : 0;
|
|
13373
|
+
}
|
|
13374
|
+
var LocalAwareness = class {
|
|
13375
|
+
host;
|
|
13376
|
+
element;
|
|
13377
|
+
send;
|
|
13378
|
+
selectionFilter;
|
|
13379
|
+
onError;
|
|
13380
|
+
intervalMs;
|
|
13381
|
+
heartbeatMs;
|
|
13382
|
+
identity;
|
|
13383
|
+
fields;
|
|
13384
|
+
lastPointer = null;
|
|
13385
|
+
selection = [];
|
|
13386
|
+
selectionFailed = false;
|
|
13387
|
+
tool;
|
|
13388
|
+
dirty = false;
|
|
13389
|
+
lastSentAt = null;
|
|
13390
|
+
throttleTimer = null;
|
|
13391
|
+
heartbeatTimer = null;
|
|
13392
|
+
unsubscribers = [];
|
|
13393
|
+
isDisposed = false;
|
|
13394
|
+
handlePointerMove = (e) => this.onPointerMove(e);
|
|
13395
|
+
handlePointerEnd = (e) => this.onPointerEnd(e);
|
|
13396
|
+
constructor(host, options) {
|
|
13397
|
+
const element = options.element ?? host.domLayer.parentElement;
|
|
13398
|
+
if (!element) throw new Error("LocalAwareness: the viewport wrapper is not mounted");
|
|
13399
|
+
this.host = host;
|
|
13400
|
+
this.element = element;
|
|
13401
|
+
this.send = options.send;
|
|
13402
|
+
this.selectionFilter = options.selectionFilter;
|
|
13403
|
+
this.onError = options.onError;
|
|
13404
|
+
this.intervalMs = normalizeIntervalMs(options.intervalMs ?? DEFAULT_INTERVAL_MS);
|
|
13405
|
+
this.heartbeatMs = normalizeHeartbeatMs(options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS);
|
|
13406
|
+
this.identity = this.normalizeIdentity(options.identity);
|
|
13407
|
+
this.fields = mergeFields(DEFAULT_FIELDS, options.fields ?? {});
|
|
13408
|
+
this.tool = host.toolManager.activeTool?.name ?? null;
|
|
13409
|
+
if (this.fields.selection) this.refreshSelection();
|
|
13410
|
+
const opts = { passive: true };
|
|
13411
|
+
element.addEventListener("pointermove", this.handlePointerMove, opts);
|
|
13412
|
+
element.addEventListener("pointerleave", this.handlePointerEnd, opts);
|
|
13413
|
+
element.addEventListener("pointercancel", this.handlePointerEnd, opts);
|
|
13414
|
+
this.unsubscribers.push(
|
|
13415
|
+
host.onSelectionChange(() => {
|
|
13416
|
+
if (this.fields.selection) this.schedule();
|
|
13417
|
+
}),
|
|
13418
|
+
host.toolManager.onChange((name) => {
|
|
13419
|
+
this.tool = name;
|
|
13420
|
+
if (this.fields.tool) this.schedule();
|
|
13421
|
+
})
|
|
13422
|
+
);
|
|
13423
|
+
this.armHeartbeat();
|
|
13424
|
+
}
|
|
13425
|
+
get disposed() {
|
|
13426
|
+
return this.isDisposed;
|
|
13427
|
+
}
|
|
13428
|
+
getFields() {
|
|
13429
|
+
return this.fields;
|
|
13430
|
+
}
|
|
13431
|
+
setIdentity(identity) {
|
|
13432
|
+
this.identity = this.normalizeIdentity(identity);
|
|
13433
|
+
this.schedule();
|
|
13434
|
+
}
|
|
13435
|
+
/** Merges the given flags into the current policy; `undefined` keys are ignored. */
|
|
13436
|
+
setFields(fields) {
|
|
13437
|
+
this.fields = mergeFields(this.fields, fields);
|
|
13438
|
+
this.schedule();
|
|
13439
|
+
}
|
|
13440
|
+
/**
|
|
13441
|
+
* Requests a full frame: immediate when idle, otherwise folded into the
|
|
13442
|
+
* pending trailing frame (so N simultaneous requests cost one frame). Hosts
|
|
13443
|
+
* call it when the connection becomes live or reconnects.
|
|
13444
|
+
*/
|
|
13445
|
+
announce() {
|
|
13446
|
+
this.schedule();
|
|
13447
|
+
}
|
|
13448
|
+
/**
|
|
13449
|
+
* The complete state a frame carries right now. Side-effecting when
|
|
13450
|
+
* selection publishing is on: re-reads `getSelectedIds()`, re-runs
|
|
13451
|
+
* `selectionFilter`, updates the fail-closed selection state, and may call
|
|
13452
|
+
* `onError`. A no-op with respect to selection while publishing is off.
|
|
13453
|
+
*/
|
|
13454
|
+
getState() {
|
|
13455
|
+
const frame = { kind: AWARENESS_PRESENCE_KIND, id: this.identity.id };
|
|
13456
|
+
if (this.identity.name !== void 0) frame.name = this.identity.name;
|
|
13457
|
+
if (this.identity.color !== void 0) frame.color = this.identity.color;
|
|
13458
|
+
if (this.identity.role !== void 0) frame.role = this.identity.role;
|
|
13459
|
+
if (this.fields.cursor && this.lastPointer !== null) {
|
|
13460
|
+
frame.cursor = { x: this.lastPointer.x, y: this.lastPointer.y };
|
|
13461
|
+
}
|
|
13462
|
+
if (this.fields.selection) {
|
|
13463
|
+
this.refreshSelection();
|
|
13464
|
+
if (!this.selectionFailed && this.selection.length > 0) {
|
|
13465
|
+
frame.selection = [...this.selection];
|
|
13466
|
+
}
|
|
13467
|
+
}
|
|
13468
|
+
if (this.fields.tool && this.tool !== null && this.tool.length <= MAX_TOOL_LENGTH2) {
|
|
13469
|
+
frame.tool = this.tool;
|
|
13470
|
+
}
|
|
13471
|
+
return frame;
|
|
13472
|
+
}
|
|
13473
|
+
dispose() {
|
|
13474
|
+
if (this.isDisposed) return;
|
|
13475
|
+
this.isDisposed = true;
|
|
13476
|
+
if (this.throttleTimer !== null) clearTimeout(this.throttleTimer);
|
|
13477
|
+
this.throttleTimer = null;
|
|
13478
|
+
if (this.heartbeatTimer !== null) clearTimeout(this.heartbeatTimer);
|
|
13479
|
+
this.heartbeatTimer = null;
|
|
13480
|
+
this.element.removeEventListener("pointermove", this.handlePointerMove);
|
|
13481
|
+
this.element.removeEventListener("pointerleave", this.handlePointerEnd);
|
|
13482
|
+
this.element.removeEventListener("pointercancel", this.handlePointerEnd);
|
|
13483
|
+
for (const unsub of this.unsubscribers) unsub();
|
|
13484
|
+
this.unsubscribers.length = 0;
|
|
13485
|
+
this.safeSend({ kind: AWARENESS_PRESENCE_KIND, id: this.identity.id, cleared: true });
|
|
13486
|
+
}
|
|
13487
|
+
now() {
|
|
13488
|
+
return Date.now();
|
|
13489
|
+
}
|
|
13490
|
+
onPointerMove(e) {
|
|
13491
|
+
if (!e.isPrimary) return;
|
|
13492
|
+
const rect = this.element.getBoundingClientRect();
|
|
13493
|
+
const world = this.host.camera.screenToWorld({
|
|
13494
|
+
x: e.clientX - rect.left,
|
|
13495
|
+
y: e.clientY - rect.top
|
|
13496
|
+
});
|
|
13497
|
+
this.lastPointer = Number.isFinite(world.x) && Number.isFinite(world.y) ? { x: world.x, y: world.y } : null;
|
|
13498
|
+
if (this.fields.cursor) this.schedule();
|
|
13499
|
+
}
|
|
13500
|
+
onPointerEnd(e) {
|
|
13501
|
+
if (!e.isPrimary || this.lastPointer === null) return;
|
|
13502
|
+
this.lastPointer = null;
|
|
13503
|
+
if (this.fields.cursor) this.schedule();
|
|
13504
|
+
}
|
|
13505
|
+
refreshSelection() {
|
|
13506
|
+
try {
|
|
13507
|
+
const raw = this.host.getSelectedIds();
|
|
13508
|
+
const ids = this.selectionFilter ? this.selectionFilter(raw) : raw;
|
|
13509
|
+
if (!Array.isArray(ids)) throw new TypeError("selectionFilter must return an array");
|
|
13510
|
+
for (const id of ids) {
|
|
13511
|
+
if (typeof id !== "string") throw new TypeError("selectionFilter must return strings");
|
|
13512
|
+
if (id.length === 0 || id.length > MAX_SELECTION_ID_LENGTH) {
|
|
13513
|
+
throw new TypeError("selectionFilter must return ids of 1..128 characters");
|
|
13514
|
+
}
|
|
13515
|
+
}
|
|
13516
|
+
this.selection = ids.slice(0, AWARENESS_MAX_SELECTION);
|
|
13517
|
+
this.selectionFailed = false;
|
|
13518
|
+
} catch (error) {
|
|
13519
|
+
this.selection = [];
|
|
13520
|
+
this.selectionFailed = true;
|
|
13521
|
+
this.report(error);
|
|
13522
|
+
}
|
|
13523
|
+
}
|
|
13524
|
+
schedule() {
|
|
13525
|
+
if (this.isDisposed) return;
|
|
13526
|
+
this.dirty = true;
|
|
13527
|
+
if (this.throttleTimer !== null) return;
|
|
13528
|
+
const elapsed = this.lastSentAt === null ? Infinity : this.now() - this.lastSentAt;
|
|
13529
|
+
if (elapsed >= this.intervalMs) {
|
|
13530
|
+
this.flush();
|
|
13531
|
+
return;
|
|
13532
|
+
}
|
|
13533
|
+
this.throttleTimer = setTimeout(
|
|
13534
|
+
() => {
|
|
13535
|
+
this.throttleTimer = null;
|
|
13536
|
+
if (this.dirty) this.flush();
|
|
13537
|
+
},
|
|
13538
|
+
Math.min(this.intervalMs - elapsed, MAX_TIMER_DELAY_MS)
|
|
13539
|
+
);
|
|
13540
|
+
}
|
|
13541
|
+
flush() {
|
|
13542
|
+
this.dirty = false;
|
|
13543
|
+
this.lastSentAt = this.now();
|
|
13544
|
+
this.safeSend(this.getState());
|
|
13545
|
+
this.armHeartbeat();
|
|
13546
|
+
}
|
|
13547
|
+
armHeartbeat() {
|
|
13548
|
+
if (this.heartbeatTimer !== null) clearTimeout(this.heartbeatTimer);
|
|
13549
|
+
this.heartbeatTimer = null;
|
|
13550
|
+
if (this.heartbeatMs <= 0 || this.isDisposed) return;
|
|
13551
|
+
this.heartbeatTimer = setTimeout(
|
|
13552
|
+
() => {
|
|
13553
|
+
this.heartbeatTimer = null;
|
|
13554
|
+
this.flush();
|
|
13555
|
+
},
|
|
13556
|
+
Math.min(this.heartbeatMs, MAX_TIMER_DELAY_MS)
|
|
13557
|
+
);
|
|
13558
|
+
}
|
|
13559
|
+
safeSend(frame) {
|
|
13560
|
+
try {
|
|
13561
|
+
this.send(frame);
|
|
13562
|
+
} catch (error) {
|
|
13563
|
+
this.report(error);
|
|
13564
|
+
}
|
|
13565
|
+
}
|
|
13566
|
+
report(error) {
|
|
13567
|
+
try {
|
|
13568
|
+
this.onError?.(error);
|
|
13569
|
+
} catch {
|
|
13570
|
+
}
|
|
13571
|
+
}
|
|
13572
|
+
/**
|
|
13573
|
+
* Truncates identity strings to the wire caps and rejects an invalid id, so a
|
|
13574
|
+
* sender can never publish a frame that the wire guard would drop outright.
|
|
13575
|
+
* A truncated field is reported through `onError` (a `RangeError`) rather
|
|
13576
|
+
* than silently shortened, so a caller passing an over-long name finds out.
|
|
13577
|
+
*/
|
|
13578
|
+
normalizeIdentity(identity) {
|
|
13579
|
+
if (identity.id.length === 0 || identity.id.length > MAX_IDENTITY_ID_LENGTH) {
|
|
13580
|
+
throw new RangeError("LocalAwareness: identity.id must be 1..128 characters");
|
|
13581
|
+
}
|
|
13582
|
+
const normalized = {
|
|
13583
|
+
id: identity.id
|
|
13584
|
+
};
|
|
13585
|
+
if (identity.name !== void 0) {
|
|
13586
|
+
normalized.name = identity.name.slice(0, MAX_IDENTITY_NAME_LENGTH);
|
|
13587
|
+
if (identity.name.length > MAX_IDENTITY_NAME_LENGTH) {
|
|
13588
|
+
this.report(
|
|
13589
|
+
new RangeError(
|
|
13590
|
+
`LocalAwareness: identity.name truncated to ${MAX_IDENTITY_NAME_LENGTH} characters`
|
|
13591
|
+
)
|
|
13592
|
+
);
|
|
13593
|
+
}
|
|
13594
|
+
}
|
|
13595
|
+
if (identity.color !== void 0) {
|
|
13596
|
+
normalized.color = identity.color.slice(0, MAX_IDENTITY_COLOR_LENGTH);
|
|
13597
|
+
if (identity.color.length > MAX_IDENTITY_COLOR_LENGTH) {
|
|
13598
|
+
this.report(
|
|
13599
|
+
new RangeError(
|
|
13600
|
+
`LocalAwareness: identity.color truncated to ${MAX_IDENTITY_COLOR_LENGTH} characters`
|
|
13601
|
+
)
|
|
13602
|
+
);
|
|
13603
|
+
}
|
|
13604
|
+
}
|
|
13605
|
+
if (identity.role !== void 0) {
|
|
13606
|
+
normalized.role = identity.role.slice(0, MAX_IDENTITY_ROLE_LENGTH);
|
|
13607
|
+
if (identity.role.length > MAX_IDENTITY_ROLE_LENGTH) {
|
|
13608
|
+
this.report(
|
|
13609
|
+
new RangeError(
|
|
13610
|
+
`LocalAwareness: identity.role truncated to ${MAX_IDENTITY_ROLE_LENGTH} characters`
|
|
13611
|
+
)
|
|
13612
|
+
);
|
|
13613
|
+
}
|
|
13614
|
+
}
|
|
13615
|
+
return normalized;
|
|
13616
|
+
}
|
|
13617
|
+
};
|
|
13618
|
+
function mergeFields(current, patch) {
|
|
13619
|
+
return Object.freeze({
|
|
13620
|
+
cursor: patch.cursor ?? current.cursor,
|
|
13621
|
+
selection: patch.selection ?? current.selection,
|
|
13622
|
+
tool: patch.tool ?? current.tool
|
|
13623
|
+
});
|
|
13624
|
+
}
|
|
13625
|
+
|
|
13626
|
+
// src/canvas/remote-cursor-overlay.ts
|
|
13627
|
+
var PEER_COLORS = Object.freeze([
|
|
13628
|
+
"#e11d48",
|
|
13629
|
+
"#ea580c",
|
|
13630
|
+
"#ca8a04",
|
|
13631
|
+
"#16a34a",
|
|
13632
|
+
"#0d9488",
|
|
13633
|
+
"#0284c7",
|
|
13634
|
+
"#2563eb",
|
|
13635
|
+
"#7c3aed",
|
|
13636
|
+
"#c026d3",
|
|
13637
|
+
"#db2777",
|
|
13638
|
+
"#4d7c0f",
|
|
13639
|
+
"#b45309"
|
|
13640
|
+
]);
|
|
13641
|
+
function defaultPeerColor(seed) {
|
|
13642
|
+
if (seed.length === 0) return PEER_COLORS[0] ?? "#2563eb";
|
|
13643
|
+
let hash = 2166136261;
|
|
13644
|
+
for (let i = 0; i < seed.length; i++) {
|
|
13645
|
+
hash ^= seed.charCodeAt(i);
|
|
13646
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
13647
|
+
}
|
|
13648
|
+
return PEER_COLORS[hash % PEER_COLORS.length] ?? "#2563eb";
|
|
13649
|
+
}
|
|
13650
|
+
var DEFAULT_LABEL_FONT = "12px sans-serif";
|
|
13651
|
+
var LABEL_PAD_X = 6;
|
|
13652
|
+
var LABEL_PAD_Y = 3;
|
|
13653
|
+
var LABEL_HEIGHT = 16;
|
|
13654
|
+
var LABEL_OFFSET = 14;
|
|
13655
|
+
var MAX_LABEL_WIDTH_CACHE = 64;
|
|
13656
|
+
var RemoteCursorOverlay = class {
|
|
13657
|
+
host;
|
|
13658
|
+
roster;
|
|
13659
|
+
colorFor;
|
|
13660
|
+
showLabels;
|
|
13661
|
+
labelFont;
|
|
13662
|
+
labelWidths = /* @__PURE__ */ new Map();
|
|
13663
|
+
unregister;
|
|
13664
|
+
unsubscribe;
|
|
13665
|
+
isDisposed = false;
|
|
13666
|
+
constructor(host, roster, options = {}) {
|
|
13667
|
+
this.host = host;
|
|
13668
|
+
this.roster = roster;
|
|
13669
|
+
this.colorFor = options.colorFor;
|
|
13670
|
+
this.showLabels = options.showLabels ?? true;
|
|
13671
|
+
this.labelFont = options.labelFont ?? DEFAULT_LABEL_FONT;
|
|
13672
|
+
this.unregister = host.registerOverlay((ctx) => this.render(ctx));
|
|
13673
|
+
this.unsubscribe = roster.onChange(() => {
|
|
13674
|
+
if (this.labelWidths.size > MAX_LABEL_WIDTH_CACHE) this.labelWidths.clear();
|
|
13675
|
+
host.requestRender();
|
|
13676
|
+
});
|
|
13677
|
+
}
|
|
13678
|
+
get disposed() {
|
|
13679
|
+
return this.isDisposed;
|
|
13680
|
+
}
|
|
13681
|
+
resolveColor(peer) {
|
|
13682
|
+
return this.colorFor?.(peer) ?? peer.color ?? defaultPeerColor(peer.id);
|
|
13683
|
+
}
|
|
13684
|
+
dispose() {
|
|
13685
|
+
if (this.isDisposed) return;
|
|
13686
|
+
this.isDisposed = true;
|
|
13687
|
+
this.unsubscribe?.();
|
|
13688
|
+
this.unsubscribe = null;
|
|
13689
|
+
this.unregister?.();
|
|
13690
|
+
this.unregister = null;
|
|
13691
|
+
this.labelWidths.clear();
|
|
13692
|
+
this.host.requestRender();
|
|
13693
|
+
}
|
|
13694
|
+
render(ctx) {
|
|
13695
|
+
if (this.isDisposed) return;
|
|
13696
|
+
const zoom = this.host.camera.zoom;
|
|
13697
|
+
const inv = zoom > 0 && Number.isFinite(zoom) ? 1 / zoom : 1;
|
|
13698
|
+
for (const peer of this.roster.getPeers()) {
|
|
13699
|
+
if (peer.cursor === null) continue;
|
|
13700
|
+
const color = this.resolveColor(peer);
|
|
13701
|
+
ctx.save();
|
|
13702
|
+
ctx.translate(peer.cursor.x, peer.cursor.y);
|
|
13703
|
+
ctx.scale(inv, inv);
|
|
13704
|
+
ctx.beginPath();
|
|
13705
|
+
ctx.moveTo(0, 0);
|
|
13706
|
+
ctx.lineTo(0, 16);
|
|
13707
|
+
ctx.lineTo(4.5, 12.5);
|
|
13708
|
+
ctx.lineTo(11, 12.5);
|
|
13709
|
+
ctx.closePath();
|
|
13710
|
+
ctx.fillStyle = color;
|
|
13711
|
+
ctx.fill();
|
|
13712
|
+
ctx.strokeStyle = "#ffffff";
|
|
13713
|
+
ctx.lineWidth = 1;
|
|
13714
|
+
ctx.stroke();
|
|
13715
|
+
if (this.showLabels && peer.name !== void 0 && peer.name.length > 0) {
|
|
13716
|
+
this.drawLabel(ctx, peer.name, color);
|
|
13717
|
+
}
|
|
13718
|
+
ctx.restore();
|
|
13719
|
+
}
|
|
13720
|
+
}
|
|
13721
|
+
drawLabel(ctx, name, color) {
|
|
13722
|
+
ctx.font = this.labelFont;
|
|
13723
|
+
const key = `${this.labelFont} ${name}`;
|
|
13724
|
+
let width = this.labelWidths.get(key);
|
|
13725
|
+
if (width === void 0) {
|
|
13726
|
+
width = ctx.measureText(name).width;
|
|
13727
|
+
this.labelWidths.set(key, width);
|
|
13728
|
+
}
|
|
13729
|
+
const w = width + LABEL_PAD_X * 2;
|
|
13730
|
+
ctx.fillStyle = color;
|
|
13731
|
+
ctx.beginPath();
|
|
13732
|
+
ctx.roundRect(LABEL_OFFSET, LABEL_OFFSET, w, LABEL_HEIGHT + LABEL_PAD_Y, 4);
|
|
13733
|
+
ctx.fill();
|
|
13734
|
+
ctx.fillStyle = "#ffffff";
|
|
13735
|
+
ctx.textAlign = "left";
|
|
13736
|
+
ctx.textBaseline = "middle";
|
|
13737
|
+
ctx.fillText(name, LABEL_OFFSET + LABEL_PAD_X, LABEL_OFFSET + (LABEL_HEIGHT + LABEL_PAD_Y) / 2);
|
|
13738
|
+
}
|
|
13739
|
+
};
|
|
13740
|
+
|
|
13741
|
+
// src/canvas/remote-selection-overlay.ts
|
|
13742
|
+
var DEFAULT_ALPHA = 0.6;
|
|
13743
|
+
var DEFAULT_LINE_WIDTH_PX = 2;
|
|
13744
|
+
var RemoteSelectionOverlay = class {
|
|
13745
|
+
host;
|
|
13746
|
+
roster;
|
|
13747
|
+
colorFor;
|
|
13748
|
+
alpha;
|
|
13749
|
+
lineWidthPx;
|
|
13750
|
+
signatures = [];
|
|
13751
|
+
outlines = [];
|
|
13752
|
+
storeDirty = true;
|
|
13753
|
+
unregister;
|
|
13754
|
+
unsubscribers = [];
|
|
13755
|
+
isDisposed = false;
|
|
13756
|
+
constructor(host, roster, options = {}) {
|
|
13757
|
+
this.host = host;
|
|
13758
|
+
this.roster = roster;
|
|
13759
|
+
this.colorFor = options.colorFor;
|
|
13760
|
+
this.alpha = options.alpha ?? DEFAULT_ALPHA;
|
|
13761
|
+
this.lineWidthPx = options.lineWidthPx ?? DEFAULT_LINE_WIDTH_PX;
|
|
13762
|
+
this.unregister = host.registerOverlay((ctx) => this.render(ctx));
|
|
13763
|
+
const invalidate = () => {
|
|
13764
|
+
this.storeDirty = true;
|
|
13765
|
+
host.requestRender();
|
|
13766
|
+
};
|
|
13767
|
+
this.unsubscribers.push(
|
|
13768
|
+
roster.onChange(() => host.requestRender()),
|
|
13769
|
+
host.store.onChange(invalidate),
|
|
13770
|
+
host.layerManager.on("change", invalidate)
|
|
13771
|
+
);
|
|
13772
|
+
}
|
|
13773
|
+
get disposed() {
|
|
13774
|
+
return this.isDisposed;
|
|
13775
|
+
}
|
|
13776
|
+
dispose() {
|
|
13777
|
+
if (this.isDisposed) return;
|
|
13778
|
+
this.isDisposed = true;
|
|
13779
|
+
for (const unsub of this.unsubscribers) unsub();
|
|
13780
|
+
this.unsubscribers.length = 0;
|
|
13781
|
+
this.unregister?.();
|
|
13782
|
+
this.unregister = null;
|
|
13783
|
+
this.signatures = [];
|
|
13784
|
+
this.outlines = [];
|
|
13785
|
+
this.host.requestRender();
|
|
13786
|
+
}
|
|
13787
|
+
resolveColor(peer) {
|
|
13788
|
+
return this.colorFor?.(peer) ?? peer.color ?? defaultPeerColor(peer.id);
|
|
13789
|
+
}
|
|
13790
|
+
/** Recomputes outlines only when the selection signature or the store/layers changed. */
|
|
13791
|
+
rebuild() {
|
|
13792
|
+
const peers = this.roster.getPeers();
|
|
13793
|
+
const next = [];
|
|
13794
|
+
for (const peer of peers) {
|
|
13795
|
+
if (peer.selection.length === 0) continue;
|
|
13796
|
+
next.push({ from: peer.from, selection: peer.selection, color: this.resolveColor(peer) });
|
|
13797
|
+
}
|
|
13798
|
+
let changed = this.storeDirty || next.length !== this.signatures.length;
|
|
13799
|
+
if (!changed) {
|
|
13800
|
+
for (let i = 0; i < next.length; i++) {
|
|
13801
|
+
const a = next[i];
|
|
13802
|
+
const b = this.signatures[i];
|
|
13803
|
+
if (!a || !b || a.from !== b.from || a.selection !== b.selection || a.color !== b.color) {
|
|
13804
|
+
changed = true;
|
|
13805
|
+
break;
|
|
13806
|
+
}
|
|
13807
|
+
}
|
|
13808
|
+
}
|
|
13809
|
+
if (!changed) return;
|
|
13810
|
+
this.storeDirty = false;
|
|
13811
|
+
this.signatures = next;
|
|
13812
|
+
if (next.length === 0) {
|
|
13813
|
+
this.outlines = [];
|
|
13814
|
+
return;
|
|
13815
|
+
}
|
|
13816
|
+
const colorById = /* @__PURE__ */ new Map();
|
|
13817
|
+
for (const sig of next) {
|
|
13818
|
+
for (const id of sig.selection) if (!colorById.has(id)) colorById.set(id, sig.color);
|
|
13819
|
+
}
|
|
13820
|
+
const layers = this.host.layerManager;
|
|
13821
|
+
const rects = computeElementRects(
|
|
13822
|
+
this.host.store,
|
|
13823
|
+
(element) => colorById.has(element.id) && layers.isLayerVisible(element.layerId) ? element.id : null
|
|
13824
|
+
);
|
|
13825
|
+
this.outlines = rects.map((rect) => ({ rect, color: colorById.get(rect.id) ?? "#2563eb" }));
|
|
13826
|
+
}
|
|
13827
|
+
render(ctx) {
|
|
13828
|
+
if (this.isDisposed) return;
|
|
13829
|
+
this.rebuild();
|
|
13830
|
+
if (this.outlines.length === 0) return;
|
|
13831
|
+
const zoom = this.host.camera.zoom;
|
|
13832
|
+
const inv = zoom > 0 && Number.isFinite(zoom) ? 1 / zoom : 1;
|
|
13833
|
+
ctx.save();
|
|
13834
|
+
ctx.globalAlpha = this.alpha;
|
|
13835
|
+
ctx.lineWidth = this.lineWidthPx * inv;
|
|
13836
|
+
for (const { rect, color } of this.outlines) {
|
|
13837
|
+
ctx.save();
|
|
13838
|
+
ctx.strokeStyle = color;
|
|
13839
|
+
ctx.translate(rect.x + rect.w / 2, rect.y + rect.h / 2);
|
|
13840
|
+
if (rect.rotation !== 0) ctx.rotate(rect.rotation);
|
|
13841
|
+
ctx.strokeRect(-rect.w / 2, -rect.h / 2, rect.w, rect.h);
|
|
13842
|
+
ctx.restore();
|
|
13843
|
+
}
|
|
13844
|
+
ctx.restore();
|
|
13845
|
+
}
|
|
13846
|
+
};
|
|
13847
|
+
|
|
13848
|
+
// src/canvas/attach-awareness.ts
|
|
13849
|
+
function attachAwareness(viewport, channel, options) {
|
|
13850
|
+
const {
|
|
13851
|
+
roster: rosterOptions,
|
|
13852
|
+
cursors: cursorOptions,
|
|
13853
|
+
selections: selectionOptions,
|
|
13854
|
+
publish,
|
|
13855
|
+
...localOptions
|
|
13856
|
+
} = options;
|
|
13857
|
+
const roster = new PeerRoster(rosterOptions);
|
|
13858
|
+
let local = null;
|
|
13859
|
+
let cursors = null;
|
|
13860
|
+
let selections = null;
|
|
13861
|
+
const unsubscribers = [];
|
|
13862
|
+
try {
|
|
13863
|
+
local = publish === false ? null : new LocalAwareness(viewport, {
|
|
13864
|
+
...localOptions,
|
|
13865
|
+
send: (data) => channel.sendPresence(data)
|
|
13866
|
+
});
|
|
13867
|
+
cursors = cursorOptions === false ? null : new RemoteCursorOverlay(viewport, roster, cursorOptions ?? {});
|
|
13868
|
+
selections = selectionOptions === void 0 || selectionOptions === false ? null : new RemoteSelectionOverlay(
|
|
13869
|
+
viewport,
|
|
13870
|
+
roster,
|
|
13871
|
+
selectionOptions === true ? {} : selectionOptions
|
|
13872
|
+
);
|
|
13873
|
+
if (publish !== false) unsubscribers.push(roster.onDiscover(() => local?.announce()));
|
|
13874
|
+
unsubscribers.push(
|
|
13875
|
+
channel.onPresence((from, data) => {
|
|
13876
|
+
roster.apply(from, data);
|
|
13877
|
+
})
|
|
13878
|
+
);
|
|
13879
|
+
unsubscribers.push(channel.onPresenceLeave((from) => roster.remove(from)));
|
|
13880
|
+
} catch (error) {
|
|
13881
|
+
for (let i = unsubscribers.length - 1; i >= 0; i--) {
|
|
13882
|
+
try {
|
|
13883
|
+
unsubscribers[i]?.();
|
|
13884
|
+
} catch {
|
|
13885
|
+
}
|
|
13886
|
+
}
|
|
13887
|
+
unsubscribers.length = 0;
|
|
13888
|
+
try {
|
|
13889
|
+
selections?.dispose();
|
|
13890
|
+
} catch {
|
|
13891
|
+
}
|
|
13892
|
+
try {
|
|
13893
|
+
cursors?.dispose();
|
|
13894
|
+
} catch {
|
|
13895
|
+
}
|
|
13896
|
+
try {
|
|
13897
|
+
local?.dispose();
|
|
13898
|
+
} catch {
|
|
13899
|
+
}
|
|
13900
|
+
try {
|
|
13901
|
+
roster.dispose();
|
|
13902
|
+
} catch {
|
|
13903
|
+
}
|
|
13904
|
+
throw error;
|
|
13905
|
+
}
|
|
13906
|
+
let disposed = false;
|
|
13907
|
+
return {
|
|
13908
|
+
roster,
|
|
13909
|
+
local,
|
|
13910
|
+
cursors,
|
|
13911
|
+
selections,
|
|
13912
|
+
announce: () => local?.announce(),
|
|
13913
|
+
setFields: (fields) => local?.setFields(fields),
|
|
13914
|
+
dispose: () => {
|
|
13915
|
+
if (disposed) return;
|
|
13916
|
+
disposed = true;
|
|
13917
|
+
try {
|
|
13918
|
+
local?.dispose();
|
|
13919
|
+
} catch {
|
|
13920
|
+
}
|
|
13921
|
+
try {
|
|
13922
|
+
cursors?.dispose();
|
|
13923
|
+
} catch {
|
|
13924
|
+
}
|
|
13925
|
+
try {
|
|
13926
|
+
selections?.dispose();
|
|
13927
|
+
} catch {
|
|
13928
|
+
}
|
|
13929
|
+
try {
|
|
13930
|
+
roster.dispose();
|
|
13931
|
+
} catch {
|
|
13932
|
+
}
|
|
13933
|
+
for (const unsub of unsubscribers) {
|
|
13934
|
+
try {
|
|
13935
|
+
unsub();
|
|
13936
|
+
} catch {
|
|
13937
|
+
}
|
|
13938
|
+
}
|
|
13939
|
+
unsubscribers.length = 0;
|
|
13940
|
+
}
|
|
13941
|
+
};
|
|
13942
|
+
}
|
|
13943
|
+
|
|
13944
|
+
// src/tools/hand-tool.ts
|
|
13945
|
+
var HandTool = class {
|
|
13946
|
+
name = "hand";
|
|
13947
|
+
panning = false;
|
|
13948
|
+
lastScreen = { x: 0, y: 0 };
|
|
13949
|
+
onActivate(ctx) {
|
|
13950
|
+
ctx.setCursor?.("grab");
|
|
13951
|
+
}
|
|
13952
|
+
onDeactivate(ctx) {
|
|
13953
|
+
ctx.setCursor?.("default");
|
|
13954
|
+
}
|
|
13955
|
+
onPointerDown(state, ctx) {
|
|
13956
|
+
this.panning = true;
|
|
13957
|
+
this.lastScreen = { x: state.x, y: state.y };
|
|
13958
|
+
ctx.setCursor?.("grabbing");
|
|
13959
|
+
}
|
|
13960
|
+
onPointerMove(state, ctx) {
|
|
13961
|
+
if (!this.panning) return;
|
|
13962
|
+
const dx = state.x - this.lastScreen.x;
|
|
13963
|
+
const dy = state.y - this.lastScreen.y;
|
|
12123
13964
|
this.lastScreen = { x: state.x, y: state.y };
|
|
12124
13965
|
ctx.camera.pan(dx, dy);
|
|
12125
13966
|
}
|
|
@@ -13765,7 +15606,7 @@ var MeasureTool = class {
|
|
|
13765
15606
|
// src/tools/path-tool.ts
|
|
13766
15607
|
var EPS2 = 1e-6;
|
|
13767
15608
|
var DEFAULT_COMMIT_TAP_RADIUS_PX = 12;
|
|
13768
|
-
function
|
|
15609
|
+
function samePoint3(a, b) {
|
|
13769
15610
|
return Math.abs(a.x - b.x) < EPS2 && Math.abs(a.y - b.y) < EPS2;
|
|
13770
15611
|
}
|
|
13771
15612
|
var PathTool = class {
|
|
@@ -13909,7 +15750,7 @@ var PathTool = class {
|
|
|
13909
15750
|
return;
|
|
13910
15751
|
}
|
|
13911
15752
|
const last = this.lastWaypoint();
|
|
13912
|
-
if (this.cursor && last && !
|
|
15753
|
+
if (this.cursor && last && !samePoint3(this.cursor, last)) {
|
|
13913
15754
|
this.waypoints.push({ ...this.cursor });
|
|
13914
15755
|
}
|
|
13915
15756
|
this.scheduleEmission();
|
|
@@ -13970,7 +15811,7 @@ var PathTool = class {
|
|
|
13970
15811
|
* world point.
|
|
13971
15812
|
*/
|
|
13972
15813
|
withinCommitRadius(point, last, ctx) {
|
|
13973
|
-
if (
|
|
15814
|
+
if (samePoint3(point, last)) return true;
|
|
13974
15815
|
if (this.hasSnappingGrid()) return false;
|
|
13975
15816
|
const zoom = ctx.camera.zoom;
|
|
13976
15817
|
if (!(zoom > 0)) return false;
|
|
@@ -14018,7 +15859,7 @@ var PathTool = class {
|
|
|
14018
15859
|
measure(cursor) {
|
|
14019
15860
|
const points = this.waypoints.map((p) => ({ ...p }));
|
|
14020
15861
|
const last = points[points.length - 1];
|
|
14021
|
-
if (cursor && (!last || !
|
|
15862
|
+
if (cursor && (!last || !samePoint3(cursor, last))) points.push({ ...cursor });
|
|
14022
15863
|
const { total, cumulative } = pathDistanceCells(points, {
|
|
14023
15864
|
gridSize: this.gridSize,
|
|
14024
15865
|
gridType: this.gridType,
|
|
@@ -14665,9 +16506,191 @@ var PingTool = class {
|
|
|
14665
16506
|
}
|
|
14666
16507
|
};
|
|
14667
16508
|
|
|
16509
|
+
// src/tools/fog-tool.ts
|
|
16510
|
+
var DEFAULT_RADIUS5 = 40;
|
|
16511
|
+
var MIN_POINT_DISTANCE = 4;
|
|
16512
|
+
var FogTool = class {
|
|
16513
|
+
name = "fog";
|
|
16514
|
+
drawing = false;
|
|
16515
|
+
points = [];
|
|
16516
|
+
startPoint = null;
|
|
16517
|
+
operation;
|
|
16518
|
+
shape;
|
|
16519
|
+
radius;
|
|
16520
|
+
manager;
|
|
16521
|
+
optionListeners = /* @__PURE__ */ new Set();
|
|
16522
|
+
constructor(manager, options = {}) {
|
|
16523
|
+
this.manager = manager;
|
|
16524
|
+
this.operation = options.operation ?? "reveal";
|
|
16525
|
+
this.shape = options.shape ?? "brush";
|
|
16526
|
+
this.radius = options.radius ?? DEFAULT_RADIUS5;
|
|
16527
|
+
}
|
|
16528
|
+
onActivate(ctx) {
|
|
16529
|
+
ctx.setCursor?.("crosshair");
|
|
16530
|
+
}
|
|
16531
|
+
onDeactivate(ctx) {
|
|
16532
|
+
this.cancelGesture(ctx);
|
|
16533
|
+
ctx.setCursor?.("default");
|
|
16534
|
+
}
|
|
16535
|
+
getOptions() {
|
|
16536
|
+
return {
|
|
16537
|
+
operation: this.operation,
|
|
16538
|
+
shape: this.shape,
|
|
16539
|
+
radius: this.radius
|
|
16540
|
+
};
|
|
16541
|
+
}
|
|
16542
|
+
setOptions(options) {
|
|
16543
|
+
if (options.operation !== void 0) this.operation = options.operation;
|
|
16544
|
+
if (options.shape !== void 0) this.shape = options.shape;
|
|
16545
|
+
if (options.radius !== void 0 && Number.isFinite(options.radius) && options.radius > 0) {
|
|
16546
|
+
this.radius = options.radius;
|
|
16547
|
+
}
|
|
16548
|
+
for (const listener of this.optionListeners) listener();
|
|
16549
|
+
}
|
|
16550
|
+
onOptionsChange(listener) {
|
|
16551
|
+
this.optionListeners.add(listener);
|
|
16552
|
+
return () => this.optionListeners.delete(listener);
|
|
16553
|
+
}
|
|
16554
|
+
onPointerDown(state, ctx) {
|
|
16555
|
+
if (this.drawing) return;
|
|
16556
|
+
this.drawing = true;
|
|
16557
|
+
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
16558
|
+
this.startPoint = world;
|
|
16559
|
+
this.points = [world];
|
|
16560
|
+
ctx.requestRender();
|
|
16561
|
+
}
|
|
16562
|
+
onPointerMove(state, ctx) {
|
|
16563
|
+
if (!this.drawing) return;
|
|
16564
|
+
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
16565
|
+
if (this.shape === "rectangle") {
|
|
16566
|
+
if (this.startPoint) this.points = [this.startPoint, world];
|
|
16567
|
+
} else {
|
|
16568
|
+
const last = this.points[this.points.length - 1];
|
|
16569
|
+
if (last) {
|
|
16570
|
+
const dx = world.x - last.x;
|
|
16571
|
+
const dy = world.y - last.y;
|
|
16572
|
+
if (dx * dx + dy * dy < MIN_POINT_DISTANCE * MIN_POINT_DISTANCE) return;
|
|
16573
|
+
}
|
|
16574
|
+
this.points.push(world);
|
|
16575
|
+
}
|
|
16576
|
+
ctx.requestRender();
|
|
16577
|
+
}
|
|
16578
|
+
onPointerUp(_state, ctx) {
|
|
16579
|
+
if (!this.drawing) return;
|
|
16580
|
+
this.drawing = false;
|
|
16581
|
+
const region = this.buildRegion();
|
|
16582
|
+
if (region) {
|
|
16583
|
+
this.manager.applyRegion(region, this.operation);
|
|
16584
|
+
}
|
|
16585
|
+
this.points = [];
|
|
16586
|
+
this.startPoint = null;
|
|
16587
|
+
ctx.requestRender();
|
|
16588
|
+
}
|
|
16589
|
+
onPointerCancel(_state, ctx) {
|
|
16590
|
+
this.cancelGesture(ctx);
|
|
16591
|
+
}
|
|
16592
|
+
onKeyDown(event, ctx) {
|
|
16593
|
+
if (event.key === "Escape" && this.drawing) {
|
|
16594
|
+
this.cancelGesture(ctx);
|
|
16595
|
+
return true;
|
|
16596
|
+
}
|
|
16597
|
+
return false;
|
|
16598
|
+
}
|
|
16599
|
+
renderOverlay(ctx) {
|
|
16600
|
+
if (!this.drawing || this.points.length === 0) return;
|
|
16601
|
+
ctx.save();
|
|
16602
|
+
ctx.strokeStyle = this.operation === "reveal" ? "rgba(255,255,255,0.6)" : "rgba(0,0,0,0.4)";
|
|
16603
|
+
ctx.fillStyle = this.operation === "reveal" ? "rgba(255,255,255,0.15)" : "rgba(0,0,0,0.1)";
|
|
16604
|
+
ctx.lineWidth = 2;
|
|
16605
|
+
ctx.setLineDash([6, 4]);
|
|
16606
|
+
switch (this.shape) {
|
|
16607
|
+
case "brush":
|
|
16608
|
+
this.renderBrushPreview(ctx);
|
|
16609
|
+
break;
|
|
16610
|
+
case "rectangle":
|
|
16611
|
+
this.renderRectanglePreview(ctx);
|
|
16612
|
+
break;
|
|
16613
|
+
case "polygon":
|
|
16614
|
+
this.renderPolygonPreview(ctx);
|
|
16615
|
+
break;
|
|
16616
|
+
}
|
|
16617
|
+
ctx.restore();
|
|
16618
|
+
}
|
|
16619
|
+
buildRegion() {
|
|
16620
|
+
switch (this.shape) {
|
|
16621
|
+
case "brush": {
|
|
16622
|
+
if (this.points.length === 0) return null;
|
|
16623
|
+
return { kind: "brush", points: this.points, radius: this.radius };
|
|
16624
|
+
}
|
|
16625
|
+
case "rectangle": {
|
|
16626
|
+
if (!this.startPoint || this.points.length < 2) return null;
|
|
16627
|
+
const end = this.points[this.points.length - 1];
|
|
16628
|
+
if (this.startPoint.x === end.x && this.startPoint.y === end.y) return null;
|
|
16629
|
+
return { kind: "rectangle", from: this.startPoint, to: end };
|
|
16630
|
+
}
|
|
16631
|
+
case "polygon": {
|
|
16632
|
+
if (this.points.length < 3) return null;
|
|
16633
|
+
return { kind: "polygon", points: this.points };
|
|
16634
|
+
}
|
|
16635
|
+
}
|
|
16636
|
+
}
|
|
16637
|
+
cancelGesture(ctx) {
|
|
16638
|
+
this.drawing = false;
|
|
16639
|
+
this.points = [];
|
|
16640
|
+
this.startPoint = null;
|
|
16641
|
+
ctx.requestRender();
|
|
16642
|
+
}
|
|
16643
|
+
renderBrushPreview(ctx) {
|
|
16644
|
+
if (this.points.length === 1) {
|
|
16645
|
+
const p = this.points[0];
|
|
16646
|
+
ctx.beginPath();
|
|
16647
|
+
ctx.arc(p.x, p.y, this.radius, 0, Math.PI * 2);
|
|
16648
|
+
ctx.fill();
|
|
16649
|
+
ctx.stroke();
|
|
16650
|
+
return;
|
|
16651
|
+
}
|
|
16652
|
+
ctx.beginPath();
|
|
16653
|
+
for (let i = 0; i < this.points.length; i++) {
|
|
16654
|
+
const p = this.points[i];
|
|
16655
|
+
if (i === 0) ctx.moveTo(p.x, p.y);
|
|
16656
|
+
else ctx.lineTo(p.x, p.y);
|
|
16657
|
+
}
|
|
16658
|
+
ctx.lineWidth = this.radius * 2;
|
|
16659
|
+
ctx.lineCap = "round";
|
|
16660
|
+
ctx.lineJoin = "round";
|
|
16661
|
+
ctx.stroke();
|
|
16662
|
+
}
|
|
16663
|
+
renderRectanglePreview(ctx) {
|
|
16664
|
+
if (this.points.length < 2) return;
|
|
16665
|
+
const from = this.points[0];
|
|
16666
|
+
const to = this.points[this.points.length - 1];
|
|
16667
|
+
const x = Math.min(from.x, to.x);
|
|
16668
|
+
const y = Math.min(from.y, to.y);
|
|
16669
|
+
const w = Math.abs(to.x - from.x);
|
|
16670
|
+
const h = Math.abs(to.y - from.y);
|
|
16671
|
+
ctx.fillRect(x, y, w, h);
|
|
16672
|
+
ctx.strokeRect(x, y, w, h);
|
|
16673
|
+
}
|
|
16674
|
+
renderPolygonPreview(ctx) {
|
|
16675
|
+
if (this.points.length < 2) return;
|
|
16676
|
+
const first = this.points[0];
|
|
16677
|
+
ctx.beginPath();
|
|
16678
|
+
ctx.moveTo(first.x, first.y);
|
|
16679
|
+
for (let i = 1; i < this.points.length; i++) {
|
|
16680
|
+
const p = this.points[i];
|
|
16681
|
+
ctx.lineTo(p.x, p.y);
|
|
16682
|
+
}
|
|
16683
|
+
ctx.closePath();
|
|
16684
|
+
ctx.fill();
|
|
16685
|
+
ctx.stroke();
|
|
16686
|
+
}
|
|
16687
|
+
};
|
|
16688
|
+
|
|
14668
16689
|
// src/index.ts
|
|
14669
|
-
var VERSION = "0.
|
|
16690
|
+
var VERSION = "0.66.0";
|
|
14670
16691
|
export {
|
|
16692
|
+
AWARENESS_MAX_SELECTION,
|
|
16693
|
+
AWARENESS_PRESENCE_KIND,
|
|
14671
16694
|
ArrowTool,
|
|
14672
16695
|
AutoSave,
|
|
14673
16696
|
Camera,
|
|
@@ -14677,6 +16700,12 @@ export {
|
|
|
14677
16700
|
ElementStore,
|
|
14678
16701
|
EraserTool,
|
|
14679
16702
|
FOCUS_PRESENCE_KIND,
|
|
16703
|
+
FOG_MAX_TILES,
|
|
16704
|
+
FOG_STATE_VERSION,
|
|
16705
|
+
FOG_TILE_CELLS,
|
|
16706
|
+
FogManager,
|
|
16707
|
+
FogRenderer,
|
|
16708
|
+
FogTool,
|
|
14680
16709
|
HandTool,
|
|
14681
16710
|
HistoryStack,
|
|
14682
16711
|
HtmlPainterMissingError,
|
|
@@ -14686,6 +16715,7 @@ export {
|
|
|
14686
16715
|
LASER_TRAIL_PRESENCE_KIND,
|
|
14687
16716
|
LaserTool,
|
|
14688
16717
|
LayerManager,
|
|
16718
|
+
LocalAwareness,
|
|
14689
16719
|
LocalStorageAdapter,
|
|
14690
16720
|
MEASURE_PRESENCE_KIND,
|
|
14691
16721
|
MeasureTool,
|
|
@@ -14694,16 +16724,20 @@ export {
|
|
|
14694
16724
|
NoteTool,
|
|
14695
16725
|
PATH_PRESENCE_KIND,
|
|
14696
16726
|
PATH_PRESENCE_MAX_POINTS,
|
|
16727
|
+
PEER_COLORS,
|
|
14697
16728
|
PING_PRESENCE_KIND,
|
|
14698
16729
|
PathTool,
|
|
16730
|
+
PeerRoster,
|
|
14699
16731
|
PencilTool,
|
|
14700
16732
|
PingInput,
|
|
14701
16733
|
PingTool,
|
|
16734
|
+
RemoteCursorOverlay,
|
|
14702
16735
|
RemoteFocusReceiver,
|
|
14703
16736
|
RemoteLaserOverlay,
|
|
14704
16737
|
RemoteMeasureOverlay,
|
|
14705
16738
|
RemotePathOverlay,
|
|
14706
16739
|
RemotePingOverlay,
|
|
16740
|
+
RemoteSelectionOverlay,
|
|
14707
16741
|
SelectTool,
|
|
14708
16742
|
ShapeTool,
|
|
14709
16743
|
TemplateTool,
|
|
@@ -14712,8 +16746,10 @@ export {
|
|
|
14712
16746
|
VERSION,
|
|
14713
16747
|
Viewport,
|
|
14714
16748
|
applyCameraView,
|
|
16749
|
+
attachAwareness,
|
|
14715
16750
|
boundsIntersect,
|
|
14716
16751
|
cameraOriginForView,
|
|
16752
|
+
canonicalizeFogTile,
|
|
14717
16753
|
captureCameraView,
|
|
14718
16754
|
computeElementRects,
|
|
14719
16755
|
createArrow,
|
|
@@ -14725,11 +16761,14 @@ export {
|
|
|
14725
16761
|
createStroke,
|
|
14726
16762
|
createTemplate,
|
|
14727
16763
|
createText,
|
|
16764
|
+
defaultPeerColor,
|
|
14728
16765
|
drawHexPath,
|
|
14729
16766
|
elementRectsEqual,
|
|
14730
16767
|
exportImage,
|
|
14731
16768
|
exportSvg,
|
|
14732
16769
|
fitZoomForView,
|
|
16770
|
+
decodeBase64 as fogDecodeBase64,
|
|
16771
|
+
encodeBase64 as fogEncodeBase64,
|
|
14733
16772
|
footprintFromSize,
|
|
14734
16773
|
getActiveFormats,
|
|
14735
16774
|
getArrowBounds,
|
|
@@ -14747,6 +16786,7 @@ export {
|
|
|
14747
16786
|
getHexCellsInSquare,
|
|
14748
16787
|
getHexDistance,
|
|
14749
16788
|
gridDistanceCells,
|
|
16789
|
+
isAwarenessPresence,
|
|
14750
16790
|
isFocusPresence,
|
|
14751
16791
|
isLaserTrailPresence,
|
|
14752
16792
|
isMeasurePresence,
|
|
@@ -14754,6 +16794,7 @@ export {
|
|
|
14754
16794
|
isPathPresence,
|
|
14755
16795
|
isPingPresence,
|
|
14756
16796
|
pathDistanceCells,
|
|
16797
|
+
recommendedFogCellSize,
|
|
14757
16798
|
resolveHtmlRouting,
|
|
14758
16799
|
setFontSize,
|
|
14759
16800
|
smartSnap,
|
|
@@ -14770,6 +16811,9 @@ export {
|
|
|
14770
16811
|
toggleBold,
|
|
14771
16812
|
toggleItalic,
|
|
14772
16813
|
toggleStrikethrough,
|
|
14773
|
-
toggleUnderline
|
|
16814
|
+
toggleUnderline,
|
|
16815
|
+
validateFogDefinition,
|
|
16816
|
+
validateFogState,
|
|
16817
|
+
validateFogTile
|
|
14774
16818
|
};
|
|
14775
16819
|
//# sourceMappingURL=index.js.map
|