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>
A shader is an asset carrying a node graph (Texture color, Output, Property,
Color, Add/Subtract/Multiply/Divide, Mix, Luminance) that compiles to the GLSL
fragment of a Pixi filter. The Shader tab reuses the Behavior canvas; a sprite
wears a shader through its Shader picker, and the rows below it are the
graph's Property nodes. Property schema, resolution and deviation take the
project because a sprite's rows now live in another asset. The built-in
registry of the prototype is gone; "Adjust colors" ships as a graph in the
fixture and the three example games.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The instance inspector drew its own, flatter version of a struct: one row per
field, no bundle head. It now draws the same block as the asset side, because
there is only one implementation left - both call buildStructBlock. What the
instance does not get is everything that touches the shape: renaming a field,
'+ Field', and the per-field '×'. The shape belongs to the asset; a placed
object overrides values. The revert and override marks sit on the head rather
than on a field, since an override covers the whole bundle at once.
Underneath this, editing a single field of an unoverridden struct used to blank
its siblings: structFieldsOf filled the untouched fields from spec.read, which
is object.overrides[key] and therefore undefined until something is overridden.
structOf then handed back the empty shape. PreviewedValue carries 'current' now
- the resolved value the row actually shows - and the fields come from that.
Without it the old path stays, which is right for the asset side, whose prop
bag is dense.