Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion packages/exojs-tilemap/src/TileLayerNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ export interface TileLayerNodeOptions {
* `offset` is applied to this node's transform; `visible` and `opacity` are
* read live from the runtime layer on every frame (no rebuild required).
*
* A `parallax` factor other than `1` makes this node's rendered position
* camera-dependent, resolved per frame in {@link _collectContent} by patching
* the node position against the camera centre, then restoring it — the cull
* test runs before that patch, so a bounded layer with parallax would be
* culled at its static base-offset bounds while actually rendering elsewhere.
* Such a layer therefore opts out of view culling (`cullable = false`), same
* as an unbounded layer (exactly like {@link import('./ImageLayerNode').ImageLayerNode}).
*
* The node references — but never owns — the {@link TileLayer}: destroying it
* frees its chunk nodes and their cached geometry but leaves the layer, map,
* and Loader-owned textures intact.
Expand Down Expand Up @@ -93,7 +101,7 @@ export class TileLayerNode extends Container {
this.setPosition(layer.offsetX, layer.offsetY);
this._buildChunkNodes();

if (!this._layer.bounded) {
if (!this._layer.bounded || layer.parallaxX !== 1 || layer.parallaxY !== 1) {
this.cullable = false;
}

Expand Down
14 changes: 14 additions & 0 deletions packages/exojs-tilemap/test/nodes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,20 @@ describe('TileLayerNode', () => {
expect(node.y).toBe(20);
});

it('bounded parallax layer opts out of static-bounds culling', () => {
const tileset = makeTileset();
const layer = new TileLayer({
id: 1, name: 'bg', width: 4, height: 4, tileWidth: 32, tileHeight: 32,
tilesets: [tileset], parallaxX: 0.5,
});
const node = new TileLayerNode(layer);

// The cull test runs before the collect-time parallax patch, so a bounded
// parallax layer's static bounds no longer match its render-time position
// — it must opt out of culling entirely, exactly like an unbounded layer.
expect(node.cullable).toBe(false);
});

it('reports local bounds as the layer pixel rect (even when empty)', () => {
const tileset = makeTileset();
const layer = makeLayer(tileset, { width: 5, height: 3 });
Expand Down
3 changes: 2 additions & 1 deletion site/src/content/api/tile-layer-node.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"title": "TileLayerNode",
"description": "A scene node that renders one generic TileLayer as a Container of per-chunk TileChunkNode drawables. Each non-empty loaded chunk becomes one child positioned at its pixel origin, so the engine's existing per-node culling drops individual chunks and the render-plan optimiser batches them by tileset texture. The layer's pixel `offset` is applied to this node's transform; `visible` and `opacity` are read live from the runtime layer on every frame (no rebuild required). The node references — but never owns — the TileLayer: destroying it frees its chunk nodes and their cached geometry but leaves the layer, map, and Loader-owned textures intact. Structural changes made via `_adoptChunk`/`_evictChunk` (chunk streaming) are picked up incrementally as they happen, without a full rebuild. TileLayerNode.refresh remains available for callers that mutate chunk structure by other means (e.g. a bulk edit that bypasses `_adoptChunk`/`_evictChunk`) and need a full resync. In-place tile edits to existing chunks are picked up automatically via chunk revisions.",
"description": "A scene node that renders one generic TileLayer as a Container of per-chunk TileChunkNode drawables. Each non-empty loaded chunk becomes one child positioned at its pixel origin, so the engine's existing per-node culling drops individual chunks and the render-plan optimiser batches them by tileset texture. The layer's pixel `offset` is applied to this node's transform; `visible` and `opacity` are read live from the runtime layer on every frame (no rebuild required). A `parallax` factor other than `1` makes this node's rendered position camera-dependent, resolved per frame in _collectContent by patching the node position against the camera centre, then restoring it — the cull test runs before that patch, so a bounded layer with parallax would be culled at its static base-offset bounds while actually rendering elsewhere. Such a layer therefore opts out of view culling (`cullable = false`), same as an unbounded layer (exactly like import('./ImageLayerNode').ImageLayerNode). The node references — but never owns — the TileLayer: destroying it frees its chunk nodes and their cached geometry but leaves the layer, map, and Loader-owned textures intact. Structural changes made via `_adoptChunk`/`_evictChunk` (chunk streaming) are picked up incrementally as they happen, without a full rebuild. TileLayerNode.refresh remains available for callers that mutate chunk structure by other means (e.g. a bulk edit that bypasses `_adoptChunk`/`_evictChunk`) and need a full resync. In-place tile edits to existing chunks are picked up automatically via chunk revisions.",
"symbol": "TileLayerNode",
"kind": "class",
"subsystem": "tilemap",
Expand All @@ -21,6 +21,7 @@
"paragraphs": [
"A scene node that renders one generic TileLayer as a Container of per-chunk TileChunkNode drawables.",
"Each non-empty loaded chunk becomes one child positioned at its pixel origin, so the engine's existing per-node culling drops individual chunks and the render-plan optimiser batches them by tileset texture. The layer's pixel `offset` is applied to this node's transform; `visible` and `opacity` are read live from the runtime layer on every frame (no rebuild required).",
"A `parallax` factor other than `1` makes this node's rendered position camera-dependent, resolved per frame in _collectContent by patching the node position against the camera centre, then restoring it — the cull test runs before that patch, so a bounded layer with parallax would be culled at its static base-offset bounds while actually rendering elsewhere. Such a layer therefore opts out of view culling (`cullable = false`), same as an unbounded layer (exactly like import('./ImageLayerNode').ImageLayerNode).",
"The node references — but never owns — the TileLayer: destroying it frees its chunk nodes and their cached geometry but leaves the layer, map, and Loader-owned textures intact.",
"Structural changes made via `_adoptChunk`/`_evictChunk` (chunk streaming) are picked up incrementally as they happen, without a full rebuild. TileLayerNode.refresh remains available for callers that mutate chunk structure by other means (e.g. a bulk edit that bypasses `_adoptChunk`/`_evictChunk`) and need a full resync. In-place tile edits to existing chunks are picked up automatically via chunk revisions."
],
Expand Down
Loading