Drop the collider and delete-node buttons from the inspector

Adding a collider now lives only in the Scene objects context menu, deleting a node on
the Delete key and the node context menu. The e2e checks that pinned the inspector button
now drive the hierarchy menu instead, including that its entry is disabled on template
content and on a placed container without raising a notice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-11 12:54:07 +02:00
1 parent 0dcb3b4432
commit 5f8a308d62
7 files changed
+48 -36

No files matched your search

+3 -3
View File
@@ -351,8 +351,8 @@ inspector below X/Y/Z, undoable, with ↺ back to 0. It belongs to the placed ob
A **collider** is an asset (`shape` box/circle, `width`/`height` or `radius`) placed like any A **collider** is an asset (`shape` box/circle, `width`/`height` or `radius`) placed like any
other object – typically as a child of the sprite it belongs to, so its position is the offset other object – typically as a child of the sprite it belongs to, so its position is the offset
from that *body* and it moves with it. Right-click an object in **Scene objects** (or press from that *body* and it moves with it. Right-click an object in **Scene objects** → *Add
**+ Add collider** in its inspector) to get one with one click: a box collider sized like the collider* to get one with one click: a box collider sized like the
sprite, parented at its centre. Colliders can be reused across objects, overridden per instance sprite, parented at its centre. Colliders can be reused across objects, overridden per instance
and stacked (hitbox + hurtbox on one sprite; colliders of the same body never collide with each and stacked (hitbox + hurtbox on one sprite; colliders of the same body never collide with each
other). They are editor symbols: green outlines in edit mode, invisible in play mode. other). They are editor symbols: green outlines in edit mode, invisible in play mode.
@@ -644,7 +644,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 # code is lost (tail's is reported), so a red run reads as green
npm run build # writes dist/ and regenerates index.html npm run build # writes dist/ and regenerates index.html
export GM_REMOTE=user@host # optional: run the browser elsewhere; see below export GM_REMOTE=user@host # optional: run the browser elsewhere; see below
node tools/seed-e2e.mjs && node tools/e2e.mjs # 535 editor checks, screenshots in /tmp/gm-e2e-*.png node tools/seed-e2e.mjs && node tools/e2e.mjs # 537 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 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 start [GM_STEPS] # the same run detached, see below
./e2e.sh status # live progress, or the last run's result ./e2e.sh status # live progress, or the last run's result
+2 -11
View File
@@ -1,4 +1,3 @@
import { RemoveNodeCommand } from '../../core/commands/graphCommands';
import { import {
callProblem, callProblem,
declaresVariablePorts, declaresVariablePorts,
@@ -13,9 +12,9 @@ import type { Asset } from '../../core/model/Asset';
import { behaviorLookup } from '../../core/model/behaviorPrograms'; import { behaviorLookup } from '../../core/model/behaviorPrograms';
import { findAssetOf, type Project } from '../../core/model/Project'; import { findAssetOf, type Project } from '../../core/model/Project';
import type { PropertyDef } from '../../core/properties/PropertySchema'; import type { PropertyDef } from '../../core/properties/PropertySchema';
import { Button, Label } from '../../ui'; import { Label } from '../../ui';
import { createFieldRow, type FieldHandlers } from './fieldFactory'; import { createFieldRow, type FieldHandlers } from './fieldFactory';
import { addGap, addNote, addTitle, type InspectorHost } from './host'; import { addNote, addTitle, type InspectorHost } from './host';
import { buildPortSection, buildVariablePortSection } from './portSection'; import { buildPortSection, buildVariablePortSection } from './portSection';
import { listOpener, previewingHandlers } from './previewHandlers'; import { listOpener, previewingHandlers } from './previewHandlers';
import { currentNode, nodeParamValue } from './valueSpecs'; import { currentNode, nodeParamValue } from './valueSpecs';
@@ -81,14 +80,6 @@ export function buildNodeSection(host: InspectorHost, assetId: string, nodeId: s
host.fields.set(`input:${port.key}`, row); host.fields.set(`input:${port.key}`, row);
host.body.add(row.row); host.body.add(row.row);
} }
addGap(host);
host.body.add(
new Button('Delete node', {
variant: 'danger',
onClick: () => host.state.execute(new RemoveNodeCommand(assetId, nodeId)),
}),
);
} }
/** Pushes the node's current params into the rows. False when the node is gone (rebuild). */ /** Pushes the node's current params into the rows. False when the node is gone (rebuild). */
+1 -12
View File
@@ -38,7 +38,7 @@ import {
resolveSceneObjectProps, resolveSceneObjectProps,
} from '../../core/properties/resolve'; } from '../../core/properties/resolve';
import { Button, TextField } from '../../ui'; import { Button, TextField } from '../../ui';
import { addCollider, canAddCollider, createContainer, removeObjects } from '../actions'; import { createContainer, removeObjects } from '../actions';
import { aspectActions, sizeCommand } from './aspectAction'; import { aspectActions, sizeCommand } from './aspectAction';
import { defSignature } from './assetSection'; import { defSignature } from './assetSection';
import { createFieldRow, type FieldRow, labelledRow, type RowActions } from './fieldFactory'; import { createFieldRow, type FieldRow, labelledRow, type RowActions } from './fieldFactory';
@@ -230,17 +230,6 @@ export function buildObjectSection(host: InspectorHost, holderId: string, object
markObjectRows(host, object, asset, target); markObjectRows(host, object, asset, target);
if (asset.type === 'container') contentSection(host, holderId, object, asset.objects.length); if (asset.type === 'container') contentSection(host, holderId, object, asset.objects.length);
const stored = findObjectHolder(project, holderId);
if (stored && canAddCollider(project, stored, object)) {
addGap(host);
host.body.add(
new Button('+ Add collider', {
variant: 'ghost',
onClick: () => addCollider(host.state, holderId, objectId),
}),
);
}
} }
/** /**
@@ -0,0 +1,7 @@
- Scene: Add collider button aus insepctor entfernen
- Graph: Delete node button aus inspector entfernen
Umgesetzt: beide Aktionen bleiben erreichbar, nur nicht mehr im Inspector.
- Collider: Rechtsklick auf das Objekt in **Scene objects** → *Add collider*. Das ist jetzt der
einzige Weg; seine Sperre (Template-Inhalt, platzierter Container) prüft e2e 09 über das Menü.
- Node: `Delete` oder Rechtsklick auf den Node → *Delete node*.
@@ -1,2 +0,0 @@
- Scene: Add collider button aus insepctor entfernen
- Graph: Delete node button aus inspector entfernen
+2
View File
@@ -233,6 +233,8 @@ export async function graph(t) {
// 13e. Delete removes the selected node together with its edge; undo brings both back. // 13e. Delete removes the selected node together with its edge; undo brings both back.
await select({ kind: 'node', assetId: 'mover', nodeId: newNode.id }); await select({ kind: 'node', assetId: 'mover', nodeId: newNode.id });
await sleep(200);
check('the node inspector has no delete button of its own', !(await t.has('Delete node')));
await press('Delete'); await press('Delete');
await sleep(150); await sleep(150);
const deleted = await graphCounts(); const deleted = await graphCounts();
+33 -8
View File
@@ -296,12 +296,33 @@ export async function containers(t) {
// 16i. Structure is the template's business: resolved content cannot be removed, re-parented, // 16i. Structure is the template's business: resolved content cannot be removed, re-parented,
// given a collider or turned into a container of its own. // given a collider or turned into a container of its own.
await selectObject(redA); // The hierarchy menu is the only way to a collider, and a disabled entry must not even reach
check('the inspector offers no collider for resolved content', !(await has('+ Add collider'))); // the command: the command refuses too, but with a notice.
await selectObject(instanceA.id); const colliderFromMenu = async (id) => {
await selectObject(id);
const row = await json(`__gm.hierarchyRow(${JSON.stringify(id)})`);
const before = (await objects()).length;
const noticeNow = await notice();
await click(row.x, row.y, 'right');
await sleep(150);
await clickText('Add collider');
await sleep(200);
await press('Escape');
await sleep(100);
return { added: (await objects()).length - before, notice: await noticeNow() };
};
const refused = JSON.stringify({ added: 0, notice: '' });
const fromContent = await colliderFromMenu(redA);
check( check(
'nor for the placed container itself, which the command refuses as the hierarchy menu does', 'the menu offers no collider for resolved content',
!(await has('+ Add collider')), JSON.stringify(fromContent) === refused,
JSON.stringify(fromContent),
);
const fromInstance = await colliderFromMenu(instanceA.id);
check(
'nor for the placed container itself',
JSON.stringify(fromInstance) === refused,
JSON.stringify(fromInstance),
); );
const plainId = await json(`(() => { const plainId = await json(`(() => {
const p = __gm.state.project; const p = __gm.state.project;
@@ -311,11 +332,15 @@ export async function containers(t) {
return sprite ? sprite.id : null; return sprite ? sprite.id : null;
})()`); })()`);
await selectObject(plainId); await selectObject(plainId);
check('the inspector has no collider button of its own', !(await has('+ Add collider')));
const fromPlain = await colliderFromMenu(plainId);
check( check(
'while a plain sprite still gets it', 'while a plain sprite still gets one from the menu',
Boolean(plainId) && (await has('+ Add collider')), fromPlain.added === 1,
JSON.stringify({ plainId }), JSON.stringify(fromPlain),
); );
await undo();
await sleep(200);
await selectObject(redA); await selectObject(redA);
let noticeAfter = await notice(); let noticeAfter = await notice();
await press('Delete'); await press('Delete');