@fieldnotes/core 0.65.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 +1268 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +195 -19
- package/dist/index.d.ts +195 -19
- package/dist/index.js +1254 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -31,6 +31,12 @@ __export(index_exports, {
|
|
|
31
31
|
ElementStore: () => ElementStore,
|
|
32
32
|
EraserTool: () => EraserTool,
|
|
33
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,
|
|
34
40
|
HandTool: () => HandTool,
|
|
35
41
|
HistoryStack: () => HistoryStack,
|
|
36
42
|
HtmlPainterMissingError: () => HtmlPainterMissingError,
|
|
@@ -74,6 +80,7 @@ __export(index_exports, {
|
|
|
74
80
|
attachAwareness: () => attachAwareness,
|
|
75
81
|
boundsIntersect: () => boundsIntersect,
|
|
76
82
|
cameraOriginForView: () => cameraOriginForView,
|
|
83
|
+
canonicalizeFogTile: () => canonicalizeFogTile,
|
|
77
84
|
captureCameraView: () => captureCameraView,
|
|
78
85
|
computeElementRects: () => computeElementRects,
|
|
79
86
|
createArrow: () => createArrow,
|
|
@@ -91,6 +98,8 @@ __export(index_exports, {
|
|
|
91
98
|
exportImage: () => exportImage,
|
|
92
99
|
exportSvg: () => exportSvg,
|
|
93
100
|
fitZoomForView: () => fitZoomForView,
|
|
101
|
+
fogDecodeBase64: () => decodeBase64,
|
|
102
|
+
fogEncodeBase64: () => encodeBase64,
|
|
94
103
|
footprintFromSize: () => footprintFromSize,
|
|
95
104
|
getActiveFormats: () => getActiveFormats,
|
|
96
105
|
getArrowBounds: () => getArrowBounds,
|
|
@@ -116,6 +125,7 @@ __export(index_exports, {
|
|
|
116
125
|
isPathPresence: () => isPathPresence,
|
|
117
126
|
isPingPresence: () => isPingPresence,
|
|
118
127
|
pathDistanceCells: () => pathDistanceCells,
|
|
128
|
+
recommendedFogCellSize: () => recommendedFogCellSize,
|
|
119
129
|
resolveHtmlRouting: () => resolveHtmlRouting,
|
|
120
130
|
setFontSize: () => setFontSize,
|
|
121
131
|
smartSnap: () => smartSnap,
|
|
@@ -132,7 +142,10 @@ __export(index_exports, {
|
|
|
132
142
|
toggleBold: () => toggleBold,
|
|
133
143
|
toggleItalic: () => toggleItalic,
|
|
134
144
|
toggleStrikethrough: () => toggleStrikethrough,
|
|
135
|
-
toggleUnderline: () => toggleUnderline
|
|
145
|
+
toggleUnderline: () => toggleUnderline,
|
|
146
|
+
validateFogDefinition: () => validateFogDefinition,
|
|
147
|
+
validateFogState: () => validateFogState,
|
|
148
|
+
validateFogTile: () => validateFogTile
|
|
136
149
|
});
|
|
137
150
|
module.exports = __toCommonJS(index_exports);
|
|
138
151
|
|
|
@@ -592,8 +605,464 @@ function sanitizeAttributes(el, tag) {
|
|
|
592
605
|
}
|
|
593
606
|
}
|
|
594
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
|
+
|
|
595
1064
|
// src/core/state-serializer.ts
|
|
596
|
-
var CURRENT_VERSION =
|
|
1065
|
+
var CURRENT_VERSION = 3;
|
|
597
1066
|
var ELEMENT_TYPES = [
|
|
598
1067
|
"stroke",
|
|
599
1068
|
"note",
|
|
@@ -605,7 +1074,7 @@ var ELEMENT_TYPES = [
|
|
|
605
1074
|
"grid",
|
|
606
1075
|
"template"
|
|
607
1076
|
];
|
|
608
|
-
function exportState(elements, camera, layers = [], activeLayerId) {
|
|
1077
|
+
function exportState(elements, camera, layers = [], activeLayerId, fog) {
|
|
609
1078
|
const state = {
|
|
610
1079
|
version: CURRENT_VERSION,
|
|
611
1080
|
camera: {
|
|
@@ -622,6 +1091,7 @@ function exportState(elements, camera, layers = [], activeLayerId) {
|
|
|
622
1091
|
layers: layers.map((l) => ({ ...l }))
|
|
623
1092
|
};
|
|
624
1093
|
if (activeLayerId) state.activeLayerId = activeLayerId;
|
|
1094
|
+
if (fog) state.fog = structuredClone(fog);
|
|
625
1095
|
return state;
|
|
626
1096
|
}
|
|
627
1097
|
function parseState(json) {
|
|
@@ -699,6 +1169,9 @@ function validateState(data) {
|
|
|
699
1169
|
}
|
|
700
1170
|
}
|
|
701
1171
|
cleanBindings(elements);
|
|
1172
|
+
if (obj["fog"] !== void 0 && obj["fog"] !== null) {
|
|
1173
|
+
validateFogState(obj["fog"]);
|
|
1174
|
+
}
|
|
702
1175
|
}
|
|
703
1176
|
function validateElement(el) {
|
|
704
1177
|
if (!isRecord(el)) {
|
|
@@ -844,12 +1317,14 @@ var AutoSave = class {
|
|
|
844
1317
|
this.key = options.key ?? DEFAULT_KEY;
|
|
845
1318
|
this.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
846
1319
|
this.layerManager = options.layerManager;
|
|
1320
|
+
this.fogManager = options.fogManager;
|
|
847
1321
|
this.adapter = options.adapter ?? new LocalStorageAdapter();
|
|
848
1322
|
this.onError = options.onError;
|
|
849
1323
|
}
|
|
850
1324
|
key;
|
|
851
1325
|
debounceMs;
|
|
852
1326
|
layerManager;
|
|
1327
|
+
fogManager;
|
|
853
1328
|
adapter;
|
|
854
1329
|
timerId = null;
|
|
855
1330
|
unsubscribers = [];
|
|
@@ -867,6 +1342,9 @@ var AutoSave = class {
|
|
|
867
1342
|
if (this.layerManager) {
|
|
868
1343
|
this.unsubscribers.push(this.layerManager.on("change", schedule));
|
|
869
1344
|
}
|
|
1345
|
+
if (this.fogManager) {
|
|
1346
|
+
this.unsubscribers.push(this.fogManager.on("change", schedule));
|
|
1347
|
+
}
|
|
870
1348
|
}
|
|
871
1349
|
stop() {
|
|
872
1350
|
this.cancelPending();
|
|
@@ -903,7 +1381,8 @@ var AutoSave = class {
|
|
|
903
1381
|
this.saving = true;
|
|
904
1382
|
try {
|
|
905
1383
|
const layers = this.layerManager?.snapshot() ?? [];
|
|
906
|
-
const
|
|
1384
|
+
const fog = this.fogManager?.getState() ?? void 0;
|
|
1385
|
+
const state = exportState(this.store.snapshot(), this.camera, layers, void 0, fog);
|
|
907
1386
|
await this.adapter.save(this.key, JSON.stringify(state));
|
|
908
1387
|
} catch (e) {
|
|
909
1388
|
this.onError?.(e instanceof Error ? e : new Error(String(e)));
|
|
@@ -5235,6 +5714,8 @@ var MinimapController = class {
|
|
|
5235
5714
|
// read the `viewport` parameter. Assigned in the constructor body instead.
|
|
5236
5715
|
htmlPainters;
|
|
5237
5716
|
scene = null;
|
|
5717
|
+
fogRenderer = null;
|
|
5718
|
+
fogUnsub = null;
|
|
5238
5719
|
frameId = null;
|
|
5239
5720
|
debounceTimer = null;
|
|
5240
5721
|
dragging = false;
|
|
@@ -5249,6 +5730,15 @@ var MinimapController = class {
|
|
|
5249
5730
|
this.renderScene();
|
|
5250
5731
|
this.requestDraw();
|
|
5251
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
|
+
}
|
|
5252
5742
|
requestDraw() {
|
|
5253
5743
|
if (this.disposed || this.frameId !== null) return;
|
|
5254
5744
|
this.frameId = this.requestFrame(this.draw);
|
|
@@ -5310,8 +5800,15 @@ var MinimapController = class {
|
|
|
5310
5800
|
}
|
|
5311
5801
|
currentMapping() {
|
|
5312
5802
|
const viewportRect = this.viewport.getVisibleRect();
|
|
5313
|
-
|
|
5314
|
-
|
|
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;
|
|
5315
5812
|
}
|
|
5316
5813
|
onViewChanged() {
|
|
5317
5814
|
if (this.disposed) return;
|
|
@@ -5369,6 +5866,23 @@ var MinimapController = class {
|
|
|
5369
5866
|
ctx.drawImage(layerCanvas, 0, 0);
|
|
5370
5867
|
ctx.restore();
|
|
5371
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
|
+
}
|
|
5372
5886
|
this.scene = { canvas: sceneCanvas, transform, mapping };
|
|
5373
5887
|
}
|
|
5374
5888
|
renderLayerElements(ctx, elements, t, dpr) {
|
|
@@ -5481,9 +5995,15 @@ var Minimap = class {
|
|
|
5481
5995
|
this.canvas = canvas;
|
|
5482
5996
|
this.controller = new MinimapController(viewport, canvas, { width: WIDTH, height: HEIGHT });
|
|
5483
5997
|
}
|
|
5998
|
+
setFogRenderer(renderer) {
|
|
5999
|
+
this.controller.setFogRenderer(renderer);
|
|
6000
|
+
}
|
|
5484
6001
|
scheduleDraw() {
|
|
5485
6002
|
this.controller.requestDraw();
|
|
5486
6003
|
}
|
|
6004
|
+
invalidateScene() {
|
|
6005
|
+
this.controller.invalidateScene();
|
|
6006
|
+
}
|
|
5487
6007
|
destroy() {
|
|
5488
6008
|
this.controller.dispose();
|
|
5489
6009
|
this.canvas.remove();
|
|
@@ -6805,6 +7325,220 @@ async function renderHtmlElements(elements, options) {
|
|
|
6805
7325
|
return sources;
|
|
6806
7326
|
}
|
|
6807
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
|
+
|
|
6808
7542
|
// src/canvas/export-image.ts
|
|
6809
7543
|
var DEFAULT_IMAGE_TIMEOUT_MS = 1e4;
|
|
6810
7544
|
var DEFAULT_MAX_DIMENSION = 16384;
|
|
@@ -7217,6 +7951,10 @@ async function exportImage(store, options = {}, layerManager) {
|
|
|
7217
7951
|
renderGridForBounds(ctx, grid, bounds);
|
|
7218
7952
|
ctx.restore();
|
|
7219
7953
|
}
|
|
7954
|
+
if (options.fog) {
|
|
7955
|
+
const fogRenderer = new FogRenderer();
|
|
7956
|
+
fogRenderer.renderForExport(ctx, options.fog.state, options.fog.mode, options.fog.color);
|
|
7957
|
+
}
|
|
7220
7958
|
const mimeType = format === "jpeg" ? "image/jpeg" : "image/png";
|
|
7221
7959
|
return new Promise((resolve) => {
|
|
7222
7960
|
canvas.toBlob((blob) => resolve(blob), mimeType, options.quality);
|
|
@@ -7598,6 +8336,27 @@ async function exportSvg(store, options = {}, layerManager) {
|
|
|
7598
8336
|
const opacity = layerManager?.getLayer?.(grid.layerId)?.opacity ?? 1;
|
|
7599
8337
|
body += opacity === 1 ? emitted : `<g opacity="${n(opacity)}">${emitted}</g>`;
|
|
7600
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
|
+
}
|
|
7601
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>`;
|
|
7602
8361
|
}
|
|
7603
8362
|
function emitElement(el, imageDataUris, htmlDataUris, rasterScale, firstGrid, store, resourceOptions) {
|
|
@@ -8435,6 +9194,7 @@ var RenderLoop = class {
|
|
|
8435
9194
|
layerCache;
|
|
8436
9195
|
marginViewport;
|
|
8437
9196
|
hybridSurface;
|
|
9197
|
+
fogRenderer;
|
|
8438
9198
|
activeDrawingLayerId = null;
|
|
8439
9199
|
gridCacheDirty = true;
|
|
8440
9200
|
// set on recenter/viewport-change; consumed by the grid block
|
|
@@ -8458,6 +9218,7 @@ var RenderLoop = class {
|
|
|
8458
9218
|
this.layerCache = deps.layerCache;
|
|
8459
9219
|
this.marginViewport = deps.marginViewport;
|
|
8460
9220
|
this.hybridSurface = deps.hybridSurface;
|
|
9221
|
+
this.fogRenderer = deps.fogRenderer;
|
|
8461
9222
|
}
|
|
8462
9223
|
requestRender() {
|
|
8463
9224
|
this.needsRender = true;
|
|
@@ -8767,9 +9528,12 @@ var RenderLoop = class {
|
|
|
8767
9528
|
group.push(element);
|
|
8768
9529
|
}
|
|
8769
9530
|
const activeTool = this.toolManager.activeTool;
|
|
8770
|
-
const
|
|
9531
|
+
const fogVisible = this.fogRenderer?.isVisible() ?? false;
|
|
9532
|
+
const fogOrder = visibleElements.length + 1;
|
|
9533
|
+
const overlayOrder = fogVisible ? fogOrder + 1 : visibleElements.length + 1;
|
|
8771
9534
|
const hasOverlay = activeTool?.renderOverlay !== void 0 || this.overlays.size > 0;
|
|
8772
|
-
if (
|
|
9535
|
+
if (fogVisible) hybridOrders.add(fogOrder);
|
|
9536
|
+
if (hasOverlay && (hybridActive || fogVisible)) hybridOrders.add(overlayOrder);
|
|
8773
9537
|
this.hybridSurface.beginFrame(hybridOrders, this.canvasEl.width, this.canvasEl.height);
|
|
8774
9538
|
for (const [layerId, elements] of this.layerGroups) {
|
|
8775
9539
|
const isActiveDrawingLayer = layerId === this.activeDrawingLayerId;
|
|
@@ -8883,8 +9647,18 @@ var RenderLoop = class {
|
|
|
8883
9647
|
}
|
|
8884
9648
|
hybridCtx.restore();
|
|
8885
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
|
+
}
|
|
8886
9660
|
const overlayT0 = performance.now();
|
|
8887
|
-
if (hybridActive && hasOverlay) {
|
|
9661
|
+
if ((hybridActive || fogVisible) && hasOverlay) {
|
|
8888
9662
|
const overlayCtx = this.hybridSurface.getContext(overlayOrder);
|
|
8889
9663
|
if (overlayCtx) {
|
|
8890
9664
|
overlayCtx.clearRect(0, 0, this.canvasEl.width, this.canvasEl.height);
|
|
@@ -9895,6 +10669,248 @@ var ElementActivation = class {
|
|
|
9895
10669
|
}
|
|
9896
10670
|
};
|
|
9897
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
|
+
|
|
9898
10914
|
// src/canvas/viewport.ts
|
|
9899
10915
|
var EMPTY_IDS = [];
|
|
9900
10916
|
function noop2() {
|
|
@@ -10021,8 +11037,13 @@ var Viewport = class _Viewport {
|
|
|
10021
11037
|
});
|
|
10022
11038
|
}
|
|
10023
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);
|
|
10024
11044
|
if (options.minimap) {
|
|
10025
11045
|
this.minimap = new Minimap(this.wrapper, this);
|
|
11046
|
+
this.minimap.setFogRenderer(this.fogRenderer);
|
|
10026
11047
|
}
|
|
10027
11048
|
this.domNodeManager = new DomNodeManager({
|
|
10028
11049
|
domLayer: this.paintStack,
|
|
@@ -10052,7 +11073,18 @@ var Viewport = class _Viewport {
|
|
|
10052
11073
|
domNodeManager: this.domNodeManager,
|
|
10053
11074
|
layerCache,
|
|
10054
11075
|
marginViewport: this.marginViewport,
|
|
10055
|
-
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();
|
|
10056
11088
|
});
|
|
10057
11089
|
this.unsubHtmlPainters = this.htmlPainters.onChange(() => this.onHtmlRegistryChanged());
|
|
10058
11090
|
this.unsubCamera = this.camera.onChange(() => {
|
|
@@ -10164,6 +11196,8 @@ var Viewport = class _Viewport {
|
|
|
10164
11196
|
_smartGuides = false;
|
|
10165
11197
|
_gridSize;
|
|
10166
11198
|
renderLoop;
|
|
11199
|
+
fogManager;
|
|
11200
|
+
fogRenderer;
|
|
10167
11201
|
domNodeManager;
|
|
10168
11202
|
interactMode;
|
|
10169
11203
|
onHtmlElementMount;
|
|
@@ -10199,6 +11233,9 @@ var Viewport = class _Viewport {
|
|
|
10199
11233
|
get ctx() {
|
|
10200
11234
|
return this.canvasEl.getContext("2d");
|
|
10201
11235
|
}
|
|
11236
|
+
get fog() {
|
|
11237
|
+
return this.fogManager;
|
|
11238
|
+
}
|
|
10202
11239
|
get snapToGrid() {
|
|
10203
11240
|
return this._snapToGrid;
|
|
10204
11241
|
}
|
|
@@ -10283,7 +11320,8 @@ var Viewport = class _Viewport {
|
|
|
10283
11320
|
this.store.snapshot(),
|
|
10284
11321
|
this.camera,
|
|
10285
11322
|
this.layerManager.snapshot(),
|
|
10286
|
-
this.layerManager.activeLayerId
|
|
11323
|
+
this.layerManager.activeLayerId,
|
|
11324
|
+
this.fogManager.getState()
|
|
10287
11325
|
);
|
|
10288
11326
|
}
|
|
10289
11327
|
exportJSON() {
|
|
@@ -10305,12 +11343,31 @@ var Viewport = class _Viewport {
|
|
|
10305
11343
|
return { ...base, htmlPainters: registry, expectedCanvasTypes: expected };
|
|
10306
11344
|
}
|
|
10307
11345
|
async exportImage(options) {
|
|
10308
|
-
|
|
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);
|
|
10309
11355
|
}
|
|
10310
11356
|
async exportSVG(options) {
|
|
10311
|
-
|
|
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);
|
|
10312
11366
|
}
|
|
10313
11367
|
loadState(state) {
|
|
11368
|
+
if (state.fog != null) {
|
|
11369
|
+
validateFogState(state.fog);
|
|
11370
|
+
}
|
|
10314
11371
|
this.inputHandler.flushPendingHistory();
|
|
10315
11372
|
this.historyRecorder.pause();
|
|
10316
11373
|
this.noteEditor.destroy(this.store);
|
|
@@ -10345,6 +11402,7 @@ var Viewport = class _Viewport {
|
|
|
10345
11402
|
}
|
|
10346
11403
|
}
|
|
10347
11404
|
}
|
|
11405
|
+
this.fogManager.loadState(state.fog ?? null);
|
|
10348
11406
|
this.history.clear();
|
|
10349
11407
|
this.historyRecorder.resume();
|
|
10350
11408
|
this.camera.moveTo(state.camera.position.x, state.camera.position.y);
|
|
@@ -10756,6 +11814,8 @@ var Viewport = class _Viewport {
|
|
|
10756
11814
|
this.unsubToolRegister();
|
|
10757
11815
|
this.unsubRecorderEnd();
|
|
10758
11816
|
this.unsubHtmlPainters();
|
|
11817
|
+
this.fogManager.dispose();
|
|
11818
|
+
this.fogRenderer.dispose();
|
|
10759
11819
|
this.activation?.dispose();
|
|
10760
11820
|
this.activation = null;
|
|
10761
11821
|
this.activationListeners.clear();
|
|
@@ -15597,8 +16657,188 @@ var PingTool = class {
|
|
|
15597
16657
|
}
|
|
15598
16658
|
};
|
|
15599
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
|
+
|
|
15600
16840
|
// src/index.ts
|
|
15601
|
-
var VERSION = "0.
|
|
16841
|
+
var VERSION = "0.66.0";
|
|
15602
16842
|
// Annotate the CommonJS export names for ESM import in node:
|
|
15603
16843
|
0 && (module.exports = {
|
|
15604
16844
|
AWARENESS_MAX_SELECTION,
|
|
@@ -15612,6 +16852,12 @@ var VERSION = "0.65.0";
|
|
|
15612
16852
|
ElementStore,
|
|
15613
16853
|
EraserTool,
|
|
15614
16854
|
FOCUS_PRESENCE_KIND,
|
|
16855
|
+
FOG_MAX_TILES,
|
|
16856
|
+
FOG_STATE_VERSION,
|
|
16857
|
+
FOG_TILE_CELLS,
|
|
16858
|
+
FogManager,
|
|
16859
|
+
FogRenderer,
|
|
16860
|
+
FogTool,
|
|
15615
16861
|
HandTool,
|
|
15616
16862
|
HistoryStack,
|
|
15617
16863
|
HtmlPainterMissingError,
|
|
@@ -15655,6 +16901,7 @@ var VERSION = "0.65.0";
|
|
|
15655
16901
|
attachAwareness,
|
|
15656
16902
|
boundsIntersect,
|
|
15657
16903
|
cameraOriginForView,
|
|
16904
|
+
canonicalizeFogTile,
|
|
15658
16905
|
captureCameraView,
|
|
15659
16906
|
computeElementRects,
|
|
15660
16907
|
createArrow,
|
|
@@ -15672,6 +16919,8 @@ var VERSION = "0.65.0";
|
|
|
15672
16919
|
exportImage,
|
|
15673
16920
|
exportSvg,
|
|
15674
16921
|
fitZoomForView,
|
|
16922
|
+
fogDecodeBase64,
|
|
16923
|
+
fogEncodeBase64,
|
|
15675
16924
|
footprintFromSize,
|
|
15676
16925
|
getActiveFormats,
|
|
15677
16926
|
getArrowBounds,
|
|
@@ -15697,6 +16946,7 @@ var VERSION = "0.65.0";
|
|
|
15697
16946
|
isPathPresence,
|
|
15698
16947
|
isPingPresence,
|
|
15699
16948
|
pathDistanceCells,
|
|
16949
|
+
recommendedFogCellSize,
|
|
15700
16950
|
resolveHtmlRouting,
|
|
15701
16951
|
setFontSize,
|
|
15702
16952
|
smartSnap,
|
|
@@ -15713,6 +16963,9 @@ var VERSION = "0.65.0";
|
|
|
15713
16963
|
toggleBold,
|
|
15714
16964
|
toggleItalic,
|
|
15715
16965
|
toggleStrikethrough,
|
|
15716
|
-
toggleUnderline
|
|
16966
|
+
toggleUnderline,
|
|
16967
|
+
validateFogDefinition,
|
|
16968
|
+
validateFogState,
|
|
16969
|
+
validateFogTile
|
|
15717
16970
|
});
|
|
15718
16971
|
//# sourceMappingURL=index.cjs.map
|