@codexo/exojs-tilemap 0.15.2 → 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,342 +1,389 @@
1
- import { assertPixelSnapMode } from './pixelSnap.js';
2
- import { TileLayerNode } from './TileLayerNode.js';
3
- import { TileMapBand } from './TileMapBand.js';
1
+ import { ImageLayer } from "./ImageLayer.js";
2
+ import { assertPixelSnapMode } from "./pixelSnap.js";
3
+ import { ImageLayerNode } from "./ImageLayerNode.js";
4
+ import { TileLayerNode } from "./TileLayerNode.js";
5
+ import { TileMapBand } from "./TileMapBand.js";
6
+ import { PixelSnapMode } from "@codexo/exojs/renderer-sdk";
4
7
 
8
+ //#region src/TileMapView.ts
5
9
  /**
6
- * Groups a {@link TileMap}'s layers into independently placeable scene nodes so
7
- * application actors can be interleaved **between** tile layers.
8
- *
9
- * A view produces exactly one canonical {@link TileLayerNode} per map layer
10
- * (stable identity, map document order) and, optionally, named {@link TileMapBand}s
11
- * grouping subsets of those nodes. The application parents the bands / layer
12
- * nodes wherever it wants typically as siblings of its own actor containers:
13
- *
14
- * ```ts
15
- * const view = map.createView({ bands: { ground: ['background', 'ground'], roof: ['roofs'] } });
16
- * worldRoot.addChild(view.band('ground'), actors, view.band('roof'));
17
- * // or without bands:
18
- * worldRoot.addChild(view.getLayerNodeById(groundId)!, actors, view.getLayerNodeById(roofId)!);
19
- * ```
20
- *
21
- * **Actors are application-owned siblings.** A `TileMapView` never adopts or
22
- * destroys actors. The view is a helper, not a scene node it does not own the
23
- * world container, the {@link TileMap}, the {@link TileLayer}s, or tileset
24
- * textures.
25
- *
26
- * **Ownership:** the view owns its generated layer nodes and bands. A layer
27
- * assigned to a band is owned by that band; an unbanded layer is owned by the
28
- * view directly. {@link TileMapView.destroy} destroys every band and layer node
29
- * (detaching them from their application parents) but leaves actors, the map,
30
- * its layers, and Loader-owned textures untouched. There is no map-replacement
31
- * mutation API: to swap maps, destroy the old view, construct a new one, and
32
- * re-parent its bands the actor tree is never involved.
33
- *
34
- * @advanced
35
- */
36
- class TileMapView {
37
- _map;
38
- _cullable;
39
- /** All canonical layer nodes, in map document order. */
40
- _layerNodes = [];
41
- /** Layer id its canonical layer node. */
42
- _layerNodeById = new Map();
43
- /** Bands in definition (insertion) order. */
44
- _bands = [];
45
- /** Band name → band. */
46
- _bandByName = new Map();
47
- /** Original band definitions (frozen) for re-resolution on refresh. */
48
- _bandDefs = [];
49
- /** Layer node → its owning band (absent = view-owned / unbanded). */
50
- _nodeBand = new Map();
51
- /** Unbanded layer nodes owned directly by the view, in map document order. */
52
- _directLayerNodes = [];
53
- _destroyed = false;
54
- _pixelSnapMode = 'none';
55
- /**
56
- * @param map The runtime map to compose. Referenced, never owned.
57
- * @param options Band definitions and culling.
58
- * @throws When a band definition references an unknown/ambiguous layer or
59
- * assigns a layer twice / to multiple bands.
60
- */
61
- constructor(map, options) {
62
- this._map = map;
63
- this._cullable = options?.cullable ?? true;
64
- for (const layer of map.layers) {
65
- const node = new TileLayerNode(layer, { cullable: this._cullable });
66
- this._layerNodes.push(node);
67
- this._layerNodeById.set(layer.id, node);
68
- }
69
- if (options?.bands) {
70
- for (const [name, selectors] of Object.entries(options.bands)) {
71
- this._defineBand(name, selectors);
72
- }
73
- }
74
- for (const node of this._layerNodes) {
75
- if (!this._nodeBand.has(node)) {
76
- this._directLayerNodes.push(node);
77
- }
78
- }
79
- }
80
- /** The runtime map this view composes. Referenced, never owned. */
81
- get map() {
82
- return this._map;
83
- }
84
- /** All canonical layer nodes, one per map layer, in map document order. */
85
- get layers() {
86
- return this._layerNodes;
87
- }
88
- /** The bands, in definition (insertion) order. */
89
- get bands() {
90
- return this._bands;
91
- }
92
- /** Whether this view has been destroyed. */
93
- get destroyed() {
94
- return this._destroyed;
95
- }
96
- /**
97
- * Render-only pixel-snap mode applied to every layer node this view owns (and
98
- * forwarded to every chunk drawable, current and rebuilt by
99
- * {@link refreshLayers}). Snaps tile chunk origins to the active render
100
- * target's device-pixel grid for crisp tiles; with integer tile pitch the grid
101
- * stays exact and adjacent chunks cannot drift apart. Purely visual — tile
102
- * data, layer offsets, chunk revisions, and culling are unchanged. Setting the
103
- * current value is a no-op; an invalid value throws and leaves the prior mode
104
- * unchanged.
105
- *
106
- * @default 'none'
107
- * @stable
108
- */
109
- get pixelSnapMode() {
110
- return this._pixelSnapMode;
111
- }
112
- set pixelSnapMode(mode) {
113
- if (mode === this._pixelSnapMode) {
114
- return;
115
- }
116
- assertPixelSnapMode(mode);
117
- this._pixelSnapMode = mode;
118
- for (const node of this._layerNodes) {
119
- node.pixelSnapMode = mode;
120
- }
121
- }
122
- /**
123
- * The canonical layer node for the layer with the given **id**, or
124
- * `undefined`. Ids are authoritative and unique — this is the unambiguous
125
- * lookup. The returned node may be reparented into the caller's own
126
- * containers; the view still tracks it for refresh and destruction.
127
- */
128
- getLayerNodeById(id) {
129
- return this._layerNodeById.get(id);
130
- }
131
- /**
132
- * Every canonical layer node whose layer has the given **name**, in map
133
- * document order. Layer names are not guaranteed unique, so this returns an
134
- * array (empty when no layer matches). Prefer {@link getLayerNodeById} when
135
- * you have the id.
136
- */
137
- getLayerNodesByName(name) {
138
- return this._layerNodes.filter(node => node.layer.name === name);
139
- }
140
- /**
141
- * The band registered under `name`.
142
- * @throws If no band with that name was defined.
143
- */
144
- band(name) {
145
- const band = this._bandByName.get(name);
146
- if (!band) {
147
- throw new Error(`TileMapView has no band named "${name}". Defined bands: ${this._bands.length ? this._bands.map(b => `"${b.name}"`).join(', ') : '(none)'}.`);
148
- }
149
- return band;
150
- }
151
- /** Whether a band with the given name was defined. */
152
- hasBand(name) {
153
- return this._bandByName.has(name);
154
- }
155
- /**
156
- * Rebuild the view after **structural** map changes (layers added to or
157
- * removed from the map). Ordinary tile edits and chunk creation/removal do
158
- * NOT need this those are handled by chunk revisions and
159
- * {@link TileLayerNode.refresh} respectively.
160
- *
161
- * - Removed layers: their generated layer node is detached and destroyed.
162
- * - Unchanged layers: keep their existing layer node (stable identity).
163
- * - Added layers: a new layer node is created and assigned to the first band
164
- * whose definition selects it (by id, or by a currently-unambiguous name),
165
- * otherwise owned directly by the view.
166
- * - Every band's children are re-ordered to map document order.
167
- *
168
- * Application actors are never touched, and bands keep their placement in the
169
- * application scene graph.
170
- *
171
- * @throws If the view has been destroyed.
172
- */
173
- refreshLayers() {
174
- if (this._destroyed) {
175
- throw new Error('Cannot refresh a destroyed TileMapView.');
176
- }
177
- const currentLayers = this._map.layers;
178
- const currentIds = new Set();
179
- for (const layer of currentLayers) {
180
- currentIds.add(layer.id);
181
- }
182
- // 1. Remove nodes whose layer no longer exists.
183
- for (const node of [...this._layerNodes]) {
184
- if (!currentIds.has(node.layer.id)) {
185
- this._removeNode(node);
186
- }
187
- }
188
- // 2. Re-derive the doc-ordered node list, creating + assigning new nodes.
189
- const newOrder = [];
190
- for (const layer of currentLayers) {
191
- let node = this._layerNodeById.get(layer.id);
192
- if (!node) {
193
- node = new TileLayerNode(layer, { cullable: this._cullable });
194
- if (this._pixelSnapMode !== 'none') {
195
- node.pixelSnapMode = this._pixelSnapMode;
196
- }
197
- this._layerNodeById.set(layer.id, node);
198
- this._assignNewNode(node, layer);
199
- }
200
- newOrder.push(node);
201
- }
202
- this._layerNodes.length = 0;
203
- this._layerNodes.push(...newOrder);
204
- // 3. Re-order each band's children to map document order.
205
- const documentIndexById = new Map();
206
- for (const [index, layer] of currentLayers.entries())
207
- documentIndexById.set(layer.id, index);
208
- for (const band of this._bands) {
209
- band._reorder(documentIndexById);
210
- }
211
- // 4. Rebuild the unbanded set in doc order.
212
- this._directLayerNodes.length = 0;
213
- for (const node of newOrder) {
214
- if (!this._nodeBand.has(node)) {
215
- this._directLayerNodes.push(node);
216
- }
217
- }
218
- return this;
219
- }
220
- /**
221
- * Destroy the view: every band and generated layer node is destroyed (and
222
- * detached from its application parent), freeing cached chunk geometry.
223
- * Idempotent. Application actors, sibling content, the {@link TileMap}, its
224
- * {@link TileLayer}s, and Loader-owned tileset textures all survive.
225
- */
226
- destroy() {
227
- if (this._destroyed) {
228
- return;
229
- }
230
- this._destroyed = true;
231
- for (const band of this._bands) {
232
- band.destroy();
233
- }
234
- for (const node of this._directLayerNodes) {
235
- node.parent?.removeChild(node);
236
- node.destroy();
237
- }
238
- this._bands.length = 0;
239
- this._bandByName.clear();
240
- this._bandDefs.length = 0;
241
- this._directLayerNodes.length = 0;
242
- this._layerNodes.length = 0;
243
- this._layerNodeById.clear();
244
- this._nodeBand.clear();
245
- }
246
- // ── Internals ──────────────────────────────────────────────────────────
247
- /** Resolve one band definition into a {@link TileMapBand} and record it. */
248
- _defineBand(name, selectors) {
249
- const memberIds = new Set();
250
- for (const selector of selectors) {
251
- const id = this._resolveSelector(name, selector);
252
- if (memberIds.has(id)) {
253
- throw new Error(`TileMapView band "${name}" lists layer ${id} more than once.`);
254
- }
255
- const existing = this._layerNodeById.get(id);
256
- if (existing && this._nodeBand.has(existing)) {
257
- throw new Error(`TileMapView layer ${id} is assigned to multiple bands ` +
258
- `("${this._nodeBand.get(existing).name}" and "${name}").`);
259
- }
260
- memberIds.add(id);
261
- }
262
- // Order members by map document order (membership selects; doc order renders).
263
- const orderedNodes = this._layerNodes.filter(node => memberIds.has(node.layer.id));
264
- const band = new TileMapBand(name, orderedNodes);
265
- for (const node of orderedNodes) {
266
- this._nodeBand.set(node, band);
267
- }
268
- this._bands.push(band);
269
- this._bandByName.set(name, band);
270
- this._bandDefs.push({ name, selectors: Object.freeze([...selectors]) });
271
- }
272
- /** Resolve a single selector to a layer id, throwing on unknown/ambiguous. */
273
- _resolveSelector(bandName, selector) {
274
- if (typeof selector === 'number') {
275
- if (!this._layerNodeById.has(selector)) {
276
- throw new Error(`TileMapView band "${bandName}": no layer with id ${selector} in map "${this._map.name}".`);
277
- }
278
- return selector;
279
- }
280
- const matches = this._map.layers.filter(layer => layer.name === selector);
281
- if (matches.length === 0) {
282
- throw new Error(`TileMapView band "${bandName}": no layer named "${selector}" in map "${this._map.name}".`);
283
- }
284
- if (matches.length > 1) {
285
- throw new Error(`TileMapView band "${bandName}": layer name "${selector}" is ambiguous ` +
286
- `(${matches.length} layers share it); reference it by id instead.`);
287
- }
288
- return matches[0].id;
289
- }
290
- /** Assign a freshly created node to the first band that selects its layer. */
291
- _assignNewNode(node, layer) {
292
- for (const def of this._bandDefs) {
293
- if (this._definitionSelects(def, layer)) {
294
- const band = this._bandByName.get(def.name);
295
- band._adopt(node);
296
- this._nodeBand.set(node, band);
297
- return;
298
- }
299
- }
300
- }
301
- /** Whether a band definition selects the given layer (unambiguously by name). */
302
- _definitionSelects(def, layer) {
303
- for (const selector of def.selectors) {
304
- if (typeof selector === 'number') {
305
- if (selector === layer.id) {
306
- return true;
307
- }
308
- }
309
- else if (selector === layer.name) {
310
- const sameName = this._map.layers.filter(other => other.name === selector).length;
311
- if (sameName === 1) {
312
- return true;
313
- }
314
- }
315
- }
316
- return false;
317
- }
318
- /** Detach + destroy a node, dropping it from band membership and registries. */
319
- _removeNode(node) {
320
- const band = this._nodeBand.get(node);
321
- if (band) {
322
- band._release(node);
323
- this._nodeBand.delete(node);
324
- }
325
- else {
326
- const directIndex = this._directLayerNodes.indexOf(node);
327
- if (directIndex !== -1) {
328
- this._directLayerNodes.splice(directIndex, 1);
329
- }
330
- }
331
- node.parent?.removeChild(node);
332
- this._layerNodeById.delete(node.layer.id);
333
- const orderIndex = this._layerNodes.indexOf(node);
334
- if (orderIndex !== -1) {
335
- this._layerNodes.splice(orderIndex, 1);
336
- }
337
- node.destroy();
338
- }
339
- }
10
+ * Groups a {@link TileMap}'s layers into independently placeable scene nodes so
11
+ * application actors can be interleaved **between** tile layers.
12
+ *
13
+ * A view produces exactly one canonical {@link TileLayerNode} per map layer
14
+ * (stable identity, map document order) and, optionally, named {@link TileMapBand}s
15
+ * grouping subsets of those nodes. The application parents the bands / layer
16
+ * nodes wherever it wants - typically as siblings of its own actor containers:
17
+ *
18
+ * ```ts
19
+ * const view = map.createView({ bands: { ground: ['background', 'ground'], roof: ['roofs'] } });
20
+ * worldRoot.addChild(view.band('ground'), actors, view.band('roof'));
21
+ * // or without bands:
22
+ * worldRoot.addChild(view.getLayerNodeById(groundId)!, actors, view.getLayerNodeById(roofId)!);
23
+ * ```
24
+ *
25
+ * **Actors are application-owned siblings.** A `TileMapView` never adopts or
26
+ * destroys actors. The view is a helper, not a scene node - it does not own the
27
+ * world container, the {@link TileMap}, the {@link TileLayer}s, or tileset
28
+ * textures.
29
+ *
30
+ * **Ownership:** the view owns its generated layer nodes and bands. A layer
31
+ * assigned to a band is owned by that band; an unbanded layer is owned by the
32
+ * view directly. {@link TileMapView.destroy} destroys every band and layer node
33
+ * (detaching them from their application parents) but leaves actors, the map,
34
+ * its layers, and Loader-owned textures untouched. There is no map-replacement
35
+ * mutation API: to swap maps, destroy the old view, construct a new one, and
36
+ * re-parent its bands - the actor tree is never involved.
37
+ *
38
+ * **Image layers.** A view also produces exactly one canonical
39
+ * {@link ImageLayerNode} per {@link TileMap.imageLayers} entry (stable identity,
40
+ * map document order), reachable through {@link imageLayerNodes},
41
+ * {@link getImageLayerNodeById}, and {@link getImageLayerNodeByName}. Image
42
+ * layers are selectable in {@link TileMapViewOptions.bands} exactly like tile
43
+ * layers: a band member list may mix both kinds, and the band stacks its
44
+ * members by the map's combined document order
45
+ * ({@link import('./TileMap').TileMap.renderableLayers}). A banded image node
46
+ * is owned by its band; an unbanded one is owned by the view directly, and the
47
+ * application parents it wherever the image belongs in draw order - the same
48
+ * way actors are interleaved.
49
+ *
50
+ * @advanced
51
+ */
52
+ var TileMapView = class {
53
+ _map;
54
+ _cullable;
55
+ /** All canonical tile-layer nodes, in map document order. */
56
+ _layerNodes = [];
57
+ /** Tile layer id → its canonical layer node. */
58
+ _layerNodeById = /* @__PURE__ */ new Map();
59
+ /** Bands in definition (insertion) order. */
60
+ _bands = [];
61
+ /** Band name band. */
62
+ _bandByName = /* @__PURE__ */ new Map();
63
+ /** Original band definitions (frozen) for re-resolution on refresh. */
64
+ _bandDefs = [];
65
+ /** Layer node → its owning band (absent = view-owned / unbanded). */
66
+ _nodeBand = /* @__PURE__ */ new Map();
67
+ /** Unbanded tile-layer nodes owned directly by the view, in map document order. */
68
+ _directLayerNodes = [];
69
+ /** All canonical image layer nodes, in map document order. */
70
+ _imageLayerNodes = [];
71
+ /** Image layer id → its canonical image layer node. */
72
+ _imageLayerNodeById = /* @__PURE__ */ new Map();
73
+ _destroyed = false;
74
+ _pixelSnapMode = PixelSnapMode.None;
75
+ /**
76
+ * @param map The runtime map to compose. Referenced, never owned.
77
+ * @param options Band definitions and culling.
78
+ * @throws When a band definition references an unknown/ambiguous layer or
79
+ * assigns a layer twice / to multiple bands.
80
+ */
81
+ constructor(map, options) {
82
+ this._map = map;
83
+ this._cullable = options?.cullable ?? true;
84
+ for (const layer of map.layers) {
85
+ const node = new TileLayerNode(layer, { cullable: this._cullable });
86
+ this._layerNodes.push(node);
87
+ this._layerNodeById.set(layer.id, node);
88
+ }
89
+ for (const imageLayer of map.imageLayers) {
90
+ const imageNode = new ImageLayerNode(imageLayer);
91
+ this._imageLayerNodes.push(imageNode);
92
+ this._imageLayerNodeById.set(imageLayer.id, imageNode);
93
+ }
94
+ if (options?.bands) for (const [name, selectors] of Object.entries(options.bands)) this._defineBand(name, selectors);
95
+ for (const node of this._layerNodes) if (!this._nodeBand.has(node)) this._directLayerNodes.push(node);
96
+ }
97
+ /** The runtime map this view composes. Referenced, never owned. */
98
+ get map() {
99
+ return this._map;
100
+ }
101
+ /** All canonical tile-layer nodes, one per map tile layer, in map document order. */
102
+ get layers() {
103
+ return this._layerNodes;
104
+ }
105
+ /** The bands, in definition (insertion) order. */
106
+ get bands() {
107
+ return this._bands;
108
+ }
109
+ /** Whether this view has been destroyed. */
110
+ get destroyed() {
111
+ return this._destroyed;
112
+ }
113
+ /**
114
+ * Render-only pixel-snap mode applied to every layer node this view owns (and
115
+ * forwarded to every chunk drawable, current and rebuilt by
116
+ * {@link refreshLayers}). Snaps tile chunk origins to the active render
117
+ * target's device-pixel grid for crisp tiles; with integer tile pitch the grid
118
+ * stays exact and adjacent chunks cannot drift apart. Purely visual - tile
119
+ * data, layer offsets, chunk revisions, and culling are unchanged. Setting the
120
+ * current value is a no-op; an invalid value throws and leaves the prior mode
121
+ * unchanged.
122
+ *
123
+ * @default PixelSnapMode.None
124
+ * @stable
125
+ */
126
+ get pixelSnapMode() {
127
+ return this._pixelSnapMode;
128
+ }
129
+ set pixelSnapMode(mode) {
130
+ if (mode === this._pixelSnapMode) return;
131
+ assertPixelSnapMode(mode);
132
+ this._pixelSnapMode = mode;
133
+ for (const node of this._layerNodes) node.pixelSnapMode = mode;
134
+ for (const node of this._imageLayerNodes) node.pixelSnapMode = mode;
135
+ }
136
+ /**
137
+ * The canonical layer node for the **tile** layer with the given **id**, or
138
+ * `undefined`. Tile ids are authoritative and unique - this is the
139
+ * unambiguous lookup. The returned node may be reparented into the caller's
140
+ * own containers; the view still tracks it for refresh and destruction.
141
+ */
142
+ getLayerNodeById(id) {
143
+ return this._layerNodeById.get(id);
144
+ }
145
+ /**
146
+ * Every canonical tile-layer node whose layer has the given **name**, in map
147
+ * document order. Layer names are not guaranteed unique, so this returns an
148
+ * array (empty when no layer matches). Prefer {@link getLayerNodeById} when
149
+ * you have the id.
150
+ */
151
+ getLayerNodesByName(name) {
152
+ return this._layerNodes.filter((node) => node.layer.name === name);
153
+ }
154
+ /** All canonical image layer nodes, one per map image layer, in map document order. */
155
+ get imageLayerNodes() {
156
+ return this._imageLayerNodes;
157
+ }
158
+ /**
159
+ * The canonical image layer node for the image layer with the given **id**,
160
+ * or `undefined`. Ids are authoritative and unique - this is the unambiguous
161
+ * lookup. The returned node may be reparented into the caller's own
162
+ * containers; the view still tracks it for refresh and destruction.
163
+ */
164
+ getImageLayerNodeById(id) {
165
+ return this._imageLayerNodeById.get(id);
166
+ }
167
+ /**
168
+ * The canonical image layer node for the image layer with the given
169
+ * **name**, or `undefined` if no image layer has that name. Prefer
170
+ * {@link getImageLayerNodeById} when you have the id.
171
+ * @throws If more than one image layer shares that name (reference such
172
+ * layers by id instead).
173
+ */
174
+ getImageLayerNodeByName(name) {
175
+ const matches = this._map.imageLayers.filter((layer) => layer.name === name);
176
+ if (matches.length === 0) return;
177
+ if (matches.length > 1) throw new Error(`TileMapView image layer name "${name}" is ambiguous (${matches.length} image layers share it); reference it by id instead.`);
178
+ return this._imageLayerNodeById.get(matches[0].id);
179
+ }
180
+ /**
181
+ * The band registered under `name`.
182
+ * @throws If no band with that name was defined.
183
+ */
184
+ band(name) {
185
+ const band = this._bandByName.get(name);
186
+ if (!band) throw new Error(`TileMapView has no band named "${name}". Defined bands: ${this._bands.length > 0 ? this._bands.map((b) => `"${b.name}"`).join(", ") : "(none)"}.`);
187
+ return band;
188
+ }
189
+ /** Whether a band with the given name was defined. */
190
+ hasBand(name) {
191
+ return this._bandByName.has(name);
192
+ }
193
+ /**
194
+ * Rebuild the view after **structural** map changes (tile or image layers
195
+ * added to or removed from the map). Ordinary tile edits and chunk
196
+ * creation/removal do NOT need this - those are handled by chunk revisions
197
+ * and {@link TileLayerNode.refresh} respectively.
198
+ *
199
+ * - Removed layers (tile or image): their generated node is detached and
200
+ * destroyed.
201
+ * - Unchanged layers: keep their existing node (stable identity).
202
+ * - Added layers (tile or image): a new node is created and assigned to the
203
+ * first band whose definition selects it (by a currently-unambiguous id or
204
+ * name), otherwise owned directly by the view.
205
+ * - Every band's children are re-ordered to the map's combined document
206
+ * order ({@link import('./TileMap').TileMap.renderableLayers}).
207
+ *
208
+ * Application actors are never touched, and bands keep their placement in the
209
+ * application scene graph.
210
+ *
211
+ * @throws If the view has been destroyed.
212
+ */
213
+ refreshLayers() {
214
+ if (this._destroyed) throw new Error("Cannot refresh a destroyed TileMapView.");
215
+ const currentLayers = this._map.layers;
216
+ const currentIds = /* @__PURE__ */ new Set();
217
+ for (const layer of currentLayers) currentIds.add(layer.id);
218
+ for (const node of [...this._layerNodes]) if (!currentIds.has(node.layer.id)) this._removeTileNode(node);
219
+ const currentImageLayers = new Set(this._map.imageLayers);
220
+ for (const node of [...this._imageLayerNodes]) if (!currentImageLayers.has(node.layer)) this._removeImageNode(node);
221
+ const newOrder = [];
222
+ for (const layer of currentLayers) {
223
+ let node = this._layerNodeById.get(layer.id);
224
+ if (!node) {
225
+ node = new TileLayerNode(layer, { cullable: this._cullable });
226
+ if (this._pixelSnapMode !== PixelSnapMode.None) node.pixelSnapMode = this._pixelSnapMode;
227
+ this._layerNodeById.set(layer.id, node);
228
+ this._assignNewNode(node, layer);
229
+ }
230
+ newOrder.push(node);
231
+ }
232
+ this._layerNodes.length = 0;
233
+ this._layerNodes.push(...newOrder);
234
+ const newImageOrder = [];
235
+ for (const imageLayer of this._map.imageLayers) {
236
+ let node = this._imageLayerNodes.find((candidate) => candidate.layer === imageLayer);
237
+ if (!node) {
238
+ node = new ImageLayerNode(imageLayer);
239
+ if (this._pixelSnapMode !== PixelSnapMode.None) node.pixelSnapMode = this._pixelSnapMode;
240
+ this._imageLayerNodeById.set(imageLayer.id, node);
241
+ this._assignNewNode(node, imageLayer);
242
+ }
243
+ newImageOrder.push(node);
244
+ }
245
+ this._imageLayerNodes.length = 0;
246
+ this._imageLayerNodes.push(...newImageOrder);
247
+ const documentIndex = /* @__PURE__ */ new Map();
248
+ for (const [index, layer] of this._map.renderableLayers.entries()) documentIndex.set(layer, index);
249
+ for (const band of this._bands) band._reorder(documentIndex);
250
+ this._directLayerNodes.length = 0;
251
+ for (const node of newOrder) if (!this._nodeBand.has(node)) this._directLayerNodes.push(node);
252
+ return this;
253
+ }
254
+ /**
255
+ * Destroy the view: every band, generated tile-layer node, and generated
256
+ * {@link ImageLayerNode} is destroyed (and detached from its application
257
+ * parent), freeing cached chunk geometry. Banded nodes are destroyed by
258
+ * their band; unbanded ones by the view directly. Idempotent. Application
259
+ * actors, sibling content, the {@link TileMap}, its {@link TileLayer}s and
260
+ * image layers, and Loader-owned textures all survive.
261
+ */
262
+ destroy() {
263
+ if (this._destroyed) return;
264
+ this._destroyed = true;
265
+ for (const band of this._bands) band.destroy();
266
+ for (const node of this._directLayerNodes) {
267
+ node.parent?.removeChild(node);
268
+ node.destroy();
269
+ }
270
+ for (const node of this._imageLayerNodes) {
271
+ if (this._nodeBand.has(node)) continue;
272
+ node.parent?.removeChild(node);
273
+ node.destroy();
274
+ }
275
+ this._bands.length = 0;
276
+ this._bandByName.clear();
277
+ this._bandDefs.length = 0;
278
+ this._directLayerNodes.length = 0;
279
+ this._layerNodes.length = 0;
280
+ this._layerNodeById.clear();
281
+ this._nodeBand.clear();
282
+ this._imageLayerNodes.length = 0;
283
+ this._imageLayerNodeById.clear();
284
+ }
285
+ /** Resolve one band definition into a {@link TileMapBand} and record it. */
286
+ _defineBand(name, selectors) {
287
+ const members = /* @__PURE__ */ new Set();
288
+ for (const selector of selectors) {
289
+ const layer = this._resolveSelector(name, selector);
290
+ if (members.has(layer)) throw new Error(`TileMapView band "${name}" lists layer ${layer.id} more than once.`);
291
+ const existing = this._nodeFor(layer);
292
+ if (existing && this._nodeBand.has(existing)) throw new Error(`TileMapView layer ${layer.id} is assigned to multiple bands ("${this._nodeBand.get(existing).name}" and "${name}").`);
293
+ members.add(layer);
294
+ }
295
+ const orderedNodes = [];
296
+ for (const layer of this._map.renderableLayers) if (members.has(layer)) orderedNodes.push(this._nodeFor(layer));
297
+ const band = new TileMapBand(name, orderedNodes);
298
+ for (const node of orderedNodes) this._nodeBand.set(node, band);
299
+ this._bands.push(band);
300
+ this._bandByName.set(name, band);
301
+ this._bandDefs.push({
302
+ name,
303
+ selectors: Object.freeze([...selectors])
304
+ });
305
+ }
306
+ /**
307
+ * Resolve a single selector to a tile or image layer instance, throwing on
308
+ * unknown/ambiguous. Instances (not ids) disambiguate fallback-ordered maps
309
+ * in which a tile layer and an image layer share an id.
310
+ */
311
+ _resolveSelector(bandName, selector) {
312
+ if (typeof selector === "number") {
313
+ const tileLayer = this._map.getTileLayerById(selector);
314
+ const imageLayer = this._map.imageLayers.find((layer) => layer.id === selector);
315
+ if (tileLayer && imageLayer) throw new Error(`TileMapView band "${bandName}": layer id ${selector} is ambiguous (a tile layer and an image layer share it); reference it by a unique name instead.`);
316
+ const layer = tileLayer ?? imageLayer;
317
+ if (!layer) throw new Error(`TileMapView band "${bandName}": no layer with id ${selector} in map "${this._map.name}".`);
318
+ return layer;
319
+ }
320
+ const matches = [...this._map.layers.filter((layer) => layer.name === selector), ...this._map.imageLayers.filter((layer) => layer.name === selector)];
321
+ if (matches.length === 0) throw new Error(`TileMapView band "${bandName}": no layer named "${selector}" in map "${this._map.name}".`);
322
+ if (matches.length > 1) throw new Error(`TileMapView band "${bandName}": layer name "${selector}" is ambiguous (${matches.length} layers share it); reference it by id instead.`);
323
+ return matches[0];
324
+ }
325
+ /** The canonical node for a tile or image layer instance, if one exists. */
326
+ _nodeFor(layer) {
327
+ if (layer instanceof ImageLayer) return this._imageLayerNodes.find((node) => node.layer === layer);
328
+ return this._layerNodeById.get(layer.id);
329
+ }
330
+ /** Assign a freshly created node to the first band that selects its layer. */
331
+ _assignNewNode(node, layer) {
332
+ for (const def of this._bandDefs) if (this._definitionSelects(def, layer)) {
333
+ const band = this._bandByName.get(def.name);
334
+ band._adopt(node);
335
+ this._nodeBand.set(node, band);
336
+ return;
337
+ }
338
+ }
339
+ /**
340
+ * Whether a band definition selects the given layer - only by a selector
341
+ * that is currently unambiguous (a unique name, or an id not shared across
342
+ * kinds), mirroring {@link _resolveSelector}'s construction-time rules.
343
+ */
344
+ _definitionSelects(def, layer) {
345
+ for (const selector of def.selectors) if (typeof selector === "number") {
346
+ if (selector === layer.id && !this._isCrossKindId(selector)) return true;
347
+ } else if (selector === layer.name) {
348
+ if (this._map.layers.filter((other) => other.name === selector).length + this._map.imageLayers.filter((other) => other.name === selector).length === 1) return true;
349
+ }
350
+ return false;
351
+ }
352
+ /** Whether an id currently belongs to both a tile layer and an image layer. */
353
+ _isCrossKindId(id) {
354
+ return this._map.getTileLayerById(id) !== void 0 && this._map.imageLayers.some((layer) => layer.id === id);
355
+ }
356
+ /** Detach + destroy a tile node, dropping it from bands and registries. */
357
+ _removeTileNode(node) {
358
+ this._releaseFromOwner(node);
359
+ node.parent?.removeChild(node);
360
+ this._layerNodeById.delete(node.layer.id);
361
+ const orderIndex = this._layerNodes.indexOf(node);
362
+ if (orderIndex !== -1) this._layerNodes.splice(orderIndex, 1);
363
+ node.destroy();
364
+ }
365
+ /** Detach + destroy an image node, dropping it from bands and registries. */
366
+ _removeImageNode(node) {
367
+ this._releaseFromOwner(node);
368
+ node.parent?.removeChild(node);
369
+ if (this._imageLayerNodeById.get(node.layer.id) === node) this._imageLayerNodeById.delete(node.layer.id);
370
+ const orderIndex = this._imageLayerNodes.indexOf(node);
371
+ if (orderIndex !== -1) this._imageLayerNodes.splice(orderIndex, 1);
372
+ node.destroy();
373
+ }
374
+ /** Release a node from its band, or from the view's direct-ownership list. */
375
+ _releaseFromOwner(node) {
376
+ const band = this._nodeBand.get(node);
377
+ if (band) {
378
+ band._release(node);
379
+ this._nodeBand.delete(node);
380
+ } else if (node instanceof TileLayerNode) {
381
+ const directIndex = this._directLayerNodes.indexOf(node);
382
+ if (directIndex !== -1) this._directLayerNodes.splice(directIndex, 1);
383
+ }
384
+ }
385
+ };
340
386
 
387
+ //#endregion
341
388
  export { TileMapView };
342
- //# sourceMappingURL=TileMapView.js.map
389
+ //# sourceMappingURL=TileMapView.js.map