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:
root committed 2026-09-10 08:14:18 +02:00
1 parent 2cf788eedd
commit b6a609ae24
23 files changed
+438 -98

No files matched your search

+10 -6
View File
@@ -28,8 +28,8 @@ function doubler(fixture: ProjectFixture, name = 'Double'): BehaviorAsset {
const edit = graphEditor(fixture, asset);
const start = edit.addNode(BEHAVIOR_START);
const end = edit.addNode(BEHAVIOR_END);
fixture.stack.execute(new AddPortCommand(asset.id, BEHAVIOR_START, { key: 'in1', kind: 'number' }));
fixture.stack.execute(new AddPortCommand(asset.id, BEHAVIOR_END, { key: 'out1', kind: 'number' }));
fixture.stack.execute(new AddPortCommand(asset.id, start.id, { key: 'in1', kind: 'number' }));
fixture.stack.execute(new AddPortCommand(asset.id, end.id, { key: 'out1', kind: 'number' }));
const times = edit.addNode('Multiply', { b: 2 });
edit.wire([
[start, 'exec', end, 'exec'],
@@ -86,7 +86,7 @@ describe('a behavior used as a node', () => {
const edit = graphEditor(fixture, callee);
const start = edit.addNode(BEHAVIOR_START);
const end = edit.addNode(BEHAVIOR_END);
fixture.stack.execute(new AddPortCommand(callee.id, BEHAVIOR_END, { key: 'out1', kind: 'number' }));
fixture.stack.execute(new AddPortCommand(callee.id, end.id, { key: 'out1', kind: 'number' }));
const get = edit.addNode('GetVariable', { variable: 'count' });
const plus = edit.addNode('Add', { b: 1 });
const set = edit.addNode('SetVariable', { variable: 'count' });
@@ -124,7 +124,7 @@ describe('a behavior used as a node', () => {
const cEdit = graphEditor(fixture, callee);
const start = cEdit.addNode(BEHAVIOR_START);
const end = cEdit.addNode(BEHAVIOR_END);
fixture.stack.execute(new AddPortCommand(callee.id, BEHAVIOR_END, { key: 'out1', kind: 'number' }));
fixture.stack.execute(new AddPortCommand(callee.id, end.id, { key: 'out1', kind: 'number' }));
const theirs = cEdit.addNode('Number', { value: 99 });
cEdit.wire([
[start, 'exec', end, 'exec'],
@@ -250,7 +250,9 @@ describe('declared ports', () => {
const fixture = makeProject();
const callee = doubler(fixture);
const caller = callerOf(fixture, callee, 3);
fixture.stack.execute(new RenamePortCommand(callee.id, BEHAVIOR_START, 'in1', 'factor'));
fixture.stack.execute(
new RenamePortCommand(callee.id, terminalNode(callee.graph, BEHAVIOR_START)!.id, 'in1', 'factor'),
);
const start = terminalNode(callee.graph, BEHAVIOR_START)!;
expect(start.ports).toEqual([{ key: 'factor', kind: 'number' }]);
expect(callee.graph.edges.some((e) => e.from.port === 'factor')).toBe(true);
@@ -264,7 +266,9 @@ describe('declared ports', () => {
const callee = doubler(fixture);
const caller = callerOf(fixture, callee, 4);
const edgesBefore = { callee: callee.graph.edges.length, caller: caller.graph.edges.length };
fixture.stack.execute(new RemovePortCommand(callee.id, BEHAVIOR_END, 'out1'));
fixture.stack.execute(
new RemovePortCommand(callee.id, terminalNode(callee.graph, BEHAVIOR_END)!.id, 'out1'),
);
expect(callee.graph.edges.length).toBe(edgesBefore.callee - 1);
expect(caller.graph.edges.length).toBe(edgesBefore.caller - 1);
fixture.stack.undo();