import { textOf } from '../model/Properties'; import { flatDef } from '../properties/PropertySchema'; import { edgesInto, type Graph, type GraphNode } from './Graph'; import type { MachineState, MachineTransition, StateMachine } from './lookup'; import type { NodeDef, PortDef, PortSource } from './NodeRegistry'; export const STATE = 'State'; export const STATE_MACHINE = 'StateMachine'; export const STATE_TRANSITION = 'StateTransition'; export const STATE_NAME_PARAM = 'name'; /** The input naming the machine on `State branch` and `Set state`. */ export const MACHINE_PORT = 'machine'; /** The setting naming the transition on `State transition`. */ export const TRANSITION_PARAM = 'transition'; /** The two ports a transition hangs on. A port is the only anchor an edge has – without one it * cannot be drawn, selected or undone – so a state carries these, and the editor never draws * them: a transition joins whole states, not sockets. */ export const STATE_IN = 'in'; export const STATE_OUT = 'out'; /** * One state of a state machine: a node 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: [{ key: STATE_IN, label: '', type: 'exec' }], outputs: [{ key: STATE_OUT, label: '', type: 'exec' }], params: [flatDef(STATE_NAME_PARAM, 'text', { label: 'Name', value: 'State' })], transitionEdges: true, 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) })); } /** The transitions of a machine's graph: every edge between two states, in edge order. */ export function transitionsOf(graph: Graph): MachineTransition[] { const states = new Set(graph.nodes.filter(isStateNode).map((n) => n.id)); return graph.edges .filter((e) => states.has(e.from.node) && states.has(e.to.node)) .map((e) => ({ from: e.from.node, to: e.to.node, name: e.label ?? '' })); } /** * Where `State transition` moves from `current`: along the transition of that name leaving it. The * name is matched against the current state alone, so one node asking for "alarm" moves every state * that has an "alarm". Nothing happens when the state has no such transition, or two of the name – * with nothing drawn to pick between them, choosing either would be a guess the graph never made. */ export function transitionTo(machine: StateMachine, current: string, name: string): string { const out = machine.transitions.filter((t) => t.from === current && t.name === name); const targets = [...new Set(out.map((t) => t.to))]; return targets.length === 1 ? targets[0]! : ''; } /** The names a machine's transitions carry, deduped and in edge order – what a node may ask for. */ export function transitionNames(machine: StateMachine | null): string[] { return [...new Set((machine?.transitions ?? []).map((t) => t.name).filter((name) => name))]; } /** 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' })); }