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
@@ -4,7 +4,7 @@ import { previewObjectId } from '../core/model/animationPreview';
|
||||
import { resolveObjects } from '../core/model/instance';
|
||||
import { findAsset, findAssetOf, findObjectHolder } from '../core/model/Project';
|
||||
import type { FrameRect } from '../core/model/texture';
|
||||
import { findLayer } from '../core/model/tilemap';
|
||||
import { findLayer, layerZ } from '../core/model/tilemap';
|
||||
import { effectiveScale, gameClock } from '../core/time';
|
||||
import { physicsStatus } from '../runtime/physics/engine';
|
||||
import {
|
||||
@@ -554,6 +554,7 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
|
||||
id: layer.id,
|
||||
name: layer.name,
|
||||
hidden: layer.hidden === true,
|
||||
z: layerZ(layer),
|
||||
cells: Object.keys(layer.cells).length,
|
||||
})) ?? null,
|
||||
/** What the scene view drew: meshes (one draw call each), cells and visible layers. */
|
||||
@@ -598,6 +599,11 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
|
||||
const field = currentTilemapWidgets()?.layerName;
|
||||
return field ? { ...centreOf(field), value: field.value } : null;
|
||||
},
|
||||
/** The Z field of the painted layer, with what it holds (#174). */
|
||||
layerZAt: () => {
|
||||
const field = currentTilemapWidgets()?.layerZ;
|
||||
return field ? { ...centreOf(field), value: field.value } : null;
|
||||
},
|
||||
focusAt: () => {
|
||||
const button = currentTilemapWidgets()?.focus;
|
||||
return button ? { ...centreOf(button), label: button.text } : null;
|
||||
|
||||
@@ -13,19 +13,21 @@ import {
|
||||
RemoveTilemapLayerCommand,
|
||||
RenameTilemapLayerCommand,
|
||||
SetTilemapLayerHiddenCommand,
|
||||
SetTilemapLayerZCommand,
|
||||
} from '../../core/commands/tilemapCommands';
|
||||
import type { Command } from '../../core/commands/Command';
|
||||
import type { TilemapAsset } from '../../core/model/Asset';
|
||||
import { findObject } from '../../core/model/hierarchy';
|
||||
import { resolvedHolder } from '../../core/model/instance';
|
||||
import { findAssetOf } from '../../core/model/Project';
|
||||
import { findLayer, isLayerVisible } from '../../core/model/tilemap';
|
||||
import { findLayer, isLayerVisible, layerZ } from '../../core/model/tilemap';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Dropdown,
|
||||
HStack,
|
||||
Menu,
|
||||
NumberField,
|
||||
Panel,
|
||||
ScrollContainer,
|
||||
TextField,
|
||||
@@ -134,6 +136,8 @@ export interface TilemapPanelWidgets {
|
||||
layers: { id: string; name: Button; visible: Checkbox; up: Button; down: Button; remove: Button }[];
|
||||
/** The name of the layer painted on (#138). */
|
||||
layerName: TextField;
|
||||
/** Its `z` above the map's own (#174). */
|
||||
layerZ: NumberField;
|
||||
/** How the other layers are shown in the scene view (#138). */
|
||||
focus: Dropdown;
|
||||
addLayer: Button;
|
||||
@@ -236,7 +240,8 @@ function buildPanelContent(host: InspectorHost, asset: TilemapAsset): void {
|
||||
const { state } = host;
|
||||
const paint = state.tilemapPaint;
|
||||
const painted = paintedLayerId(state, asset);
|
||||
const paintedName = findLayer(asset, painted)?.name ?? '';
|
||||
const paintedLayer = findLayer(asset, painted);
|
||||
const paintedName = paintedLayer?.name ?? '';
|
||||
const layerName: TextField = new TextField(paintedName, {
|
||||
onChange: (name) => {
|
||||
// The command rejects a blank name but cannot put the old text back into the field,
|
||||
@@ -245,6 +250,12 @@ function buildPanelContent(host: InspectorHost, asset: TilemapAsset): void {
|
||||
else layerName.setValue(paintedName);
|
||||
},
|
||||
});
|
||||
const layerZField = new NumberField(paintedLayer ? layerZ(paintedLayer) : 0, {
|
||||
onChange: (z) => {
|
||||
if (paintedLayer && z !== layerZ(paintedLayer))
|
||||
state.execute(new SetTilemapLayerZCommand(asset.id, painted, z));
|
||||
},
|
||||
});
|
||||
const focus: Dropdown = new Dropdown(FOCUS.find((f) => f.focus === paint.focus)?.label ?? '', {
|
||||
onClick: () =>
|
||||
new Menu(
|
||||
@@ -259,6 +270,7 @@ function buildPanelContent(host: InspectorHost, asset: TilemapAsset): void {
|
||||
palette: null,
|
||||
layers: [],
|
||||
layerName,
|
||||
layerZ: layerZField,
|
||||
focus,
|
||||
addLayer: new Button('Add layer', {
|
||||
onClick: () => state.execute(new AddTilemapLayerCommand(asset.id)),
|
||||
@@ -326,6 +338,10 @@ function buildPanelContent(host: InspectorHost, asset: TilemapAsset): void {
|
||||
const nameRow = labelledRow('Name').row;
|
||||
nameRow.add(layerName, 1);
|
||||
host.body.add(nameRow);
|
||||
const zRow = labelledRow('Z').row;
|
||||
zRow.add(layerZField, 1);
|
||||
host.body.add(zRow);
|
||||
addNote(host, 'Added to the map’s own Z, as a child’s is.', 'small');
|
||||
const showRow = labelledRow('Show').row;
|
||||
showRow.add(focus, 1);
|
||||
host.body.add(showRow);
|
||||
@@ -346,7 +362,9 @@ function tileIdsOf(state: EditorState): string[] {
|
||||
*/
|
||||
function tilemapSignature(state: EditorState, asset: TilemapAsset): string {
|
||||
const paint = state.tilemapPaint;
|
||||
const layers = asset.layers.map((l) => `${l.id}:${l.name}:${isLayerVisible(l) ? 1 : 0}`).join(',');
|
||||
const layers = asset.layers
|
||||
.map((l) => `${l.id}:${l.name}:${isLayerVisible(l) ? 1 : 0}:${layerZ(l)}`)
|
||||
.join(',');
|
||||
const tiles = tileIdsOf(state).join(',');
|
||||
return `${paint.tool}|${paint.tileId}|${paintedLayerId(state, asset)}|${paint.focus}|${layers}|${tiles}`;
|
||||
}
|
||||
@@ -9,12 +9,13 @@ import {
|
||||
} from 'pixi.js';
|
||||
import { isSymbolType } from '../../core/model/AssetTypes';
|
||||
import { type Extent, scaleOf } from '../../core/model/matrix';
|
||||
import { sortByZ, type Vec2, textOf } from '../../core/model/Properties';
|
||||
import { type Vec2, textOf } from '../../core/model/Properties';
|
||||
import { footprintExtent, footprintOf, placementOf, textAnchor } from '../../core/properties/bounds';
|
||||
import { rectsIntersect } from '../../core/properties/camera';
|
||||
import type { EditorState } from '../state';
|
||||
import { type Size, Theme, Widget } from '../../ui';
|
||||
import { fontLibrary } from '../../viewport/FontLibrary';
|
||||
import { drawSlots } from '../../viewport/drawOrder';
|
||||
import type { DrawItem } from '../../viewport/items';
|
||||
import { hashId } from '../../core/particles/nodes';
|
||||
import { ParticleField } from '../../viewport/particles';
|
||||
@@ -55,7 +56,7 @@ export abstract class WorldViewport extends Widget {
|
||||
private readonly sprites = new Map<string, Mesh>();
|
||||
/** The drawn particles of each source, one container – and one draw call – per source. */
|
||||
private readonly fields = new Map<string, ParticleField>();
|
||||
/** The meshes of each placed tilemap, one container per map. */
|
||||
/** The meshes of each placed tilemap, one container per layer (#174). */
|
||||
private readonly tilemaps = new Map<string, TilemapField>();
|
||||
private visibleCount = 0;
|
||||
private animated = false;
|
||||
@@ -274,25 +275,28 @@ export abstract class WorldViewport extends Widget {
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-attaches the object nodes in ascending `z`, ties in hierarchy order. Explicit rather than
|
||||
* Pixi's lazy `sortableChildren` so the order is deterministic and honours hierarchy changes.
|
||||
* Re-attaches the object nodes in ascending `z`, ties in hierarchy order, each layer of a map
|
||||
* sorted in on its own (#174, see `drawSlots`). Explicit rather than Pixi's lazy
|
||||
* `sortableChildren` so the order is deterministic and honours hierarchy changes.
|
||||
*/
|
||||
private syncDrawOrder(items: DrawItem[]): void {
|
||||
const ordered = sortByZ(items.filter((item) => !isSymbolType(item.type)));
|
||||
this.objects.removeChildren();
|
||||
for (const item of ordered) {
|
||||
for (const { id, layerId } of drawSlots(items)) {
|
||||
if (layerId) {
|
||||
const layer = this.tilemaps.get(id)?.layerContainer(layerId);
|
||||
if (layer) this.objects.addChild(layer);
|
||||
continue;
|
||||
}
|
||||
// Picture and text are siblings because Graphics may not have children: the picture right
|
||||
// under its node, which only holds the hit area, the text right above its box.
|
||||
const sprite = this.sprites.get(item.id);
|
||||
const sprite = this.sprites.get(id);
|
||||
if (sprite) this.objects.addChild(sprite);
|
||||
const node = this.nodes.get(item.id);
|
||||
const node = this.nodes.get(id);
|
||||
if (node) this.objects.addChild(node);
|
||||
const text = this.texts.get(item.id);
|
||||
const text = this.texts.get(id);
|
||||
if (text) this.objects.addChild(text);
|
||||
const field = this.fields.get(item.id);
|
||||
const field = this.fields.get(id);
|
||||
if (field) this.objects.addChild(field.container);
|
||||
const tilemap = this.tilemaps.get(item.id);
|
||||
if (tilemap) this.objects.addChild(tilemap.container);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,15 +343,15 @@ export abstract class WorldViewport extends Widget {
|
||||
private syncTilemap(item: DrawItem, onScreen: boolean): void {
|
||||
let field = this.tilemaps.get(item.id);
|
||||
if (item.type !== 'tilemap' || !item.tilemap || !onScreen) {
|
||||
if (field) field.container.visible = false;
|
||||
if (field) field.visible = false;
|
||||
return;
|
||||
}
|
||||
if (!field) {
|
||||
// Its layers are attached by `syncDrawOrder`, which runs after every item is synced.
|
||||
field = new TilemapField();
|
||||
this.tilemaps.set(item.id, field);
|
||||
this.objects.addChild(field.container);
|
||||
}
|
||||
field.container.visible = true;
|
||||
field.visible = true;
|
||||
field.sync(item, textureLibrary, gameClock.now, this.tilemapFocus(item));
|
||||
if (field.animated) this.animated = true;
|
||||
}
|
||||
@@ -375,7 +379,7 @@ export abstract class WorldViewport extends Widget {
|
||||
tilemapInfo(objectId: string): { drawCalls: number; tiles: number; layers: number } | null {
|
||||
const field = this.tilemaps.get(objectId);
|
||||
const item = this.lastItems.find((i) => i.id === objectId);
|
||||
if (!field || !field.container.visible || !item?.tilemap) return null;
|
||||
if (!field?.visible || !item?.tilemap) return null;
|
||||
return {
|
||||
drawCalls: field.drawCalls,
|
||||
tiles: item.tilemap.layers.reduce((sum, layer) => sum + layer.tiles.length, 0),
|
||||
|
||||
Reference in new issue
Block a user