@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.
- package/README.md +33 -26
- package/dist/esm/ChunkSource.d.ts +33 -0
- package/dist/esm/ChunkSource.d.ts.map +1 -0
- package/dist/esm/ChunkStreamer.d.ts +98 -0
- package/dist/esm/ChunkStreamer.d.ts.map +1 -0
- package/dist/esm/ChunkStreamer.js +201 -0
- package/dist/esm/ChunkStreamer.js.map +1 -0
- package/dist/esm/ImageLayer.d.ts +11 -3
- package/dist/esm/ImageLayer.d.ts.map +1 -0
- package/dist/esm/ImageLayer.js +71 -64
- package/dist/esm/ImageLayer.js.map +1 -1
- package/dist/esm/ImageLayerNode.d.ts +89 -0
- package/dist/esm/ImageLayerNode.d.ts.map +1 -0
- package/dist/esm/ImageLayerNode.js +205 -0
- package/dist/esm/ImageLayerNode.js.map +1 -0
- package/dist/esm/MapObject.d.ts +70 -0
- package/dist/esm/MapObject.d.ts.map +1 -0
- package/dist/esm/MapObject.js +34 -0
- package/dist/esm/MapObject.js.map +1 -0
- package/dist/esm/MapObjectSpawner.d.ts +133 -0
- package/dist/esm/MapObjectSpawner.d.ts.map +1 -0
- package/dist/esm/MapObjectSpawner.js +147 -0
- package/dist/esm/MapObjectSpawner.js.map +1 -0
- package/dist/esm/MapSpawnSession.d.ts +53 -0
- package/dist/esm/MapSpawnSession.d.ts.map +1 -0
- package/dist/esm/MapSpawnSession.js +88 -0
- package/dist/esm/MapSpawnSession.js.map +1 -0
- package/dist/esm/MapWorld.d.ts +134 -0
- package/dist/esm/MapWorld.d.ts.map +1 -0
- package/dist/esm/MapWorld.js +150 -0
- package/dist/esm/MapWorld.js.map +1 -0
- package/dist/esm/MapWorldRuntime.d.ts +182 -0
- package/dist/esm/MapWorldRuntime.d.ts.map +1 -0
- package/dist/esm/MapWorldRuntime.js +261 -0
- package/dist/esm/MapWorldRuntime.js.map +1 -0
- package/dist/esm/ObjectLayer.d.ts +93 -7
- package/dist/esm/ObjectLayer.d.ts.map +1 -0
- package/dist/esm/ObjectLayer.js +184 -167
- package/dist/esm/ObjectLayer.js.map +1 -1
- package/dist/esm/SampledChunkSource.d.ts +43 -0
- package/dist/esm/SampledChunkSource.d.ts.map +1 -0
- package/dist/esm/SampledChunkSource.js +48 -0
- package/dist/esm/SampledChunkSource.js.map +1 -0
- package/dist/esm/TileAnimator.d.ts +12 -2
- package/dist/esm/TileAnimator.d.ts.map +1 -0
- package/dist/esm/TileAnimator.js +154 -165
- package/dist/esm/TileAnimator.js.map +1 -1
- package/dist/esm/TileChunk.d.ts +30 -5
- package/dist/esm/TileChunk.d.ts.map +1 -0
- package/dist/esm/TileChunk.js +190 -188
- package/dist/esm/TileChunk.js.map +1 -1
- package/dist/esm/TileChunkNode.d.ts +18 -3
- package/dist/esm/TileChunkNode.d.ts.map +1 -0
- package/dist/esm/TileChunkNode.js +104 -85
- package/dist/esm/TileChunkNode.js.map +1 -1
- package/dist/esm/TileLayer.d.ts +104 -24
- package/dist/esm/TileLayer.d.ts.map +1 -0
- package/dist/esm/TileLayer.js +511 -469
- package/dist/esm/TileLayer.js.map +1 -1
- package/dist/esm/TileLayerNode.d.ts +52 -11
- package/dist/esm/TileLayerNode.d.ts.map +1 -0
- package/dist/esm/TileLayerNode.js +224 -166
- package/dist/esm/TileLayerNode.js.map +1 -1
- package/dist/esm/TileMap.d.ts +96 -31
- package/dist/esm/TileMap.d.ts.map +1 -0
- package/dist/esm/TileMap.js +401 -357
- package/dist/esm/TileMap.js.map +1 -1
- package/dist/esm/TileMapBand.d.ts +42 -29
- package/dist/esm/TileMapBand.d.ts.map +1 -0
- package/dist/esm/TileMapBand.js +151 -158
- package/dist/esm/TileMapBand.js.map +1 -1
- package/dist/esm/TileMapNode.d.ts +34 -18
- package/dist/esm/TileMapNode.d.ts.map +1 -0
- package/dist/esm/TileMapNode.js +122 -109
- package/dist/esm/TileMapNode.js.map +1 -1
- package/dist/esm/TileMapView.d.ts +108 -47
- package/dist/esm/TileMapView.d.ts.map +1 -0
- package/dist/esm/TileMapView.js +385 -338
- package/dist/esm/TileMapView.js.map +1 -1
- package/dist/esm/TileSet.d.ts +15 -6
- package/dist/esm/TileSet.d.ts.map +1 -0
- package/dist/esm/TileSet.js +161 -181
- package/dist/esm/TileSet.js.map +1 -1
- package/dist/esm/WangSet.d.ts +5 -4
- package/dist/esm/WangSet.d.ts.map +1 -0
- package/dist/esm/WangSet.js +70 -78
- package/dist/esm/WangSet.js.map +1 -1
- package/dist/esm/WorkerSampledChunkSource.d.ts +95 -0
- package/dist/esm/WorkerSampledChunkSource.d.ts.map +1 -0
- package/dist/esm/WorkerSampledChunkSource.js +117 -0
- package/dist/esm/WorkerSampledChunkSource.js.map +1 -0
- package/dist/esm/autoTile.d.ts +10 -5
- package/dist/esm/autoTile.d.ts.map +1 -0
- package/dist/esm/autoTile.js +164 -205
- package/dist/esm/autoTile.js.map +1 -1
- package/dist/esm/chunkGeometry.d.ts +8 -7
- package/dist/esm/chunkGeometry.d.ts.map +1 -0
- package/dist/esm/chunkGeometry.js +83 -94
- package/dist/esm/chunkGeometry.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +26 -15
- package/dist/esm/nodeBounds.d.ts +20 -0
- package/dist/esm/nodeBounds.d.ts.map +1 -0
- package/dist/esm/nodeBounds.js +40 -0
- package/dist/esm/nodeBounds.js.map +1 -0
- package/dist/esm/pixelSnap.d.ts +4 -3
- package/dist/esm/pixelSnap.d.ts.map +1 -0
- package/dist/esm/pixelSnap.js +18 -16
- package/dist/esm/pixelSnap.js.map +1 -1
- package/dist/esm/public.d.ts +23 -3
- package/dist/esm/public.d.ts.map +1 -0
- package/dist/esm/tileCollision.d.ts +190 -0
- package/dist/esm/tileCollision.d.ts.map +1 -0
- package/dist/esm/tileCollision.js +367 -0
- package/dist/esm/tileCollision.js.map +1 -0
- package/dist/esm/tileWord.d.ts +12 -0
- package/dist/esm/tileWord.d.ts.map +1 -0
- package/dist/esm/tileWord.js +16 -0
- package/dist/esm/tileWord.js.map +1 -0
- package/dist/esm/tilemapExtension.d.ts +4 -3
- package/dist/esm/tilemapExtension.d.ts.map +1 -0
- package/dist/esm/tilemapExtension.js +47 -47
- package/dist/esm/tilemapExtension.js.map +1 -1
- package/dist/esm/tilemapSerializers.d.ts +5 -4
- package/dist/esm/tilemapSerializers.d.ts.map +1 -0
- package/dist/esm/tilemapSerializers.js +48 -41
- package/dist/esm/tilemapSerializers.js.map +1 -1
- package/dist/esm/types.d.ts +34 -18
- package/dist/esm/types.d.ts.map +1 -0
- package/dist/esm/types.js +111 -120
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/webgl2/WebGl2TileChunkRenderer.d.ts +50 -2
- package/dist/esm/webgl2/WebGl2TileChunkRenderer.d.ts.map +1 -0
- package/dist/esm/webgl2/WebGl2TileChunkRenderer.js +366 -332
- package/dist/esm/webgl2/WebGl2TileChunkRenderer.js.map +1 -1
- package/dist/esm/webgl2/shaders/tile-chunk.frag.js +6 -0
- package/dist/esm/webgl2/shaders/tile-chunk.frag.js.map +1 -0
- package/dist/esm/webgl2/shaders/tile-chunk.vert.js +6 -0
- package/dist/esm/webgl2/shaders/tile-chunk.vert.js.map +1 -0
- package/dist/esm/webgpu/WebGpuTileChunkRenderer.d.ts +70 -4
- package/dist/esm/webgpu/WebGpuTileChunkRenderer.d.ts.map +1 -0
- package/dist/esm/webgpu/WebGpuTileChunkRenderer.js +496 -383
- package/dist/esm/webgpu/WebGpuTileChunkRenderer.js.map +1 -1
- package/dist/esm/webgpu/shaders/tile-chunk.wgsl.js +6 -0
- package/dist/esm/webgpu/shaders/tile-chunk.wgsl.js.map +1 -0
- package/package.json +8 -15
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/register.d.ts +0 -1
- package/dist/esm/register.js +0 -25
- package/dist/esm/register.js.map +0 -1
package/README.md
CHANGED
|
@@ -11,9 +11,10 @@ parse their format and hand this runtime fully-resolved tiles.
|
|
|
11
11
|
npm install @codexo/exojs @codexo/exojs-tilemap
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
`@codexo/exojs` is a peer dependency
|
|
15
|
-
|
|
16
|
-
custom-format maps
|
|
14
|
+
`@codexo/exojs` is a peer dependency, so install it explicitly. This package is itself a peer of
|
|
15
|
+
the format adapters (`@codexo/exojs-tiled`, `@codexo/exojs-ldtk`) rather than something they pull
|
|
16
|
+
in transitively — install it whether you use an adapter or build procedural / custom-format maps
|
|
17
|
+
directly.
|
|
17
18
|
|
|
18
19
|
## What this package provides
|
|
19
20
|
|
|
@@ -26,6 +27,23 @@ custom-format maps.
|
|
|
26
27
|
- `ResolvedTile` / `TileTransform` — value-typed tile references with `flipX` / `flipY` /
|
|
27
28
|
`diagonal` orientation.
|
|
28
29
|
|
|
30
|
+
**Worlds and level lifetime** — format-neutral, driven by whichever adapter loaded the data:
|
|
31
|
+
|
|
32
|
+
- `MapWorld` / `MapLevel` — where levels sit: stable id, world bounds, neighbour graph,
|
|
33
|
+
externality. Metadata only; no tiles, no textures.
|
|
34
|
+
- `MapWorldRuntime` — explicit `loadLevel` / `unloadLevel`, one child `LoaderScope` per level,
|
|
35
|
+
at most one live level per id. No camera policy: the game decides what to load.
|
|
36
|
+
- `MapLevelRuntime` — one loaded level's map, scope and spawn session, with a single
|
|
37
|
+
`destroy()` that tears all three down in a safe order.
|
|
38
|
+
|
|
39
|
+
**Map object spawning**:
|
|
40
|
+
|
|
41
|
+
- `MapObjectSpawner` — a local (never global) dispatch table from object class to a factory.
|
|
42
|
+
Deterministic order, atomic rollback, `AbortSignal` cancellation.
|
|
43
|
+
- `MapObjectDescriptor` — the format-neutral view of one authored object handed to a factory.
|
|
44
|
+
- `MapSpawnSession` — owns what one spawn produced; constant-time lookup by stable source id,
|
|
45
|
+
reverse-order teardown.
|
|
46
|
+
|
|
29
47
|
**Scene / rendering (`@advanced`)**:
|
|
30
48
|
|
|
31
49
|
- `TileMapNode` — convenience root that renders a whole `TileMap`.
|
|
@@ -39,20 +57,13 @@ custom-format maps.
|
|
|
39
57
|
## Usage — procedural map
|
|
40
58
|
|
|
41
59
|
```ts
|
|
42
|
-
import { Application,
|
|
43
|
-
import {
|
|
44
|
-
TileLayer,
|
|
45
|
-
TileMap,
|
|
46
|
-
TileMapNode,
|
|
47
|
-
TileSet,
|
|
48
|
-
tilemapExtension,
|
|
49
|
-
TILE_TRANSFORM_IDENTITY,
|
|
50
|
-
} from '@codexo/exojs-tilemap';
|
|
60
|
+
import { Application, TextureRegion } from '@codexo/exojs';
|
|
61
|
+
import { TileLayer, TileMap, TileMapNode, TileSet, tilemapExtension, TILE_TRANSFORM_IDENTITY } from '@codexo/exojs-tilemap';
|
|
51
62
|
|
|
52
63
|
const app = new Application({ extensions: [tilemapExtension] /* canvas, … */ });
|
|
53
64
|
|
|
54
65
|
// Tileset over a Loader-owned atlas texture (the runtime never destroys it).
|
|
55
|
-
const atlas = await app.loader.load(
|
|
66
|
+
const atlas = await app.loader.load('tiles.png');
|
|
56
67
|
const terrain = new TileSet({
|
|
57
68
|
name: 'terrain',
|
|
58
69
|
texture: new TextureRegion(atlas, { x: 0, y: 0, width: atlas.width, height: atlas.height }),
|
|
@@ -83,13 +94,13 @@ const map = new TileMap({
|
|
|
83
94
|
});
|
|
84
95
|
|
|
85
96
|
// Render the whole map.
|
|
86
|
-
app.
|
|
97
|
+
app.scenes.root.addChild(new TileMapNode(map));
|
|
87
98
|
```
|
|
88
99
|
|
|
89
100
|
### Interleaving actors between layers — `TileMapView`
|
|
90
101
|
|
|
91
102
|
`TileMapNode` owns **only** the map's layer nodes and renders them back-to-front — use it when
|
|
92
|
-
nothing renders between layers. To draw application actors
|
|
103
|
+
nothing renders between layers. To draw application actors _between_ tile layers, create a
|
|
93
104
|
`TileMapView`: it generates one `TileLayerNode` per map layer (stable identity, map document
|
|
94
105
|
order) and groups them into named `TileMapBand`s that you parent yourself, as siblings of your
|
|
95
106
|
own actor containers:
|
|
@@ -104,7 +115,7 @@ const view = map.createView({
|
|
|
104
115
|
|
|
105
116
|
worldRoot.addChild(
|
|
106
117
|
view.band('ground'),
|
|
107
|
-
actors,
|
|
118
|
+
actors, // app-owned actor container — drawn between ground and roof
|
|
108
119
|
view.band('roof'),
|
|
109
120
|
);
|
|
110
121
|
```
|
|
@@ -130,18 +141,11 @@ Actors are application-owned siblings. `TileMapView` never adopts or destroys ac
|
|
|
130
141
|
- After layers are structurally added to or removed from the map, call `view.refreshLayers()`:
|
|
131
142
|
unchanged layer nodes keep their identity and bands keep their placement in your scene graph.
|
|
132
143
|
|
|
133
|
-
## `/register` convenience entry
|
|
134
|
-
|
|
135
|
-
```ts
|
|
136
|
-
// Side effect: registers tilemapExtension in the global ExtensionRegistry.
|
|
137
|
-
import '@codexo/exojs-tilemap/register';
|
|
138
|
-
```
|
|
139
|
-
|
|
140
144
|
## Renderer model
|
|
141
145
|
|
|
142
146
|
- **Chunk-first.** A `TileLayerNode` is a container of per-chunk `TileChunkNode` drawables (one
|
|
143
147
|
per non-empty loaded chunk). The renderer batches tiles by `(shader, tileset texture)` and
|
|
144
|
-
issues one instanced draw per batch — draw calls scale with
|
|
148
|
+
issues one instanced draw per batch — draw calls scale with _visible chunks × layers_, not
|
|
145
149
|
total tile count.
|
|
146
150
|
- **Revision-cached geometry.** Each chunk's quad geometry is built once and cached against the
|
|
147
151
|
source chunk's `revision`. Unchanged chunks never rebuild; a camera pan rebuilds nothing —
|
|
@@ -161,6 +165,9 @@ import '@codexo/exojs-tilemap/register';
|
|
|
161
165
|
## Ownership & lifecycle
|
|
162
166
|
|
|
163
167
|
- Tileset **textures are Loader-owned**. The runtime and renderer never destroy them.
|
|
168
|
+
- A `MapWorldRuntime` owns the scope it creates under the one you give it, plus one child scope
|
|
169
|
+
per loaded level - never the scope you passed in. A `MapLevelRuntime` owns the `TileMap` its
|
|
170
|
+
provider returned, its spawn session, and its scope; `destroy()` releases them in that order.
|
|
164
171
|
- `TileMapNode` / `TileMapView` / `TileMapBand` / `TileLayerNode` reference — but never own —
|
|
165
172
|
the `TileMap`. Destroying a map node, view, or band frees only the tile nodes it generated
|
|
166
173
|
(detaching them from their application parents) and their cached geometry; application actors,
|
|
@@ -174,8 +181,8 @@ import '@codexo/exojs-tilemap/register';
|
|
|
174
181
|
## Core compatibility
|
|
175
182
|
|
|
176
183
|
| `@codexo/exojs-tilemap` | `@codexo/exojs` |
|
|
177
|
-
|
|
178
|
-
| 0.x
|
|
184
|
+
| ----------------------- | --------------- |
|
|
185
|
+
| 0.x | matching `0.x` |
|
|
179
186
|
|
|
180
187
|
## Links
|
|
181
188
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A chunk payload ready to install into a {@link import('./TileLayer').TileLayer}
|
|
3
|
+
* via {@link import('./TileLayer').TileLayer._adoptChunk} - the same shape
|
|
4
|
+
* {@link import('./TileChunk').TileChunk}'s constructor accepts as its
|
|
5
|
+
* `source` parameter, named and exported so provider authors have a type to
|
|
6
|
+
* import instead of matching an inline shape.
|
|
7
|
+
* @advanced
|
|
8
|
+
*/
|
|
9
|
+
export interface ChunkPayload {
|
|
10
|
+
readonly width: number;
|
|
11
|
+
readonly height: number;
|
|
12
|
+
readonly tiles: Uint32Array;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Supplies chunk data for one {@link import('./TileLayer').TileLayer} on
|
|
16
|
+
* demand, keyed by signed chunk coordinates - the abstraction a
|
|
17
|
+
* {@link import('./ChunkStreamer').ChunkStreamer} drives to implement chunk
|
|
18
|
+
* streaming, and the seam a format adapter or a procedural generator
|
|
19
|
+
* implements.
|
|
20
|
+
*
|
|
21
|
+
* `getChunk` may return synchronously (e.g. slicing already-parsed source
|
|
22
|
+
* data) or a `Promise` (e.g. an expensive procedural-generation algorithm,
|
|
23
|
+
* plausibly off the main thread) - {@link import('./ChunkStreamer').ChunkStreamer}
|
|
24
|
+
* installs a synchronous result in the same tick it was requested, with
|
|
25
|
+
* zero pop-in. Returning `null` (synchronously or via a resolved `Promise`)
|
|
26
|
+
* means "no data for this coordinate" - e.g. outside a finite provider's
|
|
27
|
+
* authored extent, even though the layer itself may be unbounded.
|
|
28
|
+
* @advanced
|
|
29
|
+
*/
|
|
30
|
+
export interface ChunkSource {
|
|
31
|
+
getChunk(cx: number, cy: number): ChunkPayload | null | Promise<ChunkPayload | null>;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=ChunkSource.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChunkSource.d.ts","sourceRoot":"","sources":["../../src/ChunkSource.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;CACtF"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { type View } from '@codexo/exojs';
|
|
2
|
+
import type { ChunkSource } from './ChunkSource';
|
|
3
|
+
import type { TileLayer } from './TileLayer';
|
|
4
|
+
/**
|
|
5
|
+
* Options for a {@link ChunkStreamer}. All optional.
|
|
6
|
+
* @advanced
|
|
7
|
+
*/
|
|
8
|
+
export interface ChunkStreamerOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Chunks within this many chunk-units (Chebyshev distance to the visible
|
|
11
|
+
* chunk range) of the view are loaded. Default `1`.
|
|
12
|
+
*/
|
|
13
|
+
readonly loadRadius?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Own-resident chunks beyond this many chunk-units of the view are
|
|
16
|
+
* evicted. Must be `>= loadRadius` - the gap between the two is
|
|
17
|
+
* deliberate hysteresis that prevents load/unload thrashing when the
|
|
18
|
+
* view sits near a chunk boundary. Default `2`.
|
|
19
|
+
*/
|
|
20
|
+
readonly unloadRadius?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Maximum number of new chunk loads issued per {@link ChunkStreamer.update}
|
|
23
|
+
* call, after the very first call (which loads the entire initial wanted
|
|
24
|
+
* set unbudgeted, so the starting view never pops in). Default `8`.
|
|
25
|
+
*/
|
|
26
|
+
readonly maxChunkLoadsPerFrame?: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Drives {@link TileLayer._adoptChunk}/{@link TileLayer._evictChunk} calls
|
|
30
|
+
* from a {@link View}'s position: chunks near the view are requested from a
|
|
31
|
+
* {@link ChunkSource} and installed; chunks that scroll far enough away are
|
|
32
|
+
* evicted. Works on both unbounded and bounded layers (a bounded-but-large
|
|
33
|
+
* layer still benefits from not keeping every chunk resident at once) - for
|
|
34
|
+
* a bounded layer, the wanted range is clamped to {@link TileLayer.chunkRange}.
|
|
35
|
+
*
|
|
36
|
+
* Touches only the {@link TileLayer} data layer -
|
|
37
|
+
* {@link import('./TileLayerNode').TileLayerNode} reacts to
|
|
38
|
+
* `_adoptChunk`/`_evictChunk` on its own via a structural listener, so this
|
|
39
|
+
* class has no rendering dependency and no reference to any scene node.
|
|
40
|
+
*
|
|
41
|
+
* Tracks its own resident set (chunks *this instance* has loaded), never
|
|
42
|
+
* touching chunks that predate its attachment or were installed by another
|
|
43
|
+
* source.
|
|
44
|
+
*
|
|
45
|
+
* Tick it from your update loop, like {@link import('./TileAnimator').TileAnimator}:
|
|
46
|
+
*
|
|
47
|
+
* ```ts
|
|
48
|
+
* const streamer = new ChunkStreamer(layer, mySource, view);
|
|
49
|
+
* scene.systems.add({ update: () => streamer.update() });
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* @advanced
|
|
53
|
+
*/
|
|
54
|
+
export declare class ChunkStreamer {
|
|
55
|
+
private readonly _layer;
|
|
56
|
+
private readonly _source;
|
|
57
|
+
private readonly _view;
|
|
58
|
+
private readonly _loadRadius;
|
|
59
|
+
private readonly _unloadRadius;
|
|
60
|
+
private readonly _maxChunkLoadsPerFrame;
|
|
61
|
+
private readonly _resident;
|
|
62
|
+
private readonly _inFlight;
|
|
63
|
+
private _requestCounter;
|
|
64
|
+
private _primed;
|
|
65
|
+
private _destroyed;
|
|
66
|
+
constructor(layer: TileLayer, source: ChunkSource, view: View, options?: ChunkStreamerOptions);
|
|
67
|
+
/** Number of chunks this instance currently has resident in the layer. */
|
|
68
|
+
get residentCount(): number;
|
|
69
|
+
/**
|
|
70
|
+
* Recompute the wanted chunk set from the view's current position; evict
|
|
71
|
+
* own-resident chunks that fell outside `unloadRadius`, then request
|
|
72
|
+
* chunks newly inside `loadRadius`. The very first call loads the entire
|
|
73
|
+
* initial wanted set unbudgeted; every later call is capped at
|
|
74
|
+
* {@link ChunkStreamerOptions.maxChunkLoadsPerFrame}. No-op once
|
|
75
|
+
* {@link destroy}ed.
|
|
76
|
+
*/
|
|
77
|
+
update(): void;
|
|
78
|
+
/**
|
|
79
|
+
* Evict every chunk this instance has loaded and stop responding to
|
|
80
|
+
* {@link update}. Does not affect chunks predating attachment or
|
|
81
|
+
* installed by another source. Idempotent.
|
|
82
|
+
*/
|
|
83
|
+
destroy(): void;
|
|
84
|
+
private _computeCoreRange;
|
|
85
|
+
private _rangeDistance;
|
|
86
|
+
private _unload;
|
|
87
|
+
private _request;
|
|
88
|
+
/**
|
|
89
|
+
* Ignore-on-resolve: only the `_destroyed` and in-flight-token checks
|
|
90
|
+
* guard installation - a chunk that scrolled out of the wanted set while
|
|
91
|
+
* its request was in flight is still installed if it resolves (installing
|
|
92
|
+
* it is cheap and self-correcting: the next {@link update} call's unload
|
|
93
|
+
* pass evicts it again if it's genuinely no longer wanted). There is no
|
|
94
|
+
* edit-persistence to protect, so discarding is never destructive.
|
|
95
|
+
*/
|
|
96
|
+
private _onResolved;
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=ChunkStreamer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChunkStreamer.d.ts","sourceRoot":"","sources":["../../src/ChunkStreamer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,IAAI,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,KAAK,EAAgB,WAAW,EAAE,MAAM,eAAe,CAAC;AAC/D,OAAO,KAAK,EAAc,SAAS,EAAE,MAAM,aAAa,CAAC;AAGzD;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,qBAAqB,CAAC,EAAE,MAAM,CAAC;CACzC;AAWD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAY;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAc;IACtC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAO;IAC7B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAS;IAEhD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAmE;IAC7F,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA6B;IACvD,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,UAAU,CAAS;gBAER,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE,oBAAoB;IAapG,0EAA0E;IAC1E,IAAW,aAAa,IAAI,MAAM,CAEjC;IAED;;;;;;;OAOG;IACI,MAAM,IAAI,IAAI;IA4CrB;;;;OAIG;IACI,OAAO,IAAI,IAAI;IAWtB,OAAO,CAAC,iBAAiB;IA6BzB,OAAO,CAAC,cAAc;IAMtB,OAAO,CAAC,OAAO;IASf,OAAO,CAAC,QAAQ;IAqChB;;;;;;;OAOG;IACH,OAAO,CAAC,WAAW;CAUpB"}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { tileToChunkCoord } from "./types.js";
|
|
2
|
+
import { logger } from "@codexo/exojs";
|
|
3
|
+
|
|
4
|
+
//#region src/ChunkStreamer.ts
|
|
5
|
+
const DEFAULT_LOAD_RADIUS = 1;
|
|
6
|
+
const DEFAULT_UNLOAD_RADIUS = 2;
|
|
7
|
+
const DEFAULT_MAX_CHUNK_LOADS_PER_FRAME = 8;
|
|
8
|
+
const chunkKey = (cx, cy) => `${cx},${cy}`;
|
|
9
|
+
const isThenable = (value) => value !== null && typeof value === "object" && typeof value.then === "function";
|
|
10
|
+
/**
|
|
11
|
+
* Drives {@link TileLayer._adoptChunk}/{@link TileLayer._evictChunk} calls
|
|
12
|
+
* from a {@link View}'s position: chunks near the view are requested from a
|
|
13
|
+
* {@link ChunkSource} and installed; chunks that scroll far enough away are
|
|
14
|
+
* evicted. Works on both unbounded and bounded layers (a bounded-but-large
|
|
15
|
+
* layer still benefits from not keeping every chunk resident at once) - for
|
|
16
|
+
* a bounded layer, the wanted range is clamped to {@link TileLayer.chunkRange}.
|
|
17
|
+
*
|
|
18
|
+
* Touches only the {@link TileLayer} data layer -
|
|
19
|
+
* {@link import('./TileLayerNode').TileLayerNode} reacts to
|
|
20
|
+
* `_adoptChunk`/`_evictChunk` on its own via a structural listener, so this
|
|
21
|
+
* class has no rendering dependency and no reference to any scene node.
|
|
22
|
+
*
|
|
23
|
+
* Tracks its own resident set (chunks *this instance* has loaded), never
|
|
24
|
+
* touching chunks that predate its attachment or were installed by another
|
|
25
|
+
* source.
|
|
26
|
+
*
|
|
27
|
+
* Tick it from your update loop, like {@link import('./TileAnimator').TileAnimator}:
|
|
28
|
+
*
|
|
29
|
+
* ```ts
|
|
30
|
+
* const streamer = new ChunkStreamer(layer, mySource, view);
|
|
31
|
+
* scene.systems.add({ update: () => streamer.update() });
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @advanced
|
|
35
|
+
*/
|
|
36
|
+
var ChunkStreamer = class {
|
|
37
|
+
_layer;
|
|
38
|
+
_source;
|
|
39
|
+
_view;
|
|
40
|
+
_loadRadius;
|
|
41
|
+
_unloadRadius;
|
|
42
|
+
_maxChunkLoadsPerFrame;
|
|
43
|
+
_resident = /* @__PURE__ */ new Map();
|
|
44
|
+
_inFlight = /* @__PURE__ */ new Map();
|
|
45
|
+
_requestCounter = 0;
|
|
46
|
+
_primed = false;
|
|
47
|
+
_destroyed = false;
|
|
48
|
+
constructor(layer, source, view, options) {
|
|
49
|
+
this._layer = layer;
|
|
50
|
+
this._source = source;
|
|
51
|
+
this._view = view;
|
|
52
|
+
this._loadRadius = options?.loadRadius ?? DEFAULT_LOAD_RADIUS;
|
|
53
|
+
this._unloadRadius = options?.unloadRadius ?? DEFAULT_UNLOAD_RADIUS;
|
|
54
|
+
this._maxChunkLoadsPerFrame = options?.maxChunkLoadsPerFrame ?? DEFAULT_MAX_CHUNK_LOADS_PER_FRAME;
|
|
55
|
+
if (this._unloadRadius < this._loadRadius) throw new Error(`ChunkStreamer unloadRadius (${this._unloadRadius}) must be >= loadRadius (${this._loadRadius}).`);
|
|
56
|
+
}
|
|
57
|
+
/** Number of chunks this instance currently has resident in the layer. */
|
|
58
|
+
get residentCount() {
|
|
59
|
+
return this._resident.size;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Recompute the wanted chunk set from the view's current position; evict
|
|
63
|
+
* own-resident chunks that fell outside `unloadRadius`, then request
|
|
64
|
+
* chunks newly inside `loadRadius`. The very first call loads the entire
|
|
65
|
+
* initial wanted set unbudgeted; every later call is capped at
|
|
66
|
+
* {@link ChunkStreamerOptions.maxChunkLoadsPerFrame}. No-op once
|
|
67
|
+
* {@link destroy}ed.
|
|
68
|
+
*/
|
|
69
|
+
update() {
|
|
70
|
+
if (this._destroyed) return;
|
|
71
|
+
const core = this._computeCoreRange();
|
|
72
|
+
this._unload(core);
|
|
73
|
+
const range = this._layer.chunkRange();
|
|
74
|
+
let minCx = core.minCx - this._loadRadius;
|
|
75
|
+
let minCy = core.minCy - this._loadRadius;
|
|
76
|
+
let maxCx = core.maxCx + this._loadRadius;
|
|
77
|
+
let maxCy = core.maxCy + this._loadRadius;
|
|
78
|
+
if (range !== null) {
|
|
79
|
+
minCx = Math.max(minCx, range.minCx);
|
|
80
|
+
minCy = Math.max(minCy, range.minCy);
|
|
81
|
+
maxCx = Math.min(maxCx, range.maxCx);
|
|
82
|
+
maxCy = Math.min(maxCy, range.maxCy);
|
|
83
|
+
}
|
|
84
|
+
const toLoad = [];
|
|
85
|
+
for (let cy = minCy; cy <= maxCy; cy++) for (let cx = minCx; cx <= maxCx; cx++) {
|
|
86
|
+
const key = chunkKey(cx, cy);
|
|
87
|
+
if (this._resident.has(key) || this._inFlight.has(key)) continue;
|
|
88
|
+
toLoad.push({
|
|
89
|
+
cx,
|
|
90
|
+
cy
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (!this._primed) {
|
|
94
|
+
this._primed = true;
|
|
95
|
+
for (const { cx, cy } of toLoad) this._request(cx, cy);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
toLoad.sort((a, b) => this._rangeDistance(a.cx, a.cy, core) - this._rangeDistance(b.cx, b.cy, core));
|
|
99
|
+
const budget = Math.min(this._maxChunkLoadsPerFrame, toLoad.length);
|
|
100
|
+
for (const { cx, cy } of toLoad.slice(0, budget)) this._request(cx, cy);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Evict every chunk this instance has loaded and stop responding to
|
|
104
|
+
* {@link update}. Does not affect chunks predating attachment or
|
|
105
|
+
* installed by another source. Idempotent.
|
|
106
|
+
*/
|
|
107
|
+
destroy() {
|
|
108
|
+
if (this._destroyed) return;
|
|
109
|
+
this._destroyed = true;
|
|
110
|
+
for (const { cx, cy } of this._resident.values()) this._layer._evictChunk(cx, cy);
|
|
111
|
+
this._resident.clear();
|
|
112
|
+
this._inFlight.clear();
|
|
113
|
+
}
|
|
114
|
+
_computeCoreRange() {
|
|
115
|
+
const layer = this._layer;
|
|
116
|
+
const view = this._view;
|
|
117
|
+
const bounds = view.getBounds();
|
|
118
|
+
const centerX = view.center.x;
|
|
119
|
+
const centerY = view.center.y;
|
|
120
|
+
const shiftX = centerX * (1 - layer.parallaxX);
|
|
121
|
+
const shiftY = centerY * (1 - layer.parallaxY);
|
|
122
|
+
const scale = layer.parallaxScale;
|
|
123
|
+
const layerLeft = layer.offsetX + (bounds.left - shiftX - layer.offsetX) / scale;
|
|
124
|
+
const layerTop = layer.offsetY + (bounds.top - shiftY - layer.offsetY) / scale;
|
|
125
|
+
const layerRight = layer.offsetX + (bounds.right - shiftX - layer.offsetX) / scale;
|
|
126
|
+
const layerBottom = layer.offsetY + (bounds.bottom - shiftY - layer.offsetY) / scale;
|
|
127
|
+
const topLeftTile = layer.pixelToTile(layerLeft, layerTop);
|
|
128
|
+
const bottomRightTile = layer.pixelToTile(layerRight, layerBottom);
|
|
129
|
+
const topLeftChunk = tileToChunkCoord(topLeftTile.tx, topLeftTile.ty, layer.chunkWidth, layer.chunkHeight);
|
|
130
|
+
const bottomRightChunk = tileToChunkCoord(bottomRightTile.tx, bottomRightTile.ty, layer.chunkWidth, layer.chunkHeight);
|
|
131
|
+
return {
|
|
132
|
+
minCx: topLeftChunk.cx,
|
|
133
|
+
minCy: topLeftChunk.cy,
|
|
134
|
+
maxCx: bottomRightChunk.cx,
|
|
135
|
+
maxCy: bottomRightChunk.cy
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
_rangeDistance(cx, cy, range) {
|
|
139
|
+
const dx = Math.max(range.minCx - cx, 0, cx - range.maxCx);
|
|
140
|
+
const dy = Math.max(range.minCy - cy, 0, cy - range.maxCy);
|
|
141
|
+
return Math.max(dx, dy);
|
|
142
|
+
}
|
|
143
|
+
_unload(core) {
|
|
144
|
+
for (const [key, { cx, cy }] of this._resident) if (this._rangeDistance(cx, cy, core) > this._unloadRadius) {
|
|
145
|
+
this._layer._evictChunk(cx, cy);
|
|
146
|
+
this._resident.delete(key);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
_request(cx, cy) {
|
|
150
|
+
const key = chunkKey(cx, cy);
|
|
151
|
+
const token = ++this._requestCounter;
|
|
152
|
+
this._inFlight.set(key, token);
|
|
153
|
+
let result;
|
|
154
|
+
try {
|
|
155
|
+
result = this._source.getChunk(cx, cy);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
this._inFlight.delete(key);
|
|
158
|
+
logger.warn(`ChunkStreamer: source.getChunk(${cx}, ${cy}) threw synchronously.`, {
|
|
159
|
+
source: "ChunkStreamer",
|
|
160
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
161
|
+
});
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (isThenable(result)) {
|
|
165
|
+
result.then((payload) => {
|
|
166
|
+
this._onResolved(key, cx, cy, token, payload);
|
|
167
|
+
}, (error) => {
|
|
168
|
+
this._inFlight.delete(key);
|
|
169
|
+
logger.warn(`ChunkStreamer: source.getChunk(${cx}, ${cy}) rejected.`, {
|
|
170
|
+
source: "ChunkStreamer",
|
|
171
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
this._onResolved(key, cx, cy, token, result);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Ignore-on-resolve: only the `_destroyed` and in-flight-token checks
|
|
180
|
+
* guard installation - a chunk that scrolled out of the wanted set while
|
|
181
|
+
* its request was in flight is still installed if it resolves (installing
|
|
182
|
+
* it is cheap and self-correcting: the next {@link update} call's unload
|
|
183
|
+
* pass evicts it again if it's genuinely no longer wanted). There is no
|
|
184
|
+
* edit-persistence to protect, so discarding is never destructive.
|
|
185
|
+
*/
|
|
186
|
+
_onResolved(key, cx, cy, token, payload) {
|
|
187
|
+
if (this._destroyed) return;
|
|
188
|
+
if (this._inFlight.get(key) !== token) return;
|
|
189
|
+
this._inFlight.delete(key);
|
|
190
|
+
if (payload === null) return;
|
|
191
|
+
this._layer._adoptChunk(cx, cy, payload);
|
|
192
|
+
this._resident.set(key, {
|
|
193
|
+
cx,
|
|
194
|
+
cy
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
//#endregion
|
|
200
|
+
export { ChunkStreamer };
|
|
201
|
+
//# sourceMappingURL=ChunkStreamer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChunkStreamer.js","names":[],"sources":["../../src/ChunkStreamer.ts"],"sourcesContent":["import { logger, type View } from '@codexo/exojs';\n\nimport type { ChunkPayload, ChunkSource } from './ChunkSource';\nimport type { ChunkRange, TileLayer } from './TileLayer';\nimport { tileToChunkCoord } from './types';\n\n/**\n * Options for a {@link ChunkStreamer}. All optional.\n * @advanced\n */\nexport interface ChunkStreamerOptions {\n /**\n * Chunks within this many chunk-units (Chebyshev distance to the visible\n * chunk range) of the view are loaded. Default `1`.\n */\n readonly loadRadius?: number;\n /**\n * Own-resident chunks beyond this many chunk-units of the view are\n * evicted. Must be `>= loadRadius` - the gap between the two is\n * deliberate hysteresis that prevents load/unload thrashing when the\n * view sits near a chunk boundary. Default `2`.\n */\n readonly unloadRadius?: number;\n /**\n * Maximum number of new chunk loads issued per {@link ChunkStreamer.update}\n * call, after the very first call (which loads the entire initial wanted\n * set unbudgeted, so the starting view never pops in). Default `8`.\n */\n readonly maxChunkLoadsPerFrame?: number;\n}\n\nconst DEFAULT_LOAD_RADIUS = 1;\nconst DEFAULT_UNLOAD_RADIUS = 2;\nconst DEFAULT_MAX_CHUNK_LOADS_PER_FRAME = 8;\n\nconst chunkKey = (cx: number, cy: number): string => `${cx},${cy}`;\n\nconst isThenable = (value: unknown): value is Promise<ChunkPayload | null> =>\n value !== null && typeof value === 'object' && typeof (value as { then?: unknown }).then === 'function';\n\n/**\n * Drives {@link TileLayer._adoptChunk}/{@link TileLayer._evictChunk} calls\n * from a {@link View}'s position: chunks near the view are requested from a\n * {@link ChunkSource} and installed; chunks that scroll far enough away are\n * evicted. Works on both unbounded and bounded layers (a bounded-but-large\n * layer still benefits from not keeping every chunk resident at once) - for\n * a bounded layer, the wanted range is clamped to {@link TileLayer.chunkRange}.\n *\n * Touches only the {@link TileLayer} data layer -\n * {@link import('./TileLayerNode').TileLayerNode} reacts to\n * `_adoptChunk`/`_evictChunk` on its own via a structural listener, so this\n * class has no rendering dependency and no reference to any scene node.\n *\n * Tracks its own resident set (chunks *this instance* has loaded), never\n * touching chunks that predate its attachment or were installed by another\n * source.\n *\n * Tick it from your update loop, like {@link import('./TileAnimator').TileAnimator}:\n *\n * ```ts\n * const streamer = new ChunkStreamer(layer, mySource, view);\n * scene.systems.add({ update: () => streamer.update() });\n * ```\n *\n * @advanced\n */\nexport class ChunkStreamer {\n private readonly _layer: TileLayer;\n private readonly _source: ChunkSource;\n private readonly _view: View;\n private readonly _loadRadius: number;\n private readonly _unloadRadius: number;\n private readonly _maxChunkLoadsPerFrame: number;\n\n private readonly _resident = new Map<string, { readonly cx: number; readonly cy: number }>();\n private readonly _inFlight = new Map<string, number>();\n private _requestCounter = 0;\n private _primed = false;\n private _destroyed = false;\n\n public constructor(layer: TileLayer, source: ChunkSource, view: View, options?: ChunkStreamerOptions) {\n this._layer = layer;\n this._source = source;\n this._view = view;\n this._loadRadius = options?.loadRadius ?? DEFAULT_LOAD_RADIUS;\n this._unloadRadius = options?.unloadRadius ?? DEFAULT_UNLOAD_RADIUS;\n this._maxChunkLoadsPerFrame = options?.maxChunkLoadsPerFrame ?? DEFAULT_MAX_CHUNK_LOADS_PER_FRAME;\n\n if (this._unloadRadius < this._loadRadius) {\n throw new Error(`ChunkStreamer unloadRadius (${this._unloadRadius}) must be >= loadRadius (${this._loadRadius}).`);\n }\n }\n\n /** Number of chunks this instance currently has resident in the layer. */\n public get residentCount(): number {\n return this._resident.size;\n }\n\n /**\n * Recompute the wanted chunk set from the view's current position; evict\n * own-resident chunks that fell outside `unloadRadius`, then request\n * chunks newly inside `loadRadius`. The very first call loads the entire\n * initial wanted set unbudgeted; every later call is capped at\n * {@link ChunkStreamerOptions.maxChunkLoadsPerFrame}. No-op once\n * {@link destroy}ed.\n */\n public update(): void {\n if (this._destroyed) return;\n\n const core = this._computeCoreRange();\n\n this._unload(core);\n\n const range = this._layer.chunkRange();\n let minCx = core.minCx - this._loadRadius;\n let minCy = core.minCy - this._loadRadius;\n let maxCx = core.maxCx + this._loadRadius;\n let maxCy = core.maxCy + this._loadRadius;\n if (range !== null) {\n minCx = Math.max(minCx, range.minCx);\n minCy = Math.max(minCy, range.minCy);\n maxCx = Math.min(maxCx, range.maxCx);\n maxCy = Math.min(maxCy, range.maxCy);\n }\n\n const toLoad: Array<{ cx: number; cy: number }> = [];\n for (let cy = minCy; cy <= maxCy; cy++) {\n for (let cx = minCx; cx <= maxCx; cx++) {\n const key = chunkKey(cx, cy);\n if (this._resident.has(key) || this._inFlight.has(key)) continue;\n toLoad.push({ cx, cy });\n }\n }\n\n if (!this._primed) {\n this._primed = true;\n for (const { cx, cy } of toLoad) {\n this._request(cx, cy);\n }\n return;\n }\n\n toLoad.sort((a, b) => this._rangeDistance(a.cx, a.cy, core) - this._rangeDistance(b.cx, b.cy, core));\n\n const budget = Math.min(this._maxChunkLoadsPerFrame, toLoad.length);\n for (const { cx, cy } of toLoad.slice(0, budget)) {\n this._request(cx, cy);\n }\n }\n\n /**\n * Evict every chunk this instance has loaded and stop responding to\n * {@link update}. Does not affect chunks predating attachment or\n * installed by another source. Idempotent.\n */\n public destroy(): void {\n if (this._destroyed) return;\n this._destroyed = true;\n\n for (const { cx, cy } of this._resident.values()) {\n this._layer._evictChunk(cx, cy);\n }\n this._resident.clear();\n this._inFlight.clear();\n }\n\n private _computeCoreRange(): ChunkRange {\n const layer = this._layer;\n const view = this._view;\n const bounds = view.getBounds();\n const centerX = view.center.x;\n const centerY = view.center.y;\n\n const shiftX = centerX * (1 - layer.parallaxX);\n const shiftY = centerY * (1 - layer.parallaxY);\n const scale = layer.parallaxScale;\n\n const layerLeft = layer.offsetX + (bounds.left - shiftX - layer.offsetX) / scale;\n const layerTop = layer.offsetY + (bounds.top - shiftY - layer.offsetY) / scale;\n const layerRight = layer.offsetX + (bounds.right - shiftX - layer.offsetX) / scale;\n const layerBottom = layer.offsetY + (bounds.bottom - shiftY - layer.offsetY) / scale;\n const topLeftTile = layer.pixelToTile(layerLeft, layerTop);\n const bottomRightTile = layer.pixelToTile(layerRight, layerBottom);\n\n const topLeftChunk = tileToChunkCoord(topLeftTile.tx, topLeftTile.ty, layer.chunkWidth, layer.chunkHeight);\n const bottomRightChunk = tileToChunkCoord(bottomRightTile.tx, bottomRightTile.ty, layer.chunkWidth, layer.chunkHeight);\n\n return {\n minCx: topLeftChunk.cx,\n minCy: topLeftChunk.cy,\n maxCx: bottomRightChunk.cx,\n maxCy: bottomRightChunk.cy,\n };\n }\n\n private _rangeDistance(cx: number, cy: number, range: ChunkRange): number {\n const dx = Math.max(range.minCx - cx, 0, cx - range.maxCx);\n const dy = Math.max(range.minCy - cy, 0, cy - range.maxCy);\n return Math.max(dx, dy);\n }\n\n private _unload(core: ChunkRange): void {\n for (const [key, { cx, cy }] of this._resident) {\n if (this._rangeDistance(cx, cy, core) > this._unloadRadius) {\n this._layer._evictChunk(cx, cy);\n this._resident.delete(key);\n }\n }\n }\n\n private _request(cx: number, cy: number): void {\n const key = chunkKey(cx, cy);\n const token = ++this._requestCounter;\n this._inFlight.set(key, token);\n\n let result: ChunkPayload | null | Promise<ChunkPayload | null>;\n try {\n result = this._source.getChunk(cx, cy);\n } catch (error) {\n this._inFlight.delete(key);\n logger.warn(`ChunkStreamer: source.getChunk(${cx}, ${cy}) threw synchronously.`, {\n source: 'ChunkStreamer',\n error: error instanceof Error ? error : new Error(String(error)),\n });\n\n return;\n }\n\n if (isThenable(result)) {\n result.then(\n payload => {\n this._onResolved(key, cx, cy, token, payload);\n },\n (error: unknown) => {\n this._inFlight.delete(key);\n logger.warn(`ChunkStreamer: source.getChunk(${cx}, ${cy}) rejected.`, {\n source: 'ChunkStreamer',\n error: error instanceof Error ? error : new Error(String(error)),\n });\n },\n );\n return;\n }\n\n this._onResolved(key, cx, cy, token, result);\n }\n\n /**\n * Ignore-on-resolve: only the `_destroyed` and in-flight-token checks\n * guard installation - a chunk that scrolled out of the wanted set while\n * its request was in flight is still installed if it resolves (installing\n * it is cheap and self-correcting: the next {@link update} call's unload\n * pass evicts it again if it's genuinely no longer wanted). There is no\n * edit-persistence to protect, so discarding is never destructive.\n */\n private _onResolved(key: string, cx: number, cy: number, token: number, payload: ChunkPayload | null): void {\n if (this._destroyed) return;\n if (this._inFlight.get(key) !== token) return;\n this._inFlight.delete(key);\n\n if (payload === null) return;\n\n this._layer._adoptChunk(cx, cy, payload);\n this._resident.set(key, { cx, cy });\n }\n}\n"],"mappings":"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"}
|
package/dist/esm/ImageLayer.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export interface ImageLayerOptions {
|
|
|
24
24
|
readonly parallaxX?: number;
|
|
25
25
|
/** Vertical parallax factor. Default `1`. */
|
|
26
26
|
readonly parallaxY?: number;
|
|
27
|
+
/** Uniform parallax scale around the layer origin. Default `1`. */
|
|
28
|
+
readonly parallaxScale?: number;
|
|
27
29
|
/** Tint colour as `0xRRGGBB`, or `null`. Default `null`. */
|
|
28
30
|
readonly tintColor?: number | null;
|
|
29
31
|
/** Whether the image repeats horizontally. Default `false`. */
|
|
@@ -35,9 +37,12 @@ export interface ImageLayerOptions {
|
|
|
35
37
|
}
|
|
36
38
|
/**
|
|
37
39
|
* A data-only image layer: a single image (texture + resolved URL) placed as a
|
|
38
|
-
* background or foreground layer.
|
|
39
|
-
*
|
|
40
|
-
*
|
|
40
|
+
* background or foreground layer. The layer itself holds no scene-graph state -
|
|
41
|
+
* it is rendered by an {@link import('./ImageLayerNode').ImageLayerNode}, which
|
|
42
|
+
* {@link import('./TileMapNode').TileMapNode} and
|
|
43
|
+
* {@link import('./TileMapView').TileMapView} generate and interleave with tile
|
|
44
|
+
* layers by the map's combined document order
|
|
45
|
+
* ({@link import('./TileMap').TileMap.renderableLayers}).
|
|
41
46
|
*
|
|
42
47
|
* Parallax, opacity, tint, offset, and repeat flags are carried from the
|
|
43
48
|
* source Tiled map.
|
|
@@ -69,6 +74,8 @@ export declare class ImageLayer {
|
|
|
69
74
|
readonly parallaxX: number;
|
|
70
75
|
/** Vertical parallax factor. */
|
|
71
76
|
readonly parallaxY: number;
|
|
77
|
+
/** Uniform scale applied around the layer origin during parallax rendering. */
|
|
78
|
+
readonly parallaxScale: number;
|
|
72
79
|
/** Tint colour as `0xRRGGBB`, or `null`. */
|
|
73
80
|
readonly tintColor: number | null;
|
|
74
81
|
/** Whether the image repeats horizontally. */
|
|
@@ -79,3 +86,4 @@ export declare class ImageLayer {
|
|
|
79
86
|
readonly properties: TileProperties;
|
|
80
87
|
constructor(options: ImageLayerOptions);
|
|
81
88
|
}
|
|
89
|
+
//# sourceMappingURL=ImageLayer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImageLayer.d.ts","sourceRoot":"","sources":["../../src/ImageLayer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAE7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,sDAAsD;AACtD,MAAM,WAAW,iBAAiB;IAChC,wCAAwC;IACxC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,kBAAkB;IAClB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,0BAA0B;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,0BAA0B;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,8DAA8D;IAC9D,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAClC,oDAAoD;IACpD,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,8CAA8C;IAC9C,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,yCAAyC;IACzC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,yCAAyC;IACzC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,+CAA+C;IAC/C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,6CAA6C;IAC7C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,mEAAmE;IACnE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,4DAA4D;IAC5D,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,6DAA6D;IAC7D,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,4CAA4C;IAC5C,QAAQ,CAAC,UAAU,CAAC,EAAE,cAAc,CAAC;CACtC;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,UAAU;IACrB,+BAA+B;IAC/B,SAAgB,IAAI,EAAG,OAAO,CAAU;IAExC,wCAAwC;IACxC,SAAgB,EAAE,EAAE,MAAM,CAAC;IAC3B,kBAAkB;IAClB,SAAgB,IAAI,EAAE,MAAM,CAAC;IAC7B,0BAA0B;IAC1B,SAAgB,KAAK,EAAE,MAAM,CAAC;IAC9B,uCAAuC;IACvC,SAAgB,KAAK,EAAE,MAAM,CAAC;IAC9B,8DAA8D;IAC9D,SAAgB,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACxC,oCAAoC;IACpC,SAAgB,OAAO,EAAE,OAAO,CAAC;IACjC,iCAAiC;IACjC,SAAgB,OAAO,EAAE,MAAM,CAAC;IAChC,4BAA4B;IAC5B,SAAgB,OAAO,EAAE,MAAM,CAAC;IAChC,4BAA4B;IAC5B,SAAgB,OAAO,EAAE,MAAM,CAAC;IAChC,kCAAkC;IAClC,SAAgB,SAAS,EAAE,MAAM,CAAC;IAClC,gCAAgC;IAChC,SAAgB,SAAS,EAAE,MAAM,CAAC;IAClC,+EAA+E;IAC/E,SAAgB,aAAa,EAAE,MAAM,CAAC;IACtC,4CAA4C;IAC5C,SAAgB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzC,8CAA8C;IAC9C,SAAgB,OAAO,EAAE,OAAO,CAAC;IACjC,4CAA4C;IAC5C,SAAgB,OAAO,EAAE,OAAO,CAAC;IACjC,kCAAkC;IAClC,SAAgB,UAAU,EAAE,cAAc,CAAC;gBAExB,OAAO,EAAE,iBAAiB;CAqB9C"}
|