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
@@ -15,7 +15,7 @@ import {
|
||||
terminalNode,
|
||||
} from '../../src/core/graph';
|
||||
import type { BehaviorAsset } from '../../src/core/model/Asset';
|
||||
import { behaviorLookup } from '../../src/core/model/behaviorPrograms';
|
||||
import { graphLookup } from '../../src/core/model/graphLookup';
|
||||
import { deserializeProject, serializeProject } from '../../src/core/store/serialize';
|
||||
import { fakeContext, graphEditor, makeBehavior, makeProject, type ProjectFixture } from '../helpers';
|
||||
|
||||
@@ -55,7 +55,7 @@ function callerOf(fixture: ProjectFixture, callee: BehaviorAsset, input: number)
|
||||
}
|
||||
|
||||
function runOnStart(fixture: ProjectFixture, asset: BehaviorAsset, variables: Record<string, number> = {}) {
|
||||
const interpreter = new GraphInterpreter(asset.graph, behaviorLookup(fixture.project));
|
||||
const interpreter = new GraphInterpreter(asset.graph, graphLookup(fixture.project));
|
||||
const ctx = fakeContext({ variables });
|
||||
interpreter.fire('OnStart', ctx);
|
||||
return { ctx, interpreter };
|
||||
@@ -73,7 +73,7 @@ describe('a behavior used as a node', () => {
|
||||
const callee = doubler(fixture);
|
||||
const caller = callerOf(fixture, callee, 1);
|
||||
const call = caller.graph.nodes.find((n) => n.type === RUN_BEHAVIOR)!;
|
||||
const def = resolveDef(call, caller.graph, behaviorLookup(fixture.project))!;
|
||||
const def = resolveDef(call, caller.graph, graphLookup(fixture.project))!;
|
||||
expect(def.inputs.map((p) => p.key)).toEqual(['exec', 'in1']);
|
||||
expect(def.outputs.map((p) => p.key)).toEqual(['exec', 'out1']);
|
||||
// Named after the behavior it runs: a graph of calls is unreadable as a row of "Run behavior".
|
||||
@@ -185,7 +185,7 @@ describe('a behavior used as a node', () => {
|
||||
describe('a call that cannot do anything says so', () => {
|
||||
it('names each of the four silent cases, and stays quiet about a healthy call', () => {
|
||||
const fixture = makeProject();
|
||||
const lookup = () => behaviorLookup(fixture.project);
|
||||
const lookup = () => graphLookup(fixture.project);
|
||||
const callee = doubler(fixture);
|
||||
const caller = makeBehavior(fixture, 'Caller');
|
||||
const edit = graphEditor(fixture, caller);
|
||||
@@ -214,11 +214,11 @@ describe('a call that cannot do anything says so', () => {
|
||||
const start = graphEditor(fixture, callee).addNode(BEHAVIOR_START);
|
||||
const caller = makeBehavior(fixture, 'Caller');
|
||||
const call = graphEditor(fixture, caller).addNode(RUN_BEHAVIOR, { [BEHAVIOR_PARAM]: callee.id });
|
||||
const signature = () => portSignature(call, caller.graph, behaviorLookup(fixture.project), caller.id);
|
||||
const signature = () => portSignature(call, caller.graph, graphLookup(fixture.project), caller.id);
|
||||
|
||||
const healthy = signature();
|
||||
fixture.stack.execute(new RemoveNodeCommand(callee.id, start.id));
|
||||
expect(resolveDef(call, caller.graph, behaviorLookup(fixture.project))!.label).toBe('Bare');
|
||||
expect(resolveDef(call, caller.graph, graphLookup(fixture.project))!.label).toBe('Bare');
|
||||
expect(signature()).not.toBe(healthy);
|
||||
});
|
||||
|
||||
@@ -239,7 +239,7 @@ describe('a call that cannot do anything says so', () => {
|
||||
]);
|
||||
|
||||
// A missing end node is not a defect, so nothing is reported.
|
||||
expect(callProblem(call, behaviorLookup(fixture.project), caller.id)).toBe(null);
|
||||
expect(callProblem(call, graphLookup(fixture.project), caller.id)).toBe(null);
|
||||
// "Done" is the nested flow running out: the sub graph is finished before the caller goes on.
|
||||
expect(runOnStart(fixture, caller).ctx.logs).toEqual(['sub', 'after']);
|
||||
});
|
||||
@@ -293,7 +293,7 @@ describe('declared ports', () => {
|
||||
const reloaded = loaded.assets.find((a) => a.id === callee.id) as BehaviorAsset;
|
||||
expect(terminalNode(reloaded.graph, BEHAVIOR_START)?.ports).toEqual([{ key: 'in1', kind: 'number' }]);
|
||||
const caller = loaded.assets.find((a) => a.name === 'Caller') as BehaviorAsset;
|
||||
const interpreter = new GraphInterpreter(caller.graph, behaviorLookup(loaded));
|
||||
const interpreter = new GraphInterpreter(caller.graph, graphLookup(loaded));
|
||||
const ctx = fakeContext({ variables: { result: 0 } });
|
||||
interpreter.fire('OnStart', ctx);
|
||||
expect(ctx.variables.result).toBe(12);
|
||||
|
||||
Reference in new issue
Block a user