import type { PropertyBag, PropertyValue } from '../model/Properties'; import type { PropertyDef } from '../properties/PropertySchema'; import type { PortType } from './Graph'; import type { BehaviorLookup } from './lookup'; import type { NodeContext } from './NodeContext'; import { asBoolean, asNumber, asString, type Value } from './values'; export interface PortDef { key: string; label: string; type: PortType; /** Inline value used while the input is unconnected and the node has no own value (default 0/''/false). */ default?: PropertyValue; } export type NodeCategory = 'event' | 'flow' | 'data' | 'variable' | 'action'; /** * How a loop node asks for one of its exec outputs to be run repeatedly: `port` carries the body, * `index` is the number output seeded with the round number. */ export interface RepeatResult { port: string; index: string; times: number; } /** How a node asks for another behavior's graph to be run in place, `inputs` seeding its start node. */ export interface CallResult { behaviorId: string; inputs: Record; } export interface RunResult { /** Cached for the rest of the pulse. */ outputs?: Record; /** Exec output(s) to continue along; omitted = the node ends the flow. */ next?: string | string[]; /** Runs `port` that many times before continuing along `next`. */ repeat?: RepeatResult; /** Runs another behavior before continuing along `next`; its results join `outputs`. */ call?: CallResult; } /** What a node may compute its ports from: its own settings, and the behaviors around it. */ export interface PortSource { params: PropertyBag; behaviors: BehaviorLookup; } /** Ports a single node has on top of the fixed ones of its type, and a label naming what it does. */ export interface DerivedShape { label?: string; inputs?: PortDef[]; outputs?: PortDef[]; } /** Palette, port rendering, connection validation and inspector fields are all derived from this. */ export interface NodeDef { type: string; label: string; category: NodeCategory; description: string; inputs: PortDef[]; outputs: PortDef[]; /** Node settings that are not ports (key name, operator, variable). */ params: PropertyDef[]; /** * Settings that say nothing at this value set, so the node body leaves them off; the inspector * still offers them. Without it a switched-off repeat is two rows of "0" on every key node. */ hiddenParams?(params: PropertyBag): string[]; /** * Which side the ports this node declares for itself (`GraphNode.ports`) appear on. Setting it * is what gives a node the port editor in the inspector. */ declaresPorts?: 'inputs' | 'outputs'; /** * Ports this node computes beyond the fixed and the declared ones – how many outputs a Sequence * offers, which values the behavior a call node names takes. `resolveDef` applies it. */ derivePorts?(source: PortSource): DerivedShape; /** * Pure data nodes are evaluated on demand when a downstream input needs their value; exec nodes * run when the flow reaches them. Event nodes have no `run` – the interpreter seeds their outputs. */ run?(ctx: NodeContext, inputs: Record, params: PropertyBag): RunResult; } const defs = new Map(); export function registerNode(def: NodeDef): NodeDef { if (defs.has(def.type)) throw new Error(`Node type "${def.type}" registered twice`); defs.set(def.type, def); return def; } export function nodeDef(type: string): NodeDef | undefined { return defs.get(type); } export function requireNodeDef(type: string): NodeDef { const def = defs.get(type); if (!def) throw new Error(`Unknown node type "${type}"`); return def; } export function allNodeDefs(): NodeDef[] { return [...defs.values()]; } export function isEventNode(def: NodeDef): boolean { return def.category === 'event'; } export function hasExecInput(def: NodeDef): boolean { return def.inputs.some((p) => p.type === 'exec'); } /** * Data inputs that may carry an inline value while unconnected. An object is a live handle and a * struct is assembled from its parts, so neither has a value that could be typed on one line. */ export function inlineInputs(def: NodeDef): PortDef[] { return def.inputs.filter((p) => p.type !== 'exec' && p.type !== 'object' && p.type !== 'struct'); } export function inlineInputDef(port: PortDef): PropertyDef { switch (port.type) { case 'boolean': return { key: port.key, label: port.label, kind: 'boolean', default: asBoolean(port.default ?? false) }; case 'string': case 'any': return { key: port.key, label: port.label, kind: 'text', default: asString(port.default ?? '') }; default: return { key: port.key, label: port.label, kind: 'number', default: asNumber(port.default ?? 0), step: 1, }; } } /** The value an unconnected input carries: the node's own inline value, else the port default. */ export function inlineValue(node: { params: PropertyBag }, port: PortDef): Value { return node.params[port.key] ?? port.default ?? null; }