Files
feuerwerk/src/editor/panels/SceneVisibilityPopup.ts
T
adminandClaude Opus 5.5 30de4d35a8
game-maker / gate (push) Successful in 3m21s
game-maker / suite (push) Failing after 9m15s
Give the scene tab a header with Fit and the eye (#245)
The scene view had no header bar of its own, and its eye sat in the
top bar. Switching between Scene and Game also showed two different
pictures, since nothing tied the free view to the camera.

- Theme.headerPadding (4) replaces Theme.padding in the top bar and
  in TabHeader, for the padding and for minHeight. Both bars shrink
  from 40 to 32 px. Nothing else uses the new value.
- The new ScenePane wraps the scene view in a header bar with Fit
  and the visibility eye. The eye is gone from the top bar, which
  keeps the menu, undo and redo.
- Fit with an active camera shows the world exactly as the Game tab
  does at its chosen step: the same zoom, and the camera centre on
  the same screen pixel. The rule is the pure freeViewOfCamera in
  viewport/view.ts, which also mirrors the letterbox snapping.
- Fit without a camera frames all content on a whole step, as
  before.
- New hook __gm.sceneHeaderAt(name). e2e 46g opens the eye there.
- The doc comment above Theme.treeIndent went away in the working
  tree along with this. It is committed as found.

Not checked: the two views only match while they have the same size.
With the tilemap panel open the scene view is narrower, and Auto may
pick a smaller step there.

Tests:
- ./check.sh: 178 files, 2575 tests, CHECK EXIT 0
- 2 new unit tests in view.test.ts: three cameras times four steps
  land on the same screen point as in the Game tab, Auto stays whole
- e2e 3h (2 checks): a real click on Fit at Auto and at Fit, crate,
  camera frame and view box match in both tabs
- GM_STEPS=1-3,46: ALL PASSED, 94 checks
- Counter-probes: without the centre offset the unit test fails,
  with Fit ignoring the camera both 3h checks fail

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 12:57:49 +02:00

101 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 scene tab's header: 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;
}