import { callProblem, declaresPropertyPorts, declaresVariablePorts, edgesInto, edgesOfNode, findNode, type Graph, type GraphEdge, type GraphNode, inlineInputDef, inlineInputs, isStateNode, MACHINE_PORT, OBJECT_BEHAVIOR_PARAM, OBJECT_TYPE_PARAM, portSignature, resolveDef, STATE_TRANSITION, stateName, TRANSITION_PARAM, variablesSourceOf, } from '../../core/graph'; import type { GraphAsset } from '../../core/model/Asset'; import { graphLookup } from '../../core/model/graphLookup'; import { findGraphAsset, type Project } from '../../core/model/Project'; import { textOf } from '../../core/model/Properties'; 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, variableChoices, } from './portSection'; import { listSource, previewingHandlers } from './previewHandlers'; import { buildTransitionPicker, buildTransitionSection } from './transitionSection'; 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 = findGraphAsset(project, assetId); if (!asset) return 'missing'; const node = asset.graph.nodes.find((n) => n.id === nodeId); if (!node) return 'missing'; const lookup = graphLookup(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}${p.write ? ':set' : ''}`).join(','); // They decide which rows the inspector builds, and two behaviors may share a name in the title. const target = `${textOf(node.params[OBJECT_TYPE_PARAM] ?? '')}:${textOf(node.params[OBJECT_BEHAVIOR_PARAM] ?? '')}`; // The transition rows and the picked name are drawn, not filled in by `refreshNodeSection`, so // every part of them a command can change has to force a rebuild. const transitions = isStateNode(node) ? edgesOfNode(asset.graph, node.id) .map((e) => `${e.id}:${e.label ?? ''}:${farStateName(asset.graph, e, node.id)}`) .join(',') : `${textOf(node.params[TRANSITION_PARAM] ?? '')}:${textOf(node.params[MACHINE_PORT] ?? '')}`; // The source names which declarations the port menu offers, so switching it has to rebuild. const source = variablesSourceOf(node.params); const choices = variableChoices(project, assetId, node) .map((v) => v.key) .join(','); return `${portSignature(node, asset.graph, lookup, assetId)}|${declared}|${connected.join( ',', )}|${source}:${choices}|${target}|${transitions}`; } /** The name of the state at the other end of an edge – what a transition row is labelled with. */ function farStateName(graph: Graph, edge: GraphEdge, nodeId: string): string { const far = findNode(graph, edge.from.node === nodeId ? edge.to.node : edge.from.node); return far && isStateNode(far) ? stateName(far) : ''; } /** A state machine declares no variables; its one node type has none to name. */ const variablesOf = (asset: GraphAsset): PropertyDef[] => (asset.type === 'behavior' ? asset.variables : []); /** Description, settings (params) and the inline values of unconnected data inputs. */ export function buildNodeSection(host: InspectorHost, assetId: string, nodeId: string): void { const asset = findGraphAsset(host.state.project, assetId); if (!asset) return; const node = asset.graph.nodes.find((n) => n.id === nodeId); const def = node && resolveDef(node, asset.graph, graphLookup(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, graphLookup(host.state.project), assetId); if (problem) addNote(host, problem, 'warning'); const addSetting = (param: PropertyDef) => { const row = createFieldRow( host, param, node.params[param.key] ?? param.default, paramHandlers(host, assetId, nodeId, param), ); host.fields.set(`param:${param.key}`, row); host.body.add(row.row); }; // Both draw their settings inside the port section: a setting decides what the menu offers. const ownSettings = declaresPropertyPorts(node.type) || declaresVariablePorts(node.type); if (declaresVariablePorts(node.type)) buildVariablePortSection(host, assetId, node, addSetting); else if (ownSettings) buildPropertyPortSection(host, assetId, node, addSetting); else if (def.declaresPorts) buildPortSection(host, assetId, node, variablesOf(asset)); if (!ownSettings && def.params.length > 0) { addTitle(host, 'SETTINGS'); for (const param of def.params) { if (param.key === TRANSITION_PARAM && node.type === STATE_TRANSITION) buildTransitionPicker(host, assetId, node, asset.graph); else addSetting(param); } } if (isStateNode(node)) buildTransitionSection(host, assetId, node, asset.graph); 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, graphLookup(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; } /** 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)), ...listSource(host, def, () => ({ kind: 'node', assetId, nodeId, key })), }; }