State machines: asset, tab, State branch and Set state nodes (#51)

A state machine is an asset whose graph holds one `State` node per state
(no ports, name as title), edited in a new "State machine" tab that reuses
the graph canvas: right-click places "State n", snap toggle shared with the
Behavior tab via SnapToggle. `statemachine` is a value kind like texture
(port, literal node, variable, struct field, tagged list item).

Behaviors use it through `State branch` (one exec output per state) and
`Set state` (one exec input per state; the interpreter now hands `run` the
entry port). States live per behavior instance in NodeContext.states, with
no initial state. Ports derive from the inline picker or a wired
`State machine` literal; BehaviorLookup became GraphLookup with
behavior(id) and machine(id).

The Graph tab is labelled "Behavior" (key unchanged). Unit tests
(state-machine, kinds, catalog, ports), e2e step 26 and README added.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Fable 5.1 committed 2026-09-12 18:00:23 +02:00
1 parent ce42d16657
commit e7d82bd1a9
60 files changed
+1269 -213

No files matched your search

+6 -2
View File
@@ -9,6 +9,7 @@ import {
isValidPortName,
fixedPortKeys,
nodeDef,
STATE,
} from '../graph';
import type { Asset, TextureFrame } from '../model/Asset';
import { isAssetType } from '../model/AssetTypes';
@@ -89,6 +90,8 @@ export function deserializeProject(raw: unknown, fallbackId = ''): Project {
variables: variables.get(id) ?? [],
graph: normalizeGraph(a.graph),
});
} else if (type === 'statemachine') {
assets.push({ id, type, name, props, graph: normalizeGraph(a.graph, STATE) });
} else if (type === 'texture') {
assets.push({ id, type, name, props, ...normalizeTexture(a, takenIds) });
} else if (type === 'animation') {
@@ -242,7 +245,8 @@ function normalizePorts(raw: unknown, fixed: readonly string[], writable: boolea
return ports;
}
function normalizeGraph(raw: unknown): Graph {
/** `only` restricts the node types kept: a state machine's graph holds states and nothing else. */
function normalizeGraph(raw: unknown, only?: string): Graph {
const g = asRecord(raw);
const graph = createEmptyGraph();
const nodeIds = new Set<string>();
@@ -251,7 +255,7 @@ function normalizeGraph(raw: unknown): Graph {
const id = typeof n.id === 'string' ? n.id : '';
const type = typeof n.type === 'string' ? n.type : '';
const def = nodeDef(type);
if (!id || nodeIds.has(id) || !def) continue;
if (!id || nodeIds.has(id) || !def || (only !== undefined && type !== only)) continue;
nodeIds.add(id);
// Params carry both settings and inline input values, so there is no schema to coerce against.
const params: PropertyBag = {};