@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.cjs
CHANGED
|
@@ -20,6 +20,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
AWARENESS_MAX_SELECTION: () => AWARENESS_MAX_SELECTION,
|
|
24
|
+
AWARENESS_PRESENCE_KIND: () => AWARENESS_PRESENCE_KIND,
|
|
23
25
|
ArrowTool: () => ArrowTool,
|
|
24
26
|
AutoSave: () => AutoSave,
|
|
25
27
|
Camera: () => Camera,
|
|
@@ -29,6 +31,12 @@ __export(index_exports, {
|
|
|
29
31
|
ElementStore: () => ElementStore,
|
|
30
32
|
EraserTool: () => EraserTool,
|
|
31
33
|
FOCUS_PRESENCE_KIND: () => FOCUS_PRESENCE_KIND,
|
|
34
|
+
FOG_MAX_TILES: () => FOG_MAX_TILES,
|
|
35
|
+
FOG_STATE_VERSION: () => FOG_STATE_VERSION,
|
|
36
|
+
FOG_TILE_CELLS: () => FOG_TILE_CELLS,
|
|
37
|
+
FogManager: () => FogManager,
|
|
38
|
+
FogRenderer: () => FogRenderer,
|
|
39
|
+
FogTool: () => FogTool,
|
|
32
40
|
HandTool: () => HandTool,
|
|
33
41
|
HistoryStack: () => HistoryStack,
|
|
34
42
|
HtmlPainterMissingError: () => HtmlPainterMissingError,
|
|
@@ -38,6 +46,7 @@ __export(index_exports, {
|
|
|
38
46
|
LASER_TRAIL_PRESENCE_KIND: () => LASER_TRAIL_PRESENCE_KIND,
|
|
39
47
|
LaserTool: () => LaserTool,
|
|
40
48
|
LayerManager: () => LayerManager,
|
|
49
|
+
LocalAwareness: () => LocalAwareness,
|
|
41
50
|
LocalStorageAdapter: () => LocalStorageAdapter,
|
|
42
51
|
MEASURE_PRESENCE_KIND: () => MEASURE_PRESENCE_KIND,
|
|
43
52
|
MeasureTool: () => MeasureTool,
|
|
@@ -46,16 +55,20 @@ __export(index_exports, {
|
|
|
46
55
|
NoteTool: () => NoteTool,
|
|
47
56
|
PATH_PRESENCE_KIND: () => PATH_PRESENCE_KIND,
|
|
48
57
|
PATH_PRESENCE_MAX_POINTS: () => PATH_PRESENCE_MAX_POINTS,
|
|
58
|
+
PEER_COLORS: () => PEER_COLORS,
|
|
49
59
|
PING_PRESENCE_KIND: () => PING_PRESENCE_KIND,
|
|
50
60
|
PathTool: () => PathTool,
|
|
61
|
+
PeerRoster: () => PeerRoster,
|
|
51
62
|
PencilTool: () => PencilTool,
|
|
52
63
|
PingInput: () => PingInput,
|
|
53
64
|
PingTool: () => PingTool,
|
|
65
|
+
RemoteCursorOverlay: () => RemoteCursorOverlay,
|
|
54
66
|
RemoteFocusReceiver: () => RemoteFocusReceiver,
|
|
55
67
|
RemoteLaserOverlay: () => RemoteLaserOverlay,
|
|
56
68
|
RemoteMeasureOverlay: () => RemoteMeasureOverlay,
|
|
57
69
|
RemotePathOverlay: () => RemotePathOverlay,
|
|
58
70
|
RemotePingOverlay: () => RemotePingOverlay,
|
|
71
|
+
RemoteSelectionOverlay: () => RemoteSelectionOverlay,
|
|
59
72
|
SelectTool: () => SelectTool,
|
|
60
73
|
ShapeTool: () => ShapeTool,
|
|
61
74
|
TemplateTool: () => TemplateTool,
|
|
@@ -64,8 +77,10 @@ __export(index_exports, {
|
|
|
64
77
|
VERSION: () => VERSION,
|
|
65
78
|
Viewport: () => Viewport,
|
|
66
79
|
applyCameraView: () => applyCameraView,
|
|
80
|
+
attachAwareness: () => attachAwareness,
|
|
67
81
|
boundsIntersect: () => boundsIntersect,
|
|
68
82
|
cameraOriginForView: () => cameraOriginForView,
|
|
83
|
+
canonicalizeFogTile: () => canonicalizeFogTile,
|
|
69
84
|
captureCameraView: () => captureCameraView,
|
|
70
85
|
computeElementRects: () => computeElementRects,
|
|
71
86
|
createArrow: () => createArrow,
|
|
@@ -77,11 +92,14 @@ __export(index_exports, {
|
|
|
77
92
|
createStroke: () => createStroke,
|
|
78
93
|
createTemplate: () => createTemplate,
|
|
79
94
|
createText: () => createText,
|
|
95
|
+
defaultPeerColor: () => defaultPeerColor,
|
|
80
96
|
drawHexPath: () => drawHexPath,
|
|
81
97
|
elementRectsEqual: () => elementRectsEqual,
|
|
82
98
|
exportImage: () => exportImage,
|
|
83
99
|
exportSvg: () => exportSvg,
|
|
84
100
|
fitZoomForView: () => fitZoomForView,
|
|
101
|
+
fogDecodeBase64: () => decodeBase64,
|
|
102
|
+
fogEncodeBase64: () => encodeBase64,
|
|
85
103
|
footprintFromSize: () => footprintFromSize,
|
|
86
104
|
getActiveFormats: () => getActiveFormats,
|
|
87
105
|
getArrowBounds: () => getArrowBounds,
|
|
@@ -99,6 +117,7 @@ __export(index_exports, {
|
|
|
99
117
|
getHexCellsInSquare: () => getHexCellsInSquare,
|
|
100
118
|
getHexDistance: () => getHexDistance,
|
|
101
119
|
gridDistanceCells: () => gridDistanceCells,
|
|
120
|
+
isAwarenessPresence: () => isAwarenessPresence,
|
|
102
121
|
isFocusPresence: () => isFocusPresence,
|
|
103
122
|
isLaserTrailPresence: () => isLaserTrailPresence,
|
|
104
123
|
isMeasurePresence: () => isMeasurePresence,
|
|
@@ -106,6 +125,7 @@ __export(index_exports, {
|
|
|
106
125
|
isPathPresence: () => isPathPresence,
|
|
107
126
|
isPingPresence: () => isPingPresence,
|
|
108
127
|
pathDistanceCells: () => pathDistanceCells,
|
|
128
|
+
recommendedFogCellSize: () => recommendedFogCellSize,
|
|
109
129
|
resolveHtmlRouting: () => resolveHtmlRouting,
|
|
110
130
|
setFontSize: () => setFontSize,
|
|
111
131
|
smartSnap: () => smartSnap,
|
|
@@ -122,7 +142,10 @@ __export(index_exports, {
|
|
|
122
142
|
toggleBold: () => toggleBold,
|
|
123
143
|
toggleItalic: () => toggleItalic,
|
|
124
144
|
toggleStrikethrough: () => toggleStrikethrough,
|
|
125
|
-
toggleUnderline: () => toggleUnderline
|
|
145
|
+
toggleUnderline: () => toggleUnderline,
|
|
146
|
+
validateFogDefinition: () => validateFogDefinition,
|
|
147
|
+
validateFogState: () => validateFogState,
|
|
148
|
+
validateFogTile: () => validateFogTile
|
|
126
149
|
});
|
|
127
150
|
module.exports = __toCommonJS(index_exports);
|
|
128
151
|
|
|
@@ -582,8 +605,464 @@ function sanitizeAttributes(el, tag) {
|
|
|
582
605
|
}
|
|
583
606
|
}
|
|
584
607
|
|
|
608
|
+
// src/fog/types.ts
|
|
609
|
+
var FOG_STATE_VERSION = 1;
|
|
610
|
+
var FOG_TILE_CELLS = 128;
|
|
611
|
+
var FOG_MAX_TILES = 256;
|
|
612
|
+
|
|
613
|
+
// src/fog/tile-codec.ts
|
|
614
|
+
var TILE_BYTES = FOG_TILE_CELLS * FOG_TILE_CELLS / 8;
|
|
615
|
+
var CANONICAL_B64_LENGTH = Math.ceil(TILE_BYTES / 3) * 4;
|
|
616
|
+
var B64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
617
|
+
var B64_LOOKUP = new Uint8Array(128);
|
|
618
|
+
for (let i = 0; i < B64_CHARS.length; i++) B64_LOOKUP[B64_CHARS.charCodeAt(i)] = i;
|
|
619
|
+
function encodeBase64(bytes) {
|
|
620
|
+
let result = "";
|
|
621
|
+
const len = bytes.length;
|
|
622
|
+
for (let i = 0; i < len; i += 3) {
|
|
623
|
+
const a = bytes[i];
|
|
624
|
+
const b = i + 1 < len ? bytes[i + 1] : 0;
|
|
625
|
+
const c = i + 2 < len ? bytes[i + 2] : 0;
|
|
626
|
+
result += B64_CHARS[a >> 2 & 63];
|
|
627
|
+
result += B64_CHARS[(a << 4 | b >> 4) & 63];
|
|
628
|
+
result += i + 1 < len ? B64_CHARS[(b << 2 | c >> 6) & 63] : "=";
|
|
629
|
+
result += i + 2 < len ? B64_CHARS[c & 63] : "=";
|
|
630
|
+
}
|
|
631
|
+
return result;
|
|
632
|
+
}
|
|
633
|
+
function decodeBase64(str) {
|
|
634
|
+
if (str.length % 4 !== 0) throw new Error("Invalid base64 length");
|
|
635
|
+
let padCount = 0;
|
|
636
|
+
if (str.length >= 2 && str[str.length - 1] === "=") {
|
|
637
|
+
padCount++;
|
|
638
|
+
if (str[str.length - 2] === "=") padCount++;
|
|
639
|
+
}
|
|
640
|
+
const byteLen = str.length / 4 * 3 - padCount;
|
|
641
|
+
const bytes = new Uint8Array(byteLen);
|
|
642
|
+
let j = 0;
|
|
643
|
+
for (let i = 0; i < str.length; i += 4) {
|
|
644
|
+
const a = B64_LOOKUP[str.charCodeAt(i)];
|
|
645
|
+
const b = B64_LOOKUP[str.charCodeAt(i + 1)];
|
|
646
|
+
const c = str[i + 2] === "=" ? 0 : B64_LOOKUP[str.charCodeAt(i + 2)];
|
|
647
|
+
const d = str[i + 3] === "=" ? 0 : B64_LOOKUP[str.charCodeAt(i + 3)];
|
|
648
|
+
bytes[j++] = a << 2 | b >> 4;
|
|
649
|
+
if (j < byteLen) bytes[j++] = (b << 4 | c >> 2) & 255;
|
|
650
|
+
if (j < byteLen) bytes[j++] = (c << 6 | d) & 255;
|
|
651
|
+
}
|
|
652
|
+
return bytes;
|
|
653
|
+
}
|
|
654
|
+
function createTileBytes(fill) {
|
|
655
|
+
const bytes = new Uint8Array(TILE_BYTES);
|
|
656
|
+
if (fill) bytes.fill(255);
|
|
657
|
+
return bytes;
|
|
658
|
+
}
|
|
659
|
+
function setBit(bytes, col, row, value) {
|
|
660
|
+
const index = row * FOG_TILE_CELLS + col;
|
|
661
|
+
const byteIndex = index >> 3;
|
|
662
|
+
const bitIndex = 7 - (index & 7);
|
|
663
|
+
if (value) {
|
|
664
|
+
bytes[byteIndex] = bytes[byteIndex] | 1 << bitIndex;
|
|
665
|
+
} else {
|
|
666
|
+
bytes[byteIndex] = bytes[byteIndex] & ~(1 << bitIndex);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
function isTileAllValue(bytes, value) {
|
|
670
|
+
const expected = value ? 255 : 0;
|
|
671
|
+
for (let i = 0; i < TILE_BYTES; i++) {
|
|
672
|
+
if (bytes[i] !== expected) return false;
|
|
673
|
+
}
|
|
674
|
+
return true;
|
|
675
|
+
}
|
|
676
|
+
function isBaseValue(base) {
|
|
677
|
+
return base === "revealed";
|
|
678
|
+
}
|
|
679
|
+
function isTileBase(bytes, base) {
|
|
680
|
+
return isTileAllValue(bytes, isBaseValue(base));
|
|
681
|
+
}
|
|
682
|
+
function canonicalizeEdgePadding(bytes, def, tileX, tileY) {
|
|
683
|
+
const baseVal = isBaseValue(def.base);
|
|
684
|
+
const worldX = tileX * FOG_TILE_CELLS * def.cellSize;
|
|
685
|
+
const worldY = tileY * FOG_TILE_CELLS * def.cellSize;
|
|
686
|
+
const boundsRight = def.bounds.x + def.bounds.w;
|
|
687
|
+
const boundsBottom = def.bounds.y + def.bounds.h;
|
|
688
|
+
for (let row = 0; row < FOG_TILE_CELLS; row++) {
|
|
689
|
+
for (let col = 0; col < FOG_TILE_CELLS; col++) {
|
|
690
|
+
const cellWorldX = worldX + col * def.cellSize;
|
|
691
|
+
const cellWorldY = worldY + row * def.cellSize;
|
|
692
|
+
const outside = cellWorldX < def.bounds.x || cellWorldY < def.bounds.y || cellWorldX >= boundsRight || cellWorldY >= boundsBottom;
|
|
693
|
+
if (outside) {
|
|
694
|
+
setBit(bytes, col, row, baseVal);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
function canonicalizeFogTile(tile, def) {
|
|
700
|
+
const bytes = decodeBase64(tile.data);
|
|
701
|
+
canonicalizeEdgePadding(bytes, def, tile.x, tile.y);
|
|
702
|
+
if (isTileBase(bytes, def.base)) return null;
|
|
703
|
+
return { x: tile.x, y: tile.y, data: encodeBase64(bytes) };
|
|
704
|
+
}
|
|
705
|
+
function isCanonicalBase64(str) {
|
|
706
|
+
if (str.length !== CANONICAL_B64_LENGTH) return false;
|
|
707
|
+
if (str[str.length - 1] !== "=" || str[str.length - 2] === "=") return false;
|
|
708
|
+
for (let i = 0; i < str.length - 1; i++) {
|
|
709
|
+
if (!B64_CHARS.includes(str[i])) return false;
|
|
710
|
+
}
|
|
711
|
+
const finalSextet = B64_CHARS.indexOf(str[str.length - 2]);
|
|
712
|
+
return finalSextet >= 0 && (finalSextet & 3) === 0;
|
|
713
|
+
}
|
|
714
|
+
function isSafeInteger(n2) {
|
|
715
|
+
return typeof n2 === "number" && Number.isSafeInteger(n2);
|
|
716
|
+
}
|
|
717
|
+
function isFinitePositive(n2) {
|
|
718
|
+
return typeof n2 === "number" && Number.isFinite(n2) && n2 > 0;
|
|
719
|
+
}
|
|
720
|
+
function isFiniteBounds(b) {
|
|
721
|
+
if (typeof b !== "object" || b === null) return false;
|
|
722
|
+
const r = b;
|
|
723
|
+
return typeof r["x"] === "number" && Number.isFinite(r["x"]) && typeof r["y"] === "number" && Number.isFinite(r["y"]) && isFinitePositive(r["w"]) && isFinitePositive(r["h"]);
|
|
724
|
+
}
|
|
725
|
+
function tileIntersectsBounds(x, y, def) {
|
|
726
|
+
const tileWorldX = x * FOG_TILE_CELLS * def.cellSize;
|
|
727
|
+
const tileWorldY = y * FOG_TILE_CELLS * def.cellSize;
|
|
728
|
+
const tileWorldW = FOG_TILE_CELLS * def.cellSize;
|
|
729
|
+
const tileWorldH = FOG_TILE_CELLS * def.cellSize;
|
|
730
|
+
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);
|
|
731
|
+
}
|
|
732
|
+
function validateFogDefinition(def) {
|
|
733
|
+
if (typeof def !== "object" || def === null) {
|
|
734
|
+
throw new Error("Invalid fog definition: expected an object");
|
|
735
|
+
}
|
|
736
|
+
const d = def;
|
|
737
|
+
if (d["version"] !== 1) throw new Error("Invalid fog definition: unsupported version");
|
|
738
|
+
if (typeof d["generation"] !== "string" || d["generation"].length === 0 || d["generation"].length > 128 || !/^[\x20-\x7e]+$/.test(d["generation"])) {
|
|
739
|
+
throw new Error("Invalid fog definition: invalid generation");
|
|
740
|
+
}
|
|
741
|
+
if (!isFiniteBounds(d["bounds"])) {
|
|
742
|
+
throw new Error("Invalid fog definition: invalid bounds");
|
|
743
|
+
}
|
|
744
|
+
if (!isFinitePositive(d["cellSize"])) {
|
|
745
|
+
throw new Error("Invalid fog definition: invalid cellSize");
|
|
746
|
+
}
|
|
747
|
+
if (d["tileCells"] !== 128) {
|
|
748
|
+
throw new Error("Invalid fog definition: tileCells must be 128");
|
|
749
|
+
}
|
|
750
|
+
if (d["base"] !== "covered" && d["base"] !== "revealed") {
|
|
751
|
+
throw new Error("Invalid fog definition: invalid base");
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
function validateFogTile(tile, def) {
|
|
755
|
+
if (typeof tile !== "object" || tile === null) {
|
|
756
|
+
throw new Error("Invalid fog tile: expected an object");
|
|
757
|
+
}
|
|
758
|
+
const t = tile;
|
|
759
|
+
if (!isSafeInteger(t["x"]) || !isSafeInteger(t["y"])) {
|
|
760
|
+
throw new Error("Invalid fog tile: coordinates must be safe integers");
|
|
761
|
+
}
|
|
762
|
+
if (!tileIntersectsBounds(t["x"], t["y"], def)) {
|
|
763
|
+
throw new Error("Invalid fog tile: coordinates outside bounds");
|
|
764
|
+
}
|
|
765
|
+
if (typeof t["data"] !== "string" || !isCanonicalBase64(t["data"])) {
|
|
766
|
+
throw new Error("Invalid fog tile: invalid data");
|
|
767
|
+
}
|
|
768
|
+
const decoded = decodeBase64(t["data"]);
|
|
769
|
+
if (decoded.length !== TILE_BYTES) {
|
|
770
|
+
throw new Error("Invalid fog tile: decoded data wrong length");
|
|
771
|
+
}
|
|
772
|
+
if (isTileBase(decoded, def.base)) {
|
|
773
|
+
throw new Error("Invalid fog tile: base-value tiles must be omitted");
|
|
774
|
+
}
|
|
775
|
+
const canonical = new Uint8Array(decoded);
|
|
776
|
+
canonicalizeEdgePadding(canonical, def, t["x"], t["y"]);
|
|
777
|
+
for (let i = 0; i < decoded.length; i++) {
|
|
778
|
+
if (decoded[i] !== canonical[i]) {
|
|
779
|
+
throw new Error("Invalid fog tile: non-canonical edge padding");
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
function validateFogState(state) {
|
|
784
|
+
if (typeof state !== "object" || state === null) {
|
|
785
|
+
throw new Error("Invalid fog state: expected an object");
|
|
786
|
+
}
|
|
787
|
+
const s = state;
|
|
788
|
+
validateFogDefinition(s["definition"]);
|
|
789
|
+
const def = s["definition"];
|
|
790
|
+
if (!Array.isArray(s["tiles"])) {
|
|
791
|
+
throw new Error("Invalid fog state: tiles must be an array");
|
|
792
|
+
}
|
|
793
|
+
const tiles = s["tiles"];
|
|
794
|
+
if (tiles.length > FOG_MAX_TILES) {
|
|
795
|
+
throw new Error(`Invalid fog state: too many tiles (${tiles.length} > ${FOG_MAX_TILES})`);
|
|
796
|
+
}
|
|
797
|
+
const seen = /* @__PURE__ */ new Set();
|
|
798
|
+
for (const tile of tiles) {
|
|
799
|
+
validateFogTile(tile, def);
|
|
800
|
+
const t = tile;
|
|
801
|
+
const key = `${t.x},${t.y}`;
|
|
802
|
+
if (seen.has(key)) {
|
|
803
|
+
throw new Error(`Invalid fog state: duplicate tile at (${t.x}, ${t.y})`);
|
|
804
|
+
}
|
|
805
|
+
seen.add(key);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
function recommendedFogCellSize(bounds) {
|
|
809
|
+
let cellSize = 1;
|
|
810
|
+
while (true) {
|
|
811
|
+
const tileWorldSize = FOG_TILE_CELLS * cellSize;
|
|
812
|
+
const minTX = Math.floor(bounds.x / tileWorldSize);
|
|
813
|
+
const minTY = Math.floor(bounds.y / tileWorldSize);
|
|
814
|
+
const maxTX = Math.ceil((bounds.x + bounds.w) / tileWorldSize) - 1;
|
|
815
|
+
const maxTY = Math.ceil((bounds.y + bounds.h) / tileWorldSize) - 1;
|
|
816
|
+
const tw = maxTX - minTX + 1;
|
|
817
|
+
const th = maxTY - minTY + 1;
|
|
818
|
+
if (tw * th <= FOG_MAX_TILES) return cellSize;
|
|
819
|
+
cellSize++;
|
|
820
|
+
if (cellSize > 1e4) return cellSize;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
function tileKey(x, y) {
|
|
824
|
+
return `${x},${y}`;
|
|
825
|
+
}
|
|
826
|
+
function worldToCell(worldX, worldY, cellSize) {
|
|
827
|
+
const cellX = Math.floor(worldX / cellSize);
|
|
828
|
+
const cellY = Math.floor(worldY / cellSize);
|
|
829
|
+
const tx = Math.floor(cellX / FOG_TILE_CELLS);
|
|
830
|
+
const ty = Math.floor(cellY / FOG_TILE_CELLS);
|
|
831
|
+
let col = cellX - tx * FOG_TILE_CELLS;
|
|
832
|
+
let row = cellY - ty * FOG_TILE_CELLS;
|
|
833
|
+
if (col < 0) col += FOG_TILE_CELLS;
|
|
834
|
+
if (row < 0) row += FOG_TILE_CELLS;
|
|
835
|
+
return { tx, ty, col, row };
|
|
836
|
+
}
|
|
837
|
+
function rasterizeRegion(state, region, operation) {
|
|
838
|
+
const { definition } = state;
|
|
839
|
+
const bitValue = operation === "reveal";
|
|
840
|
+
const tileMap = /* @__PURE__ */ new Map();
|
|
841
|
+
for (const tile of state.tiles) {
|
|
842
|
+
tileMap.set(tileKey(tile.x, tile.y), decodeBase64(tile.data));
|
|
843
|
+
}
|
|
844
|
+
const baseFill = isBaseValue(definition.base);
|
|
845
|
+
const affectedTiles = /* @__PURE__ */ new Set();
|
|
846
|
+
const setCellIfInBounds = (worldX, worldY) => {
|
|
847
|
+
if (worldX < definition.bounds.x || worldY < definition.bounds.y || worldX >= definition.bounds.x + definition.bounds.w || worldY >= definition.bounds.y + definition.bounds.h) {
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
const { tx, ty, col, row } = worldToCell(worldX, worldY, definition.cellSize);
|
|
851
|
+
if (!tileIntersectsBounds(tx, ty, definition)) return;
|
|
852
|
+
const key = tileKey(tx, ty);
|
|
853
|
+
let bytes = tileMap.get(key);
|
|
854
|
+
if (!bytes) {
|
|
855
|
+
bytes = createTileBytes(baseFill);
|
|
856
|
+
tileMap.set(key, bytes);
|
|
857
|
+
} else if (!affectedTiles.has(key)) {
|
|
858
|
+
const clone = new Uint8Array(bytes);
|
|
859
|
+
tileMap.set(key, clone);
|
|
860
|
+
bytes = clone;
|
|
861
|
+
}
|
|
862
|
+
affectedTiles.add(key);
|
|
863
|
+
setBit(bytes, col, row, bitValue);
|
|
864
|
+
};
|
|
865
|
+
switch (region.kind) {
|
|
866
|
+
case "brush":
|
|
867
|
+
rasterizeBrush(region.points, region.radius, definition, setCellIfInBounds);
|
|
868
|
+
break;
|
|
869
|
+
case "rectangle":
|
|
870
|
+
rasterizeRectangle(region.from, region.to, definition, setCellIfInBounds);
|
|
871
|
+
break;
|
|
872
|
+
case "polygon":
|
|
873
|
+
rasterizePolygon(region.points, definition, setCellIfInBounds);
|
|
874
|
+
break;
|
|
875
|
+
}
|
|
876
|
+
if (affectedTiles.size === 0) return { changed: [], noop: true };
|
|
877
|
+
const changedTiles = [];
|
|
878
|
+
let hasChange = false;
|
|
879
|
+
for (const key of affectedTiles) {
|
|
880
|
+
const bytes = tileMap.get(key);
|
|
881
|
+
const [txStr, tyStr] = key.split(",");
|
|
882
|
+
const tx = Number(txStr);
|
|
883
|
+
const ty = Number(tyStr);
|
|
884
|
+
canonicalizeEdgePadding(bytes, definition, tx, ty);
|
|
885
|
+
const newData = encodeBase64(bytes);
|
|
886
|
+
const originalTile = state.tiles.find((t) => t.x === tx && t.y === ty);
|
|
887
|
+
if (originalTile) {
|
|
888
|
+
if (originalTile.data !== newData) {
|
|
889
|
+
hasChange = true;
|
|
890
|
+
changedTiles.push({ x: tx, y: ty, data: newData });
|
|
891
|
+
}
|
|
892
|
+
} else if (!isTileBase(bytes, definition.base)) {
|
|
893
|
+
hasChange = true;
|
|
894
|
+
changedTiles.push({ x: tx, y: ty, data: newData });
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
if (!hasChange) return { changed: [], noop: true };
|
|
898
|
+
return { changed: changedTiles, noop: false };
|
|
899
|
+
}
|
|
900
|
+
function applyRasterResult(state, result) {
|
|
901
|
+
if (result.noop) return state;
|
|
902
|
+
const changedMap = /* @__PURE__ */ new Map();
|
|
903
|
+
for (const t of result.changed) changedMap.set(tileKey(t.x, t.y), t);
|
|
904
|
+
const tiles = [];
|
|
905
|
+
const seen = /* @__PURE__ */ new Set();
|
|
906
|
+
for (const tile of state.tiles) {
|
|
907
|
+
const key = tileKey(tile.x, tile.y);
|
|
908
|
+
seen.add(key);
|
|
909
|
+
const changed = changedMap.get(key);
|
|
910
|
+
if (changed) {
|
|
911
|
+
const bytes = decodeBase64(changed.data);
|
|
912
|
+
if (!isTileBase(bytes, state.definition.base)) {
|
|
913
|
+
tiles.push(changed);
|
|
914
|
+
}
|
|
915
|
+
} else {
|
|
916
|
+
tiles.push(tile);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
for (const t of result.changed) {
|
|
920
|
+
const key = tileKey(t.x, t.y);
|
|
921
|
+
if (seen.has(key)) continue;
|
|
922
|
+
const bytes = decodeBase64(t.data);
|
|
923
|
+
if (!isTileBase(bytes, state.definition.base)) {
|
|
924
|
+
tiles.push(t);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
if (tiles.length > FOG_MAX_TILES) {
|
|
928
|
+
throw new Error(`Fog tile cap exceeded: ${tiles.length} > ${FOG_MAX_TILES}`);
|
|
929
|
+
}
|
|
930
|
+
return { definition: state.definition, tiles };
|
|
931
|
+
}
|
|
932
|
+
function sampleAndSimplify(points, tolerance) {
|
|
933
|
+
if (points.length <= 2) return [...points];
|
|
934
|
+
const sampled = [points[0]];
|
|
935
|
+
let lastSampled = points[0];
|
|
936
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
937
|
+
const p = points[i];
|
|
938
|
+
const dx = p.x - lastSampled.x;
|
|
939
|
+
const dy = p.y - lastSampled.y;
|
|
940
|
+
if (dx * dx + dy * dy >= tolerance * tolerance) {
|
|
941
|
+
sampled.push(p);
|
|
942
|
+
lastSampled = p;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
sampled.push(points[points.length - 1]);
|
|
946
|
+
return sampled;
|
|
947
|
+
}
|
|
948
|
+
function rasterizeBrush(points, radius, def, setCell) {
|
|
949
|
+
if (points.length === 0) return;
|
|
950
|
+
const simplified = sampleAndSimplify(points, def.cellSize * 0.5);
|
|
951
|
+
for (let i = 0; i < simplified.length; i++) {
|
|
952
|
+
const p = simplified[i];
|
|
953
|
+
rasterizeDisc(p.x, p.y, radius, def, setCell);
|
|
954
|
+
if (i > 0) {
|
|
955
|
+
const prev = simplified[i - 1];
|
|
956
|
+
rasterizeCapsuleSegment(prev.x, prev.y, p.x, p.y, radius, def, setCell);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
function rasterizeDisc(cx, cy, radius, def, setCell) {
|
|
961
|
+
const minX = Math.max(def.bounds.x, cx - radius);
|
|
962
|
+
const maxX = Math.min(def.bounds.x + def.bounds.w - 1, cx + radius);
|
|
963
|
+
const minY = Math.max(def.bounds.y, cy - radius);
|
|
964
|
+
const maxY = Math.min(def.bounds.y + def.bounds.h - 1, cy + radius);
|
|
965
|
+
const startCol = Math.floor(minX / def.cellSize) * def.cellSize;
|
|
966
|
+
const startRow = Math.floor(minY / def.cellSize) * def.cellSize;
|
|
967
|
+
const r2 = radius * radius;
|
|
968
|
+
for (let wy = startRow; wy <= maxY; wy += def.cellSize) {
|
|
969
|
+
for (let wx = startCol; wx <= maxX; wx += def.cellSize) {
|
|
970
|
+
const cellCenterX = wx + def.cellSize * 0.5;
|
|
971
|
+
const cellCenterY = wy + def.cellSize * 0.5;
|
|
972
|
+
const dx = cellCenterX - cx;
|
|
973
|
+
const dy = cellCenterY - cy;
|
|
974
|
+
if (dx * dx + dy * dy <= r2) {
|
|
975
|
+
setCell(wx, wy);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
function rasterizeCapsuleSegment(x1, y1, x2, y2, radius, def, setCell) {
|
|
981
|
+
const segDx = x2 - x1;
|
|
982
|
+
const segDy = y2 - y1;
|
|
983
|
+
const segLen2 = segDx * segDx + segDy * segDy;
|
|
984
|
+
if (segLen2 === 0) return;
|
|
985
|
+
const minX = Math.max(def.bounds.x, Math.min(x1, x2) - radius);
|
|
986
|
+
const maxX = Math.min(def.bounds.x + def.bounds.w - 1, Math.max(x1, x2) + radius);
|
|
987
|
+
const minY = Math.max(def.bounds.y, Math.min(y1, y2) - radius);
|
|
988
|
+
const maxY = Math.min(def.bounds.y + def.bounds.h - 1, Math.max(y1, y2) + radius);
|
|
989
|
+
const startCol = Math.floor(minX / def.cellSize) * def.cellSize;
|
|
990
|
+
const startRow = Math.floor(minY / def.cellSize) * def.cellSize;
|
|
991
|
+
const r2 = radius * radius;
|
|
992
|
+
for (let wy = startRow; wy <= maxY; wy += def.cellSize) {
|
|
993
|
+
for (let wx = startCol; wx <= maxX; wx += def.cellSize) {
|
|
994
|
+
const cellCenterX = wx + def.cellSize * 0.5;
|
|
995
|
+
const cellCenterY = wy + def.cellSize * 0.5;
|
|
996
|
+
const t = Math.max(
|
|
997
|
+
0,
|
|
998
|
+
Math.min(1, ((cellCenterX - x1) * segDx + (cellCenterY - y1) * segDy) / segLen2)
|
|
999
|
+
);
|
|
1000
|
+
const projX = x1 + t * segDx;
|
|
1001
|
+
const projY = y1 + t * segDy;
|
|
1002
|
+
const dx = cellCenterX - projX;
|
|
1003
|
+
const dy = cellCenterY - projY;
|
|
1004
|
+
if (dx * dx + dy * dy <= r2) {
|
|
1005
|
+
setCell(wx, wy);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
function rasterizeRectangle(from, to, def, setCell) {
|
|
1011
|
+
const minX = Math.max(def.bounds.x, Math.min(from.x, to.x));
|
|
1012
|
+
const maxX = Math.min(def.bounds.x + def.bounds.w - 1, Math.max(from.x, to.x));
|
|
1013
|
+
const minY = Math.max(def.bounds.y, Math.min(from.y, to.y));
|
|
1014
|
+
const maxY = Math.min(def.bounds.y + def.bounds.h - 1, Math.max(from.y, to.y));
|
|
1015
|
+
const startCol = Math.floor(minX / def.cellSize) * def.cellSize;
|
|
1016
|
+
const startRow = Math.floor(minY / def.cellSize) * def.cellSize;
|
|
1017
|
+
for (let wy = startRow; wy <= maxY; wy += def.cellSize) {
|
|
1018
|
+
for (let wx = startCol; wx <= maxX; wx += def.cellSize) {
|
|
1019
|
+
setCell(wx, wy);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
function rasterizePolygon(points, def, setCell) {
|
|
1024
|
+
if (points.length < 3) return;
|
|
1025
|
+
let minX = Infinity;
|
|
1026
|
+
let maxX = -Infinity;
|
|
1027
|
+
let minY = Infinity;
|
|
1028
|
+
let maxY = -Infinity;
|
|
1029
|
+
for (const p of points) {
|
|
1030
|
+
if (p.x < minX) minX = p.x;
|
|
1031
|
+
if (p.x > maxX) maxX = p.x;
|
|
1032
|
+
if (p.y < minY) minY = p.y;
|
|
1033
|
+
if (p.y > maxY) maxY = p.y;
|
|
1034
|
+
}
|
|
1035
|
+
minX = Math.max(def.bounds.x, minX);
|
|
1036
|
+
maxX = Math.min(def.bounds.x + def.bounds.w - 1, maxX);
|
|
1037
|
+
minY = Math.max(def.bounds.y, minY);
|
|
1038
|
+
maxY = Math.min(def.bounds.y + def.bounds.h - 1, maxY);
|
|
1039
|
+
const startCol = Math.floor(minX / def.cellSize) * def.cellSize;
|
|
1040
|
+
const startRow = Math.floor(minY / def.cellSize) * def.cellSize;
|
|
1041
|
+
for (let wy = startRow; wy <= maxY; wy += def.cellSize) {
|
|
1042
|
+
for (let wx = startCol; wx <= maxX; wx += def.cellSize) {
|
|
1043
|
+
const cx = wx + def.cellSize * 0.5;
|
|
1044
|
+
const cy = wy + def.cellSize * 0.5;
|
|
1045
|
+
if (pointInPolygon(cx, cy, points)) {
|
|
1046
|
+
setCell(wx, wy);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
function pointInPolygon(x, y, polygon) {
|
|
1052
|
+
let inside = false;
|
|
1053
|
+
const n2 = polygon.length;
|
|
1054
|
+
for (let i = 0, j = n2 - 1; i < n2; j = i++) {
|
|
1055
|
+
const pi = polygon[i];
|
|
1056
|
+
const pj = polygon[j];
|
|
1057
|
+
if (pi.y > y !== pj.y > y && x < (pj.x - pi.x) * (y - pi.y) / (pj.y - pi.y) + pi.x) {
|
|
1058
|
+
inside = !inside;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
return inside;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
585
1064
|
// src/core/state-serializer.ts
|
|
586
|
-
var CURRENT_VERSION =
|
|
1065
|
+
var CURRENT_VERSION = 3;
|
|
587
1066
|
var ELEMENT_TYPES = [
|
|
588
1067
|
"stroke",
|
|
589
1068
|
"note",
|
|
@@ -595,7 +1074,7 @@ var ELEMENT_TYPES = [
|
|
|
595
1074
|
"grid",
|
|
596
1075
|
"template"
|
|
597
1076
|
];
|
|
598
|
-
function exportState(elements, camera, layers = [], activeLayerId) {
|
|
1077
|
+
function exportState(elements, camera, layers = [], activeLayerId, fog) {
|
|
599
1078
|
const state = {
|
|
600
1079
|
version: CURRENT_VERSION,
|
|
601
1080
|
camera: {
|
|
@@ -612,6 +1091,7 @@ function exportState(elements, camera, layers = [], activeLayerId) {
|
|
|
612
1091
|
layers: layers.map((l) => ({ ...l }))
|
|
613
1092
|
};
|
|
614
1093
|
if (activeLayerId) state.activeLayerId = activeLayerId;
|
|
1094
|
+
if (fog) state.fog = structuredClone(fog);
|
|
615
1095
|
return state;
|
|
616
1096
|
}
|
|
617
1097
|
function parseState(json) {
|
|
@@ -689,6 +1169,9 @@ function validateState(data) {
|
|
|
689
1169
|
}
|
|
690
1170
|
}
|
|
691
1171
|
cleanBindings(elements);
|
|
1172
|
+
if (obj["fog"] !== void 0 && obj["fog"] !== null) {
|
|
1173
|
+
validateFogState(obj["fog"]);
|
|
1174
|
+
}
|
|
692
1175
|
}
|
|
693
1176
|
function validateElement(el) {
|
|
694
1177
|
if (!isRecord(el)) {
|
|
@@ -834,12 +1317,14 @@ var AutoSave = class {
|
|
|
834
1317
|
this.key = options.key ?? DEFAULT_KEY;
|
|
835
1318
|
this.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
836
1319
|
this.layerManager = options.layerManager;
|
|
1320
|
+
this.fogManager = options.fogManager;
|
|
837
1321
|
this.adapter = options.adapter ?? new LocalStorageAdapter();
|
|
838
1322
|
this.onError = options.onError;
|
|
839
1323
|
}
|
|
840
1324
|
key;
|
|
841
1325
|
debounceMs;
|
|
842
1326
|
layerManager;
|
|
1327
|
+
fogManager;
|
|
843
1328
|
adapter;
|
|
844
1329
|
timerId = null;
|
|
845
1330
|
unsubscribers = [];
|
|
@@ -857,6 +1342,9 @@ var AutoSave = class {
|
|
|
857
1342
|
if (this.layerManager) {
|
|
858
1343
|
this.unsubscribers.push(this.layerManager.on("change", schedule));
|
|
859
1344
|
}
|
|
1345
|
+
if (this.fogManager) {
|
|
1346
|
+
this.unsubscribers.push(this.fogManager.on("change", schedule));
|
|
1347
|
+
}
|
|
860
1348
|
}
|
|
861
1349
|
stop() {
|
|
862
1350
|
this.cancelPending();
|
|
@@ -893,7 +1381,8 @@ var AutoSave = class {
|
|
|
893
1381
|
this.saving = true;
|
|
894
1382
|
try {
|
|
895
1383
|
const layers = this.layerManager?.snapshot() ?? [];
|
|
896
|
-
const
|
|
1384
|
+
const fog = this.fogManager?.getState() ?? void 0;
|
|
1385
|
+
const state = exportState(this.store.snapshot(), this.camera, layers, void 0, fog);
|
|
897
1386
|
await this.adapter.save(this.key, JSON.stringify(state));
|
|
898
1387
|
} catch (e) {
|
|
899
1388
|
this.onError?.(e instanceof Error ? e : new Error(String(e)));
|
|
@@ -5225,6 +5714,8 @@ var MinimapController = class {
|
|
|
5225
5714
|
// read the `viewport` parameter. Assigned in the constructor body instead.
|
|
5226
5715
|
htmlPainters;
|
|
5227
5716
|
scene = null;
|
|
5717
|
+
fogRenderer = null;
|
|
5718
|
+
fogUnsub = null;
|
|
5228
5719
|
frameId = null;
|
|
5229
5720
|
debounceTimer = null;
|
|
5230
5721
|
dragging = false;
|
|
@@ -5239,6 +5730,15 @@ var MinimapController = class {
|
|
|
5239
5730
|
this.renderScene();
|
|
5240
5731
|
this.requestDraw();
|
|
5241
5732
|
}
|
|
5733
|
+
setFogRenderer(renderer) {
|
|
5734
|
+
if (this.disposed) return;
|
|
5735
|
+
if (this.fogUnsub) {
|
|
5736
|
+
this.fogUnsub();
|
|
5737
|
+
this.fogUnsub = null;
|
|
5738
|
+
}
|
|
5739
|
+
this.fogRenderer = renderer;
|
|
5740
|
+
this.invalidateScene();
|
|
5741
|
+
}
|
|
5242
5742
|
requestDraw() {
|
|
5243
5743
|
if (this.disposed || this.frameId !== null) return;
|
|
5244
5744
|
this.frameId = this.requestFrame(this.draw);
|
|
@@ -5300,8 +5800,15 @@ var MinimapController = class {
|
|
|
5300
5800
|
}
|
|
5301
5801
|
currentMapping() {
|
|
5302
5802
|
const viewportRect = this.viewport.getVisibleRect();
|
|
5303
|
-
|
|
5304
|
-
|
|
5803
|
+
let mapping = getElementsBoundingBox(this.sceneElements());
|
|
5804
|
+
mapping = mapping ? unionBounds(mapping, viewportRect) : viewportRect;
|
|
5805
|
+
if (this.fogRenderer?.isVisible()) {
|
|
5806
|
+
const fogState = this.fogRenderer.getState();
|
|
5807
|
+
if (fogState) {
|
|
5808
|
+
mapping = unionBounds(mapping, fogState.definition.bounds);
|
|
5809
|
+
}
|
|
5810
|
+
}
|
|
5811
|
+
return mapping;
|
|
5305
5812
|
}
|
|
5306
5813
|
onViewChanged() {
|
|
5307
5814
|
if (this.disposed) return;
|
|
@@ -5359,6 +5866,23 @@ var MinimapController = class {
|
|
|
5359
5866
|
ctx.drawImage(layerCanvas, 0, 0);
|
|
5360
5867
|
ctx.restore();
|
|
5361
5868
|
}
|
|
5869
|
+
if (this.fogRenderer?.isVisible()) {
|
|
5870
|
+
const fogState = this.fogRenderer.getState();
|
|
5871
|
+
const fogMode = this.fogRenderer.getViewMode();
|
|
5872
|
+
if (fogState && (fogMode === "editor" || fogMode === "player")) {
|
|
5873
|
+
ctx.save();
|
|
5874
|
+
ctx.setTransform(
|
|
5875
|
+
dpr * transform.scale,
|
|
5876
|
+
0,
|
|
5877
|
+
0,
|
|
5878
|
+
dpr * transform.scale,
|
|
5879
|
+
dpr * transform.offsetX,
|
|
5880
|
+
dpr * transform.offsetY
|
|
5881
|
+
);
|
|
5882
|
+
this.fogRenderer.renderForExport(ctx, fogState, fogMode);
|
|
5883
|
+
ctx.restore();
|
|
5884
|
+
}
|
|
5885
|
+
}
|
|
5362
5886
|
this.scene = { canvas: sceneCanvas, transform, mapping };
|
|
5363
5887
|
}
|
|
5364
5888
|
renderLayerElements(ctx, elements, t, dpr) {
|
|
@@ -5471,9 +5995,15 @@ var Minimap = class {
|
|
|
5471
5995
|
this.canvas = canvas;
|
|
5472
5996
|
this.controller = new MinimapController(viewport, canvas, { width: WIDTH, height: HEIGHT });
|
|
5473
5997
|
}
|
|
5998
|
+
setFogRenderer(renderer) {
|
|
5999
|
+
this.controller.setFogRenderer(renderer);
|
|
6000
|
+
}
|
|
5474
6001
|
scheduleDraw() {
|
|
5475
6002
|
this.controller.requestDraw();
|
|
5476
6003
|
}
|
|
6004
|
+
invalidateScene() {
|
|
6005
|
+
this.controller.invalidateScene();
|
|
6006
|
+
}
|
|
5477
6007
|
destroy() {
|
|
5478
6008
|
this.controller.dispose();
|
|
5479
6009
|
this.canvas.remove();
|
|
@@ -6795,6 +7325,220 @@ async function renderHtmlElements(elements, options) {
|
|
|
6795
7325
|
return sources;
|
|
6796
7326
|
}
|
|
6797
7327
|
|
|
7328
|
+
// src/fog/fog-renderer.ts
|
|
7329
|
+
var DEFAULT_EDITOR_COLOR = "rgba(30, 40, 60, 0.45)";
|
|
7330
|
+
var DEFAULT_PLAYER_COLOR = "#0b1020";
|
|
7331
|
+
var FogRenderer = class {
|
|
7332
|
+
tileCache = /* @__PURE__ */ new Map();
|
|
7333
|
+
state = null;
|
|
7334
|
+
viewMode = "off";
|
|
7335
|
+
dirty = true;
|
|
7336
|
+
editorColor;
|
|
7337
|
+
playerColor;
|
|
7338
|
+
constructor(options = {}) {
|
|
7339
|
+
this.editorColor = options.editorColor ?? DEFAULT_EDITOR_COLOR;
|
|
7340
|
+
this.playerColor = options.playerColor ?? DEFAULT_PLAYER_COLOR;
|
|
7341
|
+
}
|
|
7342
|
+
setState(state) {
|
|
7343
|
+
this.state = state;
|
|
7344
|
+
this.dirty = true;
|
|
7345
|
+
}
|
|
7346
|
+
setViewMode(mode) {
|
|
7347
|
+
if (mode === this.viewMode) return;
|
|
7348
|
+
this.viewMode = mode;
|
|
7349
|
+
this.dirty = true;
|
|
7350
|
+
}
|
|
7351
|
+
getState() {
|
|
7352
|
+
return this.state;
|
|
7353
|
+
}
|
|
7354
|
+
getViewMode() {
|
|
7355
|
+
return this.viewMode;
|
|
7356
|
+
}
|
|
7357
|
+
markDirty() {
|
|
7358
|
+
this.dirty = true;
|
|
7359
|
+
}
|
|
7360
|
+
isDirty() {
|
|
7361
|
+
return this.dirty;
|
|
7362
|
+
}
|
|
7363
|
+
isVisible() {
|
|
7364
|
+
return this.viewMode !== "off" && this.state !== null;
|
|
7365
|
+
}
|
|
7366
|
+
render(ctx, camera, viewportWidth, viewportHeight, _dpr) {
|
|
7367
|
+
if (!this.state || this.viewMode === "off") return;
|
|
7368
|
+
const def = this.state.definition;
|
|
7369
|
+
const cellSize = def.cellSize;
|
|
7370
|
+
const tileWorldSize = FOG_TILE_CELLS * cellSize;
|
|
7371
|
+
const color = this.viewMode === "editor" ? this.editorColor : this.playerColor;
|
|
7372
|
+
const worldBounds = getVisibleWorld(camera, viewportWidth, viewportHeight);
|
|
7373
|
+
const minTX = Math.floor(Math.max(def.bounds.x, worldBounds.x) / tileWorldSize);
|
|
7374
|
+
const minTY = Math.floor(Math.max(def.bounds.y, worldBounds.y) / tileWorldSize);
|
|
7375
|
+
const maxTX = Math.floor(
|
|
7376
|
+
Math.min(def.bounds.x + def.bounds.w - 1, worldBounds.x + worldBounds.w) / tileWorldSize
|
|
7377
|
+
);
|
|
7378
|
+
const maxTY = Math.floor(
|
|
7379
|
+
Math.min(def.bounds.y + def.bounds.h - 1, worldBounds.y + worldBounds.h) / tileWorldSize
|
|
7380
|
+
);
|
|
7381
|
+
ctx.save();
|
|
7382
|
+
ctx.translate(camera.position.x, camera.position.y);
|
|
7383
|
+
ctx.scale(camera.zoom, camera.zoom);
|
|
7384
|
+
const tileMap = /* @__PURE__ */ new Map();
|
|
7385
|
+
for (const tile of this.state.tiles) {
|
|
7386
|
+
tileMap.set(`${tile.x},${tile.y}`, tile.data);
|
|
7387
|
+
}
|
|
7388
|
+
const baseCovered = def.base === "covered";
|
|
7389
|
+
for (let ty = minTY; ty <= maxTY; ty++) {
|
|
7390
|
+
for (let tx = minTX; tx <= maxTX; tx++) {
|
|
7391
|
+
const key = `${tx},${ty}`;
|
|
7392
|
+
const data = tileMap.get(key);
|
|
7393
|
+
const tileWorldX = tx * tileWorldSize;
|
|
7394
|
+
const tileWorldY = ty * tileWorldSize;
|
|
7395
|
+
if (!data && baseCovered) {
|
|
7396
|
+
ctx.fillStyle = color;
|
|
7397
|
+
const clipX = Math.max(tileWorldX, def.bounds.x);
|
|
7398
|
+
const clipY = Math.max(tileWorldY, def.bounds.y);
|
|
7399
|
+
const clipR = Math.min(tileWorldX + tileWorldSize, def.bounds.x + def.bounds.w);
|
|
7400
|
+
const clipB = Math.min(tileWorldY + tileWorldSize, def.bounds.y + def.bounds.h);
|
|
7401
|
+
if (clipR > clipX && clipB > clipY) {
|
|
7402
|
+
ctx.fillRect(clipX, clipY, clipR - clipX, clipB - clipY);
|
|
7403
|
+
}
|
|
7404
|
+
continue;
|
|
7405
|
+
}
|
|
7406
|
+
if (!data && !baseCovered) {
|
|
7407
|
+
continue;
|
|
7408
|
+
}
|
|
7409
|
+
if (data) {
|
|
7410
|
+
this.renderTile(ctx, data, tx, ty, def, color);
|
|
7411
|
+
}
|
|
7412
|
+
}
|
|
7413
|
+
}
|
|
7414
|
+
ctx.restore();
|
|
7415
|
+
this.dirty = false;
|
|
7416
|
+
}
|
|
7417
|
+
renderForExport(ctx, state, mode, color) {
|
|
7418
|
+
const def = state.definition;
|
|
7419
|
+
const cellSize = def.cellSize;
|
|
7420
|
+
const tileWorldSize = FOG_TILE_CELLS * cellSize;
|
|
7421
|
+
const fogColor = color ?? (mode === "editor" ? this.editorColor : this.playerColor);
|
|
7422
|
+
const baseCovered = def.base === "covered";
|
|
7423
|
+
const tileMap = /* @__PURE__ */ new Map();
|
|
7424
|
+
for (const tile of state.tiles) {
|
|
7425
|
+
tileMap.set(`${tile.x},${tile.y}`, tile.data);
|
|
7426
|
+
}
|
|
7427
|
+
const minTX = Math.floor(def.bounds.x / tileWorldSize);
|
|
7428
|
+
const minTY = Math.floor(def.bounds.y / tileWorldSize);
|
|
7429
|
+
const maxTX = Math.floor((def.bounds.x + def.bounds.w - 1) / tileWorldSize);
|
|
7430
|
+
const maxTY = Math.floor((def.bounds.y + def.bounds.h - 1) / tileWorldSize);
|
|
7431
|
+
for (let ty = minTY; ty <= maxTY; ty++) {
|
|
7432
|
+
for (let tx = minTX; tx <= maxTX; tx++) {
|
|
7433
|
+
const key = `${tx},${ty}`;
|
|
7434
|
+
const data = tileMap.get(key);
|
|
7435
|
+
const tileWorldX = tx * tileWorldSize;
|
|
7436
|
+
const tileWorldY = ty * tileWorldSize;
|
|
7437
|
+
if (!data && baseCovered) {
|
|
7438
|
+
ctx.fillStyle = fogColor;
|
|
7439
|
+
const clipX = Math.max(tileWorldX, def.bounds.x);
|
|
7440
|
+
const clipY = Math.max(tileWorldY, def.bounds.y);
|
|
7441
|
+
const clipR = Math.min(tileWorldX + tileWorldSize, def.bounds.x + def.bounds.w);
|
|
7442
|
+
const clipB = Math.min(tileWorldY + tileWorldSize, def.bounds.y + def.bounds.h);
|
|
7443
|
+
if (clipR > clipX && clipB > clipY) {
|
|
7444
|
+
ctx.fillRect(clipX, clipY, clipR - clipX, clipB - clipY);
|
|
7445
|
+
}
|
|
7446
|
+
continue;
|
|
7447
|
+
}
|
|
7448
|
+
if (data) {
|
|
7449
|
+
this.renderTileForExport(ctx, data, tx, ty, def, fogColor);
|
|
7450
|
+
}
|
|
7451
|
+
}
|
|
7452
|
+
}
|
|
7453
|
+
}
|
|
7454
|
+
dispose() {
|
|
7455
|
+
this.tileCache.clear();
|
|
7456
|
+
this.state = null;
|
|
7457
|
+
}
|
|
7458
|
+
tileRaster(data, color) {
|
|
7459
|
+
const key = `${color}\0${data}`;
|
|
7460
|
+
const cached = this.tileCache.get(key);
|
|
7461
|
+
if (cached) return cached;
|
|
7462
|
+
if (typeof document === "undefined") return null;
|
|
7463
|
+
const canvas = document.createElement("canvas");
|
|
7464
|
+
canvas.width = FOG_TILE_CELLS;
|
|
7465
|
+
canvas.height = FOG_TILE_CELLS;
|
|
7466
|
+
const ctx = canvas.getContext("2d");
|
|
7467
|
+
if (!ctx) return null;
|
|
7468
|
+
const bytes = decodeBase64(data);
|
|
7469
|
+
ctx.fillStyle = color;
|
|
7470
|
+
for (let row = 0; row < FOG_TILE_CELLS; row++) {
|
|
7471
|
+
for (let col = 0; col < FOG_TILE_CELLS; col++) {
|
|
7472
|
+
const index = row * FOG_TILE_CELLS + col;
|
|
7473
|
+
const byteIndex = index >> 3;
|
|
7474
|
+
const bitIndex = 7 - (index & 7);
|
|
7475
|
+
const revealed = (bytes[byteIndex] >> bitIndex & 1) === 1;
|
|
7476
|
+
if (!revealed) ctx.fillRect(col, row, 1, 1);
|
|
7477
|
+
}
|
|
7478
|
+
}
|
|
7479
|
+
if (this.tileCache.size >= 256) {
|
|
7480
|
+
const oldest = this.tileCache.keys().next().value;
|
|
7481
|
+
if (oldest !== void 0) this.tileCache.delete(oldest);
|
|
7482
|
+
}
|
|
7483
|
+
this.tileCache.set(key, canvas);
|
|
7484
|
+
return canvas;
|
|
7485
|
+
}
|
|
7486
|
+
renderTile(ctx, data, tx, ty, def, color) {
|
|
7487
|
+
const cellSize = def.cellSize;
|
|
7488
|
+
const tileWorldX = tx * FOG_TILE_CELLS * cellSize;
|
|
7489
|
+
const tileWorldY = ty * FOG_TILE_CELLS * cellSize;
|
|
7490
|
+
const raster = this.tileRaster(data, color);
|
|
7491
|
+
if (raster) {
|
|
7492
|
+
ctx.save();
|
|
7493
|
+
ctx.beginPath();
|
|
7494
|
+
ctx.rect(def.bounds.x, def.bounds.y, def.bounds.w, def.bounds.h);
|
|
7495
|
+
ctx.clip();
|
|
7496
|
+
ctx.imageSmoothingEnabled = false;
|
|
7497
|
+
ctx.drawImage(
|
|
7498
|
+
raster,
|
|
7499
|
+
tileWorldX,
|
|
7500
|
+
tileWorldY,
|
|
7501
|
+
FOG_TILE_CELLS * cellSize,
|
|
7502
|
+
FOG_TILE_CELLS * cellSize
|
|
7503
|
+
);
|
|
7504
|
+
ctx.restore();
|
|
7505
|
+
return;
|
|
7506
|
+
}
|
|
7507
|
+
const bytes = decodeBase64(data);
|
|
7508
|
+
ctx.fillStyle = color;
|
|
7509
|
+
for (let row = 0; row < FOG_TILE_CELLS; row++) {
|
|
7510
|
+
for (let col = 0; col < FOG_TILE_CELLS; col++) {
|
|
7511
|
+
const cellWorldX = tileWorldX + col * cellSize;
|
|
7512
|
+
const cellWorldY = tileWorldY + row * cellSize;
|
|
7513
|
+
if (cellWorldX < def.bounds.x || cellWorldY < def.bounds.y || cellWorldX >= def.bounds.x + def.bounds.w || cellWorldY >= def.bounds.y + def.bounds.h) {
|
|
7514
|
+
continue;
|
|
7515
|
+
}
|
|
7516
|
+
const index = row * FOG_TILE_CELLS + col;
|
|
7517
|
+
const byteIndex = index >> 3;
|
|
7518
|
+
const bitIndex = 7 - (index & 7);
|
|
7519
|
+
const revealed = (bytes[byteIndex] >> bitIndex & 1) === 1;
|
|
7520
|
+
const covered = !revealed;
|
|
7521
|
+
if (covered) {
|
|
7522
|
+
ctx.fillRect(cellWorldX, cellWorldY, cellSize, cellSize);
|
|
7523
|
+
}
|
|
7524
|
+
}
|
|
7525
|
+
}
|
|
7526
|
+
}
|
|
7527
|
+
renderTileForExport(ctx, data, tx, ty, def, color) {
|
|
7528
|
+
this.renderTile(ctx, data, tx, ty, def, color);
|
|
7529
|
+
}
|
|
7530
|
+
};
|
|
7531
|
+
function getVisibleWorld(camera, viewportWidth, viewportHeight) {
|
|
7532
|
+
const topLeft = camera.screenToWorld({ x: 0, y: 0 });
|
|
7533
|
+
const bottomRight = camera.screenToWorld({ x: viewportWidth, y: viewportHeight });
|
|
7534
|
+
return {
|
|
7535
|
+
x: topLeft.x,
|
|
7536
|
+
y: topLeft.y,
|
|
7537
|
+
w: bottomRight.x - topLeft.x,
|
|
7538
|
+
h: bottomRight.y - topLeft.y
|
|
7539
|
+
};
|
|
7540
|
+
}
|
|
7541
|
+
|
|
6798
7542
|
// src/canvas/export-image.ts
|
|
6799
7543
|
var DEFAULT_IMAGE_TIMEOUT_MS = 1e4;
|
|
6800
7544
|
var DEFAULT_MAX_DIMENSION = 16384;
|
|
@@ -7207,6 +7951,10 @@ async function exportImage(store, options = {}, layerManager) {
|
|
|
7207
7951
|
renderGridForBounds(ctx, grid, bounds);
|
|
7208
7952
|
ctx.restore();
|
|
7209
7953
|
}
|
|
7954
|
+
if (options.fog) {
|
|
7955
|
+
const fogRenderer = new FogRenderer();
|
|
7956
|
+
fogRenderer.renderForExport(ctx, options.fog.state, options.fog.mode, options.fog.color);
|
|
7957
|
+
}
|
|
7210
7958
|
const mimeType = format === "jpeg" ? "image/jpeg" : "image/png";
|
|
7211
7959
|
return new Promise((resolve) => {
|
|
7212
7960
|
canvas.toBlob((blob) => resolve(blob), mimeType, options.quality);
|
|
@@ -7588,6 +8336,27 @@ async function exportSvg(store, options = {}, layerManager) {
|
|
|
7588
8336
|
const opacity = layerManager?.getLayer?.(grid.layerId)?.opacity ?? 1;
|
|
7589
8337
|
body += opacity === 1 ? emitted : `<g opacity="${n(opacity)}">${emitted}</g>`;
|
|
7590
8338
|
}
|
|
8339
|
+
if (options.fog && typeof document !== "undefined") {
|
|
8340
|
+
const fogState = options.fog.state;
|
|
8341
|
+
const fogW = Math.max(1, Math.ceil(bounds.w));
|
|
8342
|
+
const fogH = Math.max(1, Math.ceil(bounds.h));
|
|
8343
|
+
const fogCanvas = document.createElement("canvas");
|
|
8344
|
+
fogCanvas.width = fogW;
|
|
8345
|
+
fogCanvas.height = fogH;
|
|
8346
|
+
const fogCtx = fogCanvas.getContext("2d");
|
|
8347
|
+
if (fogCtx) {
|
|
8348
|
+
fogCtx.translate(-bounds.x, -bounds.y);
|
|
8349
|
+
const fogRenderer = new FogRenderer();
|
|
8350
|
+
fogRenderer.renderForExport(fogCtx, fogState, options.fog.mode, options.fog.color);
|
|
8351
|
+
try {
|
|
8352
|
+
const fogDataUri = fogCanvas.toDataURL("image/png");
|
|
8353
|
+
if (fogDataUri.startsWith("data:")) {
|
|
8354
|
+
body += `<image href="${esc(fogDataUri)}" x="${n(bounds.x)}" y="${n(bounds.y)}" width="${n(bounds.w)}" height="${n(bounds.h)}" />`;
|
|
8355
|
+
}
|
|
8356
|
+
} catch {
|
|
8357
|
+
}
|
|
8358
|
+
}
|
|
8359
|
+
}
|
|
7591
8360
|
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>`;
|
|
7592
8361
|
}
|
|
7593
8362
|
function emitElement(el, imageDataUris, htmlDataUris, rasterScale, firstGrid, store, resourceOptions) {
|
|
@@ -8425,6 +9194,7 @@ var RenderLoop = class {
|
|
|
8425
9194
|
layerCache;
|
|
8426
9195
|
marginViewport;
|
|
8427
9196
|
hybridSurface;
|
|
9197
|
+
fogRenderer;
|
|
8428
9198
|
activeDrawingLayerId = null;
|
|
8429
9199
|
gridCacheDirty = true;
|
|
8430
9200
|
// set on recenter/viewport-change; consumed by the grid block
|
|
@@ -8448,6 +9218,7 @@ var RenderLoop = class {
|
|
|
8448
9218
|
this.layerCache = deps.layerCache;
|
|
8449
9219
|
this.marginViewport = deps.marginViewport;
|
|
8450
9220
|
this.hybridSurface = deps.hybridSurface;
|
|
9221
|
+
this.fogRenderer = deps.fogRenderer;
|
|
8451
9222
|
}
|
|
8452
9223
|
requestRender() {
|
|
8453
9224
|
this.needsRender = true;
|
|
@@ -8757,9 +9528,12 @@ var RenderLoop = class {
|
|
|
8757
9528
|
group.push(element);
|
|
8758
9529
|
}
|
|
8759
9530
|
const activeTool = this.toolManager.activeTool;
|
|
8760
|
-
const
|
|
9531
|
+
const fogVisible = this.fogRenderer?.isVisible() ?? false;
|
|
9532
|
+
const fogOrder = visibleElements.length + 1;
|
|
9533
|
+
const overlayOrder = fogVisible ? fogOrder + 1 : visibleElements.length + 1;
|
|
8761
9534
|
const hasOverlay = activeTool?.renderOverlay !== void 0 || this.overlays.size > 0;
|
|
8762
|
-
if (
|
|
9535
|
+
if (fogVisible) hybridOrders.add(fogOrder);
|
|
9536
|
+
if (hasOverlay && (hybridActive || fogVisible)) hybridOrders.add(overlayOrder);
|
|
8763
9537
|
this.hybridSurface.beginFrame(hybridOrders, this.canvasEl.width, this.canvasEl.height);
|
|
8764
9538
|
for (const [layerId, elements] of this.layerGroups) {
|
|
8765
9539
|
const isActiveDrawingLayer = layerId === this.activeDrawingLayerId;
|
|
@@ -8873,8 +9647,18 @@ var RenderLoop = class {
|
|
|
8873
9647
|
}
|
|
8874
9648
|
hybridCtx.restore();
|
|
8875
9649
|
}
|
|
9650
|
+
if (fogVisible && this.fogRenderer) {
|
|
9651
|
+
const fogCtx = this.hybridSurface.getContext(fogOrder);
|
|
9652
|
+
if (fogCtx) {
|
|
9653
|
+
fogCtx.clearRect(0, 0, this.canvasEl.width, this.canvasEl.height);
|
|
9654
|
+
fogCtx.save();
|
|
9655
|
+
fogCtx.scale(dpr, dpr);
|
|
9656
|
+
this.fogRenderer.render(fogCtx, this.camera, cssWidth, cssHeight, dpr);
|
|
9657
|
+
fogCtx.restore();
|
|
9658
|
+
}
|
|
9659
|
+
}
|
|
8876
9660
|
const overlayT0 = performance.now();
|
|
8877
|
-
if (hybridActive && hasOverlay) {
|
|
9661
|
+
if ((hybridActive || fogVisible) && hasOverlay) {
|
|
8878
9662
|
const overlayCtx = this.hybridSurface.getContext(overlayOrder);
|
|
8879
9663
|
if (overlayCtx) {
|
|
8880
9664
|
overlayCtx.clearRect(0, 0, this.canvasEl.width, this.canvasEl.height);
|
|
@@ -9885,6 +10669,248 @@ var ElementActivation = class {
|
|
|
9885
10669
|
}
|
|
9886
10670
|
};
|
|
9887
10671
|
|
|
10672
|
+
// src/fog/fog-command.ts
|
|
10673
|
+
var FogRegionCommand = class {
|
|
10674
|
+
constructor(manager, before, after) {
|
|
10675
|
+
this.manager = manager;
|
|
10676
|
+
this.before = before;
|
|
10677
|
+
this.after = after;
|
|
10678
|
+
}
|
|
10679
|
+
execute(_store) {
|
|
10680
|
+
this.manager.applyTilesDirect(this.after);
|
|
10681
|
+
}
|
|
10682
|
+
undo(_store) {
|
|
10683
|
+
this.manager.applyTilesDirect(this.before);
|
|
10684
|
+
}
|
|
10685
|
+
};
|
|
10686
|
+
var FogResetCommand = class {
|
|
10687
|
+
constructor(manager, before, after) {
|
|
10688
|
+
this.manager = manager;
|
|
10689
|
+
this.before = before;
|
|
10690
|
+
this.after = after;
|
|
10691
|
+
}
|
|
10692
|
+
execute(_store) {
|
|
10693
|
+
this.manager.restoreHistoryState(this.after);
|
|
10694
|
+
}
|
|
10695
|
+
undo(_store) {
|
|
10696
|
+
this.manager.restoreHistoryState(this.before);
|
|
10697
|
+
}
|
|
10698
|
+
};
|
|
10699
|
+
|
|
10700
|
+
// src/fog/fog-manager.ts
|
|
10701
|
+
function defaultIdFactory() {
|
|
10702
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
10703
|
+
return crypto.randomUUID();
|
|
10704
|
+
}
|
|
10705
|
+
return `fog-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
10706
|
+
}
|
|
10707
|
+
var FogManager = class {
|
|
10708
|
+
state = null;
|
|
10709
|
+
viewMode = "off";
|
|
10710
|
+
idFactory;
|
|
10711
|
+
onCommand;
|
|
10712
|
+
changeListeners = /* @__PURE__ */ new Set();
|
|
10713
|
+
viewListeners = /* @__PURE__ */ new Set();
|
|
10714
|
+
constructor(options = {}) {
|
|
10715
|
+
this.idFactory = options.idFactory ?? defaultIdFactory;
|
|
10716
|
+
this.onCommand = options.onCommand;
|
|
10717
|
+
}
|
|
10718
|
+
getState() {
|
|
10719
|
+
if (!this.state) return null;
|
|
10720
|
+
return {
|
|
10721
|
+
definition: { ...this.state.definition, bounds: { ...this.state.definition.bounds } },
|
|
10722
|
+
tiles: this.state.tiles.map((t) => ({ ...t }))
|
|
10723
|
+
};
|
|
10724
|
+
}
|
|
10725
|
+
getViewMode() {
|
|
10726
|
+
return this.viewMode;
|
|
10727
|
+
}
|
|
10728
|
+
initialize(options) {
|
|
10729
|
+
const base = options.base ?? "covered";
|
|
10730
|
+
const cellSize = options.cellSize ?? recommendedFogCellSize(options.bounds);
|
|
10731
|
+
const generation = this.idFactory();
|
|
10732
|
+
const newState = {
|
|
10733
|
+
definition: {
|
|
10734
|
+
version: 1,
|
|
10735
|
+
generation,
|
|
10736
|
+
bounds: { ...options.bounds },
|
|
10737
|
+
cellSize,
|
|
10738
|
+
tileCells: FOG_TILE_CELLS,
|
|
10739
|
+
base
|
|
10740
|
+
},
|
|
10741
|
+
tiles: []
|
|
10742
|
+
};
|
|
10743
|
+
validateFogState(newState);
|
|
10744
|
+
const before = this.state;
|
|
10745
|
+
this.state = newState;
|
|
10746
|
+
const command = new FogResetCommand(this, before, newState);
|
|
10747
|
+
this.onCommand?.(command);
|
|
10748
|
+
this.notifyChange({ kind: "definition" });
|
|
10749
|
+
return structuredClone(newState);
|
|
10750
|
+
}
|
|
10751
|
+
loadState(state, meta) {
|
|
10752
|
+
if (state !== null) {
|
|
10753
|
+
validateFogState(state);
|
|
10754
|
+
this.state = structuredClone(state);
|
|
10755
|
+
} else {
|
|
10756
|
+
this.state = null;
|
|
10757
|
+
}
|
|
10758
|
+
this.notifyChange({
|
|
10759
|
+
kind: state === null ? "disable" : "definition",
|
|
10760
|
+
origin: meta?.origin
|
|
10761
|
+
});
|
|
10762
|
+
}
|
|
10763
|
+
/** Restores a historical visual state without reusing its causal generation id. */
|
|
10764
|
+
restoreHistoryState(state) {
|
|
10765
|
+
if (state === null) {
|
|
10766
|
+
this.loadState(null);
|
|
10767
|
+
return;
|
|
10768
|
+
}
|
|
10769
|
+
this.loadState({
|
|
10770
|
+
definition: { ...state.definition, generation: this.idFactory() },
|
|
10771
|
+
tiles: state.tiles
|
|
10772
|
+
});
|
|
10773
|
+
}
|
|
10774
|
+
setBounds(bounds) {
|
|
10775
|
+
if (!this.state) return;
|
|
10776
|
+
const def = this.state.definition;
|
|
10777
|
+
validateFogState({
|
|
10778
|
+
definition: { ...def, bounds: { ...bounds } },
|
|
10779
|
+
tiles: []
|
|
10780
|
+
});
|
|
10781
|
+
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;
|
|
10782
|
+
const nextDefinition = {
|
|
10783
|
+
...def,
|
|
10784
|
+
bounds: { ...bounds },
|
|
10785
|
+
generation: shrinks ? this.idFactory() : def.generation
|
|
10786
|
+
};
|
|
10787
|
+
const tiles = this.state.tiles.flatMap((tile) => {
|
|
10788
|
+
const tileWorldX = tile.x * FOG_TILE_CELLS * def.cellSize;
|
|
10789
|
+
const tileWorldY = tile.y * FOG_TILE_CELLS * def.cellSize;
|
|
10790
|
+
const tileWorldW = FOG_TILE_CELLS * def.cellSize;
|
|
10791
|
+
const tileWorldH = FOG_TILE_CELLS * def.cellSize;
|
|
10792
|
+
const intersects2 = !(tileWorldX + tileWorldW <= bounds.x || tileWorldY + tileWorldH <= bounds.y || tileWorldX >= bounds.x + bounds.w || tileWorldY >= bounds.y + bounds.h);
|
|
10793
|
+
if (!intersects2) return [];
|
|
10794
|
+
const canonical = canonicalizeFogTile(tile, nextDefinition);
|
|
10795
|
+
return canonical ? [canonical] : [];
|
|
10796
|
+
});
|
|
10797
|
+
const before = this.state;
|
|
10798
|
+
this.state = {
|
|
10799
|
+
definition: nextDefinition,
|
|
10800
|
+
tiles
|
|
10801
|
+
};
|
|
10802
|
+
const command = new FogResetCommand(this, before, this.state);
|
|
10803
|
+
this.onCommand?.(command);
|
|
10804
|
+
this.notifyChange({ kind: "definition" });
|
|
10805
|
+
}
|
|
10806
|
+
reset(base) {
|
|
10807
|
+
if (!this.state) return;
|
|
10808
|
+
const before = this.state;
|
|
10809
|
+
const generation = this.idFactory();
|
|
10810
|
+
this.state = {
|
|
10811
|
+
definition: { ...this.state.definition, base, generation },
|
|
10812
|
+
tiles: []
|
|
10813
|
+
};
|
|
10814
|
+
const command = new FogResetCommand(this, before, this.state);
|
|
10815
|
+
this.onCommand?.(command);
|
|
10816
|
+
this.notifyChange({ kind: "reset" });
|
|
10817
|
+
}
|
|
10818
|
+
disable() {
|
|
10819
|
+
if (!this.state) return;
|
|
10820
|
+
const before = this.state;
|
|
10821
|
+
this.state = null;
|
|
10822
|
+
const command = new FogResetCommand(this, before, null);
|
|
10823
|
+
this.onCommand?.(command);
|
|
10824
|
+
this.notifyChange({ kind: "disable" });
|
|
10825
|
+
}
|
|
10826
|
+
setViewMode(mode) {
|
|
10827
|
+
if (mode === this.viewMode) return;
|
|
10828
|
+
this.viewMode = mode;
|
|
10829
|
+
this.notifyView({ mode });
|
|
10830
|
+
}
|
|
10831
|
+
applyRegion(region, operation) {
|
|
10832
|
+
if (!this.state) return;
|
|
10833
|
+
const result = rasterizeRegion(this.state, region, operation);
|
|
10834
|
+
if (result.noop) return;
|
|
10835
|
+
const before = this.collectTiles(result.changed);
|
|
10836
|
+
const newState = applyRasterResult(this.state, result);
|
|
10837
|
+
this.state = newState;
|
|
10838
|
+
const command = new FogRegionCommand(this, before, result.changed);
|
|
10839
|
+
this.onCommand?.(command);
|
|
10840
|
+
this.notifyChange({
|
|
10841
|
+
kind: "tiles",
|
|
10842
|
+
tiles: result.changed.map((t) => ({ x: t.x, y: t.y }))
|
|
10843
|
+
});
|
|
10844
|
+
}
|
|
10845
|
+
applyPatchDirect(patch, meta) {
|
|
10846
|
+
if (!this.state) return;
|
|
10847
|
+
const result = applyRasterResult(this.state, { changed: patch.tiles, noop: false });
|
|
10848
|
+
this.state = result;
|
|
10849
|
+
this.notifyChange({
|
|
10850
|
+
kind: "tiles",
|
|
10851
|
+
tiles: patch.tiles.map((t) => ({ x: t.x, y: t.y })),
|
|
10852
|
+
origin: meta?.origin
|
|
10853
|
+
});
|
|
10854
|
+
}
|
|
10855
|
+
applyTilesDirect(tiles) {
|
|
10856
|
+
if (!this.state) return;
|
|
10857
|
+
const result = applyRasterResult(this.state, { changed: tiles, noop: false });
|
|
10858
|
+
this.state = result;
|
|
10859
|
+
this.notifyChange({
|
|
10860
|
+
kind: "tiles",
|
|
10861
|
+
tiles: tiles.map((t) => ({ x: t.x, y: t.y }))
|
|
10862
|
+
});
|
|
10863
|
+
}
|
|
10864
|
+
on(event, listener) {
|
|
10865
|
+
if (event === "change") {
|
|
10866
|
+
const l2 = listener;
|
|
10867
|
+
this.changeListeners.add(l2);
|
|
10868
|
+
return () => this.changeListeners.delete(l2);
|
|
10869
|
+
}
|
|
10870
|
+
const l = listener;
|
|
10871
|
+
this.viewListeners.add(l);
|
|
10872
|
+
return () => this.viewListeners.delete(l);
|
|
10873
|
+
}
|
|
10874
|
+
dispose() {
|
|
10875
|
+
this.changeListeners.clear();
|
|
10876
|
+
this.viewListeners.clear();
|
|
10877
|
+
}
|
|
10878
|
+
collectTiles(changed) {
|
|
10879
|
+
if (!this.state) return [];
|
|
10880
|
+
const result = [];
|
|
10881
|
+
for (const c of changed) {
|
|
10882
|
+
const existing = this.state.tiles.find((t) => t.x === c.x && t.y === c.y);
|
|
10883
|
+
if (existing) {
|
|
10884
|
+
result.push(existing);
|
|
10885
|
+
} else {
|
|
10886
|
+
const baseVal = this.state.definition.base === "revealed";
|
|
10887
|
+
result.push({
|
|
10888
|
+
x: c.x,
|
|
10889
|
+
y: c.y,
|
|
10890
|
+
data: encodeBase64(createTileBytes(baseVal))
|
|
10891
|
+
});
|
|
10892
|
+
}
|
|
10893
|
+
}
|
|
10894
|
+
return result;
|
|
10895
|
+
}
|
|
10896
|
+
notifyChange(event) {
|
|
10897
|
+
for (const listener of this.changeListeners) {
|
|
10898
|
+
try {
|
|
10899
|
+
listener(event);
|
|
10900
|
+
} catch {
|
|
10901
|
+
}
|
|
10902
|
+
}
|
|
10903
|
+
}
|
|
10904
|
+
notifyView(event) {
|
|
10905
|
+
for (const listener of this.viewListeners) {
|
|
10906
|
+
try {
|
|
10907
|
+
listener(event);
|
|
10908
|
+
} catch {
|
|
10909
|
+
}
|
|
10910
|
+
}
|
|
10911
|
+
}
|
|
10912
|
+
};
|
|
10913
|
+
|
|
9888
10914
|
// src/canvas/viewport.ts
|
|
9889
10915
|
var EMPTY_IDS = [];
|
|
9890
10916
|
function noop2() {
|
|
@@ -10011,8 +11037,13 @@ var Viewport = class _Viewport {
|
|
|
10011
11037
|
});
|
|
10012
11038
|
}
|
|
10013
11039
|
this.unsubToolChange = this.toolManager.onChange(() => this.contextMenu?.close());
|
|
11040
|
+
this.fogManager = new FogManager({
|
|
11041
|
+
onCommand: (cmd) => this.history.push(cmd)
|
|
11042
|
+
});
|
|
11043
|
+
this.fogRenderer = new FogRenderer(options.fog);
|
|
10014
11044
|
if (options.minimap) {
|
|
10015
11045
|
this.minimap = new Minimap(this.wrapper, this);
|
|
11046
|
+
this.minimap.setFogRenderer(this.fogRenderer);
|
|
10016
11047
|
}
|
|
10017
11048
|
this.domNodeManager = new DomNodeManager({
|
|
10018
11049
|
domLayer: this.paintStack,
|
|
@@ -10042,7 +11073,18 @@ var Viewport = class _Viewport {
|
|
|
10042
11073
|
domNodeManager: this.domNodeManager,
|
|
10043
11074
|
layerCache,
|
|
10044
11075
|
marginViewport: this.marginViewport,
|
|
10045
|
-
hybridSurface: new HybridRenderSurface(this.paintStack)
|
|
11076
|
+
hybridSurface: new HybridRenderSurface(this.paintStack),
|
|
11077
|
+
fogRenderer: this.fogRenderer
|
|
11078
|
+
});
|
|
11079
|
+
this.fogManager.on("change", () => {
|
|
11080
|
+
this.fogRenderer.setState(this.fogManager.getState());
|
|
11081
|
+
this.renderLoop.requestRender();
|
|
11082
|
+
this.minimap?.invalidateScene();
|
|
11083
|
+
});
|
|
11084
|
+
this.fogManager.on("view", () => {
|
|
11085
|
+
this.fogRenderer.setViewMode(this.fogManager.getViewMode());
|
|
11086
|
+
this.renderLoop.requestRender();
|
|
11087
|
+
this.minimap?.invalidateScene();
|
|
10046
11088
|
});
|
|
10047
11089
|
this.unsubHtmlPainters = this.htmlPainters.onChange(() => this.onHtmlRegistryChanged());
|
|
10048
11090
|
this.unsubCamera = this.camera.onChange(() => {
|
|
@@ -10154,6 +11196,8 @@ var Viewport = class _Viewport {
|
|
|
10154
11196
|
_smartGuides = false;
|
|
10155
11197
|
_gridSize;
|
|
10156
11198
|
renderLoop;
|
|
11199
|
+
fogManager;
|
|
11200
|
+
fogRenderer;
|
|
10157
11201
|
domNodeManager;
|
|
10158
11202
|
interactMode;
|
|
10159
11203
|
onHtmlElementMount;
|
|
@@ -10189,6 +11233,9 @@ var Viewport = class _Viewport {
|
|
|
10189
11233
|
get ctx() {
|
|
10190
11234
|
return this.canvasEl.getContext("2d");
|
|
10191
11235
|
}
|
|
11236
|
+
get fog() {
|
|
11237
|
+
return this.fogManager;
|
|
11238
|
+
}
|
|
10192
11239
|
get snapToGrid() {
|
|
10193
11240
|
return this._snapToGrid;
|
|
10194
11241
|
}
|
|
@@ -10273,7 +11320,8 @@ var Viewport = class _Viewport {
|
|
|
10273
11320
|
this.store.snapshot(),
|
|
10274
11321
|
this.camera,
|
|
10275
11322
|
this.layerManager.snapshot(),
|
|
10276
|
-
this.layerManager.activeLayerId
|
|
11323
|
+
this.layerManager.activeLayerId,
|
|
11324
|
+
this.fogManager.getState()
|
|
10277
11325
|
);
|
|
10278
11326
|
}
|
|
10279
11327
|
exportJSON() {
|
|
@@ -10295,12 +11343,31 @@ var Viewport = class _Viewport {
|
|
|
10295
11343
|
return { ...base, htmlPainters: registry, expectedCanvasTypes: expected };
|
|
10296
11344
|
}
|
|
10297
11345
|
async exportImage(options) {
|
|
10298
|
-
|
|
11346
|
+
const opts = this.withHtmlDefaults(options);
|
|
11347
|
+
if (opts.fog === void 0 && this.fogRenderer.isVisible()) {
|
|
11348
|
+
const state = this.fogManager.getState();
|
|
11349
|
+
if (state) {
|
|
11350
|
+
const mode = this.fogRenderer.getViewMode();
|
|
11351
|
+
opts.fog = { state, mode };
|
|
11352
|
+
}
|
|
11353
|
+
}
|
|
11354
|
+
return exportImage(this.store, opts, this.layerManager);
|
|
10299
11355
|
}
|
|
10300
11356
|
async exportSVG(options) {
|
|
10301
|
-
|
|
11357
|
+
const opts = this.withHtmlDefaults(options);
|
|
11358
|
+
if (opts.fog === void 0 && this.fogRenderer.isVisible()) {
|
|
11359
|
+
const state = this.fogManager.getState();
|
|
11360
|
+
if (state) {
|
|
11361
|
+
const mode = this.fogRenderer.getViewMode();
|
|
11362
|
+
opts.fog = { state, mode };
|
|
11363
|
+
}
|
|
11364
|
+
}
|
|
11365
|
+
return exportSvg(this.store, opts, this.layerManager);
|
|
10302
11366
|
}
|
|
10303
11367
|
loadState(state) {
|
|
11368
|
+
if (state.fog != null) {
|
|
11369
|
+
validateFogState(state.fog);
|
|
11370
|
+
}
|
|
10304
11371
|
this.inputHandler.flushPendingHistory();
|
|
10305
11372
|
this.historyRecorder.pause();
|
|
10306
11373
|
this.noteEditor.destroy(this.store);
|
|
@@ -10335,6 +11402,7 @@ var Viewport = class _Viewport {
|
|
|
10335
11402
|
}
|
|
10336
11403
|
}
|
|
10337
11404
|
}
|
|
11405
|
+
this.fogManager.loadState(state.fog ?? null);
|
|
10338
11406
|
this.history.clear();
|
|
10339
11407
|
this.historyRecorder.resume();
|
|
10340
11408
|
this.camera.moveTo(state.camera.position.x, state.camera.position.y);
|
|
@@ -10746,6 +11814,8 @@ var Viewport = class _Viewport {
|
|
|
10746
11814
|
this.unsubToolRegister();
|
|
10747
11815
|
this.unsubRecorderEnd();
|
|
10748
11816
|
this.unsubHtmlPainters();
|
|
11817
|
+
this.fogManager.dispose();
|
|
11818
|
+
this.fogRenderer.dispose();
|
|
10749
11819
|
this.activation?.dispose();
|
|
10750
11820
|
this.activation = null;
|
|
10751
11821
|
this.activationListeners.clear();
|
|
@@ -12228,26 +13298,820 @@ var RemoteFocusReceiver = class {
|
|
|
12228
13298
|
}
|
|
12229
13299
|
};
|
|
12230
13300
|
|
|
12231
|
-
// src/
|
|
12232
|
-
var
|
|
12233
|
-
|
|
12234
|
-
|
|
12235
|
-
|
|
12236
|
-
|
|
12237
|
-
|
|
12238
|
-
|
|
12239
|
-
|
|
12240
|
-
|
|
12241
|
-
|
|
12242
|
-
|
|
12243
|
-
|
|
12244
|
-
|
|
12245
|
-
|
|
13301
|
+
// src/canvas/awareness-presence.ts
|
|
13302
|
+
var AWARENESS_PRESENCE_KIND = "awareness";
|
|
13303
|
+
var AWARENESS_MAX_SELECTION = 256;
|
|
13304
|
+
var MAX_ID_LENGTH = 128;
|
|
13305
|
+
var MAX_NAME_LENGTH = 64;
|
|
13306
|
+
var MAX_COLOR_LENGTH2 = 64;
|
|
13307
|
+
var MAX_ROLE_LENGTH = 32;
|
|
13308
|
+
var MAX_TOOL_LENGTH = 64;
|
|
13309
|
+
function isBoundedString(value, max) {
|
|
13310
|
+
return typeof value === "string" && value.length <= max;
|
|
13311
|
+
}
|
|
13312
|
+
function isOptionalBoundedString(value, max) {
|
|
13313
|
+
return value === void 0 || isBoundedString(value, max);
|
|
13314
|
+
}
|
|
13315
|
+
function isFinitePoint4(value) {
|
|
13316
|
+
if (typeof value !== "object" || value === null) return false;
|
|
13317
|
+
const point = value;
|
|
13318
|
+
return typeof point.x === "number" && Number.isFinite(point.x) && typeof point.y === "number" && Number.isFinite(point.y);
|
|
13319
|
+
}
|
|
13320
|
+
function isAwarenessPresence(data) {
|
|
13321
|
+
if (typeof data !== "object" || data === null) return false;
|
|
13322
|
+
const payload = data;
|
|
13323
|
+
if (payload.kind !== AWARENESS_PRESENCE_KIND) return false;
|
|
13324
|
+
if (!isBoundedString(payload.id, MAX_ID_LENGTH) || payload.id.length === 0) return false;
|
|
13325
|
+
if ("cleared" in payload) return payload.cleared === true;
|
|
13326
|
+
if (!isOptionalBoundedString(payload.name, MAX_NAME_LENGTH)) return false;
|
|
13327
|
+
if (!isOptionalBoundedString(payload.color, MAX_COLOR_LENGTH2)) return false;
|
|
13328
|
+
if (!isOptionalBoundedString(payload.role, MAX_ROLE_LENGTH)) return false;
|
|
13329
|
+
if (!isOptionalBoundedString(payload.tool, MAX_TOOL_LENGTH)) return false;
|
|
13330
|
+
if (payload.cursor !== void 0 && !isFinitePoint4(payload.cursor)) return false;
|
|
13331
|
+
if (payload.selection !== void 0) {
|
|
13332
|
+
if (!Array.isArray(payload.selection)) return false;
|
|
13333
|
+
if (payload.selection.length > AWARENESS_MAX_SELECTION) return false;
|
|
13334
|
+
for (const id of payload.selection) {
|
|
13335
|
+
if (!isBoundedString(id, MAX_ID_LENGTH) || id.length === 0) return false;
|
|
13336
|
+
}
|
|
12246
13337
|
}
|
|
12247
|
-
|
|
12248
|
-
|
|
12249
|
-
|
|
12250
|
-
|
|
13338
|
+
return true;
|
|
13339
|
+
}
|
|
13340
|
+
|
|
13341
|
+
// src/canvas/awareness-roster.ts
|
|
13342
|
+
var DEFAULT_STALE_MS = 45e3;
|
|
13343
|
+
var EMPTY_PEERS = Object.freeze([]);
|
|
13344
|
+
var EMPTY_SELECTION = Object.freeze([]);
|
|
13345
|
+
function sameSelection(a, b) {
|
|
13346
|
+
if (a.length !== b.length) return false;
|
|
13347
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
13348
|
+
return true;
|
|
13349
|
+
}
|
|
13350
|
+
function samePoint2(a, b) {
|
|
13351
|
+
if (a === null || b === null) return a === b;
|
|
13352
|
+
return a.x === b.x && a.y === b.y;
|
|
13353
|
+
}
|
|
13354
|
+
function toPeer(from, data, prev) {
|
|
13355
|
+
const cursor = data.cursor ? { x: data.cursor.x, y: data.cursor.y } : null;
|
|
13356
|
+
const incoming = data.selection ?? EMPTY_SELECTION;
|
|
13357
|
+
const selection = prev && sameSelection(prev.selection, incoming) ? prev.selection : incoming.length === 0 ? EMPTY_SELECTION : Object.freeze([...incoming]);
|
|
13358
|
+
const tool = data.tool ?? null;
|
|
13359
|
+
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)) {
|
|
13360
|
+
return prev;
|
|
13361
|
+
}
|
|
13362
|
+
const peer = {
|
|
13363
|
+
from,
|
|
13364
|
+
id: data.id,
|
|
13365
|
+
...data.name === void 0 ? {} : { name: data.name },
|
|
13366
|
+
...data.color === void 0 ? {} : { color: data.color },
|
|
13367
|
+
...data.role === void 0 ? {} : { role: data.role },
|
|
13368
|
+
cursor,
|
|
13369
|
+
selection,
|
|
13370
|
+
tool
|
|
13371
|
+
};
|
|
13372
|
+
return peer;
|
|
13373
|
+
}
|
|
13374
|
+
var PeerRoster = class {
|
|
13375
|
+
staleMs;
|
|
13376
|
+
now;
|
|
13377
|
+
rows = /* @__PURE__ */ new Map();
|
|
13378
|
+
discovered = /* @__PURE__ */ new Map();
|
|
13379
|
+
changeListeners = /* @__PURE__ */ new Set();
|
|
13380
|
+
discoverListeners = /* @__PURE__ */ new Set();
|
|
13381
|
+
leaveListeners = /* @__PURE__ */ new Set();
|
|
13382
|
+
snapshot = EMPTY_PEERS;
|
|
13383
|
+
snapshotDirty = false;
|
|
13384
|
+
staleTimer = null;
|
|
13385
|
+
isDisposed = false;
|
|
13386
|
+
constructor(options = {}) {
|
|
13387
|
+
this.staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
13388
|
+
this.now = options.now ?? (() => Date.now());
|
|
13389
|
+
}
|
|
13390
|
+
get disposed() {
|
|
13391
|
+
return this.isDisposed;
|
|
13392
|
+
}
|
|
13393
|
+
/**
|
|
13394
|
+
* Applies a presence payload from `from`. Non-awareness or malformed payloads
|
|
13395
|
+
* return `false` untouched, so hosts can feed every presence frame through.
|
|
13396
|
+
*/
|
|
13397
|
+
apply(from, data) {
|
|
13398
|
+
if (this.isDisposed || !isAwarenessPresence(data)) return false;
|
|
13399
|
+
const isNew = !this.discovered.has(from);
|
|
13400
|
+
this.discovered.set(from, this.now());
|
|
13401
|
+
if ("cleared" in data) {
|
|
13402
|
+
this.dropRow(from, "cleared");
|
|
13403
|
+
} else {
|
|
13404
|
+
const prev = this.rows.get(from);
|
|
13405
|
+
const next = toPeer(from, data, prev);
|
|
13406
|
+
if (next !== prev) {
|
|
13407
|
+
this.rows.set(from, next);
|
|
13408
|
+
this.changed();
|
|
13409
|
+
}
|
|
13410
|
+
}
|
|
13411
|
+
this.armStaleTimer();
|
|
13412
|
+
if (isNew) this.emit(this.discoverListeners, (l) => l(from));
|
|
13413
|
+
return true;
|
|
13414
|
+
}
|
|
13415
|
+
/** Server-authored presence-leave: drops the row AND the discovery entry. */
|
|
13416
|
+
remove(from) {
|
|
13417
|
+
if (this.isDisposed) return;
|
|
13418
|
+
const hadEntry = this.discovered.delete(from);
|
|
13419
|
+
this.dropRow(from, "left");
|
|
13420
|
+
if (hadEntry) this.armStaleTimer();
|
|
13421
|
+
}
|
|
13422
|
+
getPeers() {
|
|
13423
|
+
if (this.snapshotDirty) {
|
|
13424
|
+
this.snapshot = this.rows.size === 0 ? EMPTY_PEERS : Object.freeze([...this.rows.values()]);
|
|
13425
|
+
this.snapshotDirty = false;
|
|
13426
|
+
}
|
|
13427
|
+
return this.snapshot;
|
|
13428
|
+
}
|
|
13429
|
+
getPeer(from) {
|
|
13430
|
+
return this.rows.get(from);
|
|
13431
|
+
}
|
|
13432
|
+
/** Fires only when `getPeers()` would return a new reference. */
|
|
13433
|
+
onChange(listener) {
|
|
13434
|
+
this.changeListeners.add(listener);
|
|
13435
|
+
return () => this.changeListeners.delete(listener);
|
|
13436
|
+
}
|
|
13437
|
+
/** First valid frame from a sender since its discovery entry was last dropped. */
|
|
13438
|
+
onDiscover(listener) {
|
|
13439
|
+
this.discoverListeners.add(listener);
|
|
13440
|
+
return () => this.discoverListeners.delete(listener);
|
|
13441
|
+
}
|
|
13442
|
+
onLeave(listener) {
|
|
13443
|
+
this.leaveListeners.add(listener);
|
|
13444
|
+
return () => this.leaveListeners.delete(listener);
|
|
13445
|
+
}
|
|
13446
|
+
dispose() {
|
|
13447
|
+
if (this.isDisposed) return;
|
|
13448
|
+
this.isDisposed = true;
|
|
13449
|
+
if (this.staleTimer !== null) clearTimeout(this.staleTimer);
|
|
13450
|
+
this.staleTimer = null;
|
|
13451
|
+
this.rows.clear();
|
|
13452
|
+
this.discovered.clear();
|
|
13453
|
+
this.snapshot = EMPTY_PEERS;
|
|
13454
|
+
this.snapshotDirty = false;
|
|
13455
|
+
this.changeListeners.clear();
|
|
13456
|
+
this.discoverListeners.clear();
|
|
13457
|
+
this.leaveListeners.clear();
|
|
13458
|
+
}
|
|
13459
|
+
dropRow(from, reason) {
|
|
13460
|
+
const row = this.rows.get(from);
|
|
13461
|
+
if (!row) return;
|
|
13462
|
+
this.rows.delete(from);
|
|
13463
|
+
this.changed();
|
|
13464
|
+
this.emit(this.leaveListeners, (l) => l(row, reason));
|
|
13465
|
+
}
|
|
13466
|
+
changed() {
|
|
13467
|
+
this.snapshotDirty = true;
|
|
13468
|
+
this.emit(this.changeListeners, (l) => l());
|
|
13469
|
+
}
|
|
13470
|
+
emit(listeners, call) {
|
|
13471
|
+
for (const listener of [...listeners]) {
|
|
13472
|
+
try {
|
|
13473
|
+
call(listener);
|
|
13474
|
+
} catch {
|
|
13475
|
+
}
|
|
13476
|
+
}
|
|
13477
|
+
}
|
|
13478
|
+
armStaleTimer() {
|
|
13479
|
+
if (this.staleTimer !== null) clearTimeout(this.staleTimer);
|
|
13480
|
+
this.staleTimer = null;
|
|
13481
|
+
if (!Number.isFinite(this.staleMs) || this.staleMs <= 0 || this.isDisposed || this.discovered.size === 0) {
|
|
13482
|
+
return;
|
|
13483
|
+
}
|
|
13484
|
+
let earliest = Infinity;
|
|
13485
|
+
for (const seen of this.discovered.values()) if (seen < earliest) earliest = seen;
|
|
13486
|
+
const delay = Math.min(Math.max(0, earliest + this.staleMs - this.now()), 2 ** 31 - 1);
|
|
13487
|
+
this.staleTimer = setTimeout(() => {
|
|
13488
|
+
this.staleTimer = null;
|
|
13489
|
+
this.expireStale();
|
|
13490
|
+
}, delay);
|
|
13491
|
+
}
|
|
13492
|
+
expireStale() {
|
|
13493
|
+
const t = this.now();
|
|
13494
|
+
for (const from of [...this.discovered.keys()]) {
|
|
13495
|
+
const seen = this.discovered.get(from);
|
|
13496
|
+
if (seen === void 0 || t - seen < this.staleMs) continue;
|
|
13497
|
+
this.discovered.delete(from);
|
|
13498
|
+
this.dropRow(from, "stale");
|
|
13499
|
+
}
|
|
13500
|
+
this.armStaleTimer();
|
|
13501
|
+
}
|
|
13502
|
+
};
|
|
13503
|
+
|
|
13504
|
+
// src/canvas/awareness-publisher.ts
|
|
13505
|
+
var DEFAULT_FIELDS = Object.freeze({
|
|
13506
|
+
cursor: true,
|
|
13507
|
+
selection: false,
|
|
13508
|
+
tool: true
|
|
13509
|
+
});
|
|
13510
|
+
var DEFAULT_INTERVAL_MS = 50;
|
|
13511
|
+
var DEFAULT_HEARTBEAT_MS = 15e3;
|
|
13512
|
+
var MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
|
|
13513
|
+
var MAX_IDENTITY_ID_LENGTH = 128;
|
|
13514
|
+
var MAX_IDENTITY_NAME_LENGTH = 64;
|
|
13515
|
+
var MAX_IDENTITY_COLOR_LENGTH = 64;
|
|
13516
|
+
var MAX_IDENTITY_ROLE_LENGTH = 32;
|
|
13517
|
+
var MAX_TOOL_LENGTH2 = 64;
|
|
13518
|
+
var MAX_SELECTION_ID_LENGTH = 128;
|
|
13519
|
+
function normalizeIntervalMs(value) {
|
|
13520
|
+
return Number.isFinite(value) && value >= 0 ? value : 0;
|
|
13521
|
+
}
|
|
13522
|
+
function normalizeHeartbeatMs(value) {
|
|
13523
|
+
return Number.isFinite(value) && value > 0 ? value : 0;
|
|
13524
|
+
}
|
|
13525
|
+
var LocalAwareness = class {
|
|
13526
|
+
host;
|
|
13527
|
+
element;
|
|
13528
|
+
send;
|
|
13529
|
+
selectionFilter;
|
|
13530
|
+
onError;
|
|
13531
|
+
intervalMs;
|
|
13532
|
+
heartbeatMs;
|
|
13533
|
+
identity;
|
|
13534
|
+
fields;
|
|
13535
|
+
lastPointer = null;
|
|
13536
|
+
selection = [];
|
|
13537
|
+
selectionFailed = false;
|
|
13538
|
+
tool;
|
|
13539
|
+
dirty = false;
|
|
13540
|
+
lastSentAt = null;
|
|
13541
|
+
throttleTimer = null;
|
|
13542
|
+
heartbeatTimer = null;
|
|
13543
|
+
unsubscribers = [];
|
|
13544
|
+
isDisposed = false;
|
|
13545
|
+
handlePointerMove = (e) => this.onPointerMove(e);
|
|
13546
|
+
handlePointerEnd = (e) => this.onPointerEnd(e);
|
|
13547
|
+
constructor(host, options) {
|
|
13548
|
+
const element = options.element ?? host.domLayer.parentElement;
|
|
13549
|
+
if (!element) throw new Error("LocalAwareness: the viewport wrapper is not mounted");
|
|
13550
|
+
this.host = host;
|
|
13551
|
+
this.element = element;
|
|
13552
|
+
this.send = options.send;
|
|
13553
|
+
this.selectionFilter = options.selectionFilter;
|
|
13554
|
+
this.onError = options.onError;
|
|
13555
|
+
this.intervalMs = normalizeIntervalMs(options.intervalMs ?? DEFAULT_INTERVAL_MS);
|
|
13556
|
+
this.heartbeatMs = normalizeHeartbeatMs(options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS);
|
|
13557
|
+
this.identity = this.normalizeIdentity(options.identity);
|
|
13558
|
+
this.fields = mergeFields(DEFAULT_FIELDS, options.fields ?? {});
|
|
13559
|
+
this.tool = host.toolManager.activeTool?.name ?? null;
|
|
13560
|
+
if (this.fields.selection) this.refreshSelection();
|
|
13561
|
+
const opts = { passive: true };
|
|
13562
|
+
element.addEventListener("pointermove", this.handlePointerMove, opts);
|
|
13563
|
+
element.addEventListener("pointerleave", this.handlePointerEnd, opts);
|
|
13564
|
+
element.addEventListener("pointercancel", this.handlePointerEnd, opts);
|
|
13565
|
+
this.unsubscribers.push(
|
|
13566
|
+
host.onSelectionChange(() => {
|
|
13567
|
+
if (this.fields.selection) this.schedule();
|
|
13568
|
+
}),
|
|
13569
|
+
host.toolManager.onChange((name) => {
|
|
13570
|
+
this.tool = name;
|
|
13571
|
+
if (this.fields.tool) this.schedule();
|
|
13572
|
+
})
|
|
13573
|
+
);
|
|
13574
|
+
this.armHeartbeat();
|
|
13575
|
+
}
|
|
13576
|
+
get disposed() {
|
|
13577
|
+
return this.isDisposed;
|
|
13578
|
+
}
|
|
13579
|
+
getFields() {
|
|
13580
|
+
return this.fields;
|
|
13581
|
+
}
|
|
13582
|
+
setIdentity(identity) {
|
|
13583
|
+
this.identity = this.normalizeIdentity(identity);
|
|
13584
|
+
this.schedule();
|
|
13585
|
+
}
|
|
13586
|
+
/** Merges the given flags into the current policy; `undefined` keys are ignored. */
|
|
13587
|
+
setFields(fields) {
|
|
13588
|
+
this.fields = mergeFields(this.fields, fields);
|
|
13589
|
+
this.schedule();
|
|
13590
|
+
}
|
|
13591
|
+
/**
|
|
13592
|
+
* Requests a full frame: immediate when idle, otherwise folded into the
|
|
13593
|
+
* pending trailing frame (so N simultaneous requests cost one frame). Hosts
|
|
13594
|
+
* call it when the connection becomes live or reconnects.
|
|
13595
|
+
*/
|
|
13596
|
+
announce() {
|
|
13597
|
+
this.schedule();
|
|
13598
|
+
}
|
|
13599
|
+
/**
|
|
13600
|
+
* The complete state a frame carries right now. Side-effecting when
|
|
13601
|
+
* selection publishing is on: re-reads `getSelectedIds()`, re-runs
|
|
13602
|
+
* `selectionFilter`, updates the fail-closed selection state, and may call
|
|
13603
|
+
* `onError`. A no-op with respect to selection while publishing is off.
|
|
13604
|
+
*/
|
|
13605
|
+
getState() {
|
|
13606
|
+
const frame = { kind: AWARENESS_PRESENCE_KIND, id: this.identity.id };
|
|
13607
|
+
if (this.identity.name !== void 0) frame.name = this.identity.name;
|
|
13608
|
+
if (this.identity.color !== void 0) frame.color = this.identity.color;
|
|
13609
|
+
if (this.identity.role !== void 0) frame.role = this.identity.role;
|
|
13610
|
+
if (this.fields.cursor && this.lastPointer !== null) {
|
|
13611
|
+
frame.cursor = { x: this.lastPointer.x, y: this.lastPointer.y };
|
|
13612
|
+
}
|
|
13613
|
+
if (this.fields.selection) {
|
|
13614
|
+
this.refreshSelection();
|
|
13615
|
+
if (!this.selectionFailed && this.selection.length > 0) {
|
|
13616
|
+
frame.selection = [...this.selection];
|
|
13617
|
+
}
|
|
13618
|
+
}
|
|
13619
|
+
if (this.fields.tool && this.tool !== null && this.tool.length <= MAX_TOOL_LENGTH2) {
|
|
13620
|
+
frame.tool = this.tool;
|
|
13621
|
+
}
|
|
13622
|
+
return frame;
|
|
13623
|
+
}
|
|
13624
|
+
dispose() {
|
|
13625
|
+
if (this.isDisposed) return;
|
|
13626
|
+
this.isDisposed = true;
|
|
13627
|
+
if (this.throttleTimer !== null) clearTimeout(this.throttleTimer);
|
|
13628
|
+
this.throttleTimer = null;
|
|
13629
|
+
if (this.heartbeatTimer !== null) clearTimeout(this.heartbeatTimer);
|
|
13630
|
+
this.heartbeatTimer = null;
|
|
13631
|
+
this.element.removeEventListener("pointermove", this.handlePointerMove);
|
|
13632
|
+
this.element.removeEventListener("pointerleave", this.handlePointerEnd);
|
|
13633
|
+
this.element.removeEventListener("pointercancel", this.handlePointerEnd);
|
|
13634
|
+
for (const unsub of this.unsubscribers) unsub();
|
|
13635
|
+
this.unsubscribers.length = 0;
|
|
13636
|
+
this.safeSend({ kind: AWARENESS_PRESENCE_KIND, id: this.identity.id, cleared: true });
|
|
13637
|
+
}
|
|
13638
|
+
now() {
|
|
13639
|
+
return Date.now();
|
|
13640
|
+
}
|
|
13641
|
+
onPointerMove(e) {
|
|
13642
|
+
if (!e.isPrimary) return;
|
|
13643
|
+
const rect = this.element.getBoundingClientRect();
|
|
13644
|
+
const world = this.host.camera.screenToWorld({
|
|
13645
|
+
x: e.clientX - rect.left,
|
|
13646
|
+
y: e.clientY - rect.top
|
|
13647
|
+
});
|
|
13648
|
+
this.lastPointer = Number.isFinite(world.x) && Number.isFinite(world.y) ? { x: world.x, y: world.y } : null;
|
|
13649
|
+
if (this.fields.cursor) this.schedule();
|
|
13650
|
+
}
|
|
13651
|
+
onPointerEnd(e) {
|
|
13652
|
+
if (!e.isPrimary || this.lastPointer === null) return;
|
|
13653
|
+
this.lastPointer = null;
|
|
13654
|
+
if (this.fields.cursor) this.schedule();
|
|
13655
|
+
}
|
|
13656
|
+
refreshSelection() {
|
|
13657
|
+
try {
|
|
13658
|
+
const raw = this.host.getSelectedIds();
|
|
13659
|
+
const ids = this.selectionFilter ? this.selectionFilter(raw) : raw;
|
|
13660
|
+
if (!Array.isArray(ids)) throw new TypeError("selectionFilter must return an array");
|
|
13661
|
+
for (const id of ids) {
|
|
13662
|
+
if (typeof id !== "string") throw new TypeError("selectionFilter must return strings");
|
|
13663
|
+
if (id.length === 0 || id.length > MAX_SELECTION_ID_LENGTH) {
|
|
13664
|
+
throw new TypeError("selectionFilter must return ids of 1..128 characters");
|
|
13665
|
+
}
|
|
13666
|
+
}
|
|
13667
|
+
this.selection = ids.slice(0, AWARENESS_MAX_SELECTION);
|
|
13668
|
+
this.selectionFailed = false;
|
|
13669
|
+
} catch (error) {
|
|
13670
|
+
this.selection = [];
|
|
13671
|
+
this.selectionFailed = true;
|
|
13672
|
+
this.report(error);
|
|
13673
|
+
}
|
|
13674
|
+
}
|
|
13675
|
+
schedule() {
|
|
13676
|
+
if (this.isDisposed) return;
|
|
13677
|
+
this.dirty = true;
|
|
13678
|
+
if (this.throttleTimer !== null) return;
|
|
13679
|
+
const elapsed = this.lastSentAt === null ? Infinity : this.now() - this.lastSentAt;
|
|
13680
|
+
if (elapsed >= this.intervalMs) {
|
|
13681
|
+
this.flush();
|
|
13682
|
+
return;
|
|
13683
|
+
}
|
|
13684
|
+
this.throttleTimer = setTimeout(
|
|
13685
|
+
() => {
|
|
13686
|
+
this.throttleTimer = null;
|
|
13687
|
+
if (this.dirty) this.flush();
|
|
13688
|
+
},
|
|
13689
|
+
Math.min(this.intervalMs - elapsed, MAX_TIMER_DELAY_MS)
|
|
13690
|
+
);
|
|
13691
|
+
}
|
|
13692
|
+
flush() {
|
|
13693
|
+
this.dirty = false;
|
|
13694
|
+
this.lastSentAt = this.now();
|
|
13695
|
+
this.safeSend(this.getState());
|
|
13696
|
+
this.armHeartbeat();
|
|
13697
|
+
}
|
|
13698
|
+
armHeartbeat() {
|
|
13699
|
+
if (this.heartbeatTimer !== null) clearTimeout(this.heartbeatTimer);
|
|
13700
|
+
this.heartbeatTimer = null;
|
|
13701
|
+
if (this.heartbeatMs <= 0 || this.isDisposed) return;
|
|
13702
|
+
this.heartbeatTimer = setTimeout(
|
|
13703
|
+
() => {
|
|
13704
|
+
this.heartbeatTimer = null;
|
|
13705
|
+
this.flush();
|
|
13706
|
+
},
|
|
13707
|
+
Math.min(this.heartbeatMs, MAX_TIMER_DELAY_MS)
|
|
13708
|
+
);
|
|
13709
|
+
}
|
|
13710
|
+
safeSend(frame) {
|
|
13711
|
+
try {
|
|
13712
|
+
this.send(frame);
|
|
13713
|
+
} catch (error) {
|
|
13714
|
+
this.report(error);
|
|
13715
|
+
}
|
|
13716
|
+
}
|
|
13717
|
+
report(error) {
|
|
13718
|
+
try {
|
|
13719
|
+
this.onError?.(error);
|
|
13720
|
+
} catch {
|
|
13721
|
+
}
|
|
13722
|
+
}
|
|
13723
|
+
/**
|
|
13724
|
+
* Truncates identity strings to the wire caps and rejects an invalid id, so a
|
|
13725
|
+
* sender can never publish a frame that the wire guard would drop outright.
|
|
13726
|
+
* A truncated field is reported through `onError` (a `RangeError`) rather
|
|
13727
|
+
* than silently shortened, so a caller passing an over-long name finds out.
|
|
13728
|
+
*/
|
|
13729
|
+
normalizeIdentity(identity) {
|
|
13730
|
+
if (identity.id.length === 0 || identity.id.length > MAX_IDENTITY_ID_LENGTH) {
|
|
13731
|
+
throw new RangeError("LocalAwareness: identity.id must be 1..128 characters");
|
|
13732
|
+
}
|
|
13733
|
+
const normalized = {
|
|
13734
|
+
id: identity.id
|
|
13735
|
+
};
|
|
13736
|
+
if (identity.name !== void 0) {
|
|
13737
|
+
normalized.name = identity.name.slice(0, MAX_IDENTITY_NAME_LENGTH);
|
|
13738
|
+
if (identity.name.length > MAX_IDENTITY_NAME_LENGTH) {
|
|
13739
|
+
this.report(
|
|
13740
|
+
new RangeError(
|
|
13741
|
+
`LocalAwareness: identity.name truncated to ${MAX_IDENTITY_NAME_LENGTH} characters`
|
|
13742
|
+
)
|
|
13743
|
+
);
|
|
13744
|
+
}
|
|
13745
|
+
}
|
|
13746
|
+
if (identity.color !== void 0) {
|
|
13747
|
+
normalized.color = identity.color.slice(0, MAX_IDENTITY_COLOR_LENGTH);
|
|
13748
|
+
if (identity.color.length > MAX_IDENTITY_COLOR_LENGTH) {
|
|
13749
|
+
this.report(
|
|
13750
|
+
new RangeError(
|
|
13751
|
+
`LocalAwareness: identity.color truncated to ${MAX_IDENTITY_COLOR_LENGTH} characters`
|
|
13752
|
+
)
|
|
13753
|
+
);
|
|
13754
|
+
}
|
|
13755
|
+
}
|
|
13756
|
+
if (identity.role !== void 0) {
|
|
13757
|
+
normalized.role = identity.role.slice(0, MAX_IDENTITY_ROLE_LENGTH);
|
|
13758
|
+
if (identity.role.length > MAX_IDENTITY_ROLE_LENGTH) {
|
|
13759
|
+
this.report(
|
|
13760
|
+
new RangeError(
|
|
13761
|
+
`LocalAwareness: identity.role truncated to ${MAX_IDENTITY_ROLE_LENGTH} characters`
|
|
13762
|
+
)
|
|
13763
|
+
);
|
|
13764
|
+
}
|
|
13765
|
+
}
|
|
13766
|
+
return normalized;
|
|
13767
|
+
}
|
|
13768
|
+
};
|
|
13769
|
+
function mergeFields(current, patch) {
|
|
13770
|
+
return Object.freeze({
|
|
13771
|
+
cursor: patch.cursor ?? current.cursor,
|
|
13772
|
+
selection: patch.selection ?? current.selection,
|
|
13773
|
+
tool: patch.tool ?? current.tool
|
|
13774
|
+
});
|
|
13775
|
+
}
|
|
13776
|
+
|
|
13777
|
+
// src/canvas/remote-cursor-overlay.ts
|
|
13778
|
+
var PEER_COLORS = Object.freeze([
|
|
13779
|
+
"#e11d48",
|
|
13780
|
+
"#ea580c",
|
|
13781
|
+
"#ca8a04",
|
|
13782
|
+
"#16a34a",
|
|
13783
|
+
"#0d9488",
|
|
13784
|
+
"#0284c7",
|
|
13785
|
+
"#2563eb",
|
|
13786
|
+
"#7c3aed",
|
|
13787
|
+
"#c026d3",
|
|
13788
|
+
"#db2777",
|
|
13789
|
+
"#4d7c0f",
|
|
13790
|
+
"#b45309"
|
|
13791
|
+
]);
|
|
13792
|
+
function defaultPeerColor(seed) {
|
|
13793
|
+
if (seed.length === 0) return PEER_COLORS[0] ?? "#2563eb";
|
|
13794
|
+
let hash = 2166136261;
|
|
13795
|
+
for (let i = 0; i < seed.length; i++) {
|
|
13796
|
+
hash ^= seed.charCodeAt(i);
|
|
13797
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
13798
|
+
}
|
|
13799
|
+
return PEER_COLORS[hash % PEER_COLORS.length] ?? "#2563eb";
|
|
13800
|
+
}
|
|
13801
|
+
var DEFAULT_LABEL_FONT = "12px sans-serif";
|
|
13802
|
+
var LABEL_PAD_X = 6;
|
|
13803
|
+
var LABEL_PAD_Y = 3;
|
|
13804
|
+
var LABEL_HEIGHT = 16;
|
|
13805
|
+
var LABEL_OFFSET = 14;
|
|
13806
|
+
var MAX_LABEL_WIDTH_CACHE = 64;
|
|
13807
|
+
var RemoteCursorOverlay = class {
|
|
13808
|
+
host;
|
|
13809
|
+
roster;
|
|
13810
|
+
colorFor;
|
|
13811
|
+
showLabels;
|
|
13812
|
+
labelFont;
|
|
13813
|
+
labelWidths = /* @__PURE__ */ new Map();
|
|
13814
|
+
unregister;
|
|
13815
|
+
unsubscribe;
|
|
13816
|
+
isDisposed = false;
|
|
13817
|
+
constructor(host, roster, options = {}) {
|
|
13818
|
+
this.host = host;
|
|
13819
|
+
this.roster = roster;
|
|
13820
|
+
this.colorFor = options.colorFor;
|
|
13821
|
+
this.showLabels = options.showLabels ?? true;
|
|
13822
|
+
this.labelFont = options.labelFont ?? DEFAULT_LABEL_FONT;
|
|
13823
|
+
this.unregister = host.registerOverlay((ctx) => this.render(ctx));
|
|
13824
|
+
this.unsubscribe = roster.onChange(() => {
|
|
13825
|
+
if (this.labelWidths.size > MAX_LABEL_WIDTH_CACHE) this.labelWidths.clear();
|
|
13826
|
+
host.requestRender();
|
|
13827
|
+
});
|
|
13828
|
+
}
|
|
13829
|
+
get disposed() {
|
|
13830
|
+
return this.isDisposed;
|
|
13831
|
+
}
|
|
13832
|
+
resolveColor(peer) {
|
|
13833
|
+
return this.colorFor?.(peer) ?? peer.color ?? defaultPeerColor(peer.id);
|
|
13834
|
+
}
|
|
13835
|
+
dispose() {
|
|
13836
|
+
if (this.isDisposed) return;
|
|
13837
|
+
this.isDisposed = true;
|
|
13838
|
+
this.unsubscribe?.();
|
|
13839
|
+
this.unsubscribe = null;
|
|
13840
|
+
this.unregister?.();
|
|
13841
|
+
this.unregister = null;
|
|
13842
|
+
this.labelWidths.clear();
|
|
13843
|
+
this.host.requestRender();
|
|
13844
|
+
}
|
|
13845
|
+
render(ctx) {
|
|
13846
|
+
if (this.isDisposed) return;
|
|
13847
|
+
const zoom = this.host.camera.zoom;
|
|
13848
|
+
const inv = zoom > 0 && Number.isFinite(zoom) ? 1 / zoom : 1;
|
|
13849
|
+
for (const peer of this.roster.getPeers()) {
|
|
13850
|
+
if (peer.cursor === null) continue;
|
|
13851
|
+
const color = this.resolveColor(peer);
|
|
13852
|
+
ctx.save();
|
|
13853
|
+
ctx.translate(peer.cursor.x, peer.cursor.y);
|
|
13854
|
+
ctx.scale(inv, inv);
|
|
13855
|
+
ctx.beginPath();
|
|
13856
|
+
ctx.moveTo(0, 0);
|
|
13857
|
+
ctx.lineTo(0, 16);
|
|
13858
|
+
ctx.lineTo(4.5, 12.5);
|
|
13859
|
+
ctx.lineTo(11, 12.5);
|
|
13860
|
+
ctx.closePath();
|
|
13861
|
+
ctx.fillStyle = color;
|
|
13862
|
+
ctx.fill();
|
|
13863
|
+
ctx.strokeStyle = "#ffffff";
|
|
13864
|
+
ctx.lineWidth = 1;
|
|
13865
|
+
ctx.stroke();
|
|
13866
|
+
if (this.showLabels && peer.name !== void 0 && peer.name.length > 0) {
|
|
13867
|
+
this.drawLabel(ctx, peer.name, color);
|
|
13868
|
+
}
|
|
13869
|
+
ctx.restore();
|
|
13870
|
+
}
|
|
13871
|
+
}
|
|
13872
|
+
drawLabel(ctx, name, color) {
|
|
13873
|
+
ctx.font = this.labelFont;
|
|
13874
|
+
const key = `${this.labelFont} ${name}`;
|
|
13875
|
+
let width = this.labelWidths.get(key);
|
|
13876
|
+
if (width === void 0) {
|
|
13877
|
+
width = ctx.measureText(name).width;
|
|
13878
|
+
this.labelWidths.set(key, width);
|
|
13879
|
+
}
|
|
13880
|
+
const w = width + LABEL_PAD_X * 2;
|
|
13881
|
+
ctx.fillStyle = color;
|
|
13882
|
+
ctx.beginPath();
|
|
13883
|
+
ctx.roundRect(LABEL_OFFSET, LABEL_OFFSET, w, LABEL_HEIGHT + LABEL_PAD_Y, 4);
|
|
13884
|
+
ctx.fill();
|
|
13885
|
+
ctx.fillStyle = "#ffffff";
|
|
13886
|
+
ctx.textAlign = "left";
|
|
13887
|
+
ctx.textBaseline = "middle";
|
|
13888
|
+
ctx.fillText(name, LABEL_OFFSET + LABEL_PAD_X, LABEL_OFFSET + (LABEL_HEIGHT + LABEL_PAD_Y) / 2);
|
|
13889
|
+
}
|
|
13890
|
+
};
|
|
13891
|
+
|
|
13892
|
+
// src/canvas/remote-selection-overlay.ts
|
|
13893
|
+
var DEFAULT_ALPHA = 0.6;
|
|
13894
|
+
var DEFAULT_LINE_WIDTH_PX = 2;
|
|
13895
|
+
var RemoteSelectionOverlay = class {
|
|
13896
|
+
host;
|
|
13897
|
+
roster;
|
|
13898
|
+
colorFor;
|
|
13899
|
+
alpha;
|
|
13900
|
+
lineWidthPx;
|
|
13901
|
+
signatures = [];
|
|
13902
|
+
outlines = [];
|
|
13903
|
+
storeDirty = true;
|
|
13904
|
+
unregister;
|
|
13905
|
+
unsubscribers = [];
|
|
13906
|
+
isDisposed = false;
|
|
13907
|
+
constructor(host, roster, options = {}) {
|
|
13908
|
+
this.host = host;
|
|
13909
|
+
this.roster = roster;
|
|
13910
|
+
this.colorFor = options.colorFor;
|
|
13911
|
+
this.alpha = options.alpha ?? DEFAULT_ALPHA;
|
|
13912
|
+
this.lineWidthPx = options.lineWidthPx ?? DEFAULT_LINE_WIDTH_PX;
|
|
13913
|
+
this.unregister = host.registerOverlay((ctx) => this.render(ctx));
|
|
13914
|
+
const invalidate = () => {
|
|
13915
|
+
this.storeDirty = true;
|
|
13916
|
+
host.requestRender();
|
|
13917
|
+
};
|
|
13918
|
+
this.unsubscribers.push(
|
|
13919
|
+
roster.onChange(() => host.requestRender()),
|
|
13920
|
+
host.store.onChange(invalidate),
|
|
13921
|
+
host.layerManager.on("change", invalidate)
|
|
13922
|
+
);
|
|
13923
|
+
}
|
|
13924
|
+
get disposed() {
|
|
13925
|
+
return this.isDisposed;
|
|
13926
|
+
}
|
|
13927
|
+
dispose() {
|
|
13928
|
+
if (this.isDisposed) return;
|
|
13929
|
+
this.isDisposed = true;
|
|
13930
|
+
for (const unsub of this.unsubscribers) unsub();
|
|
13931
|
+
this.unsubscribers.length = 0;
|
|
13932
|
+
this.unregister?.();
|
|
13933
|
+
this.unregister = null;
|
|
13934
|
+
this.signatures = [];
|
|
13935
|
+
this.outlines = [];
|
|
13936
|
+
this.host.requestRender();
|
|
13937
|
+
}
|
|
13938
|
+
resolveColor(peer) {
|
|
13939
|
+
return this.colorFor?.(peer) ?? peer.color ?? defaultPeerColor(peer.id);
|
|
13940
|
+
}
|
|
13941
|
+
/** Recomputes outlines only when the selection signature or the store/layers changed. */
|
|
13942
|
+
rebuild() {
|
|
13943
|
+
const peers = this.roster.getPeers();
|
|
13944
|
+
const next = [];
|
|
13945
|
+
for (const peer of peers) {
|
|
13946
|
+
if (peer.selection.length === 0) continue;
|
|
13947
|
+
next.push({ from: peer.from, selection: peer.selection, color: this.resolveColor(peer) });
|
|
13948
|
+
}
|
|
13949
|
+
let changed = this.storeDirty || next.length !== this.signatures.length;
|
|
13950
|
+
if (!changed) {
|
|
13951
|
+
for (let i = 0; i < next.length; i++) {
|
|
13952
|
+
const a = next[i];
|
|
13953
|
+
const b = this.signatures[i];
|
|
13954
|
+
if (!a || !b || a.from !== b.from || a.selection !== b.selection || a.color !== b.color) {
|
|
13955
|
+
changed = true;
|
|
13956
|
+
break;
|
|
13957
|
+
}
|
|
13958
|
+
}
|
|
13959
|
+
}
|
|
13960
|
+
if (!changed) return;
|
|
13961
|
+
this.storeDirty = false;
|
|
13962
|
+
this.signatures = next;
|
|
13963
|
+
if (next.length === 0) {
|
|
13964
|
+
this.outlines = [];
|
|
13965
|
+
return;
|
|
13966
|
+
}
|
|
13967
|
+
const colorById = /* @__PURE__ */ new Map();
|
|
13968
|
+
for (const sig of next) {
|
|
13969
|
+
for (const id of sig.selection) if (!colorById.has(id)) colorById.set(id, sig.color);
|
|
13970
|
+
}
|
|
13971
|
+
const layers = this.host.layerManager;
|
|
13972
|
+
const rects = computeElementRects(
|
|
13973
|
+
this.host.store,
|
|
13974
|
+
(element) => colorById.has(element.id) && layers.isLayerVisible(element.layerId) ? element.id : null
|
|
13975
|
+
);
|
|
13976
|
+
this.outlines = rects.map((rect) => ({ rect, color: colorById.get(rect.id) ?? "#2563eb" }));
|
|
13977
|
+
}
|
|
13978
|
+
render(ctx) {
|
|
13979
|
+
if (this.isDisposed) return;
|
|
13980
|
+
this.rebuild();
|
|
13981
|
+
if (this.outlines.length === 0) return;
|
|
13982
|
+
const zoom = this.host.camera.zoom;
|
|
13983
|
+
const inv = zoom > 0 && Number.isFinite(zoom) ? 1 / zoom : 1;
|
|
13984
|
+
ctx.save();
|
|
13985
|
+
ctx.globalAlpha = this.alpha;
|
|
13986
|
+
ctx.lineWidth = this.lineWidthPx * inv;
|
|
13987
|
+
for (const { rect, color } of this.outlines) {
|
|
13988
|
+
ctx.save();
|
|
13989
|
+
ctx.strokeStyle = color;
|
|
13990
|
+
ctx.translate(rect.x + rect.w / 2, rect.y + rect.h / 2);
|
|
13991
|
+
if (rect.rotation !== 0) ctx.rotate(rect.rotation);
|
|
13992
|
+
ctx.strokeRect(-rect.w / 2, -rect.h / 2, rect.w, rect.h);
|
|
13993
|
+
ctx.restore();
|
|
13994
|
+
}
|
|
13995
|
+
ctx.restore();
|
|
13996
|
+
}
|
|
13997
|
+
};
|
|
13998
|
+
|
|
13999
|
+
// src/canvas/attach-awareness.ts
|
|
14000
|
+
function attachAwareness(viewport, channel, options) {
|
|
14001
|
+
const {
|
|
14002
|
+
roster: rosterOptions,
|
|
14003
|
+
cursors: cursorOptions,
|
|
14004
|
+
selections: selectionOptions,
|
|
14005
|
+
publish,
|
|
14006
|
+
...localOptions
|
|
14007
|
+
} = options;
|
|
14008
|
+
const roster = new PeerRoster(rosterOptions);
|
|
14009
|
+
let local = null;
|
|
14010
|
+
let cursors = null;
|
|
14011
|
+
let selections = null;
|
|
14012
|
+
const unsubscribers = [];
|
|
14013
|
+
try {
|
|
14014
|
+
local = publish === false ? null : new LocalAwareness(viewport, {
|
|
14015
|
+
...localOptions,
|
|
14016
|
+
send: (data) => channel.sendPresence(data)
|
|
14017
|
+
});
|
|
14018
|
+
cursors = cursorOptions === false ? null : new RemoteCursorOverlay(viewport, roster, cursorOptions ?? {});
|
|
14019
|
+
selections = selectionOptions === void 0 || selectionOptions === false ? null : new RemoteSelectionOverlay(
|
|
14020
|
+
viewport,
|
|
14021
|
+
roster,
|
|
14022
|
+
selectionOptions === true ? {} : selectionOptions
|
|
14023
|
+
);
|
|
14024
|
+
if (publish !== false) unsubscribers.push(roster.onDiscover(() => local?.announce()));
|
|
14025
|
+
unsubscribers.push(
|
|
14026
|
+
channel.onPresence((from, data) => {
|
|
14027
|
+
roster.apply(from, data);
|
|
14028
|
+
})
|
|
14029
|
+
);
|
|
14030
|
+
unsubscribers.push(channel.onPresenceLeave((from) => roster.remove(from)));
|
|
14031
|
+
} catch (error) {
|
|
14032
|
+
for (let i = unsubscribers.length - 1; i >= 0; i--) {
|
|
14033
|
+
try {
|
|
14034
|
+
unsubscribers[i]?.();
|
|
14035
|
+
} catch {
|
|
14036
|
+
}
|
|
14037
|
+
}
|
|
14038
|
+
unsubscribers.length = 0;
|
|
14039
|
+
try {
|
|
14040
|
+
selections?.dispose();
|
|
14041
|
+
} catch {
|
|
14042
|
+
}
|
|
14043
|
+
try {
|
|
14044
|
+
cursors?.dispose();
|
|
14045
|
+
} catch {
|
|
14046
|
+
}
|
|
14047
|
+
try {
|
|
14048
|
+
local?.dispose();
|
|
14049
|
+
} catch {
|
|
14050
|
+
}
|
|
14051
|
+
try {
|
|
14052
|
+
roster.dispose();
|
|
14053
|
+
} catch {
|
|
14054
|
+
}
|
|
14055
|
+
throw error;
|
|
14056
|
+
}
|
|
14057
|
+
let disposed = false;
|
|
14058
|
+
return {
|
|
14059
|
+
roster,
|
|
14060
|
+
local,
|
|
14061
|
+
cursors,
|
|
14062
|
+
selections,
|
|
14063
|
+
announce: () => local?.announce(),
|
|
14064
|
+
setFields: (fields) => local?.setFields(fields),
|
|
14065
|
+
dispose: () => {
|
|
14066
|
+
if (disposed) return;
|
|
14067
|
+
disposed = true;
|
|
14068
|
+
try {
|
|
14069
|
+
local?.dispose();
|
|
14070
|
+
} catch {
|
|
14071
|
+
}
|
|
14072
|
+
try {
|
|
14073
|
+
cursors?.dispose();
|
|
14074
|
+
} catch {
|
|
14075
|
+
}
|
|
14076
|
+
try {
|
|
14077
|
+
selections?.dispose();
|
|
14078
|
+
} catch {
|
|
14079
|
+
}
|
|
14080
|
+
try {
|
|
14081
|
+
roster.dispose();
|
|
14082
|
+
} catch {
|
|
14083
|
+
}
|
|
14084
|
+
for (const unsub of unsubscribers) {
|
|
14085
|
+
try {
|
|
14086
|
+
unsub();
|
|
14087
|
+
} catch {
|
|
14088
|
+
}
|
|
14089
|
+
}
|
|
14090
|
+
unsubscribers.length = 0;
|
|
14091
|
+
}
|
|
14092
|
+
};
|
|
14093
|
+
}
|
|
14094
|
+
|
|
14095
|
+
// src/tools/hand-tool.ts
|
|
14096
|
+
var HandTool = class {
|
|
14097
|
+
name = "hand";
|
|
14098
|
+
panning = false;
|
|
14099
|
+
lastScreen = { x: 0, y: 0 };
|
|
14100
|
+
onActivate(ctx) {
|
|
14101
|
+
ctx.setCursor?.("grab");
|
|
14102
|
+
}
|
|
14103
|
+
onDeactivate(ctx) {
|
|
14104
|
+
ctx.setCursor?.("default");
|
|
14105
|
+
}
|
|
14106
|
+
onPointerDown(state, ctx) {
|
|
14107
|
+
this.panning = true;
|
|
14108
|
+
this.lastScreen = { x: state.x, y: state.y };
|
|
14109
|
+
ctx.setCursor?.("grabbing");
|
|
14110
|
+
}
|
|
14111
|
+
onPointerMove(state, ctx) {
|
|
14112
|
+
if (!this.panning) return;
|
|
14113
|
+
const dx = state.x - this.lastScreen.x;
|
|
14114
|
+
const dy = state.y - this.lastScreen.y;
|
|
12251
14115
|
this.lastScreen = { x: state.x, y: state.y };
|
|
12252
14116
|
ctx.camera.pan(dx, dy);
|
|
12253
14117
|
}
|
|
@@ -13893,7 +15757,7 @@ var MeasureTool = class {
|
|
|
13893
15757
|
// src/tools/path-tool.ts
|
|
13894
15758
|
var EPS2 = 1e-6;
|
|
13895
15759
|
var DEFAULT_COMMIT_TAP_RADIUS_PX = 12;
|
|
13896
|
-
function
|
|
15760
|
+
function samePoint3(a, b) {
|
|
13897
15761
|
return Math.abs(a.x - b.x) < EPS2 && Math.abs(a.y - b.y) < EPS2;
|
|
13898
15762
|
}
|
|
13899
15763
|
var PathTool = class {
|
|
@@ -14037,7 +15901,7 @@ var PathTool = class {
|
|
|
14037
15901
|
return;
|
|
14038
15902
|
}
|
|
14039
15903
|
const last = this.lastWaypoint();
|
|
14040
|
-
if (this.cursor && last && !
|
|
15904
|
+
if (this.cursor && last && !samePoint3(this.cursor, last)) {
|
|
14041
15905
|
this.waypoints.push({ ...this.cursor });
|
|
14042
15906
|
}
|
|
14043
15907
|
this.scheduleEmission();
|
|
@@ -14098,7 +15962,7 @@ var PathTool = class {
|
|
|
14098
15962
|
* world point.
|
|
14099
15963
|
*/
|
|
14100
15964
|
withinCommitRadius(point, last, ctx) {
|
|
14101
|
-
if (
|
|
15965
|
+
if (samePoint3(point, last)) return true;
|
|
14102
15966
|
if (this.hasSnappingGrid()) return false;
|
|
14103
15967
|
const zoom = ctx.camera.zoom;
|
|
14104
15968
|
if (!(zoom > 0)) return false;
|
|
@@ -14146,7 +16010,7 @@ var PathTool = class {
|
|
|
14146
16010
|
measure(cursor) {
|
|
14147
16011
|
const points = this.waypoints.map((p) => ({ ...p }));
|
|
14148
16012
|
const last = points[points.length - 1];
|
|
14149
|
-
if (cursor && (!last || !
|
|
16013
|
+
if (cursor && (!last || !samePoint3(cursor, last))) points.push({ ...cursor });
|
|
14150
16014
|
const { total, cumulative } = pathDistanceCells(points, {
|
|
14151
16015
|
gridSize: this.gridSize,
|
|
14152
16016
|
gridType: this.gridType,
|
|
@@ -14793,10 +16657,192 @@ var PingTool = class {
|
|
|
14793
16657
|
}
|
|
14794
16658
|
};
|
|
14795
16659
|
|
|
16660
|
+
// src/tools/fog-tool.ts
|
|
16661
|
+
var DEFAULT_RADIUS5 = 40;
|
|
16662
|
+
var MIN_POINT_DISTANCE = 4;
|
|
16663
|
+
var FogTool = class {
|
|
16664
|
+
name = "fog";
|
|
16665
|
+
drawing = false;
|
|
16666
|
+
points = [];
|
|
16667
|
+
startPoint = null;
|
|
16668
|
+
operation;
|
|
16669
|
+
shape;
|
|
16670
|
+
radius;
|
|
16671
|
+
manager;
|
|
16672
|
+
optionListeners = /* @__PURE__ */ new Set();
|
|
16673
|
+
constructor(manager, options = {}) {
|
|
16674
|
+
this.manager = manager;
|
|
16675
|
+
this.operation = options.operation ?? "reveal";
|
|
16676
|
+
this.shape = options.shape ?? "brush";
|
|
16677
|
+
this.radius = options.radius ?? DEFAULT_RADIUS5;
|
|
16678
|
+
}
|
|
16679
|
+
onActivate(ctx) {
|
|
16680
|
+
ctx.setCursor?.("crosshair");
|
|
16681
|
+
}
|
|
16682
|
+
onDeactivate(ctx) {
|
|
16683
|
+
this.cancelGesture(ctx);
|
|
16684
|
+
ctx.setCursor?.("default");
|
|
16685
|
+
}
|
|
16686
|
+
getOptions() {
|
|
16687
|
+
return {
|
|
16688
|
+
operation: this.operation,
|
|
16689
|
+
shape: this.shape,
|
|
16690
|
+
radius: this.radius
|
|
16691
|
+
};
|
|
16692
|
+
}
|
|
16693
|
+
setOptions(options) {
|
|
16694
|
+
if (options.operation !== void 0) this.operation = options.operation;
|
|
16695
|
+
if (options.shape !== void 0) this.shape = options.shape;
|
|
16696
|
+
if (options.radius !== void 0 && Number.isFinite(options.radius) && options.radius > 0) {
|
|
16697
|
+
this.radius = options.radius;
|
|
16698
|
+
}
|
|
16699
|
+
for (const listener of this.optionListeners) listener();
|
|
16700
|
+
}
|
|
16701
|
+
onOptionsChange(listener) {
|
|
16702
|
+
this.optionListeners.add(listener);
|
|
16703
|
+
return () => this.optionListeners.delete(listener);
|
|
16704
|
+
}
|
|
16705
|
+
onPointerDown(state, ctx) {
|
|
16706
|
+
if (this.drawing) return;
|
|
16707
|
+
this.drawing = true;
|
|
16708
|
+
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
16709
|
+
this.startPoint = world;
|
|
16710
|
+
this.points = [world];
|
|
16711
|
+
ctx.requestRender();
|
|
16712
|
+
}
|
|
16713
|
+
onPointerMove(state, ctx) {
|
|
16714
|
+
if (!this.drawing) return;
|
|
16715
|
+
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
16716
|
+
if (this.shape === "rectangle") {
|
|
16717
|
+
if (this.startPoint) this.points = [this.startPoint, world];
|
|
16718
|
+
} else {
|
|
16719
|
+
const last = this.points[this.points.length - 1];
|
|
16720
|
+
if (last) {
|
|
16721
|
+
const dx = world.x - last.x;
|
|
16722
|
+
const dy = world.y - last.y;
|
|
16723
|
+
if (dx * dx + dy * dy < MIN_POINT_DISTANCE * MIN_POINT_DISTANCE) return;
|
|
16724
|
+
}
|
|
16725
|
+
this.points.push(world);
|
|
16726
|
+
}
|
|
16727
|
+
ctx.requestRender();
|
|
16728
|
+
}
|
|
16729
|
+
onPointerUp(_state, ctx) {
|
|
16730
|
+
if (!this.drawing) return;
|
|
16731
|
+
this.drawing = false;
|
|
16732
|
+
const region = this.buildRegion();
|
|
16733
|
+
if (region) {
|
|
16734
|
+
this.manager.applyRegion(region, this.operation);
|
|
16735
|
+
}
|
|
16736
|
+
this.points = [];
|
|
16737
|
+
this.startPoint = null;
|
|
16738
|
+
ctx.requestRender();
|
|
16739
|
+
}
|
|
16740
|
+
onPointerCancel(_state, ctx) {
|
|
16741
|
+
this.cancelGesture(ctx);
|
|
16742
|
+
}
|
|
16743
|
+
onKeyDown(event, ctx) {
|
|
16744
|
+
if (event.key === "Escape" && this.drawing) {
|
|
16745
|
+
this.cancelGesture(ctx);
|
|
16746
|
+
return true;
|
|
16747
|
+
}
|
|
16748
|
+
return false;
|
|
16749
|
+
}
|
|
16750
|
+
renderOverlay(ctx) {
|
|
16751
|
+
if (!this.drawing || this.points.length === 0) return;
|
|
16752
|
+
ctx.save();
|
|
16753
|
+
ctx.strokeStyle = this.operation === "reveal" ? "rgba(255,255,255,0.6)" : "rgba(0,0,0,0.4)";
|
|
16754
|
+
ctx.fillStyle = this.operation === "reveal" ? "rgba(255,255,255,0.15)" : "rgba(0,0,0,0.1)";
|
|
16755
|
+
ctx.lineWidth = 2;
|
|
16756
|
+
ctx.setLineDash([6, 4]);
|
|
16757
|
+
switch (this.shape) {
|
|
16758
|
+
case "brush":
|
|
16759
|
+
this.renderBrushPreview(ctx);
|
|
16760
|
+
break;
|
|
16761
|
+
case "rectangle":
|
|
16762
|
+
this.renderRectanglePreview(ctx);
|
|
16763
|
+
break;
|
|
16764
|
+
case "polygon":
|
|
16765
|
+
this.renderPolygonPreview(ctx);
|
|
16766
|
+
break;
|
|
16767
|
+
}
|
|
16768
|
+
ctx.restore();
|
|
16769
|
+
}
|
|
16770
|
+
buildRegion() {
|
|
16771
|
+
switch (this.shape) {
|
|
16772
|
+
case "brush": {
|
|
16773
|
+
if (this.points.length === 0) return null;
|
|
16774
|
+
return { kind: "brush", points: this.points, radius: this.radius };
|
|
16775
|
+
}
|
|
16776
|
+
case "rectangle": {
|
|
16777
|
+
if (!this.startPoint || this.points.length < 2) return null;
|
|
16778
|
+
const end = this.points[this.points.length - 1];
|
|
16779
|
+
if (this.startPoint.x === end.x && this.startPoint.y === end.y) return null;
|
|
16780
|
+
return { kind: "rectangle", from: this.startPoint, to: end };
|
|
16781
|
+
}
|
|
16782
|
+
case "polygon": {
|
|
16783
|
+
if (this.points.length < 3) return null;
|
|
16784
|
+
return { kind: "polygon", points: this.points };
|
|
16785
|
+
}
|
|
16786
|
+
}
|
|
16787
|
+
}
|
|
16788
|
+
cancelGesture(ctx) {
|
|
16789
|
+
this.drawing = false;
|
|
16790
|
+
this.points = [];
|
|
16791
|
+
this.startPoint = null;
|
|
16792
|
+
ctx.requestRender();
|
|
16793
|
+
}
|
|
16794
|
+
renderBrushPreview(ctx) {
|
|
16795
|
+
if (this.points.length === 1) {
|
|
16796
|
+
const p = this.points[0];
|
|
16797
|
+
ctx.beginPath();
|
|
16798
|
+
ctx.arc(p.x, p.y, this.radius, 0, Math.PI * 2);
|
|
16799
|
+
ctx.fill();
|
|
16800
|
+
ctx.stroke();
|
|
16801
|
+
return;
|
|
16802
|
+
}
|
|
16803
|
+
ctx.beginPath();
|
|
16804
|
+
for (let i = 0; i < this.points.length; i++) {
|
|
16805
|
+
const p = this.points[i];
|
|
16806
|
+
if (i === 0) ctx.moveTo(p.x, p.y);
|
|
16807
|
+
else ctx.lineTo(p.x, p.y);
|
|
16808
|
+
}
|
|
16809
|
+
ctx.lineWidth = this.radius * 2;
|
|
16810
|
+
ctx.lineCap = "round";
|
|
16811
|
+
ctx.lineJoin = "round";
|
|
16812
|
+
ctx.stroke();
|
|
16813
|
+
}
|
|
16814
|
+
renderRectanglePreview(ctx) {
|
|
16815
|
+
if (this.points.length < 2) return;
|
|
16816
|
+
const from = this.points[0];
|
|
16817
|
+
const to = this.points[this.points.length - 1];
|
|
16818
|
+
const x = Math.min(from.x, to.x);
|
|
16819
|
+
const y = Math.min(from.y, to.y);
|
|
16820
|
+
const w = Math.abs(to.x - from.x);
|
|
16821
|
+
const h = Math.abs(to.y - from.y);
|
|
16822
|
+
ctx.fillRect(x, y, w, h);
|
|
16823
|
+
ctx.strokeRect(x, y, w, h);
|
|
16824
|
+
}
|
|
16825
|
+
renderPolygonPreview(ctx) {
|
|
16826
|
+
if (this.points.length < 2) return;
|
|
16827
|
+
const first = this.points[0];
|
|
16828
|
+
ctx.beginPath();
|
|
16829
|
+
ctx.moveTo(first.x, first.y);
|
|
16830
|
+
for (let i = 1; i < this.points.length; i++) {
|
|
16831
|
+
const p = this.points[i];
|
|
16832
|
+
ctx.lineTo(p.x, p.y);
|
|
16833
|
+
}
|
|
16834
|
+
ctx.closePath();
|
|
16835
|
+
ctx.fill();
|
|
16836
|
+
ctx.stroke();
|
|
16837
|
+
}
|
|
16838
|
+
};
|
|
16839
|
+
|
|
14796
16840
|
// src/index.ts
|
|
14797
|
-
var VERSION = "0.
|
|
16841
|
+
var VERSION = "0.66.0";
|
|
14798
16842
|
// Annotate the CommonJS export names for ESM import in node:
|
|
14799
16843
|
0 && (module.exports = {
|
|
16844
|
+
AWARENESS_MAX_SELECTION,
|
|
16845
|
+
AWARENESS_PRESENCE_KIND,
|
|
14800
16846
|
ArrowTool,
|
|
14801
16847
|
AutoSave,
|
|
14802
16848
|
Camera,
|
|
@@ -14806,6 +16852,12 @@ var VERSION = "0.64.0";
|
|
|
14806
16852
|
ElementStore,
|
|
14807
16853
|
EraserTool,
|
|
14808
16854
|
FOCUS_PRESENCE_KIND,
|
|
16855
|
+
FOG_MAX_TILES,
|
|
16856
|
+
FOG_STATE_VERSION,
|
|
16857
|
+
FOG_TILE_CELLS,
|
|
16858
|
+
FogManager,
|
|
16859
|
+
FogRenderer,
|
|
16860
|
+
FogTool,
|
|
14809
16861
|
HandTool,
|
|
14810
16862
|
HistoryStack,
|
|
14811
16863
|
HtmlPainterMissingError,
|
|
@@ -14815,6 +16867,7 @@ var VERSION = "0.64.0";
|
|
|
14815
16867
|
LASER_TRAIL_PRESENCE_KIND,
|
|
14816
16868
|
LaserTool,
|
|
14817
16869
|
LayerManager,
|
|
16870
|
+
LocalAwareness,
|
|
14818
16871
|
LocalStorageAdapter,
|
|
14819
16872
|
MEASURE_PRESENCE_KIND,
|
|
14820
16873
|
MeasureTool,
|
|
@@ -14823,16 +16876,20 @@ var VERSION = "0.64.0";
|
|
|
14823
16876
|
NoteTool,
|
|
14824
16877
|
PATH_PRESENCE_KIND,
|
|
14825
16878
|
PATH_PRESENCE_MAX_POINTS,
|
|
16879
|
+
PEER_COLORS,
|
|
14826
16880
|
PING_PRESENCE_KIND,
|
|
14827
16881
|
PathTool,
|
|
16882
|
+
PeerRoster,
|
|
14828
16883
|
PencilTool,
|
|
14829
16884
|
PingInput,
|
|
14830
16885
|
PingTool,
|
|
16886
|
+
RemoteCursorOverlay,
|
|
14831
16887
|
RemoteFocusReceiver,
|
|
14832
16888
|
RemoteLaserOverlay,
|
|
14833
16889
|
RemoteMeasureOverlay,
|
|
14834
16890
|
RemotePathOverlay,
|
|
14835
16891
|
RemotePingOverlay,
|
|
16892
|
+
RemoteSelectionOverlay,
|
|
14836
16893
|
SelectTool,
|
|
14837
16894
|
ShapeTool,
|
|
14838
16895
|
TemplateTool,
|
|
@@ -14841,8 +16898,10 @@ var VERSION = "0.64.0";
|
|
|
14841
16898
|
VERSION,
|
|
14842
16899
|
Viewport,
|
|
14843
16900
|
applyCameraView,
|
|
16901
|
+
attachAwareness,
|
|
14844
16902
|
boundsIntersect,
|
|
14845
16903
|
cameraOriginForView,
|
|
16904
|
+
canonicalizeFogTile,
|
|
14846
16905
|
captureCameraView,
|
|
14847
16906
|
computeElementRects,
|
|
14848
16907
|
createArrow,
|
|
@@ -14854,11 +16913,14 @@ var VERSION = "0.64.0";
|
|
|
14854
16913
|
createStroke,
|
|
14855
16914
|
createTemplate,
|
|
14856
16915
|
createText,
|
|
16916
|
+
defaultPeerColor,
|
|
14857
16917
|
drawHexPath,
|
|
14858
16918
|
elementRectsEqual,
|
|
14859
16919
|
exportImage,
|
|
14860
16920
|
exportSvg,
|
|
14861
16921
|
fitZoomForView,
|
|
16922
|
+
fogDecodeBase64,
|
|
16923
|
+
fogEncodeBase64,
|
|
14862
16924
|
footprintFromSize,
|
|
14863
16925
|
getActiveFormats,
|
|
14864
16926
|
getArrowBounds,
|
|
@@ -14876,6 +16938,7 @@ var VERSION = "0.64.0";
|
|
|
14876
16938
|
getHexCellsInSquare,
|
|
14877
16939
|
getHexDistance,
|
|
14878
16940
|
gridDistanceCells,
|
|
16941
|
+
isAwarenessPresence,
|
|
14879
16942
|
isFocusPresence,
|
|
14880
16943
|
isLaserTrailPresence,
|
|
14881
16944
|
isMeasurePresence,
|
|
@@ -14883,6 +16946,7 @@ var VERSION = "0.64.0";
|
|
|
14883
16946
|
isPathPresence,
|
|
14884
16947
|
isPingPresence,
|
|
14885
16948
|
pathDistanceCells,
|
|
16949
|
+
recommendedFogCellSize,
|
|
14886
16950
|
resolveHtmlRouting,
|
|
14887
16951
|
setFontSize,
|
|
14888
16952
|
smartSnap,
|
|
@@ -14899,6 +16963,9 @@ var VERSION = "0.64.0";
|
|
|
14899
16963
|
toggleBold,
|
|
14900
16964
|
toggleItalic,
|
|
14901
16965
|
toggleStrikethrough,
|
|
14902
|
-
toggleUnderline
|
|
16966
|
+
toggleUnderline,
|
|
16967
|
+
validateFogDefinition,
|
|
16968
|
+
validateFogState,
|
|
16969
|
+
validateFogTile
|
|
14903
16970
|
});
|
|
14904
16971
|
//# sourceMappingURL=index.cjs.map
|