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
@@ -2,7 +2,7 @@ import { textOf } from '../model/Properties';
|
||||
import { isTaggedKind } from '../properties/kinds';
|
||||
import { flatDef, isValidFieldName } from '../properties/PropertySchema';
|
||||
import { type DeclaredPort, declaredPortType, type Graph, type GraphNode } from './Graph';
|
||||
import type { BehaviorLookup } from './lookup';
|
||||
import type { GraphLookup } from './lookup';
|
||||
import { nodeDef, type NodeDef, type PortDef } from './NodeRegistry';
|
||||
|
||||
export const BEHAVIOR_START = 'BehaviorStart';
|
||||
@@ -92,14 +92,14 @@ export function declaredPorts(graph: Graph | undefined, type: string): PortDef[]
|
||||
* The node definition as it applies to *this* node: identical to the registered one, unless the
|
||||
* type declares ports per node (`declaresPorts`) or computes them (`derivePorts`).
|
||||
*/
|
||||
export function resolveDef(node: GraphNode, graph: Graph, lookup: BehaviorLookup): NodeDef | undefined {
|
||||
export function resolveDef(node: GraphNode, graph: Graph, lookup: GraphLookup): NodeDef | undefined {
|
||||
const def = nodeDef(node.type);
|
||||
if (!def || (!def.declaresPorts && !def.derivePorts)) return def;
|
||||
const own = (side: 'inputs' | 'outputs') =>
|
||||
declaredOn(def, node.ports ?? [], side).map((p) =>
|
||||
def.declaredPort ? def.declaredPort(p, node) : portDefOf(p),
|
||||
);
|
||||
const grown = def.derivePorts?.({ node, graph, behaviors: lookup }) ?? {};
|
||||
const grown = def.derivePorts?.({ node, graph, lookup }) ?? {};
|
||||
const side = (key: 'inputs' | 'outputs') => execFirst([...def[key], ...(grown[key] ?? []), ...own(key)]);
|
||||
return { ...def, label: grown.label ?? def.label, inputs: side('inputs'), outputs: side('outputs') };
|
||||
}
|
||||
@@ -113,12 +113,12 @@ function execFirst(ports: PortDef[]): PortDef[] {
|
||||
* Why this call cannot do anything, or null if it can. A call is the one node that depends on a
|
||||
* *different* graph, so it is the one node that can look perfectly healthy and still be dead.
|
||||
*/
|
||||
export function callProblem(node: GraphNode, lookup: BehaviorLookup, ownerId = ''): string | null {
|
||||
export function callProblem(node: GraphNode, lookup: GraphLookup, ownerId = ''): string | null {
|
||||
if (node.type !== RUN_BEHAVIOR) return null;
|
||||
const id = textOf(node.params[BEHAVIOR_PARAM] ?? '');
|
||||
if (!id) return 'No behavior picked';
|
||||
if (id === ownerId) return 'A behavior cannot run itself';
|
||||
const program = lookup(id);
|
||||
const program = lookup.behavior(id);
|
||||
if (!program) return 'That behavior is gone';
|
||||
if (!terminalNode(program.graph, BEHAVIOR_START)) return 'No start node – does nothing';
|
||||
return null;
|
||||
@@ -128,7 +128,7 @@ export function callProblem(node: GraphNode, lookup: BehaviorLookup, ownerId = '
|
||||
* Everything that changes how a node looks, as one string. Views and the inspector rebuild when it
|
||||
* changes – a port list that is derived rather than fixed has no other way of being noticed.
|
||||
*/
|
||||
export function portSignature(node: GraphNode, graph: Graph, lookup: BehaviorLookup, ownerId = ''): string {
|
||||
export function portSignature(node: GraphNode, graph: Graph, lookup: GraphLookup, ownerId = ''): string {
|
||||
const def = resolveDef(node, graph, lookup);
|
||||
if (!def) return 'missing';
|
||||
const list = (ports: PortDef[]) => ports.map((p) => `${p.key}:${p.type}`).join(',');
|
||||
|
||||
Reference in new issue
Block a user