Finish the struct and list leftovers: inline lists, list fields, item kinds

An unconnected list input is a button into the List tab instead of a number
field; a struct field can hold a list, so ListSlot carries an optional field
name and the three owner cases only say what sits at a key; and a list
variable picks its item kind in the inspector (marker doubles as the chooser),
so a list of structs no longer has to be born from a project file.

The declaration keeps the storable shapes finite: a struct field may be a list,
but that list holds primitives only, so list->struct->primitive and
struct->list->primitive are the two deepest forms.

check 674, e2e 483, jump-run 24, Tetris 19.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-10 16:36:15 +02:00
1 parent fe3c63c48c
commit 10d030376a
18 files changed
+984 -184

No files matched your search

+91
View File
@@ -95,6 +95,34 @@ export async function listTab(t) {
await sleep(200);
check('and the count follows the value', (await find('2 items')) !== null);
// 6b. What a list holds is a choice in the inspector, not only in a project file: the kind mark
// doubles as the picker, and changing it runs every value through the new declaration.
const numsOf = () => json(`__gm.state.project.assets.find((a) => a.id === 'listy')`);
await clickText('\u2261#');
await sleep(150);
await clickText('Texts');
await sleep(250);
let listy = await numsOf();
check(
'the kind mark picks what the list holds',
listy.variables[0].items === 'text',
listy.variables[0].items,
);
check('and the mark itself says what it holds now', (await find('≡Aa')) !== null);
check(
'and every value follows the declaration',
JSON.stringify(listy.props.nums) === '["1","2"]',
JSON.stringify(listy.props.nums),
);
await undo();
await sleep(250);
listy = await numsOf();
check(
'undo puts the kind and the values back',
listy.variables[0].items === 'number' && JSON.stringify(listy.props.nums) === '[1,2]',
JSON.stringify(listy.props.nums),
);
// 7. A list whose target is gone empties the tab rather than showing a stale one.
await mutate("__gm.state.openList({ kind: 'asset', assetId: 'listy', key: 'nums' })");
await sleep(200);
@@ -176,10 +204,73 @@ export async function listTab(t) {
JSON.stringify(back),
);
// 8b. The item shape is declared in the inspector as well, with the control a struct has – and
// primitives only, because a list inside a list item is the one shape the model does not store.
const cellsOf = () => json(`__gm.state.project.assets.find((a) => a.id === 'celly').props.cells`);
check('a list of structs carries the add-a-field control', (await find('+ Field')) !== null);
await clickText('+ Field');
await sleep(150);
await clickText('Boolean');
await sleep(250);
check(
'+ Field grows every item of the list at once',
JSON.stringify(await cellsOf()) ===
JSON.stringify([
{ x: 30, y: 11, field1: false },
{ x: 44, y: 55, field1: false },
]),
JSON.stringify(await cellsOf()),
);
info = await json('__gm.list.info()');
check(
'and the tab shows the new field on every row',
info.items[0] === 'x: 30, y: 11, field1: false',
info.items[0],
);
await shot('06-item-field');
await undo();
await sleep(250);
check(
'undo takes it out of every item again',
JSON.stringify(await cellsOf()) ===
JSON.stringify([
{ x: 30, y: 11 },
{ x: 44, y: 55 },
]),
JSON.stringify(await cellsOf()),
);
// 9. A node's unconnected `list` input carries its value in the very same params bag a setting
// does, so it is a count button too – before this it offered a number field.
await mutate(`
const a = __gm.state.project.assets.find((x) => x.id === 'celly');
a.graph.nodes.push({ id: 'counter', type: 'Count', x: 40, y: 40, params: {} });
__gm.showTab('graph');
__gm.state.select({ kind: 'node', assetId: 'celly', nodeId: 'counter', nodeIds: ['counter'] });
__gm.state.preview();
`);
await sleep(300);
const portButton = await find('0 items');
check('an unconnected list input reads as an item count', portButton !== null);
await click(portButton.x, portButton.y);
await sleep(250);
check('clicking it opens the List tab on the port', (await json('__gm.tab()')) === 'list');
info = await json('__gm.list.info()');
check('titled by the node and the port it belongs to', info.title === 'Count · list', info.title);
await clickText('+ Item');
await sleep(250);
const params = await json(`__gm.state.project.assets.find((a) => a.id === 'celly').graph.nodes[0].params`);
check(
'and the item lands in the node params, where the node reads it',
JSON.stringify(params) === '{"list":[0]}',
JSON.stringify(params),
);
await mutate(`
const p = __gm.state.project;
p.assets = p.assets.filter((a) => a.id !== 'celly');
__gm.state.select(null);
__gm.showTab('scene');
__gm.state.preview();
`);
await sleep(200);