Let structs and lists nest to any depth (#239)
Structs and lists were capped at two fixed levels (list of structs, struct with a list field). Anything deeper was dropped silently on load, in node settings and when written to a variable. The cap came from an early design choice, not from a technical need, so it is lifted completely instead of adding a third fixed level. - One kind list NESTED_KINDS replaces ListItemKind and StructFieldKind. The flat special cases in listItems.ts are gone, every value goes through one recursive path. - A struct inside a struct is declared, like a top-level struct. Values coming in over an edge are brought into that shape. Struct items in lists still take their fields from the value. - New valuePath.ts addresses any nested spot by keys and indices. ListSlot.field is now ListSlot.path. - Field commands (add, rename, remove) take the path to the struct. They change declaration, own value and all overrides in one undo step, as before on the top level. - Inspector: "+ Field" offers Struct and List on every level. A nested struct is a header row with its own "+ Field", its fields are indented one step further. - List tab: a list item shows "n items" and opens one level deeper. The title shows the path, e.g. "Listy · nums · [3]". - Pack in Pack now writes a struct into a struct field instead of text. Objects are kept at any depth and stripped at every exit. - A list edit in a struct field without an existing object override now starts from the shown value instead of an empty struct. - Old projects load unchanged. Save, load and save again gives the same result. Tests: - ./check.sh: 172 files, 2498 tests, CHECK EXIT 0 - e2e 21 + 22: ALL PASSED, 90 checks (new 21d-iii, 22f-ii, 21d and 22f updated) - 20 new unit tests in nested-values.test.ts and nested-list-slot.test.ts, three tests that pinned the old cap turned around - 6 counter-probes, each fails 1 to 8 tests, originals restored and checked with cmp - Not covered: a nested struct inside a struct item in the list tab has no e2e click Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
483c34bb5c
commit
0c5ac32b59
29 files changed
+1046
-437
No files matched your search
+51
-4
@@ -139,12 +139,13 @@ export async function structVariable(t) {
|
||||
const info = await json('__gm.list.info()');
|
||||
check('whose title names the field inside the bundle', info.title.endsWith('· field1'), info.title);
|
||||
|
||||
// The depth the stored value goes ends here: this list sits in a struct field, so a struct item
|
||||
// would be dropped by the next write. It is left out of the menu rather than doing nothing.
|
||||
// Nothing bounds the depth any more (#239): a list in a struct field offers what any list does.
|
||||
await clickText('+ Item');
|
||||
check(
|
||||
'the menu of a list in a struct field leaves the struct kind out',
|
||||
(await find('Number')) !== null && (await find('Struct')) === null,
|
||||
'the menu of a list in a struct field offers every kind, the struct included',
|
||||
(await find('Number')) !== null &&
|
||||
(await find('Struct')) !== null &&
|
||||
(await json('__gm.list.info()')).offers.includes('list'),
|
||||
JSON.stringify((await json('__gm.list.info()')).offers),
|
||||
);
|
||||
await pickKind('Number');
|
||||
@@ -165,6 +166,52 @@ export async function structVariable(t) {
|
||||
|
||||
await mutate('__gm.state.openList(null)');
|
||||
|
||||
// 22f-ii. A field may be a struct again (#239): a head row with a `+ Field` of its own, and its
|
||||
// fields one indent step further in. Both menus with real clicks.
|
||||
const declaredLast = () =>
|
||||
json("__gm.state.project.assets.find((a) => a.id === 'structy').variables[0].fields.at(-1)");
|
||||
await clickText('+ Field');
|
||||
await pickKind('Struct');
|
||||
await sleep(250);
|
||||
check(
|
||||
'+ Field ▸ Struct gives the bundle an empty struct',
|
||||
JSON.stringify((await piece()).field1) === '{}',
|
||||
JSON.stringify(await piece()),
|
||||
);
|
||||
// The nested `+ Field` is drawn after the head's, so it is the last one.
|
||||
const nestedAdd = await json(`__gm.locate('+ Field', true)`);
|
||||
const headAdd = await json(`__gm.locate('+ Field')`);
|
||||
check(
|
||||
'and that struct has its own + Field',
|
||||
nestedAdd.y > headAdd.y,
|
||||
JSON.stringify({ nestedAdd, headAdd }),
|
||||
);
|
||||
await click(nestedAdd.x, nestedAdd.y);
|
||||
await pickKind('Number');
|
||||
await sleep(250);
|
||||
check(
|
||||
'which adds a field one level down, in the value',
|
||||
JSON.stringify(await piece()) === '{"column":30,"tag":"tee","field1":{"field1":0}}',
|
||||
JSON.stringify(await piece()),
|
||||
);
|
||||
check(
|
||||
'and in the declaration',
|
||||
JSON.stringify(await declaredLast()) ===
|
||||
'{"key":"field1","kind":"struct","fields":[{"key":"field1","kind":"number"}]}',
|
||||
JSON.stringify(await declaredLast()),
|
||||
);
|
||||
const outerName = await json(`__gm.locate('field1')`);
|
||||
const innerName = await json(`__gm.locate('field1', true)`);
|
||||
check(
|
||||
'its field sits below it, one indent step further in',
|
||||
innerName.y > outerName.y && innerName.x > outerName.x,
|
||||
JSON.stringify({ outerName, innerName }),
|
||||
);
|
||||
await shot('03b-nested-struct');
|
||||
await undo();
|
||||
await undo();
|
||||
check('undo takes both back out', !('field1' in (await piece())), JSON.stringify(await piece()));
|
||||
|
||||
// 22g. The same bundle on a placed object. An override is sparse – it starts out unset – while a
|
||||
// field edit rebuilds the whole bundle, so the untouched fields have to come from what the row
|
||||
// shows (the asset's values) rather than from the empty form of the declaration.
|
||||
|
||||
Reference in new issue
Block a user