Scene: ausgeschaltete Objekte behalten ihren Auswahlrahmen

Ein ausgeschaltetes Objekt zeichnet nichts, verschwand bisher aber auch aus
der Auswahl - im Panel angeklickt blieb der Viewport leer, es gab kein
Anzeichen, wo es liegt. Der Rahmen kommt jetzt, gedimmt auf 45 %, damit er
sich nicht wie eine fehlgeschlagene Zeichnung liest. Ziehen bleibt aus: der
Node ist weiterhin nicht interaktiv.

Dazu __gm.gizmos() als Testhaken - Gizmos waren vorher gar nicht prüfbar. Es
meldet das alpha, das wirklich in den Strich geht, kein daneben abgeleitetes
Flag.
This commit is contained in:
root committed 2026-09-10 17:36:27 +02:00
1 parent 4b47741fac
commit 2218046738
6 files changed
+81 -7

No files matched your search

+1 -1
View File
@@ -604,7 +604,7 @@ npm run check # typecheck + lint + format:check + tests, what CI would ru
# code is lost (tail's is reported), so a red run reads as green
npm run build # writes dist/ and regenerates index.html
export GM_REMOTE=user@host # optional: run the browser elsewhere; see below
node tools/seed-e2e.mjs && node tools/e2e.mjs # 491 editor checks, screenshots in /tmp/gm-e2e-*.png
node tools/seed-e2e.mjs && node tools/e2e.mjs # 496 editor checks, screenshots in /tmp/gm-e2e-*.png
GM_STEPS=20 node tools/e2e.mjs # or 1-12, or 6,20 – a subset while working on one step
./e2e.sh start [GM_STEPS] # the same run detached, see below
./e2e.sh status # live progress, or the last run's result
+2
View File
@@ -176,6 +176,8 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
assetRow: (id: string) => assets.rowGlobal(id),
assetRowColor: (id: string) => assets.rowColor(id),
gameInfo: () => gameView.debugInfo(),
/** Selection outlines in the Scene tab: a switched-off object is invisible but still framed. */
gizmos: () => viewport.gizmoInfo(),
layerOf: (id: string) => viewport.layerOf(id),
drawOrder: () => viewport.drawOrder(),
objectScreen: (id: string) => viewport.objectScreen(id),
+27 -4
View File
@@ -63,6 +63,15 @@ interface Marquee {
active: boolean;
}
/** One selection outline that was drawn – `alpha` is the value the stroke really got. */
export interface GizmoInfo {
id: string;
primary: boolean;
alpha: number;
}
const OFF_GIZMO_ALPHA = 0.45;
export class SceneViewport extends WorldViewport {
private readonly grid = new Graphics();
private readonly links = new Graphics();
@@ -74,6 +83,7 @@ export class SceneViewport extends WorldViewport {
private dragging: ObjectDrag | null = null;
private marquee: Marquee | null = null;
private dropPreview: DropPreview | null = null;
private lastGizmos: GizmoInfo[] = [];
constructor(state: EditorState) {
super(state);
@@ -348,6 +358,11 @@ export class SceneViewport extends WorldViewport {
this.gizmos.clear();
}
/** The selection outlines currently drawn (test hook). */
gizmoInfo(): GizmoInfo[] {
return this.lastGizmos;
}
protected override afterItems(items: DrawItem[], view: View): void {
this.drawParentLinks(items, view.scale);
this.drawGizmos(items, view);
@@ -443,24 +458,32 @@ export class SceneViewport extends WorldViewport {
/** Selection outlines – only the primary object gets corner handles – and the rubber band. */
private drawGizmos(items: DrawItem[], view: View): void {
this.lastGizmos = [];
const s = this.state.selection;
if (s?.kind === 'object') {
for (const id of s.objectIds) {
const item = items.find((i) => i.id === id);
if (!item || !item.enabled) continue;
if (!item) continue;
const { w, h } = footprintOf(item.type, item.props);
const tl = worldToScreen(view, item.x - w / 2, item.y - h / 2);
const sw = w * view.scale;
const sh = h * view.scale;
this.gizmos.rect(tl.x - 2, tl.y - 2, sw + 4, sh + 4).stroke({ width: 2, color: Theme.colors.gizmo });
if (id !== s.objectId) continue;
// A switched-off object draws nothing, so its outline is the only way to see where it
// sits – dimmed, because the empty frame would otherwise read as a drawing that failed.
const alpha = item.enabled ? 1 : OFF_GIZMO_ALPHA;
const primary = id === s.objectId;
this.lastGizmos.push({ id, primary, alpha });
this.gizmos
.rect(tl.x - 2, tl.y - 2, sw + 4, sh + 4)
.stroke({ width: 2, color: Theme.colors.gizmo, alpha });
if (!primary) continue;
for (const [cx, cy] of [
[tl.x, tl.y],
[tl.x + sw, tl.y],
[tl.x, tl.y + sh],
[tl.x + sw, tl.y + sh],
] as const) {
this.gizmos.rect(cx - 3, cy - 3, 6, 6).fill(Theme.colors.gizmo);
this.gizmos.rect(cx - 3, cy - 3, 6, 6).fill({ color: Theme.colors.gizmo, alpha });
}
}
}
@@ -0,0 +1,23 @@
objects die nicht enabled sind, sind komplett unsichtbar, das passt
aber: wenn ich sie im panel linksunten selektiere, sollte zumindest das "gizmo" (also der selektions rahmen) im scene viewport sichtbar sein
---
Gebaut. Drei Dinge, die weder oben stehen noch aus dem Commit hervorgehen:
**Der Rahmen ist gedimmt (45 %).** Ein ausgeschaltetes Objekt zeichnet nichts, ein Rahmen um leeren
Raum in voller Stärke liest sich sonst wie eine Zeichnung, die fehlgeschlagen ist. Die Eckgriffe des
primären Objekts kommen mit – sie sind reine Kennzeichnung, es gibt keinen Hit-Test daran, sie
versprechen also keine Geste, die ein ausgeschaltetes Objekt nicht kann.
**Ziehen bleibt aus.** Der Rahmen sagt nur, wo das Objekt liegt; sein Node ist weiterhin unsichtbar
und `eventMode: 'none'`, weil `onScreen` in `WorldViewport` "aus" und "außerhalb des Sichtfelds" in
einem Ausdruck bündelt. Bewegen geht also nur über den Inspector – oder nach dem Einschalten.
**Es greift auch bei geerbtem Aus.** `DrawItem.enabled` ist `enabledAlongChain`, also das Objekt
*und* alle seine Eltern. Ein Kind, das nur deshalb unsichtbar ist, weil sein Elternteil aus ist,
bekommt denselben gedimmten Rahmen.
Nebenbei entstanden: `__gm.gizmos()` – vorher waren Gizmos überhaupt nicht prüfbar. Es meldet das
`alpha`, das wirklich in den Strich geht, nicht ein daneben abgeleitetes Flag; sonst hätte der Test
grün bleiben können, während gar nichts gedimmt wird (als Gegenprobe nachgestellt).
@@ -1,2 +0,0 @@
objects die nicht enabled sind, sind komplett unsichtbar, das passt
aber: wenn ich sie im panel linksunten selektiere, sollte zumindest das "gizmo" (also der selektions rahmen) im scene viewport sichtbar sein
+28
View File
@@ -40,6 +40,9 @@ export async function textAndEnabled(t) {
const enabledRow = await evaluate(`__gm.locate('Enabled')`);
check('the inspector shows the Enabled switch for a scene object', Boolean(enabledRow));
const visibleBefore = (await json('__gm.viewportInfo()')).visibleItems;
const gizmoOf = async (id) => (await json('__gm.gizmos()')).find((g) => g.id === id) ?? null;
const gizmoOn = await gizmoOf('otext');
check('a selected object is framed at full strength', gizmoOn?.alpha === 1, JSON.stringify(gizmoOn));
// The checkbox sits right of the 64 px label column; locate() gives the label's text centre.
await click(enabledRow.x + 55, enabledRow.y);
await sleep(100);
@@ -62,6 +65,31 @@ export async function textAndEnabled(t) {
check('it stays selectable in the hierarchy, so its switch stays reachable', await has('Enabled'));
await shot('30-disabled');
// 17a-2. It draws nothing, so its outline is the only thing left that says where it sits – which
// is the whole point of being able to pick it in the hierarchy. Selecting another object
// and coming back proves the outline follows the panel, not a leftover from before.
const gizmoOff = await gizmoOf('otext');
check(
'a switched-off object is still framed, dimmed',
gizmoOff?.primary === true && gizmoOff.alpha > 0 && gizmoOff.alpha < 1,
JSON.stringify(gizmoOff),
);
const otherRow = await evaluate(`__gm.hierarchyRow('o3')`);
await click(otherRow.x, otherRow.y);
await sleep(100);
check('selecting something else drops the frame', (await gizmoOf('otext')) === null);
const offRow = await evaluate(`__gm.hierarchyRow('otext')`);
await click(offRow.x, offRow.y);
await sleep(100);
const gizmoBack = await gizmoOf('otext');
check(
'picking the switched-off object in the hierarchy frames it again',
gizmoBack?.alpha > 0 && gizmoBack.alpha < 1,
JSON.stringify(gizmoBack),
);
check('and it is still not drawn', !(await json('__gm.drawOrder()')).includes('otext'));
await shot('30b-disabled-gizmo');
// 17b. In the running game a switched-off object does not exist: it is not rendered, and
// switching it back on brings it back.
await play();