Let the scene view hide symbols, names and shapes per asset kind
The scene view gets crowded once a scene holds many behaviors, colliders and tilemaps. A new eye button in the topbar opens a list of all placeable asset kinds, and each row has toggles for what the scene view draws for that kind. - The popup lists all 9 placeable kinds from Behavior to Tilemap, with the columns Symbol, Name and Shape. It stays open while switching. - Sprite, Text and Tilemap draw no symbol, so their symbol toggle is greyed out. Their name toggle shows a new centred name tag, which is off by default. - Shape exists for Collider and Tilemap. A hidden collider outline is neither drawn nor clickable. A hidden tilemap shape removes the collision outlines and the paint tint, the tiles stay visible. - A hidden symbol takes no slot in the ring and draws no dashed link to its parent. A name that stays on moves to the centre of the object. - The setting lives in the project as `sceneVisibility`, not in localStorage. Older projects load with the defaults, which match the view as it was. - Switching has no undo step, like the game scale. It only changes the view. A change made during Play survives Stop. - The rules sit in `src/core/model/sceneVisibility.ts`, the popup in `src/editor/panels/SceneVisibilityPopup.ts`. Four new SVG icons. Not covered by a check: symbol off with name on for a kind with a symbol, Text and Tilemap with names, and the paint tint with shape off. Tests: - ./check.sh: 173 files, 2515 tests, CHECK EXIT 0 - e2e 46: ALL PASSED, 17 checks. 46g clicks the popup, lays a small tilemap over the crate and removes it again. - e2e 8 alone on a fresh seed: ALL PASSED, 23 checks. A red check in the partial run 2,8,46 came from the module selection. - 8 new unit tests in tests/editor/scene-visibility.test.ts. - 4 counter-probes, each fails its test: load ignores the saved value, Play skips the saved model, scene view ignores collider shape, scene view ignores tilemap shape. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
66868de10a
commit
6015869297
19 files changed
+547
-25
No files matched your search
@@ -43,7 +43,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, 1418 checks (GM_STEPS=20 for one step)
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1422 checks (GM_STEPS=20 for one step)
|
||||
# every tool signs in first – see the Accounts page
|
||||
node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs
|
||||
node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from './Asset';
|
||||
import { DEFAULT_LAYER_NAME } from './collisionLayers';
|
||||
import { DEFAULT_GAME_SCALE, DEFAULT_PLAY_FROM, type GameScale, type PlayFrom } from './playSettings';
|
||||
import { defaultSceneVisibility, type SceneVisibility } from './sceneVisibility';
|
||||
import { DEFAULT_SYMBOL_SCALE } from './symbolScale';
|
||||
|
||||
/** 2: a placed container carries no copy of its template. */
|
||||
@@ -47,6 +48,8 @@ export interface Project {
|
||||
playFrom: PlayFrom;
|
||||
/** The scale the game view draws the camera at. Saved, but not an undo step: it is how you look. */
|
||||
gameScale: GameScale;
|
||||
/** Which symbols, names and collider outlines the scene view shows, per type. Saved like `gameScale`. */
|
||||
sceneVisibility: SceneVisibility;
|
||||
}
|
||||
|
||||
export function createEmptyProject(id: string, name: string): Project {
|
||||
@@ -62,6 +65,7 @@ export function createEmptyProject(id: string, name: string): Project {
|
||||
symbolScale: DEFAULT_SYMBOL_SCALE,
|
||||
playFrom: DEFAULT_PLAY_FROM,
|
||||
gameScale: DEFAULT_GAME_SCALE,
|
||||
sceneVisibility: defaultSceneVisibility(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* What the scene view shows of each placeable type: its symbol, its name, and for a collider its
|
||||
* outline. It is saved with the project, so whoever opens it sees the same scene, and a project
|
||||
* written before there was such a field reads as the defaults.
|
||||
*/
|
||||
import { hasSceneIcon, type PlaceableAssetType } from './AssetTypes';
|
||||
|
||||
export type VisibilityPart = 'symbol' | 'label' | 'shape';
|
||||
|
||||
export type TypeVisibility = Record<VisibilityPart, boolean>;
|
||||
|
||||
export type SceneVisibility = Record<PlaceableAssetType, TypeVisibility>;
|
||||
|
||||
/** Every placeable type, in the order its name sorts – Behavior to Tilemap. */
|
||||
export const VISIBILITY_TYPES: readonly PlaceableAssetType[] = [
|
||||
'behavior',
|
||||
'camera',
|
||||
'collider',
|
||||
'container',
|
||||
'particles',
|
||||
'rigidbody',
|
||||
'sprite',
|
||||
'text',
|
||||
'tilemap',
|
||||
];
|
||||
|
||||
/**
|
||||
* The switches a type has. Only a type with a symbol can hide it, every type can show its name,
|
||||
* and only what collides has an outline to hide: a collider its shape, a tilemap the outline of
|
||||
* its solid cells – the map's art stays. A sprite without its picture would be nothing at all.
|
||||
*/
|
||||
export function visibilityParts(type: PlaceableAssetType): VisibilityPart[] {
|
||||
const parts: VisibilityPart[] = hasSceneIcon(type) ? ['symbol', 'label'] : ['label'];
|
||||
if (type === 'collider' || type === 'tilemap') parts.push('shape');
|
||||
return parts;
|
||||
}
|
||||
|
||||
/** What a type shows until it is switched: all of it, except a name nothing showed before. */
|
||||
export function defaultVisibility(type: PlaceableAssetType): TypeVisibility {
|
||||
return { symbol: true, label: hasSceneIcon(type), shape: true };
|
||||
}
|
||||
|
||||
export function defaultSceneVisibility(): SceneVisibility {
|
||||
return Object.fromEntries(
|
||||
VISIBILITY_TYPES.map((type) => [type, defaultVisibility(type)]),
|
||||
) as SceneVisibility;
|
||||
}
|
||||
|
||||
/** Read off untrusted JSON: every type and part that is missing or not a boolean is the default. */
|
||||
export function sceneVisibilityOf(raw: unknown): SceneVisibility {
|
||||
const stored = (raw as { sceneVisibility?: unknown } | null)?.sceneVisibility;
|
||||
const out = defaultSceneVisibility();
|
||||
if (!stored || typeof stored !== 'object') return out;
|
||||
for (const type of VISIBILITY_TYPES) {
|
||||
const entry = (stored as Record<string, unknown>)[type];
|
||||
if (!entry || typeof entry !== 'object') continue;
|
||||
for (const part of Object.keys(out[type]) as VisibilityPart[]) {
|
||||
const value = (entry as Record<string, unknown>)[part];
|
||||
if (typeof value === 'boolean') out[type][part] = value;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Whether the scene view draws `part` of an object of `type`; a type without that part never does. */
|
||||
export function shows(visibility: SceneVisibility, type: string, part: VisibilityPart): boolean {
|
||||
const entry = (visibility as Partial<Record<string, TypeVisibility>>)[type];
|
||||
if (!entry) return part !== 'label';
|
||||
if (!visibilityParts(type as PlaceableAssetType).includes(part)) return part === 'shape';
|
||||
return entry[part];
|
||||
}
|
||||
@@ -41,6 +41,7 @@ import { parseCellKey } from '../model/tilemap';
|
||||
import { pluginsOf } from './projectPlugins';
|
||||
import { collisionLayersOf } from '../model/collisionLayers';
|
||||
import { gameScaleOf, playFromOf } from '../model/playSettings';
|
||||
import { sceneVisibilityOf } from '../model/sceneVisibility';
|
||||
import { symbolScaleOf } from '../model/symbolScale';
|
||||
import { decodeCells, encodeCells } from './tilemapCells';
|
||||
import { isAssetType } from '../model/AssetTypes';
|
||||
@@ -255,6 +256,7 @@ export function deserializeProject(raw: unknown, fallbackId = ''): Project {
|
||||
symbolScale: symbolScaleOf(r),
|
||||
playFrom: playFromOf(r),
|
||||
gameScale: gameScaleOf(r),
|
||||
sceneVisibility: sceneVisibilityOf(r),
|
||||
};
|
||||
return project;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { findAssetOf, findObjectHolder } from '../../core/model/Project';
|
||||
import { resolveObjects } from '../../core/model/instance';
|
||||
import { findLayer, layerZ } from '../../core/model/tilemap';
|
||||
import { currentVisibilityPopup } from '../panels/SceneVisibilityPopup';
|
||||
import { currentTilemapWidgets, paintedLayerId, paintedTilemap } from '../panels/TilemapPanel';
|
||||
import type { TilemapTool } from '../state';
|
||||
import { rectOf } from '../textProbe';
|
||||
@@ -65,6 +66,13 @@ export function sceneHooks({ state, editor }: HookContext) {
|
||||
sceneIcon: (id: string) => viewport.iconInfo(id),
|
||||
/** The names shown beside the symbols, each with the plate under it, in world units and on screen. */
|
||||
sceneLabels: () => viewport.labelInfo(),
|
||||
/** A collider's outline in the Scene tab: hidden by the eye in the top bar, it is neither seen nor hit. */
|
||||
sceneShape: (id: string) => viewport.shapeInfo(id),
|
||||
/** The switches behind the eye while it stands open: per type and part, on, enabled, where to click. */
|
||||
visibilityPopup: () =>
|
||||
currentVisibilityPopup()
|
||||
?.info()
|
||||
.map(({ widget, ...entry }) => ({ ...entry, ...centreOf(widget) })) ?? null,
|
||||
drawOrder: () => viewport.drawOrder(),
|
||||
objectScreen: (id: string) => viewport.objectScreen(id),
|
||||
objectRect: (id: string) => viewport.objectRect(id),
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { ASSET_TYPE_LABELS, type PlaceableAssetType } from '../../core/model/AssetTypes';
|
||||
import { VISIBILITY_TYPES, visibilityParts, type VisibilityPart } from '../../core/model/sceneVisibility';
|
||||
import { Button, HStack, Icon, Label, Popup, Spacer, Theme, VStack, type Widget } from '../../ui';
|
||||
import type { EditorState } from '../state';
|
||||
import { ASSET_ICON_SVG } from './assetIcons';
|
||||
import { VISIBILITY_ICON_SVG } from './toolbarIcons';
|
||||
|
||||
const PARTS: readonly VisibilityPart[] = ['symbol', 'label', 'shape'];
|
||||
const PART_CAPTIONS: Record<VisibilityPart, string> = { symbol: 'Symbol', label: 'Name', shape: 'Shape' };
|
||||
/** One column per switch, wide enough for its caption. */
|
||||
const COLUMN = 44;
|
||||
const NAME_WIDTH = 90;
|
||||
|
||||
/** A gap of `w` that does not make the row taller – a plain `Spacer` is square. */
|
||||
function gap(w: number): Spacer {
|
||||
const spacer = new Spacer(w);
|
||||
spacer.minHeight = 0;
|
||||
return spacer;
|
||||
}
|
||||
|
||||
/**
|
||||
* The eye in the top bar: every type that can be placed, and per type a switch for its symbol, its
|
||||
* name and – a collider only – its outline in the scene view. A switch a type does not have is
|
||||
* greyed out, or missing where no other type has it either. Clicks keep it open: one usually
|
||||
* switches several.
|
||||
*/
|
||||
export class SceneVisibilityPopup extends Popup {
|
||||
private readonly switches = new Map<string, Button>();
|
||||
|
||||
constructor(private readonly state: EditorState) {
|
||||
super({ padding: 6, gap: 2 });
|
||||
const header = new HStack({ gap: Theme.gap, align: 'center' });
|
||||
header.add(gap(16 + Theme.gap + NAME_WIDTH));
|
||||
for (const part of PARTS)
|
||||
header.add(new Label(PART_CAPTIONS[part], { style: 'small', align: 'center', width: COLUMN }));
|
||||
this.content.add(header);
|
||||
const rows = new VStack({ gap: 2 });
|
||||
for (const type of VISIBILITY_TYPES) rows.add(this.row(type));
|
||||
this.content.add(rows);
|
||||
}
|
||||
|
||||
private row(type: PlaceableAssetType): HStack {
|
||||
const row = new HStack({ gap: Theme.gap, align: 'center' });
|
||||
row.add(new Icon(ASSET_ICON_SVG[type]));
|
||||
row.add(new Label(ASSET_TYPE_LABELS[type], { width: NAME_WIDTH }));
|
||||
const parts = visibilityParts(type);
|
||||
for (const part of PARTS) {
|
||||
if (part === 'shape' && !parts.includes(part)) {
|
||||
row.add(gap(COLUMN));
|
||||
continue;
|
||||
}
|
||||
const button = new Button('', {
|
||||
variant: 'ghost',
|
||||
width: COLUMN,
|
||||
icon: VISIBILITY_ICON_SVG[part],
|
||||
onClick: () => this.flip(type, part),
|
||||
});
|
||||
button.setEnabled(parts.includes(part));
|
||||
button.setToggled(parts.includes(part) && this.state.project.sceneVisibility[type][part]);
|
||||
this.switches.set(`${type}:${part}`, button);
|
||||
row.add(button);
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
private flip(type: PlaceableAssetType, part: VisibilityPart): void {
|
||||
const on = !this.state.project.sceneVisibility[type][part];
|
||||
this.state.setSceneVisibility(type, part, on);
|
||||
this.switches.get(`${type}:${part}`)?.setToggled(on);
|
||||
}
|
||||
|
||||
override open(globalX: number, globalY: number): void {
|
||||
super.open(globalX, globalY);
|
||||
setCurrent(this);
|
||||
}
|
||||
|
||||
override close(): void {
|
||||
if (current === this) current = null;
|
||||
super.close();
|
||||
}
|
||||
|
||||
/** Per type and switch: whether it is on, whether it takes a click, and where to click it (test hook). */
|
||||
info(): { type: string; part: VisibilityPart; on: boolean; enabled: boolean; widget: Widget }[] {
|
||||
return [...this.switches].map(([key, button]) => {
|
||||
const [type, part] = key.split(':') as [string, VisibilityPart];
|
||||
return { type, part, on: button.toggled, enabled: button.enabled, widget: button };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let current: SceneVisibilityPopup | null = null;
|
||||
|
||||
function setCurrent(popup: SceneVisibilityPopup): void {
|
||||
current = popup;
|
||||
}
|
||||
|
||||
/** The popup standing open, for the test hook. */
|
||||
export function currentVisibilityPopup(): SceneVisibilityPopup | null {
|
||||
return current;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Graphics } from 'pixi.js';
|
||||
import { Button, drawHeaderBar, Dropdown, HStack, Label, Spacer, Theme, type Widget } from '../../ui';
|
||||
import type { EditorState, SaveStatus } from '../state';
|
||||
import { SceneVisibilityPopup } from './SceneVisibilityPopup';
|
||||
import { TOOLBAR_ICON_SVG } from './toolbarIcons';
|
||||
|
||||
/** What the toolbar buttons trigger; the editor supplies the implementations. */
|
||||
@@ -37,8 +38,8 @@ function iconButton(name: IconName, onClick: () => void): Button {
|
||||
}
|
||||
|
||||
/**
|
||||
* Top bar (#183): menu, undo and redo on the left, play and pause in the middle, the account on
|
||||
* the right – icons, no captions. Everything with words is behind the menu: the project name and
|
||||
* Top bar (#183): menu, undo, redo and the scene view's eye on the left, play and pause in the
|
||||
* middle, the account on the right – icons, no captions. Everything with words is behind the menu: the project name and
|
||||
* the start-scene switch in Project settings, the project list in Open project. The scene is
|
||||
* picked where the scenes are, in the Assets panel.
|
||||
*/
|
||||
@@ -47,6 +48,8 @@ export class Toolbar extends HStack {
|
||||
private readonly menuButton: Button;
|
||||
private readonly undoButton: Button;
|
||||
private readonly redoButton: Button;
|
||||
/** What the scene view shows of each type – symbols, names, collider outlines. */
|
||||
private readonly visibilityButton: Dropdown;
|
||||
private readonly playButton: Button;
|
||||
private readonly pauseButton: Button;
|
||||
private readonly status: Label;
|
||||
@@ -71,6 +74,12 @@ export class Toolbar extends HStack {
|
||||
this.redoButton = iconButton('redo', () => state.redo());
|
||||
this.add(this.undoButton);
|
||||
this.add(this.redoButton);
|
||||
this.add(new Spacer(Theme.padding));
|
||||
this.visibilityButton = new Dropdown('', {
|
||||
icon: TOOLBAR_ICON_SVG.eye,
|
||||
onClick: () => new SceneVisibilityPopup(state).openBelow(this.visibilityButton),
|
||||
});
|
||||
this.add(this.visibilityButton);
|
||||
|
||||
this.add(new Spacer(), 1);
|
||||
this.playButton = iconButton('play', () => actions.togglePlay());
|
||||
@@ -113,12 +122,13 @@ export class Toolbar extends HStack {
|
||||
};
|
||||
}
|
||||
|
||||
/** The five icon buttons, so a check can click what carries no caption. */
|
||||
get controls(): Record<'menu' | 'undo' | 'redo' | 'play' | 'pause', Button> {
|
||||
/** The icon buttons, so a check can click what carries no caption. */
|
||||
get controls(): Record<'menu' | 'undo' | 'redo' | 'visibility' | 'play' | 'pause', Button> {
|
||||
return {
|
||||
menu: this.menuButton,
|
||||
undo: this.undoButton,
|
||||
redo: this.redoButton,
|
||||
visibility: this.visibilityButton,
|
||||
play: this.playButton,
|
||||
pause: this.pauseButton,
|
||||
};
|
||||
@@ -158,7 +168,7 @@ export class Toolbar extends HStack {
|
||||
override layout(): void {
|
||||
super.layout();
|
||||
const width = this.pauseButton.x + this.pauseButton.w - this.playButton.x;
|
||||
const leftMost = this.redoButton.x + this.redoButton.w + this.gap;
|
||||
const leftMost = this.visibilityButton.x + this.visibilityButton.w + this.gap;
|
||||
const rightMost = this.notice.x - this.gap - width;
|
||||
const x = Math.round(Math.max(leftMost, Math.min((this.w - width) / 2, rightMost)));
|
||||
const shift = x - this.playButton.x;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="eye" viewBox="-8 -8 16 16" width="16" height="16">
|
||||
<path d="M-7 0 C-4.5 -4.75, 4.5 -4.75, 7 0 C4.5 4.75, -4.5 4.75, -7 0 Z" fill="none" stroke="currentColor" stroke-width="1.5" />
|
||||
<circle cx="0" cy="0" r="2.25" fill="currentColor" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 289 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="show-label" viewBox="-8 -8 16 16" width="16" height="16">
|
||||
<rect x="-6.75" y="-4.25" width="13.5" height="8.5" rx="1.5" fill="none" stroke="currentColor" stroke-width="1.5" />
|
||||
<path d="M-3.75 0 H3.75" fill="none" stroke="currentColor" stroke-width="1.5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 311 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="show-shape" viewBox="-8 -8 16 16" width="16" height="16">
|
||||
<path d="M-6 -2 V-6 H-2 M2 -6 H6 V-2 M6 2 V6 H2 M-2 6 H-6 V2" fill="none" stroke="currentColor" stroke-width="1.5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 229 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="show-symbol" viewBox="-8 -8 16 16" width="16" height="16">
|
||||
<path d="M0 -6.5 L1.8 -1.8 L6.5 0 L1.8 1.8 L0 6.5 L-1.8 1.8 L-6.5 0 L-1.8 -1.8 Z" fill="currentColor" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 217 B |
@@ -2,11 +2,23 @@
|
||||
* The top bar's icons (#183). It carries no captions any more: menu, undo and redo on the left,
|
||||
* play and pause in the middle, so each button is its picture and nothing else.
|
||||
*/
|
||||
import type { VisibilityPart } from '../../core/model/sceneVisibility';
|
||||
import eye from './icons/eye.svg?raw';
|
||||
import menu from './icons/menu.svg?raw';
|
||||
import pause from './icons/pause.svg?raw';
|
||||
import play from './icons/play.svg?raw';
|
||||
import redo from './icons/redo.svg?raw';
|
||||
import showLabel from './icons/show-label.svg?raw';
|
||||
import showShape from './icons/show-shape.svg?raw';
|
||||
import showSymbol from './icons/show-symbol.svg?raw';
|
||||
import stop from './icons/stop.svg?raw';
|
||||
import undo from './icons/undo.svg?raw';
|
||||
|
||||
export const TOOLBAR_ICON_SVG = { menu, undo, redo, play, pause, stop };
|
||||
export const TOOLBAR_ICON_SVG = { menu, undo, redo, eye, play, pause, stop };
|
||||
|
||||
/** The switches behind the eye: a type's symbol, its name, a collider's outline. */
|
||||
export const VISIBILITY_ICON_SVG: Record<VisibilityPart, string> = {
|
||||
symbol: showSymbol,
|
||||
label: showLabel,
|
||||
shape: showShape,
|
||||
};
|
||||
@@ -20,6 +20,8 @@ import { deserializeProject, serializeProject } from '../core/store/serialize';
|
||||
import { requestRender } from '../ui';
|
||||
import { consoleLog } from './console/log';
|
||||
import type { GameScale, PlayFrom } from '../core/model/playSettings';
|
||||
import type { PlaceableAssetType } from '../core/model/AssetTypes';
|
||||
import type { VisibilityPart } from '../core/model/sceneVisibility';
|
||||
import type { ValuePath } from '../core/properties/valuePath';
|
||||
|
||||
export type Selection =
|
||||
@@ -343,6 +345,15 @@ export class EditorState {
|
||||
this.setSaveStatus('dirty');
|
||||
}
|
||||
|
||||
/** Shows or hides one part of a type in the scene view – saved, and like the game scale no undo step. */
|
||||
setSceneVisibility(type: PlaceableAssetType, part: VisibilityPart, on: boolean): void {
|
||||
if (this.project.sceneVisibility[type][part] === on) return;
|
||||
this.project.sceneVisibility[type][part] = on;
|
||||
if (this.edit) this.edit.project.sceneVisibility[type][part] = on;
|
||||
this.setSaveStatus('dirty');
|
||||
this.emit('project');
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a command through the undo stack. Returns false (and sets a notice) when invalid.
|
||||
* In play mode the command changes the runtime copy only (see `enterPlay`).
|
||||
|
||||
@@ -32,6 +32,7 @@ import { sortRowOf } from '../../viewport/drawOrder';
|
||||
import { menuTargetOf, type PointerHit, pressOn, stackAt } from '../../viewport/pick';
|
||||
import { openObjectMenu } from '../objectMenu';
|
||||
import { type LayerFocus, layerAlpha } from '../../viewport/tilemap';
|
||||
import { shows, type VisibilityPart } from '../../core/model/sceneVisibility';
|
||||
import { dashedLine } from '../../viewport/symbols';
|
||||
import { drawSymbolIcon, SYMBOL_GRID, SYMBOL_OUTLINE, type SymbolIcon } from '../../viewport/symbolIcons';
|
||||
import { type LabelBox, stackLabels, type WantedLabel } from '../../viewport/labelRows';
|
||||
@@ -586,7 +587,8 @@ export class SceneViewport extends FreeViewport {
|
||||
_extent: Extent,
|
||||
): void {
|
||||
this.syncIcon(item, onScreen);
|
||||
this.solids.sync(item, onScreen, scale, onScreen ? this.paintedSolids(item) : null);
|
||||
const solids = onScreen && this.shows(item, 'shape');
|
||||
this.solids.sync(item, solids, scale, solids ? this.paintedSolids(item) : null);
|
||||
this.syncLabel(item, onScreen, scale);
|
||||
}
|
||||
|
||||
@@ -597,6 +599,16 @@ export class SceneViewport extends FreeViewport {
|
||||
return item.tilemap!.layerSolids(paintedLayerId(this.state, target.asset));
|
||||
}
|
||||
|
||||
/** What the project's scene visibility (the eye in the top bar) says about this part of `item`. */
|
||||
private shows(item: DrawItem, part: VisibilityPart): boolean {
|
||||
return shows(this.state.project.sceneVisibility, item.type, part);
|
||||
}
|
||||
|
||||
/** A tilemap's shape switch hides only the outline of its solid cells, never the map itself. */
|
||||
protected override hidesShape(item: DrawItem): boolean {
|
||||
return item.type === 'collider' && !this.shows(item, 'shape');
|
||||
}
|
||||
|
||||
protected override disposeDecorations(id: string): void {
|
||||
this.labels.get(id)?.destroy();
|
||||
this.labels.delete(id);
|
||||
@@ -648,7 +660,7 @@ export class SceneViewport extends FreeViewport {
|
||||
*/
|
||||
private syncIcon(item: DrawItem, show: boolean): void {
|
||||
let icon = this.icons.get(item.id);
|
||||
if (!show || !hasSceneIcon(item.type)) {
|
||||
if (!show || !hasSceneIcon(item.type) || !this.shows(item, 'symbol')) {
|
||||
if (icon) {
|
||||
icon.visible = false;
|
||||
icon.eventMode = 'none';
|
||||
@@ -685,19 +697,20 @@ export class SceneViewport extends FreeViewport {
|
||||
this.overlay.setChildIndex(this.labelLayer, last);
|
||||
this.overlay.setChildIndex(this.iconLayer, last);
|
||||
const anchors = items
|
||||
.filter((item) => item.enabled && hasSceneIcon(item.type))
|
||||
.filter((item) => item.enabled && hasSceneIcon(item.type) && this.shows(item, 'symbol'))
|
||||
.map((item) => ({ id: item.id, x: item.x, y: item.y }));
|
||||
this.spots = layoutSymbols(anchors, SYMBOL_SPACING * this.symbolScale());
|
||||
for (const [id, spot] of this.spots) this.icons.get(id)?.position.set(spot.x, spot.y);
|
||||
this.placeLabels(scale);
|
||||
this.placeLabels(items, scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Each name beside its icon – on the outer side of a ring – on a dark plate, and a row up or down
|
||||
* where it would cover another name or icon (`stackLabels`). Names off screen take their row as
|
||||
* well, so none jumps while the view pans.
|
||||
* where it would cover another name or icon (`stackLabels`). A name without an icon beside it – a
|
||||
* sprite's, or a symbol's whose icon is hidden – is centred on its object. Names off screen take
|
||||
* their row as well, so none jumps while the view pans.
|
||||
*/
|
||||
private placeLabels(scale: number): void {
|
||||
private placeLabels(items: DrawItem[], scale: number): void {
|
||||
const side = SYMBOL_SIZE * this.symbolScale();
|
||||
const gap = side / 2 + 4 / scale;
|
||||
const padX = LABEL_PAD_X / scale;
|
||||
@@ -716,6 +729,14 @@ export class SceneViewport extends FreeViewport {
|
||||
const x = left ? spot.x - gap - label.width - padX : spot.x + gap - padX;
|
||||
wanted.push({ id, x, y: spot.y - h / 2, w, h });
|
||||
}
|
||||
for (const item of items) {
|
||||
const label = this.labels.get(item.id);
|
||||
if (!label || !item.enabled || this.spots.has(item.id)) continue;
|
||||
lefts.set(item.id, false);
|
||||
const w = label.width + padX * 2;
|
||||
const h = label.height + padY * 2;
|
||||
wanted.push({ id: item.id, x: item.x - w / 2, y: item.y - h / 2, w, h });
|
||||
}
|
||||
const step = (wanted[0]?.h ?? 0) + LABEL_ROW_GAP / scale;
|
||||
const tops = stackLabels(wanted, icons, step);
|
||||
this.plates.clear();
|
||||
@@ -761,7 +782,7 @@ export class SceneViewport extends FreeViewport {
|
||||
this.lastLinks = [];
|
||||
const byId = new Map(items.map((i) => [i.id, i]));
|
||||
for (const item of items) {
|
||||
if (!hasSceneIcon(item.type) || !item.parentId || item.type === 'collider' || !item.enabled) continue;
|
||||
if (!this.spots.has(item.id) || !item.parentId || item.type === 'collider') continue;
|
||||
const parent = byId.get(item.parentId);
|
||||
if (!parent || !parent.enabled) continue;
|
||||
this.lastLinks.push(item.id);
|
||||
@@ -775,13 +796,14 @@ export class SceneViewport extends FreeViewport {
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of a symbol; `placeLabels` puts it beside the icon once every icon has its spot. One
|
||||
* off screen is kept up to date but hidden: it still takes its row.
|
||||
* The name of an object whose type shows names; `placeLabels` puts it beside the icon once every
|
||||
* icon has its spot. One off screen is kept up to date but hidden: it still takes its row.
|
||||
*/
|
||||
private syncLabel(item: DrawItem, show: boolean, scale: number): void {
|
||||
let label = this.labels.get(item.id);
|
||||
if (!hasSceneIcon(item.type)) {
|
||||
if (label) label.visible = false;
|
||||
if (!this.shows(item, 'label')) {
|
||||
label?.destroy();
|
||||
this.labels.delete(item.id);
|
||||
return;
|
||||
}
|
||||
if (!label) {
|
||||
@@ -795,6 +817,12 @@ export class SceneViewport extends FreeViewport {
|
||||
label.scale.set(1 / scale);
|
||||
}
|
||||
|
||||
/** Whether an object's own drawing – a collider's outline – is on screen and takes a click (test hook). */
|
||||
shapeInfo(id: string): { visible: boolean; clickable: boolean } | null {
|
||||
const node = this.nodes.get(id);
|
||||
return node ? { visible: node.visible, clickable: node.eventMode !== 'none' } : null;
|
||||
}
|
||||
|
||||
/** The names shown and their plates in world units, `left` for one on the left of its icon (test hook). */
|
||||
labelInfo(): (WantedLabel & { left: boolean; screen: { x: number; y: number; w: number; h: number } })[] {
|
||||
const view = this.currentView();
|
||||
|
||||
@@ -135,6 +135,11 @@ export abstract class WorldViewport extends Widget {
|
||||
/** An item disappeared: drop whatever `syncDecorations` created for it. */
|
||||
protected disposeDecorations(_id: string): void {}
|
||||
|
||||
/** Whether the item's own drawing is switched off – it is then neither seen nor hit. */
|
||||
protected hidesShape(_item: DrawItem): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected onItemPointerDown(_objectId: string, _e: FederatedPointerEvent): void {}
|
||||
|
||||
/** Whether the object itself is selected – a selected collider shows its area filled. */
|
||||
@@ -312,9 +317,10 @@ export abstract class WorldViewport extends Widget {
|
||||
this.nodes.set(item.id, node);
|
||||
(isSymbolType(item.type) ? this.overlay : this.objects).addChild(node);
|
||||
}
|
||||
node.visible = onScreen;
|
||||
if (item.enabled) culled.push({ node, extent: item.type === 'particles' ? null : extent });
|
||||
node.eventMode = this.itemsInteractive && onScreen ? 'static' : 'none';
|
||||
const hidden = this.hidesShape(item);
|
||||
node.visible = onScreen && !hidden;
|
||||
if (item.enabled && !hidden) culled.push({ node, extent: item.type === 'particles' ? null : extent });
|
||||
node.eventMode = this.itemsInteractive && node.visible ? 'static' : 'none';
|
||||
this.syncDecorations(item, onScreen, view.scale, extent);
|
||||
this.syncText(item, onScreen, view.scale);
|
||||
this.syncSprite(item, onScreen, size, shaders);
|
||||
@@ -322,6 +328,7 @@ export abstract class WorldViewport extends Widget {
|
||||
this.syncTilemap(item, onScreen);
|
||||
if (!onScreen) continue;
|
||||
this.visibleCount++;
|
||||
if (hidden) continue;
|
||||
drawItem(
|
||||
node,
|
||||
item,
|
||||
|
||||
@@ -10,6 +10,7 @@ import { SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands'
|
||||
import { RemoveVariableCommand, RenameVariableCommand } from '../../src/core/commands/variableCommands';
|
||||
import type { ContainerAsset } from '../../src/core/model/Asset';
|
||||
import { findAsset, findObjectHolder, objectHolders } from '../../src/core/model/Project';
|
||||
import { defaultSceneVisibility } from '../../src/core/model/sceneVisibility';
|
||||
import { instanceChildId } from '../../src/core/model/instance';
|
||||
import { findUsages } from '../../src/core/model/usages';
|
||||
import { makeBehavior, makeSpriteScene, place } from '../helpers';
|
||||
@@ -154,6 +155,7 @@ describe('CommandStack', () => {
|
||||
symbolScale: 1,
|
||||
playFrom: 'start' as const,
|
||||
gameScale: 1,
|
||||
sceneVisibility: defaultSceneVisibility(),
|
||||
};
|
||||
const stack = new CommandStack(project);
|
||||
let failing = false;
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* What the scene view shows of each placeable type – symbol, name, a collider's outline. It lives
|
||||
* in the project like the game scale: saved, but never an undo step.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { createEmptyProject } from '../../src/core/model/Project';
|
||||
import {
|
||||
defaultSceneVisibility,
|
||||
sceneVisibilityOf,
|
||||
shows,
|
||||
VISIBILITY_TYPES,
|
||||
visibilityParts,
|
||||
} from '../../src/core/model/sceneVisibility';
|
||||
import { deserializeProject, serializeProject } from '../../src/core/store/serialize';
|
||||
import { EditorState } from '../../src/editor/state';
|
||||
|
||||
function loaded() {
|
||||
const state = new EditorState();
|
||||
state.loadProject(createEmptyProject('p', 'P'), 1);
|
||||
return state;
|
||||
}
|
||||
|
||||
describe('scene visibility', () => {
|
||||
it('lists every placeable type, Behavior to Tilemap', () => {
|
||||
expect(VISIBILITY_TYPES).toEqual([
|
||||
'behavior',
|
||||
'camera',
|
||||
'collider',
|
||||
'container',
|
||||
'particles',
|
||||
'rigidbody',
|
||||
'sprite',
|
||||
'text',
|
||||
'tilemap',
|
||||
]);
|
||||
});
|
||||
|
||||
it('gives a symbol switch only to types with a symbol, and an outline switch to what collides', () => {
|
||||
expect(visibilityParts('behavior')).toEqual(['symbol', 'label']);
|
||||
expect(visibilityParts('collider')).toEqual(['symbol', 'label', 'shape']);
|
||||
expect(visibilityParts('sprite')).toEqual(['label']);
|
||||
expect(visibilityParts('tilemap')).toEqual(['label', 'shape']);
|
||||
});
|
||||
|
||||
it('shows what it always showed in a new project and in one written before there was any', () => {
|
||||
for (const visibility of [
|
||||
createEmptyProject('p', 'P').sceneVisibility,
|
||||
deserializeProject({ id: 'p', name: 'P', assets: [] }).sceneVisibility,
|
||||
]) {
|
||||
expect(shows(visibility, 'behavior', 'symbol')).toBe(true);
|
||||
expect(shows(visibility, 'behavior', 'label')).toBe(true);
|
||||
expect(shows(visibility, 'collider', 'shape')).toBe(true);
|
||||
expect(shows(visibility, 'tilemap', 'shape')).toBe(true);
|
||||
expect(shows(visibility, 'sprite', 'label')).toBe(false);
|
||||
expect(shows(visibility, 'sprite', 'shape')).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('survives a save and a load', () => {
|
||||
const project = createEmptyProject('p', 'P');
|
||||
project.sceneVisibility.collider.shape = false;
|
||||
project.sceneVisibility.tilemap.shape = false;
|
||||
project.sceneVisibility.rigidbody.label = false;
|
||||
project.sceneVisibility.sprite.label = true;
|
||||
const back = deserializeProject(JSON.parse(JSON.stringify(serializeProject(project)))).sceneVisibility;
|
||||
expect(back.collider.shape).toBe(false);
|
||||
expect(back.tilemap.shape).toBe(false);
|
||||
expect(back.rigidbody.label).toBe(false);
|
||||
expect(back.sprite.label).toBe(true);
|
||||
expect(back.behavior).toEqual(defaultSceneVisibility().behavior);
|
||||
});
|
||||
|
||||
it('reads untrusted JSON part by part, the rest as the defaults', () => {
|
||||
const read = sceneVisibilityOf({
|
||||
sceneVisibility: { camera: { symbol: false, label: 'no' }, texture: { label: true }, text: 3 },
|
||||
});
|
||||
expect(read.camera).toEqual({ symbol: false, label: true, shape: true });
|
||||
expect(read.text).toEqual(defaultSceneVisibility().text);
|
||||
expect('texture' in read).toBe(false);
|
||||
expect(sceneVisibilityOf(null)).toEqual(defaultSceneVisibility());
|
||||
});
|
||||
|
||||
it('never hides what a type does not have to hide', () => {
|
||||
const visibility = defaultSceneVisibility();
|
||||
visibility.sprite.symbol = false;
|
||||
visibility.sprite.shape = false;
|
||||
expect(shows(visibility, 'sprite', 'shape')).toBe(true);
|
||||
visibility.behavior.shape = false;
|
||||
expect(shows(visibility, 'behavior', 'shape')).toBe(true);
|
||||
});
|
||||
|
||||
it('saves a switch without an undo step', () => {
|
||||
const state = loaded();
|
||||
state.setSceneVisibility('collider', 'shape', false);
|
||||
expect(state.project.sceneVisibility.collider.shape).toBe(false);
|
||||
expect(state.saveStatus).toBe('dirty');
|
||||
expect(state.commands.canUndo).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps a switch flipped while playing after Stop', () => {
|
||||
const state = loaded();
|
||||
state.enterPlay();
|
||||
state.setSceneVisibility('behavior', 'label', false);
|
||||
expect(state.project.sceneVisibility.behavior.label).toBe(false);
|
||||
state.exitPlay();
|
||||
expect(state.project.sceneVisibility.behavior.label).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from '../../src/editor/panels/assetIcons';
|
||||
import { HEADER_ICON_SVG } from '../../src/editor/panels/headerIcons';
|
||||
import { ARROW_ICON_SVG, KIND_ICON_SVG, ROW_ACTION_ICON_SVG } from '../../src/editor/panels/markIcons';
|
||||
import { TOOLBAR_ICON_SVG } from '../../src/editor/panels/toolbarIcons';
|
||||
import { TOOLBAR_ICON_SVG, VISIBILITY_ICON_SVG } from '../../src/editor/panels/toolbarIcons';
|
||||
import { UI_ICON_SVG } from '../../src/ui/icons';
|
||||
import { tintSvg } from '../../src/ui/svg';
|
||||
import { SYMBOL_SVG } from '../../src/viewport/symbolIcons';
|
||||
@@ -48,6 +48,7 @@ describe('icon files', () => {
|
||||
['plugins tab', PLUGINS_ICON_SVG],
|
||||
['user management tab', USERS_ICON_SVG],
|
||||
...Object.entries(TOOLBAR_ICON_SVG).map(([name, svg]) => [`toolbar ${name}`, svg]),
|
||||
...Object.entries(VISIBILITY_ICON_SVG).map(([name, svg]) => [`visibility ${name}`, svg]),
|
||||
...Object.entries(HEADER_ICON_SVG).map(([name, svg]) => [`header ${name}`, svg]),
|
||||
...Object.entries(UI_ICON_SVG).map(([name, svg]) => [`ui ${name}`, svg]),
|
||||
...Object.entries(KIND_ICON_SVG).map(([name, svg]) => [`kind ${name}`, svg]),
|
||||
@@ -96,8 +97,21 @@ describe('icon files', () => {
|
||||
});
|
||||
|
||||
it('has one icon per button of the top bar, each its own picture (#183)', () => {
|
||||
expect(Object.keys(TOOLBAR_ICON_SVG).sort()).toEqual(['menu', 'pause', 'play', 'redo', 'stop', 'undo']);
|
||||
expect(new Set(Object.values(TOOLBAR_ICON_SVG)).size).toBe(6);
|
||||
expect(Object.keys(TOOLBAR_ICON_SVG).sort()).toEqual([
|
||||
'eye',
|
||||
'menu',
|
||||
'pause',
|
||||
'play',
|
||||
'redo',
|
||||
'stop',
|
||||
'undo',
|
||||
]);
|
||||
expect(new Set(Object.values(TOOLBAR_ICON_SVG)).size).toBe(7);
|
||||
});
|
||||
|
||||
it('has one picture per switch behind the eye', () => {
|
||||
expect(Object.keys(VISIBILITY_ICON_SVG).sort()).toEqual(['label', 'shape', 'symbol']);
|
||||
expect(new Set(Object.values(VISIBILITY_ICON_SVG)).size).toBe(3);
|
||||
});
|
||||
|
||||
// The bar carries no captions any more, so a picture shared with a tab or an asset type would be
|
||||
|
||||
@@ -167,5 +167,136 @@ export async function topbar(t) {
|
||||
Math.abs(restored.icon - 18) < 1e-9,
|
||||
JSON.stringify({ opened, scaled, restored }),
|
||||
);
|
||||
|
||||
// 46g. The eye beside redo: every placeable type with a switch for its symbol and its name, and a
|
||||
// outline of a collider and of a tilemap's solid cells besides. Sprite, Text and Tilemap have no symbol, so that switch is
|
||||
// greyed out. Real clicks flip the scene view, and the switches are saved but no undo step.
|
||||
await json('(__gm.showTab("scene"), true)');
|
||||
const firstOf = (type) =>
|
||||
json(`(() => {
|
||||
const p = __gm.state.project;
|
||||
const scene = p.assets.find((a) => a.id === p.activeSceneId);
|
||||
const o = (scene?.objects ?? []).find((o) => p.assets.find((a) => a.id === o.assetId)?.type === ${JSON.stringify(type)} && o.enabled !== false);
|
||||
return o ? o.id : null;
|
||||
})()`);
|
||||
const ids = {
|
||||
sprite: await firstOf('sprite'),
|
||||
collider: await firstOf('collider'),
|
||||
behavior: await firstOf('behavior'),
|
||||
tilemap: 'vis-map-obj',
|
||||
};
|
||||
// A map of its own with one solid row, laid over the sprite so it is on screen: the seed has none.
|
||||
await t.mutate(`
|
||||
const scene = __gm.state.activeScene;
|
||||
const assets = __gm.state.project.assets;
|
||||
const at = scene.objects.find((o) => o.id === ${JSON.stringify(ids.sprite)}).position;
|
||||
assets.push({ id: 'vis-wall', type: 'tile', name: 'Vis wall', props: { kind: 'none', solid: true }, pictures: [] });
|
||||
assets.push({ id: 'vis-map', type: 'tilemap', name: 'Vis map', props: { columns: 3, rows: 1, tileSize: 32 },
|
||||
layers: [{ id: 'vis-layer', name: 'Layer 1', cells: { '0,0': 'vis-wall', '1,0': 'vis-wall', '2,0': 'vis-wall' } }] });
|
||||
const o = structuredClone(scene.objects[0]);
|
||||
Object.assign(o, { id: 'vis-map-obj', assetId: 'vis-map', name: 'Vis map', position: { ...at },
|
||||
parentId: '', overrides: {}, instanceEdits: {} });
|
||||
scene.objects.push(o);
|
||||
__gm.state.select(null)`);
|
||||
const undoBefore = await json('__gm.state.commands.canUndo');
|
||||
await clickToolbar('visibility');
|
||||
const popup = await json('__gm.visibilityPopup()');
|
||||
const entry = (list, type, part) => list?.find((e) => e.type === type && e.part === part);
|
||||
const types = [...new Set((popup ?? []).map((e) => e.type))];
|
||||
check(
|
||||
'the eye lists every placeable type, Symbol greyed out where there is none and Shape on collider and tilemap',
|
||||
JSON.stringify(types) ===
|
||||
JSON.stringify([
|
||||
'behavior',
|
||||
'camera',
|
||||
'collider',
|
||||
'container',
|
||||
'particles',
|
||||
'rigidbody',
|
||||
'sprite',
|
||||
'text',
|
||||
'tilemap',
|
||||
]) &&
|
||||
entry(popup, 'sprite', 'symbol')?.enabled === false &&
|
||||
entry(popup, 'tilemap', 'label')?.enabled === true &&
|
||||
entry(popup, 'sprite', 'label')?.on === false &&
|
||||
entry(popup, 'collider', 'shape')?.on === true &&
|
||||
entry(popup, 'tilemap', 'shape')?.on === true &&
|
||||
popup.filter((e) => e.part === 'shape').length === 2,
|
||||
JSON.stringify(popup),
|
||||
);
|
||||
const flip = async (type, part) => {
|
||||
const at = entry(await json('__gm.visibilityPopup()'), type, part);
|
||||
await t.click(at.x, at.y);
|
||||
await sleep(150);
|
||||
};
|
||||
const labelIds = async () => (await json('__gm.sceneLabels()')).map((l) => l.id);
|
||||
const mapLooks = async () => ({
|
||||
solids: (await json(`__gm.tilemap.solids(${JSON.stringify(ids.tilemap)})`))?.visible ?? null,
|
||||
drawn: (await json(`__gm.tilemap.info(${JSON.stringify(ids.tilemap)})`)) !== null,
|
||||
});
|
||||
const before = {
|
||||
shape: await json(`__gm.sceneShape(${JSON.stringify(ids.collider)})`),
|
||||
labels: await labelIds(),
|
||||
icon: (await json(`__gm.sceneIcon(${JSON.stringify(ids.behavior)})`))?.visible,
|
||||
map: await mapLooks(),
|
||||
};
|
||||
await flip('collider', 'shape');
|
||||
await flip('tilemap', 'shape');
|
||||
await flip('behavior', 'label');
|
||||
await flip('behavior', 'symbol');
|
||||
await flip('sprite', 'label');
|
||||
const after = {
|
||||
shape: await json(`__gm.sceneShape(${JSON.stringify(ids.collider)})`),
|
||||
labels: await labelIds(),
|
||||
icon: (await json(`__gm.sceneIcon(${JSON.stringify(ids.behavior)})`))?.visible,
|
||||
popup: await json('__gm.visibilityPopup()'),
|
||||
saved: await json('__gm.state.project.sceneVisibility'),
|
||||
map: await mapLooks(),
|
||||
};
|
||||
await shot('46g-visibility');
|
||||
check(
|
||||
"a tilemap's Shape hides the outline of its solid cells and leaves the map itself drawn",
|
||||
before.map.solids === true &&
|
||||
before.map.drawn === true &&
|
||||
after.map.solids === false &&
|
||||
after.map.drawn === true &&
|
||||
after.saved.tilemap.shape === false,
|
||||
JSON.stringify({ before: before.map, after: after.map }),
|
||||
);
|
||||
check(
|
||||
'switching hides the collider outline and the behavior icon and name, and puts the sprite name on',
|
||||
before.shape?.visible === true &&
|
||||
after.shape?.visible === false &&
|
||||
after.shape?.clickable === false &&
|
||||
before.icon === true &&
|
||||
after.icon === false &&
|
||||
before.labels.includes(ids.behavior) &&
|
||||
!after.labels.includes(ids.behavior) &&
|
||||
!before.labels.includes(ids.sprite) &&
|
||||
after.labels.includes(ids.sprite) &&
|
||||
entry(after.popup, 'collider', 'shape')?.on === false,
|
||||
JSON.stringify({ ids, before, after: { ...after, popup: undefined } }),
|
||||
);
|
||||
check(
|
||||
'the switches go into the project and not onto the undo stack',
|
||||
after.saved.collider.shape === false &&
|
||||
after.saved.behavior.label === false &&
|
||||
after.saved.sprite.label === true &&
|
||||
(await json('__gm.state.commands.canUndo')) === undoBefore,
|
||||
JSON.stringify({ saved: after.saved, undoBefore }),
|
||||
);
|
||||
// Handed back as found, for whatever runs after this.
|
||||
await flip('collider', 'shape');
|
||||
await flip('tilemap', 'shape');
|
||||
await flip('behavior', 'label');
|
||||
await flip('behavior', 'symbol');
|
||||
await flip('sprite', 'label');
|
||||
await press('Escape');
|
||||
await t.mutate(`
|
||||
const scene = __gm.state.activeScene;
|
||||
scene.objects.splice(scene.objects.findIndex((o) => o.id === 'vis-map-obj'), 1);
|
||||
const assets = __gm.state.project.assets;
|
||||
for (const id of ['vis-map', 'vis-wall']) assets.splice(assets.findIndex((a) => a.id === id), 1)`);
|
||||
await t.saved();
|
||||
}
|
||||
Reference in new issue
Block a user