/** * Split outputs: a struct output shown as one output per field, so reading a field needs no `Expand` * of its own. Only the drawing and the wiring see the field ports – `resolveDef` puts them in place of * the whole. The node itself still produces the whole value, and the interpreter picks the field out * of it when a field port is read (`fieldOfSplit`). */ import type { GraphNode, PortSplit } from './Graph'; import type { NodeDef, PortDef } from './NodeRegistry'; import { asStruct, unplaced, type Value } from './values'; /** Declared keys are identifiers, so the dot never clashes with a port of the node's own. */ export function splitPortKey(port: string, field: string): string { return `${port}.${field}`; } export function splitOf(node: GraphNode, port: string): PortSplit | undefined { return node.splits?.find((s) => s.port === port); } /** Which split output a key names, and which field of it – undefined for any other key. */ export function splitField(node: GraphNode, key: string): { split: PortSplit; field: string } | undefined { const dot = key.indexOf('.'); if (dot < 0) return undefined; const split = splitOf(node, key.slice(0, dot)); const field = key.slice(dot + 1); return split?.fields.some((f) => f.key === field) ? { split, field } : undefined; } /** Whether an output can be taken apart: a value that may hold a struct, and not already split. */ export function canSplit(node: GraphNode, port: PortDef): boolean { return (port.type === 'struct' || port.type === 'any') && !port.ghost && !splitOf(node, port.key); } /** The outputs as drawn and wired: each split one replaced, where it stood, by its field ports. */ export function withSplits( def: NodeDef, node: GraphNode, portOf: (key: string, kind: PortSplit['fields'][number]) => PortDef, ): NodeDef { if (!node.splits?.length) return def; const outputs = def.outputs.flatMap((port) => { const split = port.type === 'exec' ? undefined : splitOf(node, port.key); return split ? split.fields.map((f) => portOf(splitPortKey(port.key, f.key), f)) : [port]; }); return { ...def, outputs }; } /** One field of a whole output, read the way `Expand` reads it: a field the value lacks is empty. */ export function fieldValue(whole: Value | undefined, field: string): Value { return unplaced(asStruct(whole)[field]) ?? null; } /** What a node produced, with the value of every field port of its splits beside the wholes. */ export function withSplitValues(node: GraphNode, values: Record): Record { if (!node.splits?.length) return values; const out = { ...values }; for (const split of node.splits) if (split.port in values) for (const f of split.fields) out[splitPortKey(split.port, f.key)] = fieldValue(values[split.port], f.key); return out; }