@codexo/exojs-tilemap 0.13.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/LICENSE +21 -0
- package/README.md +187 -0
- package/dist/esm/TileChunk.d.ts +136 -0
- package/dist/esm/TileChunk.js +192 -0
- package/dist/esm/TileChunk.js.map +1 -0
- package/dist/esm/TileChunkNode.d.ts +59 -0
- package/dist/esm/TileChunkNode.js +89 -0
- package/dist/esm/TileChunkNode.js.map +1 -0
- package/dist/esm/TileLayer.d.ts +229 -0
- package/dist/esm/TileLayer.js +450 -0
- package/dist/esm/TileLayer.js.map +1 -0
- package/dist/esm/TileLayerNode.d.ts +81 -0
- package/dist/esm/TileLayerNode.js +142 -0
- package/dist/esm/TileLayerNode.js.map +1 -0
- package/dist/esm/TileMap.d.ts +178 -0
- package/dist/esm/TileMap.js +262 -0
- package/dist/esm/TileMap.js.map +1 -0
- package/dist/esm/TileMapBand.d.ts +97 -0
- package/dist/esm/TileMapBand.js +161 -0
- package/dist/esm/TileMapBand.js.map +1 -0
- package/dist/esm/TileMapNode.d.ts +71 -0
- package/dist/esm/TileMapNode.js +113 -0
- package/dist/esm/TileMapNode.js.map +1 -0
- package/dist/esm/TileMapView.d.ts +182 -0
- package/dist/esm/TileMapView.js +342 -0
- package/dist/esm/TileMapView.js.map +1 -0
- package/dist/esm/TileSet.d.ts +99 -0
- package/dist/esm/TileSet.js +157 -0
- package/dist/esm/TileSet.js.map +1 -0
- package/dist/esm/chunkGeometry.d.ts +71 -0
- package/dist/esm/chunkGeometry.js +98 -0
- package/dist/esm/chunkGeometry.js.map +1 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +10 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/pixelSnap.d.ts +12 -0
- package/dist/esm/pixelSnap.js +19 -0
- package/dist/esm/pixelSnap.js.map +1 -0
- package/dist/esm/public.d.ts +18 -0
- package/dist/esm/register.d.ts +1 -0
- package/dist/esm/register.js +20 -0
- package/dist/esm/register.js.map +1 -0
- package/dist/esm/tilemapExtension.d.ts +18 -0
- package/dist/esm/tilemapExtension.js +52 -0
- package/dist/esm/tilemapExtension.js.map +1 -0
- package/dist/esm/types.d.ts +139 -0
- package/dist/esm/types.js +121 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/webgl2/WebGl2TileChunkRenderer.d.ts +39 -0
- package/dist/esm/webgl2/WebGl2TileChunkRenderer.js +339 -0
- package/dist/esm/webgl2/WebGl2TileChunkRenderer.js.map +1 -0
- package/dist/esm/webgpu/WebGpuTileChunkRenderer.d.ts +42 -0
- package/dist/esm/webgpu/WebGpuTileChunkRenderer.js +390 -0
- package/dist/esm/webgpu/WebGpuTileChunkRenderer.js.map +1 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Codexo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# @codexo/exojs-tilemap
|
|
2
|
+
|
|
3
|
+
Generic, format-independent tilemap runtime **and** WebGL2/WebGPU chunk renderer for
|
|
4
|
+
[ExoJS](https://exojs.dev). No Tiled (or any other on-disk format) vocabulary leaks into this
|
|
5
|
+
package — adapters such as [`@codexo/exojs-tiled`](https://www.npmjs.com/package/@codexo/exojs-tiled)
|
|
6
|
+
parse their format and hand this runtime fully-resolved tiles.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm install @codexo/exojs @codexo/exojs-tilemap
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`@codexo/exojs` is a peer dependency. Most users load Tiled maps and get this package
|
|
15
|
+
transitively through `@codexo/exojs-tiled` — install it directly only for procedural or
|
|
16
|
+
custom-format maps.
|
|
17
|
+
|
|
18
|
+
## What this package provides
|
|
19
|
+
|
|
20
|
+
**Runtime (data model)** — pure data, no scene graph:
|
|
21
|
+
|
|
22
|
+
- `TileMap` — a finite, chunk-first map: dimensions, tile size, `tilesets`, ordered `layers`.
|
|
23
|
+
- `TileLayer` — one tile layer; packed `Uint32Array` chunk storage, `visible` / `opacity` /
|
|
24
|
+
`offsetX/Y`, queries, and revision tracking.
|
|
25
|
+
- `TileSet` — a resolved tileset grid over a Core `TextureRegion` (Loader-owned texture).
|
|
26
|
+
- `ResolvedTile` / `TileTransform` — value-typed tile references with `flipX` / `flipY` /
|
|
27
|
+
`diagonal` orientation.
|
|
28
|
+
|
|
29
|
+
**Scene / rendering (`@advanced`)**:
|
|
30
|
+
|
|
31
|
+
- `TileMapNode` — convenience root that renders a whole `TileMap`.
|
|
32
|
+
- `TileLayerNode` — renders one `TileLayer`; generated per layer by `TileMapView` (or construct
|
|
33
|
+
directly).
|
|
34
|
+
- `TileMapView` — groups a map's layers into independently placeable layer nodes and named
|
|
35
|
+
`TileMapBand`s for actor interleaving (a helper, not a scene node).
|
|
36
|
+
- `TileMapBand` — a `Container` of tile-layer nodes produced by `TileMapView`.
|
|
37
|
+
- `tilemapExtension` — extension descriptor carrying the WebGL2/WebGPU renderer bindings.
|
|
38
|
+
|
|
39
|
+
## Usage — procedural map
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { Application, Texture, TextureRegion } from '@codexo/exojs';
|
|
43
|
+
import {
|
|
44
|
+
TileLayer,
|
|
45
|
+
TileMap,
|
|
46
|
+
TileMapNode,
|
|
47
|
+
TileSet,
|
|
48
|
+
tilemapExtension,
|
|
49
|
+
TILE_TRANSFORM_IDENTITY,
|
|
50
|
+
} from '@codexo/exojs-tilemap';
|
|
51
|
+
|
|
52
|
+
const app = new Application({ extensions: [tilemapExtension] /* canvas, … */ });
|
|
53
|
+
|
|
54
|
+
// Tileset over a Loader-owned atlas texture (the runtime never destroys it).
|
|
55
|
+
const atlas = await app.loader.load(Texture, 'tiles.png');
|
|
56
|
+
const terrain = new TileSet({
|
|
57
|
+
name: 'terrain',
|
|
58
|
+
texture: new TextureRegion(atlas, { x: 0, y: 0, width: atlas.width, height: atlas.height }),
|
|
59
|
+
tileWidth: 16,
|
|
60
|
+
tileHeight: 16,
|
|
61
|
+
tileCount: 256,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const ground = new TileLayer({
|
|
65
|
+
id: 1,
|
|
66
|
+
name: 'ground',
|
|
67
|
+
width: 64,
|
|
68
|
+
height: 64,
|
|
69
|
+
tileWidth: 16,
|
|
70
|
+
tileHeight: 16,
|
|
71
|
+
tilesets: [terrain],
|
|
72
|
+
});
|
|
73
|
+
ground.setTileAt(0, 0, { tileset: terrain, localTileId: 5, transform: TILE_TRANSFORM_IDENTITY });
|
|
74
|
+
|
|
75
|
+
const map = new TileMap({
|
|
76
|
+
name: 'world',
|
|
77
|
+
width: 64,
|
|
78
|
+
height: 64,
|
|
79
|
+
tileWidth: 16,
|
|
80
|
+
tileHeight: 16,
|
|
81
|
+
tilesets: [terrain],
|
|
82
|
+
layers: [ground],
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Render the whole map.
|
|
86
|
+
app.scene.root.addChild(new TileMapNode(map));
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Interleaving actors between layers — `TileMapView`
|
|
90
|
+
|
|
91
|
+
`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 *between* tile layers, create a
|
|
93
|
+
`TileMapView`: it generates one `TileLayerNode` per map layer (stable identity, map document
|
|
94
|
+
order) and groups them into named `TileMapBand`s that you parent yourself, as siblings of your
|
|
95
|
+
own actor containers:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
const view = map.createView({
|
|
99
|
+
bands: {
|
|
100
|
+
ground: ['background', 'ground'],
|
|
101
|
+
roof: ['roofs', 'foreground'],
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
worldRoot.addChild(
|
|
106
|
+
view.band('ground'),
|
|
107
|
+
actors, // app-owned actor container — drawn between ground and roof
|
|
108
|
+
view.band('roof'),
|
|
109
|
+
);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Or, without bands, place the generated per-layer nodes directly:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
const view = map.createView();
|
|
116
|
+
|
|
117
|
+
worldRoot.addChild(view.getLayerNodeById(groundId)!, actors, view.getLayerNodeById(roofId)!);
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Actors are application-owned siblings. `TileMapView` never adopts or destroys actors.
|
|
121
|
+
|
|
122
|
+
- A band definition **selects** layers (by id, or by unique layer name); rendering order within
|
|
123
|
+
a band always follows map document order — definitions never reorder layers.
|
|
124
|
+
- Layers not listed in any band stay reachable via `view.getLayerNodeById(id)` /
|
|
125
|
+
`view.getLayerNodesByName(name)`; they are not auto-added to any band.
|
|
126
|
+
- Destroying a view or a band destroys only the tile nodes it generated — never actors, the
|
|
127
|
+
`TileMap`, its `TileLayer`s, or tileset textures.
|
|
128
|
+
- There is no map-replacement API: to swap maps, destroy the old view and create a new view from
|
|
129
|
+
the new map — the actor tree is untouched.
|
|
130
|
+
- After layers are structurally added to or removed from the map, call `view.refreshLayers()`:
|
|
131
|
+
unchanged layer nodes keep their identity and bands keep their placement in your scene graph.
|
|
132
|
+
|
|
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
|
+
## Renderer model
|
|
141
|
+
|
|
142
|
+
- **Chunk-first.** A `TileLayerNode` is a container of per-chunk `TileChunkNode` drawables (one
|
|
143
|
+
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 *visible chunks × layers*, not
|
|
145
|
+
total tile count.
|
|
146
|
+
- **Revision-cached geometry.** Each chunk's quad geometry is built once and cached against the
|
|
147
|
+
source chunk's `revision`. Unchanged chunks never rebuild; a camera pan rebuilds nothing —
|
|
148
|
+
off-screen chunks are culled by their local bounds before any geometry is touched.
|
|
149
|
+
- **Orientation.** `flipX` / `flipY` / `diagonal` are baked into the chunk geometry / resolved in
|
|
150
|
+
the shader (all 8 combinations), with no per-tile matrix or per-frame cost.
|
|
151
|
+
- **Multiple tilesets** with differing tile sizes are first-class; tiles taller than the map grid
|
|
152
|
+
are bottom-left aligned (Tiled orthogonal convention).
|
|
153
|
+
- **WebGL2 and WebGPU** share one CPU geometry builder and produce identical output (golden
|
|
154
|
+
parity tested on both backends).
|
|
155
|
+
- **Sampling.** Tile UVs are exact (no half-texel inset), which assumes **nearest** atlas
|
|
156
|
+
filtering — the typical pixel-art case. Under linear or mipmap filtering, author tilesets with
|
|
157
|
+
extruded tile margins to avoid neighbour bleed at tile edges (extrusion-aware tilemap UV
|
|
158
|
+
insetting is a planned follow-up; the `NineSlice` / `RepeatingSprite` geometry paths already
|
|
159
|
+
inset).
|
|
160
|
+
|
|
161
|
+
## Ownership & lifecycle
|
|
162
|
+
|
|
163
|
+
- Tileset **textures are Loader-owned**. The runtime and renderer never destroy them.
|
|
164
|
+
- `TileMapNode` / `TileMapView` / `TileMapBand` / `TileLayerNode` reference — but never own —
|
|
165
|
+
the `TileMap`. Destroying a map node, view, or band frees only the tile nodes it generated
|
|
166
|
+
(detaching them from their application parents) and their cached geometry; application actors,
|
|
167
|
+
the `TileMap` data, its `TileLayer`s, and textures all survive. Free those via
|
|
168
|
+
`TileMap.destroy()` and `Loader.destroy()` respectively.
|
|
169
|
+
- A `TileMapNode` or `TileMapView` reflects the layer set at construction time. After layers are
|
|
170
|
+
structurally added to or removed from the map call `node.refreshLayers()` /
|
|
171
|
+
`view.refreshLayers()`; after tiles are written into previously-empty chunks call
|
|
172
|
+
`layerNode.refresh()`. In-place edits to existing chunks are picked up automatically.
|
|
173
|
+
|
|
174
|
+
## Core compatibility
|
|
175
|
+
|
|
176
|
+
| `@codexo/exojs-tilemap` | `@codexo/exojs` |
|
|
177
|
+
|---|---|
|
|
178
|
+
| 0.x | matching `0.x` |
|
|
179
|
+
|
|
180
|
+
## Links
|
|
181
|
+
|
|
182
|
+
- [API reference](https://exojs.dev/api/exojs-tilemap)
|
|
183
|
+
- [`@codexo/exojs-tiled`](https://www.npmjs.com/package/@codexo/exojs-tiled) — load Tiled `.tmj` maps into this runtime
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
MIT
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { PackedTile } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Public readonly view of a tile chunk.
|
|
4
|
+
*
|
|
5
|
+
* Provides read-only inspection access for iteration, queries, and the
|
|
6
|
+
* future renderer. The underlying storage is never exposed directly.
|
|
7
|
+
*
|
|
8
|
+
* @advanced
|
|
9
|
+
*/
|
|
10
|
+
export interface ReadonlyTileChunk {
|
|
11
|
+
/** Signed chunk X coordinate. */
|
|
12
|
+
readonly cx: number;
|
|
13
|
+
/** Signed chunk Y coordinate. */
|
|
14
|
+
readonly cy: number;
|
|
15
|
+
/** Width of this chunk in tiles. */
|
|
16
|
+
readonly width: number;
|
|
17
|
+
/** Height of this chunk in tiles. */
|
|
18
|
+
readonly height: number;
|
|
19
|
+
/** Whether every cell is empty (all-zero). */
|
|
20
|
+
readonly empty: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Monotonic revision counter. Increments on every cell mutation
|
|
23
|
+
* within this chunk. No-ops (writing same value) do NOT increment.
|
|
24
|
+
*/
|
|
25
|
+
readonly revision: number;
|
|
26
|
+
/**
|
|
27
|
+
* Read the raw packed tile word at local-in-chunk coordinates.
|
|
28
|
+
* Returns 0 for empty.
|
|
29
|
+
* @throws If coordinates are invalid.
|
|
30
|
+
*/
|
|
31
|
+
getRawAt(lx: number, ly: number): PackedTile;
|
|
32
|
+
/**
|
|
33
|
+
* Create a defensive copy of the tile data. The caller owns the copy;
|
|
34
|
+
* mutation of the returned array cannot affect the chunk.
|
|
35
|
+
*/
|
|
36
|
+
cloneTiles(): Uint32Array;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A compact, fixed-size chunk of tile data within a layer.
|
|
40
|
+
*
|
|
41
|
+
* Storage is a single {@link Uint32Array} of length `width * height`,
|
|
42
|
+
* laid out row-major. Each cell is a packed tile word (0 = empty).
|
|
43
|
+
* Chunk coordinates are signed to support future infinite maps.
|
|
44
|
+
*
|
|
45
|
+
* The backing array is **private** — external code cannot mutate storage
|
|
46
|
+
* directly. All mutation must flow through {@link TileLayer} public APIs,
|
|
47
|
+
* which call the package-internal `_setRawAt` method.
|
|
48
|
+
*
|
|
49
|
+
* The chunk tracks a revision counter that increments on every mutation
|
|
50
|
+
* so the future renderer can detect which chunks need GPU rebuilds.
|
|
51
|
+
*
|
|
52
|
+
* @advanced
|
|
53
|
+
*/
|
|
54
|
+
export declare class TileChunk implements ReadonlyTileChunk {
|
|
55
|
+
/** Signed chunk X coordinate. */
|
|
56
|
+
readonly cx: number;
|
|
57
|
+
/** Signed chunk Y coordinate. */
|
|
58
|
+
readonly cy: number;
|
|
59
|
+
/** Width of this chunk in tiles. */
|
|
60
|
+
readonly width: number;
|
|
61
|
+
/** Height of this chunk in tiles. */
|
|
62
|
+
readonly height: number;
|
|
63
|
+
/** Packed tile storage (row-major). Private — never exposed directly. */
|
|
64
|
+
private readonly _tiles;
|
|
65
|
+
/** Whether every cell in this chunk is empty (0). Lazy-computed. */
|
|
66
|
+
private _empty;
|
|
67
|
+
/** Monotonic revision counter — incremented on every mutation. */
|
|
68
|
+
private _revision;
|
|
69
|
+
/**
|
|
70
|
+
* @param cx Signed chunk X coordinate (must be finite safe integer).
|
|
71
|
+
* @param cy Signed chunk Y coordinate (must be finite safe integer).
|
|
72
|
+
* @param width Chunk width in tiles (positive safe integer).
|
|
73
|
+
* @param height Chunk height in tiles (positive safe integer).
|
|
74
|
+
* @param source Optional source data to copy (must match length).
|
|
75
|
+
*/
|
|
76
|
+
constructor(cx: number, cy: number, width: number, height: number, source?: Uint32Array);
|
|
77
|
+
/** Row-major index for cell (lx, ly) within this chunk. */
|
|
78
|
+
private _index;
|
|
79
|
+
/**
|
|
80
|
+
* Validate local-in-chunk coordinates before access.
|
|
81
|
+
* @throws If lx or ly is not a finite integer or out of [0, dimension).
|
|
82
|
+
*/
|
|
83
|
+
private _validateLocalCoord;
|
|
84
|
+
/** @inheritdoc */
|
|
85
|
+
getRawAt(lx: number, ly: number): PackedTile;
|
|
86
|
+
/** @inheritdoc */
|
|
87
|
+
get empty(): boolean;
|
|
88
|
+
/** @inheritdoc */
|
|
89
|
+
get revision(): number;
|
|
90
|
+
/** @inheritdoc */
|
|
91
|
+
cloneTiles(): Uint32Array;
|
|
92
|
+
/**
|
|
93
|
+
* Package-internal access to the underlying tile storage.
|
|
94
|
+
* Returns the actual mutable `Uint32Array` — DO NOT USE publicly.
|
|
95
|
+
*
|
|
96
|
+
* Provided for the future tilemap renderer (same package) so it can
|
|
97
|
+
* read chunk data efficiently without a full `cloneTiles()` copy per frame.
|
|
98
|
+
*
|
|
99
|
+
* External consumers and other packages MUST NOT call this.
|
|
100
|
+
* @internal
|
|
101
|
+
*/
|
|
102
|
+
_getRawStorage(): Uint32Array;
|
|
103
|
+
/**
|
|
104
|
+
* Validate a packed tile value before storing.
|
|
105
|
+
* Accepts any finite integer — `Uint32Array` stores via unsigned 32-bit
|
|
106
|
+
* truncation (`ToUint32`). Negative values produced by JavaScript's
|
|
107
|
+
* signed bitwise ops (e.g. when the transform bits set bit 31) are valid.
|
|
108
|
+
* @throws If the value is NaN, Infinity, or non-integer.
|
|
109
|
+
* @internal
|
|
110
|
+
*/
|
|
111
|
+
private _validatePacked;
|
|
112
|
+
/**
|
|
113
|
+
* Write a packed tile word at local-in-chunk coordinates.
|
|
114
|
+
* Returns true if the stored value actually changed.
|
|
115
|
+
*
|
|
116
|
+
* Package-internal: only {@link TileLayer} may call this.
|
|
117
|
+
* @internal
|
|
118
|
+
*/
|
|
119
|
+
_setRawAt(lx: number, ly: number, packed: PackedTile): boolean;
|
|
120
|
+
/**
|
|
121
|
+
* Mark the chunk as needing a GPU rebuild.
|
|
122
|
+
* Invalidates the empty cache and increments revision.
|
|
123
|
+
*
|
|
124
|
+
* Package-internal: for future renderer use only.
|
|
125
|
+
* @internal
|
|
126
|
+
*/
|
|
127
|
+
_markDirty(): void;
|
|
128
|
+
/**
|
|
129
|
+
* Clear every cell in this chunk. Does not re-allocate storage.
|
|
130
|
+
* Increments revision if any cell was non-zero.
|
|
131
|
+
*
|
|
132
|
+
* Package-internal: only {@link TileLayer} may call this.
|
|
133
|
+
* @internal
|
|
134
|
+
*/
|
|
135
|
+
_clear(): void;
|
|
136
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { validateInteger } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A compact, fixed-size chunk of tile data within a layer.
|
|
5
|
+
*
|
|
6
|
+
* Storage is a single {@link Uint32Array} of length `width * height`,
|
|
7
|
+
* laid out row-major. Each cell is a packed tile word (0 = empty).
|
|
8
|
+
* Chunk coordinates are signed to support future infinite maps.
|
|
9
|
+
*
|
|
10
|
+
* The backing array is **private** — external code cannot mutate storage
|
|
11
|
+
* directly. All mutation must flow through {@link TileLayer} public APIs,
|
|
12
|
+
* which call the package-internal `_setRawAt` method.
|
|
13
|
+
*
|
|
14
|
+
* The chunk tracks a revision counter that increments on every mutation
|
|
15
|
+
* so the future renderer can detect which chunks need GPU rebuilds.
|
|
16
|
+
*
|
|
17
|
+
* @advanced
|
|
18
|
+
*/
|
|
19
|
+
class TileChunk {
|
|
20
|
+
/** Signed chunk X coordinate. */
|
|
21
|
+
cx;
|
|
22
|
+
/** Signed chunk Y coordinate. */
|
|
23
|
+
cy;
|
|
24
|
+
/** Width of this chunk in tiles. */
|
|
25
|
+
width;
|
|
26
|
+
/** Height of this chunk in tiles. */
|
|
27
|
+
height;
|
|
28
|
+
/** Packed tile storage (row-major). Private — never exposed directly. */
|
|
29
|
+
_tiles;
|
|
30
|
+
/** Whether every cell in this chunk is empty (0). Lazy-computed. */
|
|
31
|
+
_empty = null;
|
|
32
|
+
/** Monotonic revision counter — incremented on every mutation. */
|
|
33
|
+
_revision = 0;
|
|
34
|
+
/**
|
|
35
|
+
* @param cx Signed chunk X coordinate (must be finite safe integer).
|
|
36
|
+
* @param cy Signed chunk Y coordinate (must be finite safe integer).
|
|
37
|
+
* @param width Chunk width in tiles (positive safe integer).
|
|
38
|
+
* @param height Chunk height in tiles (positive safe integer).
|
|
39
|
+
* @param source Optional source data to copy (must match length).
|
|
40
|
+
*/
|
|
41
|
+
constructor(cx, cy, width, height, source) {
|
|
42
|
+
// Validate chunk coordinates as finite safe integers (negative OK).
|
|
43
|
+
if (!Number.isFinite(cx) || !Number.isInteger(cx) || !Number.isSafeInteger(cx)) {
|
|
44
|
+
throw new Error(`TileChunk cx must be a finite safe integer (got ${cx}).`);
|
|
45
|
+
}
|
|
46
|
+
if (!Number.isFinite(cy) || !Number.isInteger(cy) || !Number.isSafeInteger(cy)) {
|
|
47
|
+
throw new Error(`TileChunk cy must be a finite safe integer (got ${cy}).`);
|
|
48
|
+
}
|
|
49
|
+
if (width <= 0 || height <= 0) {
|
|
50
|
+
throw new Error(`TileChunk dimensions must be positive (got ${width}x${height}).`);
|
|
51
|
+
}
|
|
52
|
+
if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height)) {
|
|
53
|
+
throw new Error(`TileChunk dimensions must be safe integers (got ${width}x${height}).`);
|
|
54
|
+
}
|
|
55
|
+
const size = width * height;
|
|
56
|
+
if (!Number.isSafeInteger(size)) {
|
|
57
|
+
throw new Error(`TileChunk size overflow: ${width} * ${height} is not safe.`);
|
|
58
|
+
}
|
|
59
|
+
// Guard against TypedArray length limits.
|
|
60
|
+
if (size > 0xFFFFFFFF) {
|
|
61
|
+
throw new Error(`TileChunk size ${size} exceeds TypedArray maximum length.`);
|
|
62
|
+
}
|
|
63
|
+
this.cx = cx;
|
|
64
|
+
this.cy = cy;
|
|
65
|
+
this.width = width;
|
|
66
|
+
this.height = height;
|
|
67
|
+
if (source) {
|
|
68
|
+
if (source.length !== size) {
|
|
69
|
+
throw new Error(`TileChunk source length ${source.length} != ${size}.`);
|
|
70
|
+
}
|
|
71
|
+
// Defensive copy — caller mutation of source array will not affect storage.
|
|
72
|
+
this._tiles = new Uint32Array(source);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
this._tiles = new Uint32Array(size);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Row-major index for cell (lx, ly) within this chunk. */
|
|
79
|
+
_index(lx, ly) {
|
|
80
|
+
return ly * this.width + lx;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Validate local-in-chunk coordinates before access.
|
|
84
|
+
* @throws If lx or ly is not a finite integer or out of [0, dimension).
|
|
85
|
+
*/
|
|
86
|
+
_validateLocalCoord(lx, ly) {
|
|
87
|
+
validateInteger(lx, 'lx');
|
|
88
|
+
validateInteger(ly, 'ly');
|
|
89
|
+
if (lx < 0 || lx >= this.width) {
|
|
90
|
+
throw new Error(`lx ${lx} out of chunk bounds [0, ${this.width - 1}].`);
|
|
91
|
+
}
|
|
92
|
+
if (ly < 0 || ly >= this.height) {
|
|
93
|
+
throw new Error(`ly ${ly} out of chunk bounds [0, ${this.height - 1}].`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// ── Readonly public API (ReadonlyTileChunk) ──────────────────────────
|
|
97
|
+
/** @inheritdoc */
|
|
98
|
+
getRawAt(lx, ly) {
|
|
99
|
+
this._validateLocalCoord(lx, ly);
|
|
100
|
+
return this._tiles[this._index(lx, ly)];
|
|
101
|
+
}
|
|
102
|
+
/** @inheritdoc */
|
|
103
|
+
get empty() {
|
|
104
|
+
if (this._empty === null) {
|
|
105
|
+
this._empty = this._tiles.every(v => v === 0);
|
|
106
|
+
}
|
|
107
|
+
return this._empty;
|
|
108
|
+
}
|
|
109
|
+
/** @inheritdoc */
|
|
110
|
+
get revision() {
|
|
111
|
+
return this._revision;
|
|
112
|
+
}
|
|
113
|
+
/** @inheritdoc */
|
|
114
|
+
cloneTiles() {
|
|
115
|
+
return new Uint32Array(this._tiles);
|
|
116
|
+
}
|
|
117
|
+
// ── Internal mutation (package-private, NOT public API) ──────────────
|
|
118
|
+
/**
|
|
119
|
+
* Package-internal access to the underlying tile storage.
|
|
120
|
+
* Returns the actual mutable `Uint32Array` — DO NOT USE publicly.
|
|
121
|
+
*
|
|
122
|
+
* Provided for the future tilemap renderer (same package) so it can
|
|
123
|
+
* read chunk data efficiently without a full `cloneTiles()` copy per frame.
|
|
124
|
+
*
|
|
125
|
+
* External consumers and other packages MUST NOT call this.
|
|
126
|
+
* @internal
|
|
127
|
+
*/
|
|
128
|
+
_getRawStorage() {
|
|
129
|
+
return this._tiles;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Validate a packed tile value before storing.
|
|
133
|
+
* Accepts any finite integer — `Uint32Array` stores via unsigned 32-bit
|
|
134
|
+
* truncation (`ToUint32`). Negative values produced by JavaScript's
|
|
135
|
+
* signed bitwise ops (e.g. when the transform bits set bit 31) are valid.
|
|
136
|
+
* @throws If the value is NaN, Infinity, or non-integer.
|
|
137
|
+
* @internal
|
|
138
|
+
*/
|
|
139
|
+
_validatePacked(packed) {
|
|
140
|
+
if (typeof packed !== 'number' || !Number.isFinite(packed) || !Number.isInteger(packed)) {
|
|
141
|
+
throw new Error(`Packed tile must be a finite integer (got ${packed}).`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Write a packed tile word at local-in-chunk coordinates.
|
|
146
|
+
* Returns true if the stored value actually changed.
|
|
147
|
+
*
|
|
148
|
+
* Package-internal: only {@link TileLayer} may call this.
|
|
149
|
+
* @internal
|
|
150
|
+
*/
|
|
151
|
+
_setRawAt(lx, ly, packed) {
|
|
152
|
+
this._validateLocalCoord(lx, ly);
|
|
153
|
+
this._validatePacked(packed);
|
|
154
|
+
const i = this._index(lx, ly);
|
|
155
|
+
const prev = this._tiles[i];
|
|
156
|
+
if (prev === packed)
|
|
157
|
+
return false;
|
|
158
|
+
this._tiles[i] = packed;
|
|
159
|
+
this._revision++;
|
|
160
|
+
this._empty = null; // invalidate cache
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Mark the chunk as needing a GPU rebuild.
|
|
165
|
+
* Invalidates the empty cache and increments revision.
|
|
166
|
+
*
|
|
167
|
+
* Package-internal: for future renderer use only.
|
|
168
|
+
* @internal
|
|
169
|
+
*/
|
|
170
|
+
_markDirty() {
|
|
171
|
+
this._empty = null;
|
|
172
|
+
this._revision++;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Clear every cell in this chunk. Does not re-allocate storage.
|
|
176
|
+
* Increments revision if any cell was non-zero.
|
|
177
|
+
*
|
|
178
|
+
* Package-internal: only {@link TileLayer} may call this.
|
|
179
|
+
* @internal
|
|
180
|
+
*/
|
|
181
|
+
_clear() {
|
|
182
|
+
const hadContent = this._tiles.some(v => v !== 0);
|
|
183
|
+
if (!hadContent)
|
|
184
|
+
return;
|
|
185
|
+
this._tiles.fill(0);
|
|
186
|
+
this._revision++;
|
|
187
|
+
this._empty = true;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export { TileChunk };
|
|
192
|
+
//# sourceMappingURL=TileChunk.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TileChunk.js","sources":["../../../src/TileChunk.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Rectangle } from '@codexo/exojs';
|
|
2
|
+
import { Drawable } from '@codexo/exojs/rendering';
|
|
3
|
+
import type { ChunkPage } from './chunkGeometry';
|
|
4
|
+
import type { ReadonlyTileChunk } from './TileChunk';
|
|
5
|
+
import type { TileSet } from './TileSet';
|
|
6
|
+
/**
|
|
7
|
+
* A single renderable tile chunk. One `TileChunkNode` is a {@link Drawable}
|
|
8
|
+
* that owns the batched quad geometry for exactly one {@link ReadonlyTileChunk}
|
|
9
|
+
* of one {@link import('./TileLayer').TileLayer}, positioned at the chunk's
|
|
10
|
+
* pixel origin within the owning {@link TileLayerNode}.
|
|
11
|
+
*
|
|
12
|
+
* Geometry is built lazily and cached against the source chunk's `revision`:
|
|
13
|
+
* the renderer reads {@link TileChunkNode.pages} on each visible frame, but a
|
|
14
|
+
* rebuild only happens when the underlying chunk actually changed. Off-screen
|
|
15
|
+
* chunks are culled by their accurate {@link getLocalBounds} before `render`
|
|
16
|
+
* is ever called, so a camera pan rebuilds nothing.
|
|
17
|
+
*
|
|
18
|
+
* The node references — but never owns — the runtime chunk, tilesets, and
|
|
19
|
+
* tileset textures. Destroying it releases only its cached CPU geometry; the
|
|
20
|
+
* `TileMap`/`TileLayer` data and Loader-owned textures are untouched.
|
|
21
|
+
*
|
|
22
|
+
* @internal Package-internal render node; the renderer is registered for this
|
|
23
|
+
* class. Applications use {@link TileMapNode} / {@link TileLayerNode}.
|
|
24
|
+
*/
|
|
25
|
+
export declare class TileChunkNode extends Drawable {
|
|
26
|
+
private readonly _chunk;
|
|
27
|
+
private readonly _tilesets;
|
|
28
|
+
private readonly _tileWidth;
|
|
29
|
+
private readonly _tileHeight;
|
|
30
|
+
private readonly _pixelWidth;
|
|
31
|
+
private readonly _pixelHeight;
|
|
32
|
+
private _pages;
|
|
33
|
+
private _builtRevision;
|
|
34
|
+
/**
|
|
35
|
+
* @param chunk The readonly chunk this node renders.
|
|
36
|
+
* @param tilesets The owning layer's tileset array.
|
|
37
|
+
* @param tileWidth Map/layer tile cell width in pixels.
|
|
38
|
+
* @param tileHeight Map/layer tile cell height in pixels.
|
|
39
|
+
* @param chunkWidthTiles The layer's (unclamped) chunk width in tiles, used
|
|
40
|
+
* to compute the chunk's pixel origin. Edge chunks
|
|
41
|
+
* report a smaller `chunk.width`, but always start at
|
|
42
|
+
* `cx * chunkWidthTiles`.
|
|
43
|
+
* @param chunkHeightTiles The layer's (unclamped) chunk height in tiles.
|
|
44
|
+
*/
|
|
45
|
+
constructor(chunk: ReadonlyTileChunk, tilesets: readonly TileSet[], tileWidth: number, tileHeight: number, chunkWidthTiles: number, chunkHeightTiles: number);
|
|
46
|
+
/** The signed chunk coordinates this node renders. */
|
|
47
|
+
get chunkX(): number;
|
|
48
|
+
get chunkY(): number;
|
|
49
|
+
/**
|
|
50
|
+
* The revision-cached per-tileset page geometry. Rebuilt only when the source
|
|
51
|
+
* chunk's `revision` advances; otherwise the same arrays are returned.
|
|
52
|
+
* @internal Read by the per-backend tile chunk renderer.
|
|
53
|
+
*/
|
|
54
|
+
get pages(): readonly ChunkPage[];
|
|
55
|
+
/** `true` when the chunk has no drawable tiles (no geometry, no draw calls). */
|
|
56
|
+
get isEmpty(): boolean;
|
|
57
|
+
getLocalBounds(): Rectangle;
|
|
58
|
+
destroy(): void;
|
|
59
|
+
}
|