Each tilemap layer has its own Z: an offset added to the Z of the placed tilemap, like a child's Z to its parent, default 0 keeps the previous order (layers right after their map, in list order), a negative Z sorts a layer below the map, on a tie layers keep their list order and follow the hierarchy against other objects; editor scene and game use the same order through the new pure function drawSlots, a tilemap draws into one container per layer, and the tilemap panel gains a Z field for the selected layer with one undo step per change (#174)
game-maker / gate (push) Successful in 1m58s
game-maker / suite (push) Successful in 6m50s

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-19 14:03:20 +02:00
1 parent 5385d9e152
commit 47eea2f6e4
13 files changed
+389 -38

No files matched your search

+30
View File
@@ -0,0 +1,30 @@
import { isSymbolType } from '../core/model/AssetTypes';
import { sortByZ } from '../core/model/Properties';
import type { DrawItem } from './items';
/**
* One thing sorted into the draw order: an object, or one layer of a placed tilemap (#174) –
* `layerId` is empty for the object itself. A layer is sorted on its own, so a figure can walk
* between the ground and the treetops of one map.
*/
export interface DrawSlot {
id: string;
layerId: string;
z: number;
}
/**
* Back to front: ascending `z`, ties in hierarchy order, and a map's layers right after the map in
* their list order. With every layer at 0 that is exactly the order of before, where a map was one
* piece. Symbols are left out, they are drawn above everything.
*/
export function drawSlots(items: readonly DrawItem[]): DrawSlot[] {
const slots: DrawSlot[] = [];
for (const item of items) {
if (isSymbolType(item.type)) continue;
slots.push({ id: item.id, layerId: '', z: item.z });
for (const layer of item.tilemap?.layers ?? [])
slots.push({ id: item.id, layerId: layer.id, z: item.z + layer.z });
}
return sortByZ(slots);
}
+7 -3
View File
@@ -6,7 +6,7 @@ import type { Mat } from '../core/model/matrix';
import type { PropertyBag } from '../core/model/Properties';
import type { ParticlesAsset } from '../core/model/Asset';
import { readTile, tileFps, type TileInfo, type TilePlayback, tilePlayback } from '../core/model/tile';
import { isLayerVisible, layerTiles, type TileDraw } from '../core/model/tilemap';
import { isLayerVisible, layerTiles, layerZ, type TileDraw } from '../core/model/tilemap';
import {
type CellPolygon,
type CellRect,
@@ -63,9 +63,13 @@ export interface TilemapTile {
fps: number;
}
/** One layer, already worked out: which quarters each cell shows and which tile draws it. */
/**
* One layer, already worked out: which quarters each cell shows and which tile draws it, and how
* far above the map's own `z` it is sorted in among the other objects (#174).
*/
export interface TilemapLayerDraw {
id: string;
z: number;
tiles: TileDraw[];
}
@@ -96,7 +100,7 @@ function resolveTilemap(project: Project, assetId: string, props: PropertyBag):
for (const layer of asset.layers) {
if (!isLayerVisible(layer)) continue;
const tiles = layerTiles(project, props, layer);
layers.push({ id: layer.id, tiles });
layers.push({ id: layer.id, z: layerZ(layer), tiles });
for (const tile of tiles) {
if (used.has(tile.tileId)) continue;
const source = findAssetOf(project, tile.tileId, 'tile');
+49 -13
View File
@@ -27,16 +27,31 @@ export function layerAlpha(layerId: string, focus: LayerFocus | null): number {
return focus.mode === 'dim' ? DIMMED_LAYER_ALPHA : 0;
}
/**
* One container per layer rather than one per map: each layer has a `z` of its own (#174) and is
* sorted in among the other objects on its own – where is the viewport's call, not this one's.
*/
export class TilemapField {
readonly container = new Container();
private readonly layers = new Map<string, Container>();
private readonly meshes = new Map<string, Mesh>();
private shown = true;
/** True after a sync that drew a tile with more than one picture. */
animated = false;
/** How many meshes the last sync drew – one draw call each (test hook). */
drawCalls = 0;
constructor() {
this.container.eventMode = 'none';
get visible(): boolean {
return this.shown;
}
set visible(shown: boolean) {
this.shown = shown;
for (const layer of this.layers.values()) layer.visible = shown;
}
/** The container a layer draws into; null for a layer the last sync did not draw. */
layerContainer(layerId: string): Container | null {
return this.layers.get(layerId) ?? null;
}
/**
@@ -46,14 +61,17 @@ export class TilemapField {
sync(item: DrawItem, textures: TextureLibrary, now: number, focus: LayerFocus | null = null): void {
const draw = item.tilemap;
this.animated = false;
// The whole map sits under one node, so the object's own transform is applied once rather
// than baked into every vertex.
placeNode(this.container, item.m);
const tile = tilemapTileSize(item.props);
const origin = cellOrigin(item.props, 0, 0);
const drawn: Mesh[] = [];
const drawn: { layer: Container; mesh: Mesh }[] = [];
const alive = new Set<string>();
const aliveLayers = new Set<string>();
for (const layer of draw?.layers ?? []) {
aliveLayers.add(layer.id);
// Each layer sits under a node of its own carrying the map's transform, so that is applied
// once per layer rather than baked into every vertex.
const container = this.layerFor(item.id, layer.id);
placeNode(container, item.m);
const alpha = layerAlpha(layer.id, focus);
if (alpha === 0) continue;
for (const [tileId, tiles] of groupByTile(layer.tiles)) {
@@ -69,7 +87,7 @@ export class TilemapField {
// Per mesh rather than per layer: the meshes of one layer never overlap, so fading each
// one looks the same as fading the layer as a whole.
mesh.alpha = alpha;
drawn.push(mesh);
drawn.push({ layer: container, mesh });
}
}
for (const [key, mesh] of this.meshes) {
@@ -77,13 +95,30 @@ export class TilemapField {
mesh.destroy();
this.meshes.delete(key);
}
// Re-attached in order rather than sorted lazily: the layer order is the drawing order, and
// the list is at most one entry per tile and layer.
this.container.removeChildren();
for (const mesh of drawn) this.container.addChild(mesh);
for (const [id, layer] of this.layers) {
if (aliveLayers.has(id)) continue;
layer.destroy();
this.layers.delete(id);
}
// Re-attached rather than kept: a mesh whose layer showed nothing last time has to come back.
for (const layer of this.layers.values()) layer.removeChildren();
for (const { layer, mesh } of drawn) layer.addChild(mesh);
this.drawCalls = drawn.length;
}
private layerFor(objectId: string, layerId: string): Container {
let layer = this.layers.get(layerId);
if (!layer) {
layer = new Container();
layer.eventMode = 'none';
// The draw order reports it by this, next to the object's own id.
layer.label = `${objectId}#${layerId}`;
layer.visible = this.shown;
this.layers.set(layerId, layer);
}
return layer;
}
private meshFor(key: string, geometry: MeshGeometry, texture: Texture): Mesh {
const held = this.meshes.get(key);
if (held) {
@@ -99,7 +134,8 @@ export class TilemapField {
}
destroy(): void {
this.container.destroy({ children: true });
for (const layer of this.layers.values()) layer.destroy({ children: true });
this.layers.clear();
this.meshes.clear();
}
}