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)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5385d9e152
commit
47eea2f6e4
13 files changed
+389
-38
No files matched your search
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user