The active tilemap layer can be renamed from the painting section, and a Show setting fades or hides the other layers in the scene view while the tilemap is selected (#138)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bfbcbe604c
commit
f53279d065
10 files changed
+248
-13
No files matched your search
@@ -38,7 +38,7 @@ npm run build # writes dist/ and regenerates index.html; the folder is wh
|
||||
The browser suite and the three example games need a machine with a GPU:
|
||||
|
||||
```bash
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1023 checks (GM_STEPS=20 for one step)
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1027 checks (GM_STEPS=20 for one step)
|
||||
node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs
|
||||
node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs
|
||||
node tools/seed-actor.mjs && node tools/smoke-actor.mjs
|
||||
|
||||
@@ -147,8 +147,10 @@ export class RenameTilemapLayerCommand implements Command {
|
||||
|
||||
execute(project: Project): void {
|
||||
const layer = requireTilemapLayer(project, this.assetId, this.layerId);
|
||||
const name = this.name.trim();
|
||||
if (!name) throw new CommandError('A layer needs a name');
|
||||
this.previous = layer.name;
|
||||
layer.name = this.name;
|
||||
layer.name = name;
|
||||
}
|
||||
|
||||
undo(project: Project): void {
|
||||
|
||||
@@ -571,6 +571,15 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
|
||||
const button = currentTilemapSection()?.addLayer;
|
||||
return button ? centreOf(button) : null;
|
||||
},
|
||||
/** The name field of the painted layer and the Show dropdown, with what they hold (#138). */
|
||||
layerNameAt: () => {
|
||||
const field = currentTilemapSection()?.layerName;
|
||||
return field ? { ...centreOf(field), value: field.value } : null;
|
||||
},
|
||||
focusAt: () => {
|
||||
const button = currentTilemapSection()?.focus;
|
||||
return button ? { ...centreOf(button), label: button.text } : null;
|
||||
},
|
||||
/** One row of the layer list, top-down as drawn: the layer, its switch and its buttons. */
|
||||
layerRowAt: (index: number) => {
|
||||
const row = currentTilemapSection()?.layers[index];
|
||||
|
||||
@@ -10,15 +10,23 @@ import {
|
||||
AddTilemapLayerCommand,
|
||||
MoveTilemapLayerCommand,
|
||||
RemoveTilemapLayerCommand,
|
||||
RenameTilemapLayerCommand,
|
||||
SetTilemapLayerHiddenCommand,
|
||||
} from '../../core/commands/tilemapCommands';
|
||||
import type { TilemapAsset } from '../../core/model/Asset';
|
||||
import { isLayerVisible } from '../../core/model/tilemap';
|
||||
import { Button, Checkbox, HStack, Theme } from '../../ui';
|
||||
import type { EditorState, TilemapTool } from '../state';
|
||||
import { findLayer, isLayerVisible } from '../../core/model/tilemap';
|
||||
import { Button, Checkbox, Dropdown, HStack, Menu, TextField, Theme } from '../../ui';
|
||||
import type { EditorState, TilemapFocus, TilemapTool } from '../state';
|
||||
import { TilePalette } from '../tile/TilePalette';
|
||||
import { labelledRow } from './fieldFactory';
|
||||
import { addGap, addNote, addTitle, type InspectorHost } from './host';
|
||||
|
||||
const FOCUS: { focus: TilemapFocus; label: string }[] = [
|
||||
{ focus: 'all', label: 'All layers' },
|
||||
{ focus: 'dim', label: 'Others faded' },
|
||||
{ focus: 'solo', label: 'Active layer only' },
|
||||
];
|
||||
|
||||
const TOOLS: { tool: TilemapTool; label: string; hint: string }[] = [
|
||||
{ tool: 'select', label: 'Select', hint: 'Drag the map to move it, as any other object.' },
|
||||
{ tool: 'brush', label: 'Brush', hint: 'Drag over the map to paint a line of cells.' },
|
||||
@@ -46,6 +54,10 @@ export interface TilemapSectionWidgets {
|
||||
/** Null while the project has no tile. */
|
||||
palette: TilePalette | null;
|
||||
layers: { id: string; name: Button; visible: Checkbox; up: Button; down: Button; remove: Button }[];
|
||||
/** The name of the layer painted on (#138). */
|
||||
layerName: TextField;
|
||||
/** How the other layers are shown in the scene view (#138). */
|
||||
focus: Dropdown;
|
||||
addLayer: Button;
|
||||
}
|
||||
|
||||
@@ -58,10 +70,31 @@ export function currentTilemapSection(): TilemapSectionWidgets | null {
|
||||
export function buildTilemapSection(host: InspectorHost, asset: TilemapAsset): void {
|
||||
const { state } = host;
|
||||
const paint = state.tilemapPaint;
|
||||
const painted = paintedLayerId(state, asset);
|
||||
const paintedName = findLayer(asset, painted)?.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,
|
||||
// so an empty one is not sent at all.
|
||||
if (name.trim()) state.execute(new RenameTilemapLayerCommand(asset.id, painted, name));
|
||||
else layerName.setValue(paintedName);
|
||||
},
|
||||
});
|
||||
const focus: Dropdown = new Dropdown(FOCUS.find((f) => f.focus === paint.focus)?.label ?? '', {
|
||||
onClick: () =>
|
||||
new Menu(
|
||||
FOCUS.map((f) => ({
|
||||
label: f.focus === paint.focus ? `● ${f.label}` : f.label,
|
||||
onSelect: () => state.setTilemapPaint({ focus: f.focus }),
|
||||
})),
|
||||
).openBelow(focus),
|
||||
});
|
||||
const widgets: TilemapSectionWidgets = {
|
||||
tools: new Map(),
|
||||
palette: null,
|
||||
layers: [],
|
||||
layerName,
|
||||
focus,
|
||||
addLayer: new Button('Add layer', {
|
||||
onClick: () => state.execute(new AddTilemapLayerCommand(asset.id)),
|
||||
}),
|
||||
@@ -98,7 +131,6 @@ export function buildTilemapSection(host: InspectorHost, asset: TilemapAsset): v
|
||||
|
||||
addTitle(host, 'LAYERS');
|
||||
addNote(host, 'The last layer is drawn on top.', 'small');
|
||||
const painted = paintedLayerId(state, asset);
|
||||
// Bottom-up on screen, as in every layer list: the topmost row is the one drawn last.
|
||||
for (let index = asset.layers.length - 1; index >= 0; index--) {
|
||||
const layer = asset.layers[index]!;
|
||||
@@ -134,6 +166,15 @@ export function buildTilemapSection(host: InspectorHost, asset: TilemapAsset): v
|
||||
widgets.layers.push({ id: layer.id, name: choose, visible, up, down, remove });
|
||||
host.body.add(row);
|
||||
}
|
||||
// Below the list rather than in it: a row is a button that picks the layer, and a text field
|
||||
// in its place would take the click that chooses it.
|
||||
const nameRow = labelledRow('Name').row;
|
||||
nameRow.add(layerName, 1);
|
||||
host.body.add(nameRow);
|
||||
const showRow = labelledRow('Show').row;
|
||||
showRow.add(focus, 1);
|
||||
host.body.add(showRow);
|
||||
addNote(host, 'Show only changes the scene view, not the game.', 'small');
|
||||
addGap(host);
|
||||
host.body.add(widgets.addLayer);
|
||||
}
|
||||
@@ -152,5 +193,5 @@ export function tilemapSignature(state: EditorState, asset: TilemapAsset): strin
|
||||
const paint = state.tilemapPaint;
|
||||
const layers = asset.layers.map((l) => `${l.id}:${l.name}:${isLayerVisible(l) ? 1 : 0}`).join(',');
|
||||
const tiles = tileIdsOf(state).join(',');
|
||||
return `${paint.tool}|${paint.tileId}|${paintedLayerId(state, asset)}|${layers}|${tiles}`;
|
||||
return `${paint.tool}|${paint.tileId}|${paintedLayerId(state, asset)}|${paint.focus}|${layers}|${tiles}`;
|
||||
}
|
||||
+9
-1
@@ -146,10 +146,18 @@ function pickIds(input: readonly string[], primary?: string): { ids: string[]; p
|
||||
*/
|
||||
export type TilemapTool = 'select' | 'brush' | 'erase' | 'rect';
|
||||
|
||||
/**
|
||||
* How the scene view shows the layers of the selected tilemap next to the one painted on (#138):
|
||||
* all as they are, the others faded, or the painted one alone. A way of looking while painting,
|
||||
* so it leaves the model – and with it the game – untouched.
|
||||
*/
|
||||
export type TilemapFocus = 'all' | 'dim' | 'solo';
|
||||
|
||||
export interface TilemapPaint {
|
||||
tool: TilemapTool;
|
||||
tileId: string;
|
||||
layerId: string;
|
||||
focus: TilemapFocus;
|
||||
}
|
||||
|
||||
export type EditorMode = 'edit' | 'play';
|
||||
@@ -202,7 +210,7 @@ export class EditorState {
|
||||
* on. Editor state, not model state – like the picked tile of the Animation tab, it is a way of
|
||||
* working rather than something the project remembers.
|
||||
*/
|
||||
tilemapPaint: TilemapPaint = { tool: 'select', tileId: '', layerId: '' };
|
||||
tilemapPaint: TilemapPaint = { tool: 'select', tileId: '', layerId: '', focus: 'all' };
|
||||
/** The real (saved) model and its undo stack while `project` is the play-mode copy. */
|
||||
private edit: { project: Project; commands: CommandStack } | null = null;
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ import { type EditorState, objectSelection, type TilemapTool } from '../state';
|
||||
import { paintedLayerId } from '../inspector/tilemapSection';
|
||||
import { activeDrag, drawBand, drawGrid, endDrag, onDragChange, Theme, textStyle } from '../../ui';
|
||||
import { collectHolderItems, type DrawItem } from '../../viewport/items';
|
||||
import type { LayerFocus } from '../../viewport/tilemap';
|
||||
import { dashedLine, drawSourceIcon, placeNode } from '../../viewport/symbols';
|
||||
import { screenToWorld, type View, worldToScreen } from '../../viewport/view';
|
||||
import { FreeViewport } from './FreeViewport';
|
||||
@@ -185,6 +186,21 @@ export class SceneViewport extends FreeViewport {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The selected tilemap shows its layers the way the inspector asks (#138), whatever the tool:
|
||||
* the painted layer is worth seeing alone before the brush is taken up, too. Every other map,
|
||||
* and this one once it is deselected, is drawn whole.
|
||||
*/
|
||||
protected override tilemapFocus(item: DrawItem): LayerFocus | null {
|
||||
const mode = this.state.tilemapPaint.focus;
|
||||
const s = this.state.selection;
|
||||
if (mode === 'all' || item.type !== 'tilemap' || s?.kind !== 'object' || s.objectId !== item.id)
|
||||
return null;
|
||||
const object = findObject(this.resolved() ?? { objects: [] }, item.id);
|
||||
const asset = object && findAssetOf(this.state.project, object.assetId, 'tilemap');
|
||||
return asset ? { layerId: paintedLayerId(this.state, asset), mode } : null;
|
||||
}
|
||||
|
||||
private onPointerDown(e: FederatedPointerEvent): void {
|
||||
if (e.button === 1 || (e.button === 0 && e.target === this && e.shiftKey)) {
|
||||
this.startPan(e);
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
placeNode,
|
||||
syncSpriteMesh,
|
||||
} from '../../viewport/symbols';
|
||||
import { cellCentre, TilemapField } from '../../viewport/tilemap';
|
||||
import { cellCentre, type LayerFocus, TilemapField } from '../../viewport/tilemap';
|
||||
import { textureLibrary } from '../../viewport/TextureLibrary';
|
||||
import { gameClock } from '../../core/time';
|
||||
import { type View, worldBounds, worldToScreen } from '../../viewport/view';
|
||||
@@ -348,10 +348,18 @@ export abstract class WorldViewport extends Widget {
|
||||
this.objects.addChild(field.container);
|
||||
}
|
||||
field.container.visible = true;
|
||||
field.sync(item, textureLibrary, gameClock.now);
|
||||
field.sync(item, textureLibrary, gameClock.now, this.tilemapFocus(item));
|
||||
if (field.animated) this.animated = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Which layer of a tilemap is singled out and how (#138). The game view shows every map as the
|
||||
* game does; only the scene view, where one paints, has a say.
|
||||
*/
|
||||
protected tilemapFocus(_item: DrawItem): LayerFocus | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Where the centre of one cell of a placed tilemap is on screen, so a check can sample it. */
|
||||
cellScreen(objectId: string, x: number, y: number): Vec2 | null {
|
||||
const view = this.currentView();
|
||||
|
||||
+23
-2
@@ -12,6 +12,21 @@ import { placeNode } from './symbols';
|
||||
import type { TextureLibrary } from './TextureLibrary';
|
||||
import { groupByTile, type TileSheet, tileMesh } from './tilemapMesh';
|
||||
|
||||
/** The layer painted on and how the others are shown next to it (#138); null shows every layer. */
|
||||
export interface LayerFocus {
|
||||
layerId: string;
|
||||
mode: 'all' | 'dim' | 'solo';
|
||||
}
|
||||
|
||||
/** How far the others fade in `dim`: still readable as a map, plainly not what the brush reaches. */
|
||||
export const DIMMED_LAYER_ALPHA = 0.3;
|
||||
|
||||
/** How opaque one layer is drawn; 0 means it is not drawn at all. */
|
||||
export function layerAlpha(layerId: string, focus: LayerFocus | null): number {
|
||||
if (!focus || focus.mode === 'all' || layerId === focus.layerId) return 1;
|
||||
return focus.mode === 'dim' ? DIMMED_LAYER_ALPHA : 0;
|
||||
}
|
||||
|
||||
export class TilemapField {
|
||||
readonly container = new Container();
|
||||
private readonly meshes = new Map<string, Mesh>();
|
||||
@@ -28,7 +43,7 @@ export class TilemapField {
|
||||
* `now` is the global clock, so every tilemap showing the same tile is on the same step –
|
||||
* animated water does not run out of phase between two maps.
|
||||
*/
|
||||
sync(item: DrawItem, textures: TextureLibrary, now: number): void {
|
||||
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
|
||||
@@ -39,6 +54,8 @@ export class TilemapField {
|
||||
const drawn: Mesh[] = [];
|
||||
const alive = new Set<string>();
|
||||
for (const layer of draw?.layers ?? []) {
|
||||
const alpha = layerAlpha(layer.id, focus);
|
||||
if (alpha === 0) continue;
|
||||
for (const [tileId, tiles] of groupByTile(layer.tiles)) {
|
||||
const source = draw?.tiles.get(tileId);
|
||||
const picture = source && pictureNow(source, now);
|
||||
@@ -48,7 +65,11 @@ export class TilemapField {
|
||||
const key = `${layer.id}|${tileId}`;
|
||||
alive.add(key);
|
||||
const { positions, uvs, indices } = tileMesh(tiles, tile, origin, sheetOf(source, picture, texture));
|
||||
drawn.push(this.meshFor(key, new MeshGeometry({ positions, uvs, indices }), texture));
|
||||
const mesh = this.meshFor(key, new MeshGeometry({ positions, uvs, indices }), texture);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
for (const [key, mesh] of this.meshes) {
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
tilemapSize,
|
||||
} from '../../src/core/model/tilemap';
|
||||
import { tilemapsUsing } from '../../src/core/model/usages';
|
||||
import { DIMMED_LAYER_ALPHA, layerAlpha } from '../../src/viewport/tilemap';
|
||||
import { groupByTile, tileMesh } from '../../src/viewport/tilemapMesh';
|
||||
import { makeProject, roundTrip } from '../helpers';
|
||||
|
||||
@@ -318,6 +319,18 @@ describe('the layers of a tilemap', () => {
|
||||
expect(f.assetOf().layers.map((l) => l.name)).toEqual(['Layer 1', 'Roofs']);
|
||||
});
|
||||
|
||||
it('renames a layer without the spaces around it, refuses a blank name and undoes to the old one', () => {
|
||||
const f = makeTilemap();
|
||||
f.stack.execute(new RenameTilemapLayerCommand(f.tilemapId, f.layerId(), ' Ground '));
|
||||
expect(f.assetOf().layers[0]!.name).toBe('Ground');
|
||||
expect(() => f.stack.execute(new RenameTilemapLayerCommand(f.tilemapId, f.layerId(), ' '))).toThrow(
|
||||
'A layer needs a name',
|
||||
);
|
||||
expect(f.assetOf().layers[0]!.name).toBe('Ground');
|
||||
f.stack.undo();
|
||||
expect(f.assetOf().layers[0]!.name).toBe('Layer 1');
|
||||
});
|
||||
|
||||
it('keeps the last layer, because there has to be one to paint on', () => {
|
||||
const f = makeTilemap();
|
||||
expect(() => f.stack.execute(new RemoveTilemapLayerCommand(f.tilemapId, f.layerId()))).toThrow(
|
||||
@@ -338,6 +351,17 @@ describe('the layers of a tilemap', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('how the scene view shows the layers next to the painted one', () => {
|
||||
it('draws all of them whole, fades the others or leaves them out', () => {
|
||||
expect(layerAlpha('a', null)).toBe(1);
|
||||
expect(layerAlpha('b', { layerId: 'a', mode: 'all' })).toBe(1);
|
||||
expect(layerAlpha('a', { layerId: 'a', mode: 'dim' })).toBe(1);
|
||||
expect(layerAlpha('b', { layerId: 'a', mode: 'dim' })).toBe(DIMMED_LAYER_ALPHA);
|
||||
expect(layerAlpha('a', { layerId: 'a', mode: 'solo' })).toBe(1);
|
||||
expect(layerAlpha('b', { layerId: 'a', mode: 'solo' })).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('a tilemap in the document', () => {
|
||||
it('survives a round trip with its layers and cells', () => {
|
||||
const f = makeTilemap();
|
||||
|
||||
+107
-1
@@ -64,8 +64,31 @@ async function cellShows(t, x, y, quadrants) {
|
||||
return { ok: off.length === 0, detail: JSON.stringify({ read, want }) };
|
||||
}
|
||||
|
||||
/** `over` laid at `alpha` on `under`, both `#rrggbb` as `pixelAt` reads them. */
|
||||
function blend(over, under, alpha) {
|
||||
const channel = (hex, i) => parseInt(hex.slice(i, i + 2), 16);
|
||||
return `#${[1, 3, 5]
|
||||
.map((i) => Math.round(channel(over, i) * alpha + channel(under, i) * (1 - alpha)))
|
||||
.map((c) => c.toString(16).padStart(2, '0'))
|
||||
.join('')}`;
|
||||
}
|
||||
|
||||
export async function tilemap(t) {
|
||||
const { check, json, evaluate, mutate, click, clickText, drag, press, shot, settle, undo, select } = t;
|
||||
const {
|
||||
check,
|
||||
json,
|
||||
evaluate,
|
||||
mutate,
|
||||
click,
|
||||
clickText,
|
||||
drag,
|
||||
press,
|
||||
typeText,
|
||||
shot,
|
||||
settle,
|
||||
undo,
|
||||
select,
|
||||
} = t;
|
||||
const { saved, api, navigate, projectId } = t;
|
||||
|
||||
await press('Escape');
|
||||
@@ -283,6 +306,89 @@ export async function tilemap(t) {
|
||||
await undo();
|
||||
await settle();
|
||||
|
||||
// 40m. The painted layer is named in the field below the list, and Show singles it out in the
|
||||
// scene view (#138). It sits here, not at the end, because here both layers exist: the upper
|
||||
// one is painted on and holds the cell (4,0), the lower one the 3 × 3 block.
|
||||
const nameField = await inInspector(t, '__gm.tilemap.layerNameAt()');
|
||||
if (nameField) {
|
||||
await click(nameField.x, nameField.y);
|
||||
await press('a', { ctrl: true });
|
||||
await typeText('Roofs');
|
||||
await press('Enter');
|
||||
}
|
||||
await settle();
|
||||
layers = await json(`__gm.tilemap.layers(${JSON.stringify(made.id)})`);
|
||||
const renamedRow = await json('(__gm.tilemap.layerRowAt(0) || {}).id');
|
||||
const nameNow = await json('(__gm.tilemap.layerNameAt() || {}).value');
|
||||
check(
|
||||
'the name field renames the layer painted on, and the list shows the new name',
|
||||
layers[1].name === 'Roofs' &&
|
||||
layers[0].name === 'Layer 1' &&
|
||||
renamedRow === layers[1].id &&
|
||||
nameNow === 'Roofs',
|
||||
JSON.stringify({ layers, renamedRow, nameNow }),
|
||||
);
|
||||
|
||||
// Pixels again, so the animation is cut down to one step for the reading, as in 40e.
|
||||
await mutate("__gm.state.project.assets.find((a) => a.id === 'at-grass').pictures = ['at-floor-1']");
|
||||
await settle();
|
||||
const quarter = async (x, y) => (await cellQuarters(t, x, y))?.[0] ?? null;
|
||||
const lowerWhole = await quarter(1, 1);
|
||||
const emptyCell = await quarter(5, 3);
|
||||
const upperWhole = await quarter(4, 0);
|
||||
/** Chooses an entry of the Show dropdown: the topmost hit, which is the open menu. */
|
||||
const show = async (label) => {
|
||||
const button = await inInspector(t, '__gm.tilemap.focusAt()');
|
||||
if (button) await click(button.x, button.y);
|
||||
await settle();
|
||||
const entry = await t.locate(label, true);
|
||||
await click(entry.x, entry.y);
|
||||
await settle();
|
||||
};
|
||||
|
||||
await show('Others faded');
|
||||
const faded = await quarter(1, 1);
|
||||
// The lower layer is laid over the background at 30 %, so its colour lands in between.
|
||||
const mix = lowerWhole && emptyCell && blend(lowerWhole, emptyCell, 0.3);
|
||||
const focusLabel = await json('(__gm.tilemap.focusAt() || {}).label');
|
||||
check(
|
||||
'Others faded draws the other layers at 30 % and the painted one whole',
|
||||
Boolean(mix) &&
|
||||
colourDistance(faded, mix) <= 16 &&
|
||||
colourDistance(await quarter(4, 0), upperWhole) <= 12 &&
|
||||
focusLabel === 'Others faded',
|
||||
JSON.stringify({ faded, mix, lowerWhole, emptyCell, focusLabel }),
|
||||
);
|
||||
|
||||
await show('Active layer only');
|
||||
const solo = await quarter(1, 1);
|
||||
const soloInfo = await json(`__gm.tilemap.info(${JSON.stringify(MAP_OBJECT)})`);
|
||||
check(
|
||||
'Active layer only leaves the other layers out and keeps the painted one',
|
||||
colourDistance(solo, emptyCell) <= 12 &&
|
||||
colourDistance(await quarter(4, 0), upperWhole) <= 12 &&
|
||||
soloInfo.drawCalls === 1,
|
||||
JSON.stringify({ solo, emptyCell, soloInfo }),
|
||||
);
|
||||
|
||||
// Only the selected map is singled out; deselected it is drawn whole, and the model never changed.
|
||||
await evaluate('__gm.state.select(null)');
|
||||
await settle();
|
||||
const deselected = await quarter(1, 1);
|
||||
layers = await json(`__gm.tilemap.layers(${JSON.stringify(made.id)})`);
|
||||
check(
|
||||
'a deselected map is drawn whole again, and Show hid nothing in the model',
|
||||
colourDistance(deselected, lowerWhole) <= 12 && layers.every((l) => !l.hidden),
|
||||
JSON.stringify({ deselected, lowerWhole, layers }),
|
||||
);
|
||||
await select({ kind: 'object', holderId: 'scene1', objectIds: [MAP_OBJECT], objectId: MAP_OBJECT });
|
||||
await show('All layers');
|
||||
await mutate(
|
||||
"__gm.state.project.assets.find((a) => a.id === 'at-grass').pictures = ['at-floor-1', 'at-floor-2']",
|
||||
);
|
||||
await undo(); // the rename
|
||||
await settle();
|
||||
|
||||
// 40i. Back to Select: the map is moved again instead of painted on.
|
||||
await clickInInspector(t, '__gm.tilemap.toolAt("select")');
|
||||
await settle();
|
||||
|
||||
Reference in new issue
Block a user