import { callProblem, declaresPropertyPorts, declaresVariablePorts, edgesInto, type GraphNode, inlineInputDef, inlineInputs, portSignature, resolveDef, } from '../../core/graph'; import type { Asset } from '../../core/model/Asset'; import { behaviorLookup } from '../../core/model/behaviorPrograms'; import { findAssetOf, type Project } from '../../core/model/Project'; import type { PropertyDef } from '../../core/properties/PropertySchema'; import { Label } from '../../ui'; import { createFieldRow, type FieldHandlers } from './fieldFactory'; import { addNote, addTitle, type InspectorHost } from './host'; import { buildPortSection, buildPropertyPortSection, buildVariablePortSection } from './portSection'; import { listOpener, previewingHandlers } from './previewHandlers'; import { currentNode, nodeParamValue } from './valueSpecs'; /** Structure-defining facts of a node selection: ports, connected inputs, declared variables. */ export function nodeSignature(project: Project, assetId: string, nodeId: string): string { const asset = findAssetOf(project, assetId, 'behavior'); if (!asset) return 'missing'; const node = asset.graph.nodes.find((n) => n.id === nodeId); if (!node) return 'missing'; const lookup = behaviorLookup(project); const def = resolveDef(node, asset.graph, lookup); const connected = def ? inlineInputs(def) .filter((p) => edgesInto(asset.graph, node.id, p.key).length > 0) .map((p) => p.key) : []; const declared = (node.ports ?? []).map((p) => `${p.key}:${p.kind}`).join(','); return `${portSignature(node, asset.graph, lookup, assetId)}|${declared}|${connected.join(',')}|${asset.variables .map((v) => v.key) .join(',')}`; } /** Description, settings (params) and the inline values of unconnected data inputs. */ export function buildNodeSection(host: InspectorHost, assetId: string, nodeId: string): void { const asset = findAssetOf(host.state.project, assetId, 'behavior'); if (!asset) return; const node = asset.graph.nodes.find((n) => n.id === nodeId); const def = node && resolveDef(node, asset.graph, behaviorLookup(host.state.project)); if (!node || !def) return; addTitle(host, `${def.category.toUpperCase()} NODE`); host.body.add(new Label(def.label, { style: 'bodyBold' })); addNote(host, def.description); const problem = callProblem(node, behaviorLookup(host.state.project), assetId); if (problem) addNote(host, problem, 'warning'); if (declaresVariablePorts(node.type)) buildVariablePortSection(host, assetId, node, asset.variables); else if (declaresPropertyPorts(node.type)) buildPropertyPortSection(host, assetId, node); else if (def.declaresPorts) buildPortSection(host, assetId, node, asset.variables); if (def.params.length > 0) addTitle(host, 'SETTINGS'); for (const param of def.params) { const effective = resolveParamDef(param, asset); const row = createFieldRow( host, effective, node.params[param.key] ?? effective.default, paramHandlers(host, assetId, nodeId, effective), ); host.fields.set(`param:${param.key}`, row); host.body.add(row.row); } const free = inlineInputs(def).filter((p) => edgesInto(asset.graph, node.id, p.key).length === 0); if (free.length > 0) addTitle(host, 'INPUTS (unconnected)'); for (const port of free) { const inline = inlineInputDef(port); const row = createFieldRow( host, inline, node.params[port.key] ?? inline.default, paramHandlers(host, assetId, nodeId, inline), ); host.fields.set(`input:${port.key}`, row); host.body.add(row.row); } } /** Pushes the node's current params into the rows. False when the node is gone (rebuild). */ export function refreshNodeSection(host: InspectorHost, assetId: string, nodeId: string): boolean { const found = currentNode(host.state.project, assetId, nodeId); const def = found && resolveDef(found.node, found.graph, behaviorLookup(host.state.project)); if (!found || !def) return false; for (const [key, row] of host.fields) { const [scope, name] = key.split(':') as ['param' | 'input', string]; const port = def.inputs.find((p) => p.key === name); const fallback = scope === 'param' ? def.params.find((p) => p.key === name)?.default : port && inlineInputDef(port).default; row.setValue(found.node.params[name] ?? fallback ?? ''); } return true; } /** * Variable params are the one setting a node cannot declare on its own: their options are the * variables of the behavior it sits in. Everything else is used exactly as declared. */ function resolveParamDef(param: PropertyDef, asset: Asset): PropertyDef { if (param.kind !== 'enum' || param.key !== 'variable' || asset.type !== 'behavior') return param; return { ...param, options: asset.variables.map((v) => ({ value: v.key, label: v.key })) }; } /** Previews straight into the node's params; the command records the value from before the preview. */ function paramHandlers( host: InspectorHost, assetId: string, nodeId: string, def: PropertyDef, ): FieldHandlers { const key = def.key; return { ...previewingHandlers(host, nodeParamValue(host.state, assetId, nodeId, key)), ...listOpener(host.state, def, () => ({ kind: 'node', assetId, nodeId, key })), }; }