Files
feuerwerk/tools/e2e/10-text-and-enabled.mjs
T
rootandClaude Opus 5 0449218cb6 Let a variable declare struct fields
The value type was already there: StructValue, textOf, sameValue,
writeTyped/mergeStruct and the struct port type came with the first
part, and Get/Set variable carry any-ports, so a bundle already flowed
through the graph unchanged. What was missing was the declaration.

PropertyDef gains kind 'struct' with a list of fields, VARIABLE_KINDS a
fifth kind, variableDef a fifth parameter. A declared field is a
primitive - a list field would need a nested editor and therefore the
path in the slot address, the same step "a list of structs" is waiting
for. coerceValue now returns PropertyValue rather than FieldValue, the
first value type that is not a field; none of its five callers break.

In the inspector a struct variable is a block, not a tab. The list tab
exists because hundreds of rows fit in no inspector row; a struct is the
opposite case - few fields that fit under each other. It is the port
editor Pack already has, one level down and with a value column: type,
key and default value in one place. A placed object sees the same fields
as plain value rows: overriding yes, declaring no.

Adding or removing a field touches the declaration, the default, the
behavior's value and every instance override in one command, so a stored
value can never keep a shape its declaration no longer promises. The row
handlers read the bundle at the moment of the change rather than closing
over a snapshot - a field writes the whole bundle, so with a snapshot
the second edited row revives the first one's old value. defSignature
carries the fields, otherwise a new row appears only at the next
rebuild.

Fixes a fixture fault this uncovered: step 10 pushed its text object
into the scene without instanceEdits, which overrideSites walks with
Object.entries. AddStructFieldCommand is the first caller to reach it
from that state; the step was green alone and failed three checks in the
full suite.

Moves the dynamic-ports note to done - all three of its parts are built.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 12:29:01 +02:00

93 lines
4.1 KiB
JavaScript

/** Text assets rendered in the scene, and the enabled switch in the editor and in the running game. */
export async function textAndEnabled(t) {
const {
check,
sleep,
evaluate,
json,
shot,
click,
clickText,
locate,
mutate,
object,
play,
press,
stop,
typeText,
} = t;
const has = (text) => evaluate(`Boolean(__gm.locate(${JSON.stringify(text)}))`);
// 17. Text assets: created through the + menu, placed, and rendered as real text in the scene.
await clickText('+');
await clickText('Text');
await sleep(100);
const textAssetId = await evaluate(`(__gm.state.project.assets.find(a => a.type === 'text') || {}).id`);
check('+ menu creates a text asset', Boolean(textAssetId));
await mutate(`
const id = ${JSON.stringify(textAssetId)};
__gm.state.project.assets.find(a => a.id === id).props.text = 'Hello world';
// On the camera so the game view really renders it (outside the frame it would be culled).
const cam = __gm.state.activeScene.objects.find(o => o.id === 'o3').position;
__gm.state.activeScene.objects.push({ id: 'otext', assetId: id, name: '', position: { x: cam.x, y: cam.y, z: 5 }, editorOffset: { x: 0, y: 0 }, overrides: {}, parentId: '', instanceEdits: {} });
__gm.state.select({ kind: 'object', holderId: 'scene1', objectIds: ['otext'], objectId: 'otext' })`);
await sleep(100);
check('a placed text object draws its content in the scene', await has('Hello world'));
await shot('29-text-object');
// 17a. The enabled switch: every object has it, switching it off is a normal override, and a
// switched-off object disappears from the scene view too (it stays in the hierarchy).
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;
// 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);
const textObject = await object('otext');
check(
'clicking it writes an override instead of changing the asset',
textObject.overrides.enabled === false,
JSON.stringify(textObject.overrides),
);
check(
'a switched-off object is not drawn in the scene view',
!(await json('__gm.drawOrder()')).includes('otext'),
);
const visibleAfter = (await json('__gm.viewportInfo()')).visibleItems;
check(
'the scene view renders one item less',
visibleAfter === visibleBefore - 1,
`${visibleBefore} → ${visibleAfter}`,
);
check('it stays selectable in the hierarchy, so its switch stays reachable', await has('Enabled'));
await shot('30-disabled');
// 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();
await sleep(200);
const offItems = (await json('__gm.gameInfo()')).visibleItems;
await mutate(`__gm.state.activeScene.objects.find(o => o.id === 'otext').overrides.enabled = true`);
await sleep(200);
const onItems = (await json('__gm.gameInfo()')).visibleItems;
check(
'the game renders one item more once the object is switched on',
onItems === offItems + 1,
`${offItems} → ${onItems}`,
);
await stop();
check('stop leaves the play-mode switch behind', (await object('otext')).overrides.enabled === false);
// 17c. A field that shows no decimals must not store any either. Font size is such a field, so
// typing a fraction has to round: showing "33" while the project holds 32.6 would be a lie.
const fontLabel = await locate('Font size');
check('Font size reads as a whole number', Boolean(await has('32')) && !(await has('32.000')));
await click(fontLabel.x + 160, fontLabel.y);
await typeText('32.6');
await press('Enter');
await sleep(200);
const fontSize = (await object('otext')).overrides.fontSize;
check('typing a fraction into it rounds the stored value', fontSize === 33, String(fontSize));
check('and the field shows exactly what was stored', Boolean(await has('33')));
}