Let Spawn take an asset over an edge and find a child by name (#237)

An asset could only be picked as a setting on the Spawn node. It could
not be wired in, held in a variable or put into a list. Finding an
object below the own one needed a scene-wide search by name.

- "Asset" is now a kind like Animation or Sound: port, variable, list
  item and a new "Asset" literal node, with its own cube icon.
- Spawn and Is asset have an `asset` input. It uses the key of the old
  setting, so a saved asset reads as the value of the input and older
  projects keep spawning without a migration.
- New node "Child by name" searches below an object level by level.
  A child wins over a grandchild of the same name. Nothing found or no
  object gives null.

Tests:
- ./check.sh: 168 files, 2453 tests, CHECK EXIT 0.
- 9 new unit tests in tests/core/asset-kind.test.ts, catalog cases for
  Spawn, Is asset, Child by name and the literal.
- e2e 6z-ii (2 checks): node rows and a real drag of the literal onto
  `asset`. e2e 6: 119 checks passed. Spawn checks in 3 and 9 passed.
- Counter-checks: depth-first search fails 1, Spawn reading only the
  old setting fails 3, runtime ignoring the parent fails 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-09-26 19:36:49 +02:00
1 parent 1b3cb625a3
commit 923baf2557
18 files changed
+345 -24

No files matched your search

+45
View File
@@ -1053,6 +1053,51 @@ export async function graph(t) {
await mutate(`
const p = __gm.state.project;
p.assets = p.assets.filter((a) => a.id !== 'picky');
`);
// 6z-ii. An asset is a value (#237): Spawn keeps a pick saved as its old setting as the inline
// value of its new `asset` input, an Asset literal plugs into that input, and Child by
// name shows its two inputs.
await mutate(`
const p = __gm.state.project;
p.assets.push({
id: 'spawny',
type: 'behavior',
name: 'Spawny',
props: { enabled: true, runInEditor: false },
variables: [],
graph: {
nodes: [
{ id: 'sp-spawn', type: 'Spawn', x: 300, y: 0, params: { asset: 'crate' } },
{ id: 'sp-lit', type: 'Asset', x: 0, y: 0, params: { value: 'crate' } },
{ id: 'sp-child', type: 'ChildByName', x: 300, y: 300, params: {} },
],
edges: [],
},
});
__gm.state.select({ kind: 'asset', assetId: 'spawny' });
`);
await showTab('graph');
const spawnRows = await json(`__gm.graph.nodeInputs('sp-spawn')`);
const childRows = await json(`__gm.graph.nodeInputs('sp-child')`);
check(
'Spawn shows the asset saved before as its input; Child by name shows object and name',
JSON.stringify(spawnRows) ===
JSON.stringify(['', 'asset Crate', 'x 0', 'y 0', 'z 0', 'parent (null)']) &&
JSON.stringify(childRows) === JSON.stringify(['object (null)', 'name (empty)']),
JSON.stringify({ spawnRows, childRows }),
);
await wire('sp-lit', 'value', 'sp-spawn', 'asset');
const assetEdge = (await asset('spawny')).graph.edges.find((e) => e.to.node === 'sp-spawn');
check(
'an Asset literal connects to the asset input of Spawn',
assetEdge?.from.node === 'sp-lit' && assetEdge.to.port === 'asset',
JSON.stringify(assetEdge ?? null),
);
await shot('21-asset-input');
await mutate(`
const p = __gm.state.project;
p.assets = p.assets.filter((a) => a.id !== 'spawny');
__gm.state.select({ kind: 'asset', assetId: 'mover' });
`);