@codexo/exojs-tilemap 0.15.3 → 0.16.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.
Files changed (151) hide show
  1. package/README.md +33 -26
  2. package/dist/esm/ChunkSource.d.ts +33 -0
  3. package/dist/esm/ChunkSource.d.ts.map +1 -0
  4. package/dist/esm/ChunkStreamer.d.ts +98 -0
  5. package/dist/esm/ChunkStreamer.d.ts.map +1 -0
  6. package/dist/esm/ChunkStreamer.js +201 -0
  7. package/dist/esm/ChunkStreamer.js.map +1 -0
  8. package/dist/esm/ImageLayer.d.ts +11 -3
  9. package/dist/esm/ImageLayer.d.ts.map +1 -0
  10. package/dist/esm/ImageLayer.js +71 -64
  11. package/dist/esm/ImageLayer.js.map +1 -1
  12. package/dist/esm/ImageLayerNode.d.ts +89 -0
  13. package/dist/esm/ImageLayerNode.d.ts.map +1 -0
  14. package/dist/esm/ImageLayerNode.js +205 -0
  15. package/dist/esm/ImageLayerNode.js.map +1 -0
  16. package/dist/esm/MapObject.d.ts +70 -0
  17. package/dist/esm/MapObject.d.ts.map +1 -0
  18. package/dist/esm/MapObject.js +34 -0
  19. package/dist/esm/MapObject.js.map +1 -0
  20. package/dist/esm/MapObjectSpawner.d.ts +133 -0
  21. package/dist/esm/MapObjectSpawner.d.ts.map +1 -0
  22. package/dist/esm/MapObjectSpawner.js +147 -0
  23. package/dist/esm/MapObjectSpawner.js.map +1 -0
  24. package/dist/esm/MapSpawnSession.d.ts +53 -0
  25. package/dist/esm/MapSpawnSession.d.ts.map +1 -0
  26. package/dist/esm/MapSpawnSession.js +88 -0
  27. package/dist/esm/MapSpawnSession.js.map +1 -0
  28. package/dist/esm/MapWorld.d.ts +134 -0
  29. package/dist/esm/MapWorld.d.ts.map +1 -0
  30. package/dist/esm/MapWorld.js +150 -0
  31. package/dist/esm/MapWorld.js.map +1 -0
  32. package/dist/esm/MapWorldRuntime.d.ts +182 -0
  33. package/dist/esm/MapWorldRuntime.d.ts.map +1 -0
  34. package/dist/esm/MapWorldRuntime.js +261 -0
  35. package/dist/esm/MapWorldRuntime.js.map +1 -0
  36. package/dist/esm/ObjectLayer.d.ts +93 -7
  37. package/dist/esm/ObjectLayer.d.ts.map +1 -0
  38. package/dist/esm/ObjectLayer.js +184 -167
  39. package/dist/esm/ObjectLayer.js.map +1 -1
  40. package/dist/esm/SampledChunkSource.d.ts +43 -0
  41. package/dist/esm/SampledChunkSource.d.ts.map +1 -0
  42. package/dist/esm/SampledChunkSource.js +48 -0
  43. package/dist/esm/SampledChunkSource.js.map +1 -0
  44. package/dist/esm/TileAnimator.d.ts +12 -2
  45. package/dist/esm/TileAnimator.d.ts.map +1 -0
  46. package/dist/esm/TileAnimator.js +154 -165
  47. package/dist/esm/TileAnimator.js.map +1 -1
  48. package/dist/esm/TileChunk.d.ts +30 -5
  49. package/dist/esm/TileChunk.d.ts.map +1 -0
  50. package/dist/esm/TileChunk.js +190 -188
  51. package/dist/esm/TileChunk.js.map +1 -1
  52. package/dist/esm/TileChunkNode.d.ts +18 -3
  53. package/dist/esm/TileChunkNode.d.ts.map +1 -0
  54. package/dist/esm/TileChunkNode.js +104 -85
  55. package/dist/esm/TileChunkNode.js.map +1 -1
  56. package/dist/esm/TileLayer.d.ts +104 -24
  57. package/dist/esm/TileLayer.d.ts.map +1 -0
  58. package/dist/esm/TileLayer.js +511 -469
  59. package/dist/esm/TileLayer.js.map +1 -1
  60. package/dist/esm/TileLayerNode.d.ts +52 -11
  61. package/dist/esm/TileLayerNode.d.ts.map +1 -0
  62. package/dist/esm/TileLayerNode.js +224 -166
  63. package/dist/esm/TileLayerNode.js.map +1 -1
  64. package/dist/esm/TileMap.d.ts +96 -31
  65. package/dist/esm/TileMap.d.ts.map +1 -0
  66. package/dist/esm/TileMap.js +401 -357
  67. package/dist/esm/TileMap.js.map +1 -1
  68. package/dist/esm/TileMapBand.d.ts +42 -29
  69. package/dist/esm/TileMapBand.d.ts.map +1 -0
  70. package/dist/esm/TileMapBand.js +151 -158
  71. package/dist/esm/TileMapBand.js.map +1 -1
  72. package/dist/esm/TileMapNode.d.ts +34 -18
  73. package/dist/esm/TileMapNode.d.ts.map +1 -0
  74. package/dist/esm/TileMapNode.js +122 -109
  75. package/dist/esm/TileMapNode.js.map +1 -1
  76. package/dist/esm/TileMapView.d.ts +108 -47
  77. package/dist/esm/TileMapView.d.ts.map +1 -0
  78. package/dist/esm/TileMapView.js +385 -338
  79. package/dist/esm/TileMapView.js.map +1 -1
  80. package/dist/esm/TileSet.d.ts +15 -6
  81. package/dist/esm/TileSet.d.ts.map +1 -0
  82. package/dist/esm/TileSet.js +161 -181
  83. package/dist/esm/TileSet.js.map +1 -1
  84. package/dist/esm/WangSet.d.ts +5 -4
  85. package/dist/esm/WangSet.d.ts.map +1 -0
  86. package/dist/esm/WangSet.js +70 -78
  87. package/dist/esm/WangSet.js.map +1 -1
  88. package/dist/esm/WorkerSampledChunkSource.d.ts +95 -0
  89. package/dist/esm/WorkerSampledChunkSource.d.ts.map +1 -0
  90. package/dist/esm/WorkerSampledChunkSource.js +117 -0
  91. package/dist/esm/WorkerSampledChunkSource.js.map +1 -0
  92. package/dist/esm/autoTile.d.ts +10 -5
  93. package/dist/esm/autoTile.d.ts.map +1 -0
  94. package/dist/esm/autoTile.js +164 -205
  95. package/dist/esm/autoTile.js.map +1 -1
  96. package/dist/esm/chunkGeometry.d.ts +8 -7
  97. package/dist/esm/chunkGeometry.d.ts.map +1 -0
  98. package/dist/esm/chunkGeometry.js +83 -94
  99. package/dist/esm/chunkGeometry.js.map +1 -1
  100. package/dist/esm/index.d.ts +1 -0
  101. package/dist/esm/index.d.ts.map +1 -0
  102. package/dist/esm/index.js +26 -15
  103. package/dist/esm/nodeBounds.d.ts +20 -0
  104. package/dist/esm/nodeBounds.d.ts.map +1 -0
  105. package/dist/esm/nodeBounds.js +40 -0
  106. package/dist/esm/nodeBounds.js.map +1 -0
  107. package/dist/esm/pixelSnap.d.ts +4 -3
  108. package/dist/esm/pixelSnap.d.ts.map +1 -0
  109. package/dist/esm/pixelSnap.js +18 -16
  110. package/dist/esm/pixelSnap.js.map +1 -1
  111. package/dist/esm/public.d.ts +23 -3
  112. package/dist/esm/public.d.ts.map +1 -0
  113. package/dist/esm/tileCollision.d.ts +190 -0
  114. package/dist/esm/tileCollision.d.ts.map +1 -0
  115. package/dist/esm/tileCollision.js +367 -0
  116. package/dist/esm/tileCollision.js.map +1 -0
  117. package/dist/esm/tileWord.d.ts +12 -0
  118. package/dist/esm/tileWord.d.ts.map +1 -0
  119. package/dist/esm/tileWord.js +16 -0
  120. package/dist/esm/tileWord.js.map +1 -0
  121. package/dist/esm/tilemapExtension.d.ts +4 -3
  122. package/dist/esm/tilemapExtension.d.ts.map +1 -0
  123. package/dist/esm/tilemapExtension.js +47 -47
  124. package/dist/esm/tilemapExtension.js.map +1 -1
  125. package/dist/esm/tilemapSerializers.d.ts +5 -4
  126. package/dist/esm/tilemapSerializers.d.ts.map +1 -0
  127. package/dist/esm/tilemapSerializers.js +48 -41
  128. package/dist/esm/tilemapSerializers.js.map +1 -1
  129. package/dist/esm/types.d.ts +34 -18
  130. package/dist/esm/types.d.ts.map +1 -0
  131. package/dist/esm/types.js +111 -120
  132. package/dist/esm/types.js.map +1 -1
  133. package/dist/esm/webgl2/WebGl2TileChunkRenderer.d.ts +50 -2
  134. package/dist/esm/webgl2/WebGl2TileChunkRenderer.d.ts.map +1 -0
  135. package/dist/esm/webgl2/WebGl2TileChunkRenderer.js +366 -332
  136. package/dist/esm/webgl2/WebGl2TileChunkRenderer.js.map +1 -1
  137. package/dist/esm/webgl2/shaders/tile-chunk.frag.js +6 -0
  138. package/dist/esm/webgl2/shaders/tile-chunk.frag.js.map +1 -0
  139. package/dist/esm/webgl2/shaders/tile-chunk.vert.js +6 -0
  140. package/dist/esm/webgl2/shaders/tile-chunk.vert.js.map +1 -0
  141. package/dist/esm/webgpu/WebGpuTileChunkRenderer.d.ts +70 -4
  142. package/dist/esm/webgpu/WebGpuTileChunkRenderer.d.ts.map +1 -0
  143. package/dist/esm/webgpu/WebGpuTileChunkRenderer.js +496 -383
  144. package/dist/esm/webgpu/WebGpuTileChunkRenderer.js.map +1 -1
  145. package/dist/esm/webgpu/shaders/tile-chunk.wgsl.js +6 -0
  146. package/dist/esm/webgpu/shaders/tile-chunk.wgsl.js.map +1 -0
  147. package/package.json +8 -15
  148. package/dist/esm/index.js.map +0 -1
  149. package/dist/esm/register.d.ts +0 -1
  150. package/dist/esm/register.js +0 -25
  151. package/dist/esm/register.js.map +0 -1
@@ -1,214 +1,173 @@
1
- import { unpackTile, TILE_TRANSFORM_IDENTITY } from './types.js';
1
+ import { TILE_TRANSFORM_IDENTITY, unpackTile } from "./types.js";
2
2
 
3
- // ── Module-level mask helpers ─────────────────────────────────────────────
3
+ //#region src/autoTile.ts
4
4
  /**
5
- * Compute a 4-bit edge bitmask for the cell at `(tx, ty)`.
6
- * Top=1, Right=2, Bottom=4, Left=8.
7
- */
8
- function computeEdgeMask(tx, ty, inGroup) {
9
- let mask = 0;
10
- if (inGroup(tx, ty - 1))
11
- mask |= 1;
12
- if (inGroup(tx + 1, ty))
13
- mask |= 2;
14
- if (inGroup(tx, ty + 1))
15
- mask |= 4;
16
- if (inGroup(tx - 1, ty))
17
- mask |= 8;
18
- return mask;
19
- }
5
+ * Compute a 4-bit edge bitmask for the cell at `(tx, ty)`.
6
+ * Top=1, Right=2, Bottom=4, Left=8.
7
+ */
8
+ const computeEdgeMask = (tx, ty, inGroup) => {
9
+ let mask = 0;
10
+ if (inGroup(tx, ty - 1)) mask |= 1;
11
+ if (inGroup(tx + 1, ty)) mask |= 2;
12
+ if (inGroup(tx, ty + 1)) mask |= 4;
13
+ if (inGroup(tx - 1, ty)) mask |= 8;
14
+ return mask;
15
+ };
20
16
  /**
21
- * Compute an 8-bit blob bitmask for the cell at `(tx, ty)` using the
22
- * corner-dependency rule: diagonal bits are only set when both adjacent
23
- * cardinal directions are also set.
24
- *
25
- * Bit layout:
26
- * ```
27
- * 1 | 2 | 4
28
- * 8 | -- | 16
29
- * 32 | 64 | 128
30
- * ```
31
- */
32
- function computeBlobMask(tx, ty, inGroup) {
33
- const top = inGroup(tx, ty - 1);
34
- const right = inGroup(tx + 1, ty);
35
- const bottom = inGroup(tx, ty + 1);
36
- const left = inGroup(tx - 1, ty);
37
- let mask = 0;
38
- if (top)
39
- mask |= 2;
40
- if (right)
41
- mask |= 16;
42
- if (bottom)
43
- mask |= 64;
44
- if (left)
45
- mask |= 8;
46
- // Corner bits: only when BOTH adjacent cardinals are set.
47
- if (top && left && inGroup(tx - 1, ty - 1))
48
- mask |= 1;
49
- if (top && right && inGroup(tx + 1, ty - 1))
50
- mask |= 4;
51
- if (bottom && left && inGroup(tx - 1, ty + 1))
52
- mask |= 32;
53
- if (bottom && right && inGroup(tx + 1, ty + 1))
54
- mask |= 128;
55
- return mask;
56
- }
17
+ * Compute an 8-bit blob bitmask for the cell at `(tx, ty)` using the
18
+ * corner-dependency rule: diagonal bits are only set when both adjacent
19
+ * cardinal directions are also set.
20
+ *
21
+ * Bit layout:
22
+ * ```
23
+ * 1 | 2 | 4
24
+ * 8 | -- | 16
25
+ * 32 | 64 | 128
26
+ * ```
27
+ */
28
+ const computeBlobMask = (tx, ty, inGroup) => {
29
+ const top = inGroup(tx, ty - 1);
30
+ const right = inGroup(tx + 1, ty);
31
+ const bottom = inGroup(tx, ty + 1);
32
+ const left = inGroup(tx - 1, ty);
33
+ let mask = 0;
34
+ if (top) mask |= 2;
35
+ if (right) mask |= 16;
36
+ if (bottom) mask |= 64;
37
+ if (left) mask |= 8;
38
+ if (top && left && inGroup(tx - 1, ty - 1)) mask |= 1;
39
+ if (top && right && inGroup(tx + 1, ty - 1)) mask |= 4;
40
+ if (bottom && left && inGroup(tx - 1, ty + 1)) mask |= 32;
41
+ if (bottom && right && inGroup(tx + 1, ty + 1)) mask |= 128;
42
+ return mask;
43
+ };
57
44
  /** Compute the mask for a cell given the Wang mode and a membership predicate. */
58
- function computeMask(wangSet, tx, ty, inGroup) {
59
- return wangSet.type === 'edge'
60
- ? computeEdgeMask(tx, ty, inGroup)
61
- : computeBlobMask(tx, ty, inGroup);
62
- }
45
+ const computeMask = (wangSet, tx, ty, inGroup) => wangSet.type === "edge" ? computeEdgeMask(tx, ty, inGroup) : computeBlobMask(tx, ty, inGroup);
63
46
  /**
64
- * Apply the variant computed for cell `(tx, ty)` to `layer`, preserving the
65
- * cell's current orientation transform. No-op if the mask has no mapping or
66
- * the target tileset is missing.
67
- */
68
- function applyVariant(layer, wangSet, tx, ty, inGroup) {
69
- const newLocalTileId = wangSet.getTileId(computeMask(wangSet, tx, ty, inGroup));
70
- if (newLocalTileId === undefined)
71
- return;
72
- const tileset = layer.tilesets[wangSet.tilesetIndex];
73
- if (!tileset)
74
- return;
75
- // Preserve the existing orientation transform if the cell already holds one.
76
- const existing = layer.getTileAt(tx, ty);
77
- layer.setTileAt(tx, ty, {
78
- localTileId: newLocalTileId,
79
- tileset,
80
- transform: existing ? existing.transform : TILE_TRANSFORM_IDENTITY,
81
- });
82
- }
83
- // ── Public API ────────────────────────────────────────────────────────────
47
+ * Apply the variant computed for cell `(tx, ty)` to `layer`, preserving the
48
+ * cell's current orientation transform. No-op if the mask has no mapping or
49
+ * the target tileset is missing.
50
+ */
51
+ const applyVariant = (layer, wangSet, tx, ty, inGroup) => {
52
+ const newLocalTileId = wangSet.getTileId(computeMask(wangSet, tx, ty, inGroup));
53
+ if (newLocalTileId === void 0) return;
54
+ const tileset = layer.tilesets[wangSet.tilesetIndex];
55
+ if (!tileset) return;
56
+ const existing = layer.getTileAt(tx, ty);
57
+ layer.setTileAt(tx, ty, {
58
+ localTileId: newLocalTileId,
59
+ tileset,
60
+ transform: existing ? existing.transform : TILE_TRANSFORM_IDENTITY
61
+ });
62
+ };
84
63
  /**
85
- * Apply Wang autotiling to `layer` using `wangSet`.
86
- *
87
- * Iterates every cell in the layer. For each cell that belongs to the Wang
88
- * group, computes a neighbor bitmask and looks up the correct local tile ID
89
- * from {@link WangSet.blobMap}. The layer is mutated in place; cells whose
90
- * computed bitmask has no mapping in the blobMap are left unchanged.
91
- *
92
- * The function performs two passes (snapshot then write) so neighbor tests
93
- * always reflect the pre-call state regardless of processing order.
94
- *
95
- * **Group membership.** With no `matchFn`, a cell belongs to the group when its
96
- * `tilesetIndex` matches {@link WangSet.tilesetIndex} and its `localTileId` is a
97
- * {@link WangSet.isMember member} of the set. Membership covers every variant
98
- * the set can produce, so re-running `autoTile` (or {@link refreshCell}) on
99
- * already-autotiled data is stable. Paint with a tile ID that is a member (a
100
- * blobMap value, or one listed in {@link WangSetOptions.members}).
101
- *
102
- * **Blob mode bitmask (bit positions):**
103
- * ```
104
- * 1 | 2 | 4
105
- * 8 | -- | 16
106
- * 32 | 64 | 128
107
- * ```
108
- * Diagonal bits (1, 4, 32, 128) are only set when both adjacent cardinals
109
- * are also set (the "corner dependency" rule that reduces 256 raw
110
- * combinations to 47 valid blob states).
111
- *
112
- * **Edge mode bitmask (bit positions):** Top=1, Right=2, Bottom=4, Left=8.
113
- */
114
- function autoTile(layer, wangSet, options) {
115
- const matchFn = options?.matchFn;
116
- const wrapBorder = options?.wrapBorder ?? true;
117
- const w = layer.width;
118
- const h = layer.height;
119
- const snapshot = new Map();
120
- for (let ty = 0; ty < h; ty++) {
121
- for (let tx = 0; tx < w; tx++) {
122
- const packed = layer.getRawTileAt(tx, ty);
123
- if (packed === 0)
124
- continue;
125
- const decoded = unpackTile(packed);
126
- if (!decoded)
127
- continue;
128
- snapshot.set(ty * w + tx, decoded);
129
- }
130
- }
131
- // ── Membership test (reads the snapshot) ─────────────────────────────
132
- const isInGroup = (nx, ny) => {
133
- if (nx < 0 || nx >= w || ny < 0 || ny >= h)
134
- return wrapBorder;
135
- const cell = snapshot.get(ny * w + nx);
136
- if (!cell)
137
- return false;
138
- if (matchFn)
139
- return matchFn(cell.localTileId, cell.tilesetIndex, nx, ny);
140
- return cell.tilesetIndex === wangSet.tilesetIndex && wangSet.isMember(cell.localTileId);
141
- };
142
- // ── Pass 2: compute masks and write ──────────────────────────────────
143
- for (let ty = 0; ty < h; ty++) {
144
- for (let tx = 0; tx < w; tx++) {
145
- const cellInfo = snapshot.get(ty * w + tx);
146
- if (!cellInfo)
147
- continue;
148
- // Skip cells that are not part of the group.
149
- const isMember = matchFn
150
- ? matchFn(cellInfo.localTileId, cellInfo.tilesetIndex, tx, ty)
151
- : cellInfo.tilesetIndex === wangSet.tilesetIndex && wangSet.isMember(cellInfo.localTileId);
152
- if (!isMember)
153
- continue;
154
- applyVariant(layer, wangSet, tx, ty, isInGroup);
155
- }
156
- }
157
- }
64
+ * Apply Wang autotiling to `layer` using `wangSet`.
65
+ *
66
+ * Iterates every cell in the layer. For each cell that belongs to the Wang
67
+ * group, computes a neighbor bitmask and looks up the correct local tile ID
68
+ * from {@link WangSet.blobMap}. The layer is mutated in place; cells whose
69
+ * computed bitmask has no mapping in the blobMap are left unchanged.
70
+ *
71
+ * The function performs two passes (snapshot then write) so neighbor tests
72
+ * always reflect the pre-call state regardless of processing order.
73
+ *
74
+ * **Group membership.** With no `matchFn`, a cell belongs to the group when its
75
+ * `tilesetIndex` matches {@link WangSet.tilesetIndex} and its `localTileId` is a
76
+ * {@link WangSet.isMember member} of the set. Membership covers every variant
77
+ * the set can produce, so re-running `autoTile` (or {@link refreshCell}) on
78
+ * already-autotiled data is stable. Paint with a tile ID that is a member (a
79
+ * blobMap value, or one listed in {@link WangSetOptions.members}).
80
+ *
81
+ * **Blob mode bitmask (bit positions):**
82
+ * ```
83
+ * 1 | 2 | 4
84
+ * 8 | -- | 16
85
+ * 32 | 64 | 128
86
+ * ```
87
+ * Diagonal bits (1, 4, 32, 128) are only set when both adjacent cardinals
88
+ * are also set (the "corner dependency" rule that reduces 256 raw
89
+ * combinations to 47 valid blob states).
90
+ *
91
+ * **Edge mode bitmask (bit positions):** Top=1, Right=2, Bottom=4, Left=8.
92
+ *
93
+ * @throws If `layer` is unbounded (no fixed width/height) - there is no
94
+ * "whole layer" to sweep. Use {@link refreshCell} to autotile an unbounded
95
+ * layer incrementally, one edited cell at a time.
96
+ */
97
+ const autoTile = (layer, wangSet, options) => {
98
+ const matchFn = options?.matchFn;
99
+ const wrapBorder = options?.wrapBorder ?? true;
100
+ const w = layer.width;
101
+ const h = layer.height;
102
+ if (w === void 0 || h === void 0) throw new Error("autoTile() requires a bounded layer (sweeps the whole layer) — use refreshCell() on an unbounded layer instead.");
103
+ const snapshot = /* @__PURE__ */ new Map();
104
+ for (let ty = 0; ty < h; ty++) for (let tx = 0; tx < w; tx++) {
105
+ const packed = layer.getRawTileAt(tx, ty);
106
+ if (packed === 0) continue;
107
+ const decoded = unpackTile(packed);
108
+ if (!decoded) continue;
109
+ snapshot.set(ty * w + tx, decoded);
110
+ }
111
+ const isInGroup = (nx, ny) => {
112
+ if (nx < 0 || nx >= w || ny < 0 || ny >= h) return wrapBorder;
113
+ const cell = snapshot.get(ny * w + nx);
114
+ if (!cell) return false;
115
+ if (matchFn) return matchFn(cell.localTileId, cell.tilesetIndex, nx, ny);
116
+ return cell.tilesetIndex === wangSet.tilesetIndex && wangSet.isMember(cell.localTileId);
117
+ };
118
+ for (let ty = 0; ty < h; ty++) for (let tx = 0; tx < w; tx++) {
119
+ const cellInfo = snapshot.get(ty * w + tx);
120
+ if (!cellInfo) continue;
121
+ if (!(matchFn ? matchFn(cellInfo.localTileId, cellInfo.tilesetIndex, tx, ty) : cellInfo.tilesetIndex === wangSet.tilesetIndex && wangSet.isMember(cellInfo.localTileId))) continue;
122
+ applyVariant(layer, wangSet, tx, ty, isInGroup);
123
+ }
124
+ };
158
125
  /**
159
- * Incrementally re-autotile a single cell and its eight neighbours after an
160
- * edit, instead of re-running {@link autoTile} over the whole layer.
161
- *
162
- * A cell's blob/edge mask depends only on its immediate neighbours, so painting
163
- * or erasing cell `(x, y)` can change the variant of that cell and of the (up
164
- * to) eight cells that have it as a neighbour but nothing further out. This
165
- * recomputes exactly that 3×3 neighbourhood, touching only the 14 chunks it
166
- * spans (and rebuilding geometry only for chunks whose tiles actually change).
167
- * For a paint operation this is O(1) work versus `autoTile`'s O(width·height).
168
- *
169
- * Membership is read live from the layer, so the membership test MUST be
170
- * variant-stable: the default ({@link WangSet.isMember}) is, and any custom
171
- * `matchFn` you pass must be too (see {@link AutoTileOptions.matchFn}).
172
- *
173
- * Typical editor use: write the painted tile with `layer.setTileAt(...)` using
174
- * a member tile ID, then call `refreshCell(layer, x, y, wangSet)`.
175
- *
176
- * @param layer The layer to update in place.
177
- * @param x Tile X of the edited cell.
178
- * @param y Tile Y of the edited cell.
179
- * @param wangSet The Wang set to resolve variants from.
180
- * @param options Membership / border options (shared with {@link autoTile}).
181
- */
182
- function refreshCell(layer, x, y, wangSet, options) {
183
- const matchFn = options?.matchFn;
184
- const wrapBorder = options?.wrapBorder ?? true;
185
- const w = layer.width;
186
- const h = layer.height;
187
- // Live membership test (reads the current layer; variant-stable by default).
188
- const isInGroup = (nx, ny) => {
189
- if (nx < 0 || nx >= w || ny < 0 || ny >= h)
190
- return wrapBorder;
191
- const tile = layer.getTileAt(nx, ny);
192
- if (!tile)
193
- return false;
194
- const tsi = layer.tilesets.indexOf(tile.tileset);
195
- if (matchFn)
196
- return matchFn(tile.localTileId, tsi, nx, ny);
197
- return tsi === wangSet.tilesetIndex && wangSet.isMember(tile.localTileId);
198
- };
199
- // Recompute the edited cell and its eight neighbours.
200
- for (let dy = -1; dy <= 1; dy++) {
201
- for (let dx = -1; dx <= 1; dx++) {
202
- const tx = x + dx;
203
- const ty = y + dy;
204
- if (!layer.inBounds(tx, ty))
205
- continue;
206
- if (!isInGroup(tx, ty))
207
- continue;
208
- applyVariant(layer, wangSet, tx, ty, isInGroup);
209
- }
210
- }
211
- }
126
+ * Incrementally re-autotile a single cell and its eight neighbours after an
127
+ * edit, instead of re-running {@link autoTile} over the whole layer.
128
+ *
129
+ * A cell's blob/edge mask depends only on its immediate neighbours, so painting
130
+ * or erasing cell `(x, y)` can change the variant of that cell and of the (up
131
+ * to) eight cells that have it as a neighbour - but nothing further out. This
132
+ * recomputes exactly that 3×3 neighbourhood, touching only the 1-4 chunks it
133
+ * spans (and rebuilding geometry only for chunks whose tiles actually change).
134
+ * For a paint operation this is O(1) work versus `autoTile`'s O(width·height).
135
+ *
136
+ * Membership is read live from the layer, so the membership test MUST be
137
+ * variant-stable: the default ({@link WangSet.isMember}) is, and any custom
138
+ * `matchFn` you pass must be too (see {@link AutoTileOptions.matchFn}).
139
+ *
140
+ * Typical editor use: write the painted tile with `layer.setTileAt(...)` using
141
+ * a member tile ID, then call `refreshCell(layer, x, y, wangSet)`.
142
+ *
143
+ * @param layer The layer to update in place.
144
+ * @param x Tile X of the edited cell.
145
+ * @param y Tile Y of the edited cell.
146
+ * @param wangSet The Wang set to resolve variants from.
147
+ * @param options Membership / border options (shared with {@link autoTile}).
148
+ */
149
+ const refreshCell = (layer, x, y, wangSet, options) => {
150
+ const matchFn = options?.matchFn;
151
+ const wrapBorder = options?.wrapBorder ?? true;
152
+ const w = layer.width;
153
+ const h = layer.height;
154
+ const isInGroup = (nx, ny) => {
155
+ if (w !== void 0 && (nx < 0 || nx >= w) || h !== void 0 && (ny < 0 || ny >= h)) return wrapBorder;
156
+ const tile = layer.getTileAt(nx, ny);
157
+ if (!tile) return false;
158
+ const tsi = layer.tilesets.indexOf(tile.tileset);
159
+ if (matchFn) return matchFn(tile.localTileId, tsi, nx, ny);
160
+ return tsi === wangSet.tilesetIndex && wangSet.isMember(tile.localTileId);
161
+ };
162
+ for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
163
+ const tx = x + dx;
164
+ const ty = y + dy;
165
+ if (!layer.inBounds(tx, ty)) continue;
166
+ if (!isInGroup(tx, ty)) continue;
167
+ applyVariant(layer, wangSet, tx, ty, isInGroup);
168
+ }
169
+ };
212
170
 
171
+ //#endregion
213
172
  export { autoTile, refreshCell };
214
- //# sourceMappingURL=autoTile.js.map
173
+ //# sourceMappingURL=autoTile.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"autoTile.js","sources":["../../../src/autoTile.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"autoTile.js","names":[],"sources":["../../src/autoTile.ts"],"sourcesContent":["import type { TileLayer } from './TileLayer';\nimport { TILE_TRANSFORM_IDENTITY, unpackTile } from './types';\nimport type { WangSet } from './WangSet';\n\n/**\n * Options for {@link autoTile} and {@link refreshCell}.\n */\nexport interface AutoTileOptions {\n /**\n * When provided, only cells for which `matchFn` returns `true` are treated\n * as part of the Wang group. The function receives the cell's `localTileId`,\n * `tilesetIndex`, and tile coordinates `(x, y)`.\n *\n * If omitted, group membership defaults to {@link WangSet.isMember} on the\n * cell's `localTileId` (and a matching `tilesetIndex`). That default is\n * *variant-stable* - every autotiled variant counts as a member - which is\n * what makes {@link refreshCell} correct. If you supply a `matchFn` for use\n * with `refreshCell`, it MUST likewise be variant-stable (independent of the\n * currently-rendered variant), e.g. keyed on a separate logical-terrain grid\n * or a tile property.\n */\n matchFn?: (localTileId: number, tilesetIndex: number, x: number, y: number) => boolean;\n /**\n * When `true` (default), out-of-bounds neighbors are treated as though\n * they belong to the Wang group, so border tiles fill correctly to the\n * layer edge. Set to `false` to leave border tiles visually open.\n */\n wrapBorder?: boolean;\n}\n\n// ── Module-level mask helpers ─────────────────────────────────────────────\n\n/**\n * Compute a 4-bit edge bitmask for the cell at `(tx, ty)`.\n * Top=1, Right=2, Bottom=4, Left=8.\n */\nconst computeEdgeMask = (tx: number, ty: number, inGroup: (nx: number, ny: number) => boolean): number => {\n let mask = 0;\n if (inGroup(tx, ty - 1)) mask |= 1;\n if (inGroup(tx + 1, ty)) mask |= 2;\n if (inGroup(tx, ty + 1)) mask |= 4;\n if (inGroup(tx - 1, ty)) mask |= 8;\n return mask;\n};\n\n/**\n * Compute an 8-bit blob bitmask for the cell at `(tx, ty)` using the\n * corner-dependency rule: diagonal bits are only set when both adjacent\n * cardinal directions are also set.\n *\n * Bit layout:\n * ```\n * 1 | 2 | 4\n * 8 | -- | 16\n * 32 | 64 | 128\n * ```\n */\nconst computeBlobMask = (tx: number, ty: number, inGroup: (nx: number, ny: number) => boolean): number => {\n const top = inGroup(tx, ty - 1);\n const right = inGroup(tx + 1, ty);\n const bottom = inGroup(tx, ty + 1);\n const left = inGroup(tx - 1, ty);\n let mask = 0;\n if (top) mask |= 2;\n if (right) mask |= 16;\n if (bottom) mask |= 64;\n if (left) mask |= 8;\n // Corner bits: only when BOTH adjacent cardinals are set.\n if (top && left && inGroup(tx - 1, ty - 1)) mask |= 1;\n if (top && right && inGroup(tx + 1, ty - 1)) mask |= 4;\n if (bottom && left && inGroup(tx - 1, ty + 1)) mask |= 32;\n if (bottom && right && inGroup(tx + 1, ty + 1)) mask |= 128;\n return mask;\n};\n\n/** Compute the mask for a cell given the Wang mode and a membership predicate. */\nconst computeMask = (wangSet: WangSet, tx: number, ty: number, inGroup: (nx: number, ny: number) => boolean): number =>\n wangSet.type === 'edge' ? computeEdgeMask(tx, ty, inGroup) : computeBlobMask(tx, ty, inGroup);\n\n/**\n * Apply the variant computed for cell `(tx, ty)` to `layer`, preserving the\n * cell's current orientation transform. No-op if the mask has no mapping or\n * the target tileset is missing.\n */\nconst applyVariant = (layer: TileLayer, wangSet: WangSet, tx: number, ty: number, inGroup: (nx: number, ny: number) => boolean): void => {\n const newLocalTileId = wangSet.getTileId(computeMask(wangSet, tx, ty, inGroup));\n if (newLocalTileId === undefined) return;\n\n const tileset = layer.tilesets[wangSet.tilesetIndex];\n if (!tileset) return;\n\n // Preserve the existing orientation transform if the cell already holds one.\n const existing = layer.getTileAt(tx, ty);\n layer.setTileAt(tx, ty, {\n localTileId: newLocalTileId,\n tileset,\n transform: existing ? existing.transform : TILE_TRANSFORM_IDENTITY,\n });\n};\n\n// ── Public API ────────────────────────────────────────────────────────────\n\n/**\n * Apply Wang autotiling to `layer` using `wangSet`.\n *\n * Iterates every cell in the layer. For each cell that belongs to the Wang\n * group, computes a neighbor bitmask and looks up the correct local tile ID\n * from {@link WangSet.blobMap}. The layer is mutated in place; cells whose\n * computed bitmask has no mapping in the blobMap are left unchanged.\n *\n * The function performs two passes (snapshot then write) so neighbor tests\n * always reflect the pre-call state regardless of processing order.\n *\n * **Group membership.** With no `matchFn`, a cell belongs to the group when its\n * `tilesetIndex` matches {@link WangSet.tilesetIndex} and its `localTileId` is a\n * {@link WangSet.isMember member} of the set. Membership covers every variant\n * the set can produce, so re-running `autoTile` (or {@link refreshCell}) on\n * already-autotiled data is stable. Paint with a tile ID that is a member (a\n * blobMap value, or one listed in {@link WangSetOptions.members}).\n *\n * **Blob mode bitmask (bit positions):**\n * ```\n * 1 | 2 | 4\n * 8 | -- | 16\n * 32 | 64 | 128\n * ```\n * Diagonal bits (1, 4, 32, 128) are only set when both adjacent cardinals\n * are also set (the \"corner dependency\" rule that reduces 256 raw\n * combinations to 47 valid blob states).\n *\n * **Edge mode bitmask (bit positions):** Top=1, Right=2, Bottom=4, Left=8.\n *\n * @throws If `layer` is unbounded (no fixed width/height) - there is no\n * \"whole layer\" to sweep. Use {@link refreshCell} to autotile an unbounded\n * layer incrementally, one edited cell at a time.\n */\nexport const autoTile = (layer: TileLayer, wangSet: WangSet, options?: AutoTileOptions): void => {\n const matchFn = options?.matchFn;\n const wrapBorder = options?.wrapBorder ?? true;\n const w = layer.width;\n const h = layer.height;\n if (w === undefined || h === undefined) {\n throw new Error('autoTile() requires a bounded layer (sweeps the whole layer) — use refreshCell() on an unbounded layer instead.');\n }\n\n // ── Pass 1: snapshot ─────────────────────────────────────────────────\n // Capture each cell's (tilesetIndex, localTileId) before any writes so\n // that neighbor membership tests always see pre-mutation state.\n\n interface CellInfo {\n tilesetIndex: number;\n localTileId: number;\n }\n const snapshot = new Map<number, CellInfo>();\n\n for (let ty = 0; ty < h; ty++) {\n for (let tx = 0; tx < w; tx++) {\n const packed = layer.getRawTileAt(tx, ty);\n if (packed === 0) continue;\n const decoded = unpackTile(packed);\n if (!decoded) continue;\n snapshot.set(ty * w + tx, decoded);\n }\n }\n\n // ── Membership test (reads the snapshot) ─────────────────────────────\n\n const isInGroup = (nx: number, ny: number): boolean => {\n if (nx < 0 || nx >= w || ny < 0 || ny >= h) return wrapBorder;\n const cell = snapshot.get(ny * w + nx);\n if (!cell) return false;\n if (matchFn) return matchFn(cell.localTileId, cell.tilesetIndex, nx, ny);\n return cell.tilesetIndex === wangSet.tilesetIndex && wangSet.isMember(cell.localTileId);\n };\n\n // ── Pass 2: compute masks and write ──────────────────────────────────\n\n for (let ty = 0; ty < h; ty++) {\n for (let tx = 0; tx < w; tx++) {\n const cellInfo = snapshot.get(ty * w + tx);\n if (!cellInfo) continue;\n\n // Skip cells that are not part of the group.\n const isMember = matchFn\n ? matchFn(cellInfo.localTileId, cellInfo.tilesetIndex, tx, ty)\n : cellInfo.tilesetIndex === wangSet.tilesetIndex && wangSet.isMember(cellInfo.localTileId);\n if (!isMember) continue;\n\n applyVariant(layer, wangSet, tx, ty, isInGroup);\n }\n }\n};\n\n/**\n * Incrementally re-autotile a single cell and its eight neighbours after an\n * edit, instead of re-running {@link autoTile} over the whole layer.\n *\n * A cell's blob/edge mask depends only on its immediate neighbours, so painting\n * or erasing cell `(x, y)` can change the variant of that cell and of the (up\n * to) eight cells that have it as a neighbour - but nothing further out. This\n * recomputes exactly that 3×3 neighbourhood, touching only the 1-4 chunks it\n * spans (and rebuilding geometry only for chunks whose tiles actually change).\n * For a paint operation this is O(1) work versus `autoTile`'s O(width·height).\n *\n * Membership is read live from the layer, so the membership test MUST be\n * variant-stable: the default ({@link WangSet.isMember}) is, and any custom\n * `matchFn` you pass must be too (see {@link AutoTileOptions.matchFn}).\n *\n * Typical editor use: write the painted tile with `layer.setTileAt(...)` using\n * a member tile ID, then call `refreshCell(layer, x, y, wangSet)`.\n *\n * @param layer The layer to update in place.\n * @param x Tile X of the edited cell.\n * @param y Tile Y of the edited cell.\n * @param wangSet The Wang set to resolve variants from.\n * @param options Membership / border options (shared with {@link autoTile}).\n */\nexport const refreshCell = (layer: TileLayer, x: number, y: number, wangSet: WangSet, options?: AutoTileOptions): void => {\n const matchFn = options?.matchFn;\n const wrapBorder = options?.wrapBorder ?? true;\n const w = layer.width;\n const h = layer.height;\n\n // Live membership test (reads the current layer; variant-stable by default).\n const isInGroup = (nx: number, ny: number): boolean => {\n if ((w !== undefined && (nx < 0 || nx >= w)) || (h !== undefined && (ny < 0 || ny >= h))) return wrapBorder;\n const tile = layer.getTileAt(nx, ny);\n if (!tile) return false;\n const tsi = layer.tilesets.indexOf(tile.tileset);\n if (matchFn) return matchFn(tile.localTileId, tsi, nx, ny);\n return tsi === wangSet.tilesetIndex && wangSet.isMember(tile.localTileId);\n };\n\n // Recompute the edited cell and its eight neighbours.\n for (let dy = -1; dy <= 1; dy++) {\n for (let dx = -1; dx <= 1; dx++) {\n const tx = x + dx;\n const ty = y + dy;\n if (!layer.inBounds(tx, ty)) continue;\n if (!isInGroup(tx, ty)) continue;\n applyVariant(layer, wangSet, tx, ty, isInGroup);\n }\n }\n};\n"],"mappings":"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"}
@@ -6,7 +6,7 @@ import type { TileTransform } from './types';
6
6
  * One textured tile quad in chunk-local pixel space.
7
7
  *
8
8
  * `u0/v0/u1/v1` are the raw (min/max) source UV bounds of the tile within its
9
- * tileset texture flip/orientation is **not** baked here; it travels in
9
+ * tileset texture - flip/orientation is **not** baked here; it travels in
10
10
  * {@link TileQuad.orient} and is applied by the per-backend renderer (flipX/Y
11
11
  * via UV-corner swap, diagonal via an axis swap in the shader). Keeping the
12
12
  * geometry orientation-neutral lets both backends share one CPU builder.
@@ -42,23 +42,23 @@ export interface ChunkPage {
42
42
  readonly quads: readonly TileQuad[];
43
43
  }
44
44
  /** Pack a {@link TileTransform} into a 3-bit orientation code. @internal */
45
- export declare function orientCode(transform: TileTransform): number;
45
+ export declare const orientCode: (transform: TileTransform) => number;
46
46
  /** Read the cumulative chunk-geometry rebuild count. @internal */
47
- export declare function getTileGeometryRebuildCount(): number;
47
+ export declare const getTileGeometryRebuildCount: () => number;
48
48
  /** Reset the chunk-geometry rebuild counter (call before a measured frame). @internal */
49
- export declare function resetTileGeometryRebuildCount(): void;
49
+ export declare const resetTileGeometryRebuildCount: () => void;
50
50
  /**
51
51
  * Build per-tileset page geometry for a single chunk.
52
52
  *
53
53
  * Iterates the chunk's packed cells in deterministic row-major order, skipping
54
54
  * empties, out-of-range tilesets, and out-of-range local tile ids (all treated
55
- * as empty this is the renderer's half of the G-GID contract). Each surviving
55
+ * as empty - this is the renderer's half of the G-GID contract). Each surviving
56
56
  * cell is resolved to its source UV rect (from `tileset.getTileRect` + the
57
57
  * tileset `TextureRegion`) and a chunk-local destination rect (bottom-left
58
58
  * aligned per Tiled orthogonal semantics, so tilesets whose tiles are taller
59
59
  * than the map grid extend upward).
60
60
  *
61
- * Allocation happens here only the result is cached on the owning
61
+ * Allocation happens here only - the result is cached on the owning
62
62
  * {@link import('./TileChunkNode').TileChunkNode} keyed by `chunk.revision`, so
63
63
  * unchanged chunks never rebuild and the per-frame path stays allocation-free.
64
64
  *
@@ -68,4 +68,5 @@ export declare function resetTileGeometryRebuildCount(): void;
68
68
  * @param tileHeight Map/layer tile cell height in pixels.
69
69
  * @internal
70
70
  */
71
- export declare function buildChunkPages(chunk: ReadonlyTileChunk, tilesets: readonly TileSet[], tileWidth: number, tileHeight: number): ChunkPage[];
71
+ export declare const buildChunkPages: (chunk: ReadonlyTileChunk, tilesets: readonly TileSet[], tileWidth: number, tileHeight: number) => ChunkPage[];
72
+ //# sourceMappingURL=chunkGeometry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunkGeometry.d.ts","sourceRoot":"","sources":["../../src/chunkGeometry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAE7C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAG7C;;;;;;;;;GASG;AACH,MAAM,WAAW,QAAQ;IACvB,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,iEAAiE;IACjE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,oDAAoD;IACpD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,6EAA6E;IAC7E,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,sEAAsE;IACtE,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;CACrC;AAED,4EAA4E;AAC5E,eAAO,MAAM,UAAU,GAAI,WAAW,aAAa,KAAG,MAA8F,CAAC;AASrJ,kEAAkE;AAClE,eAAO,MAAM,2BAA2B,QAAO,MAAkC,CAAC;AAElF,yFAAyF;AACzF,eAAO,MAAM,6BAA6B,QAAO,IAEhD,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,eAAe,GAAI,OAAO,iBAAiB,EAAE,UAAU,SAAS,OAAO,EAAE,EAAE,WAAW,MAAM,EAAE,YAAY,MAAM,KAAG,SAAS,EAyFxI,CAAC"}