Let a node work out its own ports
Ports that vary per node already existed, but only for the three nested-behavior
types, and the rule for them sat as a switch on type names in nested.ts – the
one place in the project that knew node types by name. It dissolves into two
optional contracts on NodeDef, so a type carries its own rule:
declaresPorts the node holds an editable list, shown as a section in the
inspector – behavior start/end, and now Pack/Expand
derivePorts the node computes them from its settings – Sequence from a
count, Run behavior from the behavior it names
Sequence gains an Outputs setting (2 to 26). Its keys stay a, b, c and count on
with d, e, so the edges of the sequences already out there survive.
A param can now take a port away, and the edge on it would stay: it draws as
nothing yet survives every save, and comes back to life if the port returns.
SetNodeParamCommand takes those edges with it, which also closes the same leak
that switching a Run behavior node's target has always had.
The port editor addresses its node by id rather than by type, so two Pack nodes
can live in one graph, and it calls the rows Fields when they are not a
behavior's signature.
This commit is contained in:
1 parent
2cf788eedd
commit
b6a609ae24
23 files changed
+438
-98
No files matched your search
@@ -5,7 +5,6 @@ import {
|
||||
type GraphNode,
|
||||
inlineInputDef,
|
||||
inlineInputs,
|
||||
isTerminalNode,
|
||||
portSignature,
|
||||
resolveDef,
|
||||
} from '../../core/graph';
|
||||
@@ -51,7 +50,7 @@ export function buildNodeSection(host: InspectorHost, assetId: string, nodeId: s
|
||||
addNote(host, def.description);
|
||||
const problem = callProblem(node, behaviorLookup(host.state.project), assetId);
|
||||
if (problem) addNote(host, problem, 'warning');
|
||||
if (isTerminalNode(node)) buildPortSection(host, assetId, node);
|
||||
if (def.declaresPorts) buildPortSection(host, assetId, node);
|
||||
|
||||
if (def.params.length > 0) addTitle(host, 'SETTINGS');
|
||||
for (const param of def.params) {
|
||||
|
||||
@@ -4,7 +4,13 @@ import {
|
||||
RemovePortCommand,
|
||||
RenamePortCommand,
|
||||
} from '../../core/commands/portCommands';
|
||||
import { BEHAVIOR_START, DECLARED_PORT_KINDS, type DeclaredPortKind, type GraphNode } from '../../core/graph';
|
||||
import {
|
||||
BEHAVIOR_START,
|
||||
DECLARED_PORT_KINDS,
|
||||
type DeclaredPortKind,
|
||||
type GraphNode,
|
||||
isTerminalNode,
|
||||
} from '../../core/graph';
|
||||
import { Button, Dropdown, HStack, Label, Menu, Spacer, TextField, Theme } from '../../ui';
|
||||
import { addNote, type InspectorHost } from './host';
|
||||
|
||||
@@ -27,12 +33,14 @@ const KIND_MARKS: Record<DeclaredPortKind, string> = {
|
||||
};
|
||||
|
||||
/**
|
||||
* The ports a start or end node declares – the signature of the behavior seen as a node. Named from
|
||||
* the caller's side: what the start node hands into the graph is an *input* of the behavior.
|
||||
* The ports a node declares for itself: the signature of a behavior seen as a node, or the fields
|
||||
* of a struct. A start node's are named from the caller's side – what it hands into the graph is
|
||||
* an *input* of the behavior – which is why the side comes from the node type, not from here.
|
||||
*/
|
||||
export function buildPortSection(host: InspectorHost, assetId: string, node: GraphNode): void {
|
||||
const onStart = node.type === BEHAVIOR_START;
|
||||
const noun = onStart ? 'Input' : 'Output';
|
||||
const noun = isTerminalNode(node) ? (onStart ? 'Input' : 'Output') : 'Field';
|
||||
const base = noun === 'Field' ? 'field' : onStart ? 'in' : 'out';
|
||||
|
||||
const header = new HStack({ gap: Theme.gap, align: 'center' });
|
||||
header.add(new Label(`${noun.toUpperCase()}S`, { style: 'title' }), 1);
|
||||
@@ -41,12 +49,7 @@ export function buildPortSection(host: InspectorHost, assetId: string, node: Gra
|
||||
const items = DECLARED_PORT_KINDS.map((kind) => ({
|
||||
label: KIND_LABELS[kind],
|
||||
onSelect: () =>
|
||||
host.state.execute(
|
||||
new AddPortCommand(assetId, node.type, {
|
||||
key: nextPortName(node, onStart ? 'in' : 'out'),
|
||||
kind,
|
||||
}),
|
||||
),
|
||||
host.state.execute(new AddPortCommand(assetId, node.id, { key: nextPortName(node, base), kind })),
|
||||
}));
|
||||
new Menu(items).openBelow(add);
|
||||
};
|
||||
@@ -57,9 +60,11 @@ export function buildPortSection(host: InspectorHost, assetId: string, node: Gra
|
||||
if (ports.length === 0)
|
||||
addNote(
|
||||
host,
|
||||
onStart
|
||||
? 'No inputs yet. Every input added here becomes a port on the node that runs this behavior.'
|
||||
: 'No outputs yet. Every output added here becomes a result of running this behavior.',
|
||||
noun === 'Field'
|
||||
? 'No fields yet. Every field added here becomes a port of the struct.'
|
||||
: onStart
|
||||
? 'No inputs yet. Every input added here becomes a port on the node that runs this behavior.'
|
||||
: 'No outputs yet. Every output added here becomes a result of running this behavior.',
|
||||
);
|
||||
|
||||
for (const port of ports) {
|
||||
@@ -72,7 +77,7 @@ export function buildPortSection(host: InspectorHost, assetId: string, node: Gra
|
||||
new TextField(port.key, {
|
||||
width: 90,
|
||||
onChange: (next) =>
|
||||
host.state.execute(new RenamePortCommand(assetId, node.type, port.key, next.trim())),
|
||||
host.state.execute(new RenamePortCommand(assetId, node.id, port.key, next.trim())),
|
||||
}),
|
||||
1,
|
||||
);
|
||||
@@ -81,7 +86,7 @@ export function buildPortSection(host: InspectorHost, assetId: string, node: Gra
|
||||
new Button('×', {
|
||||
variant: 'ghost',
|
||||
width: Theme.controlHeight,
|
||||
onClick: () => host.state.execute(new RemovePortCommand(assetId, node.type, port.key)),
|
||||
onClick: () => host.state.execute(new RemovePortCommand(assetId, node.id, port.key)),
|
||||
}),
|
||||
);
|
||||
host.body.add(row);
|
||||
|
||||
Reference in new issue
Block a user