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>
43 lines
2.1 KiB
TypeScript
43 lines
2.1 KiB
TypeScript
import { type DeclaredPortKind, isKindWith, KIND, plainKindOf } from '../properties/kinds';
|
||
import { itemKindOf, structItemFields } from '../properties/listItems';
|
||
import type { PropertyDef, StructFieldDef } from '../properties/PropertySchema';
|
||
import { isStruct, type PropertyValue } from '../model/Properties';
|
||
import type { DeclaredPort, GraphNode } from './Graph';
|
||
|
||
/** The port a variable or property travels on. A kind no port declares (an enum, a key) is what it travels as. */
|
||
export function portKindOf(def: PropertyDef): DeclaredPortKind {
|
||
if (isKindWith('port', def.kind)) return def.kind;
|
||
const plain = plainKindOf(KIND[def.kind].travels);
|
||
return plain !== undefined && isKindWith('port', plain) ? plain : 'text';
|
||
}
|
||
|
||
/**
|
||
* The bundle shape of a variable, if it has one: a struct variable declares it, a list takes it from
|
||
* the first struct item its default holds – items declare nothing, so the value is the only source.
|
||
* Both are what a Pack builds and an Expand takes apart, so both can seed a node's fields.
|
||
*/
|
||
export function structShapeOf(def: PropertyDef): readonly StructFieldDef[] | undefined {
|
||
if (def.kind === 'struct') return def.fields;
|
||
if (def.kind !== 'list') return undefined;
|
||
const item = def.default.find((entry) => itemKindOf(entry) === 'struct');
|
||
return item === undefined ? undefined : itemShape(item);
|
||
}
|
||
|
||
/** The fields a struct item carries as a declaration would name them, nested ones included. */
|
||
function itemShape(item: PropertyValue | undefined): StructFieldDef[] {
|
||
return structItemFields(item).map(({ key, kind }) =>
|
||
kind === 'struct'
|
||
? { key, kind, fields: itemShape(isStruct(item) ? item[key] : undefined) }
|
||
: { key, kind },
|
||
);
|
||
}
|
||
|
||
/**
|
||
* The ports `fields` would add to `node`. A key the node already declares is left alone rather than
|
||
* retyped: it may carry an edge, and the manual declaration is the one the user typed.
|
||
*/
|
||
export function missingFieldPorts(node: GraphNode, fields: readonly StructFieldDef[]): DeclaredPort[] {
|
||
const taken = new Set((node.ports ?? []).map((p) => p.key));
|
||
return fields.filter((f) => !taken.has(f.key)).map((f) => ({ key: f.key, kind: f.kind }));
|
||
}
|