import type { PropertyBag } from '../model/Properties'; /** `exec` carries control flow, the others values; `any` accepts every value type. */ export type PortType = 'exec' | 'number' | 'boolean' | 'string' | 'object' | 'list' | 'struct' | 'any'; export interface PortRef { node: string; port: string; } /** * `type` names a NodeDef in the registry; `params` holds both the node's own settings and the * inline values of unconnected data inputs, keyed by param or port key. */ export interface GraphNode { id: string; type: string; x: number; y: number; params: PropertyBag; /** * Ports the node declares itself, on top of the fixed ones of its type; the types that have * them say so with `NodeDef.declaresPorts`. A behavior's start/end node is what gives the * behavior its signature; an Expand/Pack node is what gives a struct its fields. */ ports?: DeclaredPort[]; /** Drawn as its title bar alone, every link meeting at one point per side. Purely visual. */ collapsed?: boolean; } /** A port a start/end node declares. `kind` maps to the PortType of the same name (`text` → string). */ export interface DeclaredPort { key: string; kind: DeclaredPortKind; /** On a node declaring `both` sides: the port is an input too, so the node writes it. */ write?: true; } /** A color travels as a number and a texture as text; the kind says which field edits the value. */ export const DECLARED_PORT_KINDS = [ 'number', 'boolean', 'text', 'color', 'texture', 'object', 'list', 'struct', ] as const; export type DeclaredPortKind = (typeof DECLARED_PORT_KINDS)[number]; export function declaredPortType(kind: DeclaredPortKind): PortType { if (kind === 'text' || kind === 'texture') return 'string'; return kind === 'color' ? 'number' : kind; } export interface GraphEdge { id: string; from: PortRef; to: PortRef; /** Drawn as a stub at each end instead of a curve across the graph. Purely visual. */ folded?: boolean; } export interface Graph { nodes: GraphNode[]; edges: GraphEdge[]; } export function createEmptyGraph(): Graph { return { nodes: [], edges: [] }; } export function findNode(graph: Graph, nodeId: string): GraphNode | undefined { return graph.nodes.find((n) => n.id === nodeId); } /** Edges arriving at one input port (data inputs allow at most one, exec inputs any number). */ export function edgesInto(graph: Graph, nodeId: string, port: string): GraphEdge[] { return graph.edges.filter((e) => e.to.node === nodeId && e.to.port === port); } export function edgesOutOf(graph: Graph, nodeId: string, port: string): GraphEdge[] { return graph.edges.filter((e) => e.from.node === nodeId && e.from.port === port); } export function edgesOfNode(graph: Graph, nodeId: string): GraphEdge[] { return graph.edges.filter((e) => e.from.node === nodeId || e.to.node === nodeId); } export function portsCompatible(from: PortType, to: PortType): boolean { if (from === 'exec' || to === 'exec') return from === to; return from === to || from === 'any' || to === 'any'; }