import type { PropertyBag, PropertyValue } from '../model/Properties'; import type { PropertyDef } from '../properties/PropertySchema'; import type { DeclaredPort, Graph, GraphNode, PortType } from './Graph'; import type { BehaviorLookup } from './lookup'; import type { NodeContext } from './NodeContext'; import { asBoolean, asList, 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; /** The property this port carries; its typed-in value is edited as that property (color, enum, …). */ field?: PropertyDef; } 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: itself, the graph it sits in, and the behaviors around it. */ export interface PortSource { node: GraphNode; graph: Graph; 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. `both`: every port is an output, and an * input as well where it is marked `write`. */ declaresPorts?: 'inputs' | 'outputs' | 'both'; /** How a declared port becomes a port; without it, named after its key and typed by its kind. */ declaredPort?(port: DeclaredPort, node: GraphNode): PortDef; /** * 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; /** * Outputs readable at any moment, even while the node's exec input has not fired in this pulse. * Never cached, so a read after a write sees the new value. `input` pulls one input on demand: * pulling all of them would loop through an edge that feeds the node's own output back in. */ read?(ctx: NodeContext, params: PropertyBag, input: (key: string) => Value): Record; /** * Event nodes have no `run` – the interpreter seeds their outputs. `def` is resolved for the node at * hand, the only way a node with derived ports knows how many it has. */ run?(ctx: NodeContext, inputs: Record, params: PropertyBag, def: NodeDef): 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 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(isInlinePort); } function isInlinePort(port: PortDef): boolean { return port.type !== 'exec' && port.type !== 'object' && port.type !== 'struct'; } /** Untyped, an input writes the zero of its kind; as its default the node body shows it, not just the name. */ export function withInlineDefault(port: PortDef): PortDef { return isInlinePort(port) ? { ...port, default: inlineInputDef(port).default } : port; } export function inlineInputDef(port: PortDef): PropertyDef { if (port.field) return { ...port.field, key: port.key, label: port.label }; 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 ?? '') }; case 'list': return { key: port.key, label: port.label, kind: 'list', default: asList(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; }