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:
1 parent
ce42d16657
commit
e7d82bd1a9
60 files changed
+1269
-213
No files matched your search
@@ -0,0 +1,73 @@
|
||||
import { textOf } from '../model/Properties';
|
||||
import { flatDef } from '../properties/PropertySchema';
|
||||
import { edgesInto, type Graph, type GraphNode } from './Graph';
|
||||
import type { MachineState, StateMachine } from './lookup';
|
||||
import type { NodeDef, PortDef, PortSource } from './NodeRegistry';
|
||||
|
||||
export const STATE = 'State';
|
||||
export const STATE_MACHINE = 'StateMachine';
|
||||
export const STATE_NAME_PARAM = 'name';
|
||||
/** The input naming the machine on `State branch` and `Set state`. */
|
||||
export const MACHINE_PORT = 'machine';
|
||||
|
||||
/**
|
||||
* One state of a state machine: a node with no ports whose name is its title. Its graph is the
|
||||
* machine asset's, never a behavior's – the palette leaves the `state` category out.
|
||||
*/
|
||||
export const State: NodeDef = {
|
||||
type: STATE,
|
||||
label: 'State',
|
||||
category: 'state',
|
||||
description: 'One state of a state machine.',
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
params: [flatDef(STATE_NAME_PARAM, 'text', { label: 'Name', value: 'State' })],
|
||||
derivePorts: ({ node }) => ({ label: stateName(node) }),
|
||||
};
|
||||
|
||||
export function isStateNode(node: GraphNode): boolean {
|
||||
return node.type === STATE;
|
||||
}
|
||||
|
||||
export function stateName(node: GraphNode): string {
|
||||
return textOf(node.params[STATE_NAME_PARAM] ?? '') || State.label;
|
||||
}
|
||||
|
||||
/** The states of a machine's graph in node order – what its ports and the runtime name. */
|
||||
export function statesOf(graph: Graph): MachineState[] {
|
||||
return graph.nodes.filter(isStateNode).map((node) => ({ id: node.id, name: stateName(node) }));
|
||||
}
|
||||
|
||||
/** What a state placed from the palette is called: "State 3" for the third. */
|
||||
export function nextStateName(graph: Graph): string {
|
||||
return `${State.label} ${statesOf(graph).length + 1}`;
|
||||
}
|
||||
|
||||
/** The `machine` input; unconnected, it shows the machine picker. */
|
||||
export const machinePort = (): PortDef => ({
|
||||
key: MACHINE_PORT,
|
||||
label: 'machine',
|
||||
type: 'string',
|
||||
field: flatDef(MACHINE_PORT, 'statemachine', { label: 'Machine' }),
|
||||
});
|
||||
|
||||
/**
|
||||
* The machine a node's `machine` input names while the graph is drawn: the inline pick, or the
|
||||
* `State machine` literal an edge brings in. A port is derived from the graph, not from a running
|
||||
* value, so a variable or a list item feeding the input yields no state ports.
|
||||
*/
|
||||
export function machineOf({ node, graph, lookup }: PortSource): StateMachine | null {
|
||||
const edge = edgesInto(graph, node.id, MACHINE_PORT)[0];
|
||||
const source = edge && graph.nodes.find((n) => n.id === edge.from.node);
|
||||
const id = edge
|
||||
? source?.type === STATE_MACHINE
|
||||
? textOf(source.params.value ?? '')
|
||||
: ''
|
||||
: textOf(node.params[MACHINE_PORT] ?? '');
|
||||
return id ? lookup.machine(id) : null;
|
||||
}
|
||||
|
||||
/** One exec port per state, keyed by the state's id – a rename keeps every edge. */
|
||||
export function statePorts(machine: StateMachine | null): PortDef[] {
|
||||
return (machine?.states ?? []).map((state) => ({ key: state.id, label: state.name, type: 'exec' }));
|
||||
}
|
||||
Reference in new issue
Block a user