Files
feuerwerk/src/editor/inspector/nodeSection.ts
T
rootandClaude Opus 5 854ae2e756 Add Get/Set properties nodes and a breadcrumb for opened sub-behaviors
Get properties / Set properties work like their variable counterparts:
one port per chosen property, an unconnected input writes its inline
value. A port chosen from the built-in list keeps that property's field
and default (height starts at 100, color is a colour field). "object" is
reserved for the fixed input, checked by the command and on load.

The property picker names the owning type ("Sprite width"), and text
fields can now carry a suggestion list; Get/Set property use it.

Double-clicking a call node shows the path of opened behaviors under the
graph header. Clicking an entry jumps back and trims the path; choosing
a behavior any other way hides it. Reopening a behavior already on the
path steps back to it instead of repeating it. The bar shows before the
graph switches, otherwise it clips the graph that was just fitted.

The "Add node..." header button is gone; nodes come from the context menu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 15:07:07 +02:00

126 lines
5.2 KiB
TypeScript

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<GraphNode>(host, nodeParamValue(host.state, assetId, nodeId, key)),
...listOpener(host.state, def, () => ({ kind: 'node', assetId, nodeId, key })),
};
}