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
@@ -0,0 +1,23 @@
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { asStruct } from '../../values';
|
||||
import { struct } from '../shared';
|
||||
|
||||
/**
|
||||
* Takes a bundle apart into one output per field. The fields are declared on the node itself, the
|
||||
* same way a behavior's start node declares what it takes – which is what makes its ports follow
|
||||
* the shape instead of being fixed by the type.
|
||||
*/
|
||||
export const ExpandStruct: NodeDef = {
|
||||
type: 'ExpandStruct',
|
||||
label: 'Expand',
|
||||
category: 'data',
|
||||
description:
|
||||
'Reads the fields of a struct, one output per field. A field the struct does not carry ' +
|
||||
'reads as empty.',
|
||||
inputs: [struct('value')],
|
||||
outputs: [],
|
||||
params: [],
|
||||
declaresPorts: 'outputs',
|
||||
// A declared output the struct has no field for is simply absent from the cache, and reads as empty.
|
||||
run: (_ctx, inputs) => ({ outputs: { ...asStruct(inputs.value) } }),
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { FieldValue } from '../../../model/Properties';
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { asField } from '../../values';
|
||||
import { struct } from '../shared';
|
||||
|
||||
/**
|
||||
* Bundles its declared inputs into one struct, so a group of values travels along a single edge
|
||||
* instead of one edge per part. The counterpart of Expand.
|
||||
*/
|
||||
export const PackStruct: NodeDef = {
|
||||
type: 'PackStruct',
|
||||
label: 'Pack',
|
||||
category: 'data',
|
||||
description: 'Bundles named values into one struct, one input per field.',
|
||||
inputs: [],
|
||||
outputs: [struct('value')],
|
||||
params: [],
|
||||
declaresPorts: 'inputs',
|
||||
run: (_ctx, inputs) => {
|
||||
const fields: Record<string, FieldValue> = {};
|
||||
for (const [field, value] of Object.entries(inputs)) fields[field] = asField(value);
|
||||
return { outputs: { value: fields } };
|
||||
},
|
||||
};
|
||||
@@ -17,5 +17,6 @@ export const BehaviorEnd: NodeDef = {
|
||||
inputs: [exec()],
|
||||
outputs: [],
|
||||
params: [],
|
||||
declaresPorts: 'inputs',
|
||||
run: (_ctx, inputs) => ({ outputs: inputs }),
|
||||
};
|
||||
@@ -16,4 +16,5 @@ export const BehaviorStart: NodeDef = {
|
||||
inputs: [],
|
||||
outputs: [exec()],
|
||||
params: [],
|
||||
declaresPorts: 'outputs',
|
||||
};
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { BEHAVIOR_PARAM, RUN_BEHAVIOR } from '../../nested';
|
||||
import { textOf } from '../../../model/Properties';
|
||||
import { BEHAVIOR_END, BEHAVIOR_PARAM, BEHAVIOR_START, declaredPorts, RUN_BEHAVIOR } from '../../nested';
|
||||
import { exec } from '../shared';
|
||||
|
||||
/**
|
||||
* Runs another behavior's graph in place, from its start node to its end node. The ports come from
|
||||
* those two nodes (see `nested.ts`); the interpreter does the call, because only it can hold a
|
||||
* second graph's pulse.
|
||||
* Runs another behavior's graph in place, from its start node to its end node. Its ports are those
|
||||
* the named behavior declares; the interpreter does the call, because only it can hold a second
|
||||
* graph's pulse.
|
||||
*/
|
||||
export const RunBehavior: NodeDef = {
|
||||
type: RUN_BEHAVIOR,
|
||||
@@ -17,8 +18,17 @@ export const RunBehavior: NodeDef = {
|
||||
inputs: [exec()],
|
||||
outputs: [exec()],
|
||||
params: [{ key: BEHAVIOR_PARAM, label: 'Behavior', kind: 'asset', default: '', assetTypes: ['behavior'] }],
|
||||
derivePorts: (source) => {
|
||||
const program = source.behaviors(textOf(source.params[BEHAVIOR_PARAM] ?? ''));
|
||||
return {
|
||||
// The graph reads as what it does, not as a row of identical "Run behavior" boxes.
|
||||
label: program?.name,
|
||||
inputs: declaredPorts(program?.graph, BEHAVIOR_START),
|
||||
outputs: declaredPorts(program?.graph, BEHAVIOR_END),
|
||||
};
|
||||
},
|
||||
run: (_ctx, inputs, params) => ({
|
||||
call: { behaviorId: String(params[BEHAVIOR_PARAM] ?? ''), inputs },
|
||||
call: { behaviorId: textOf(params[BEHAVIOR_PARAM] ?? ''), inputs },
|
||||
next: 'exec',
|
||||
}),
|
||||
};
|
||||
@@ -1,13 +1,45 @@
|
||||
import type { PropertyBag } from '../../../model/Properties';
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { asNumber } from '../../values';
|
||||
import { exec } from '../shared';
|
||||
|
||||
const COUNT = 'count';
|
||||
/** Below two outputs a Sequence is just a link, so two are fixed and the rest are derived. */
|
||||
const FIXED = 2;
|
||||
/** One letter per output, so `a` stays `a` – the alphabet is what sets the ceiling. */
|
||||
const MAX = 26;
|
||||
|
||||
export function sequenceKey(index: number): string {
|
||||
return String.fromCharCode(97 + index);
|
||||
}
|
||||
|
||||
export function sequenceCount(params: PropertyBag): number {
|
||||
const count = Math.round(asNumber(params[COUNT] ?? 3));
|
||||
return Math.max(FIXED, Math.min(MAX, count));
|
||||
}
|
||||
|
||||
function outputAt(index: number) {
|
||||
return exec(sequenceKey(index), String(index + 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* The keys are `a`, `b`, `c`, … rather than `out1`…`outN`: those three are what every Sequence
|
||||
* saved so far names its outputs, and renaming them would drop the edges hanging on them.
|
||||
*/
|
||||
export const Sequence: NodeDef = {
|
||||
type: 'Sequence',
|
||||
label: 'Sequence',
|
||||
category: 'flow',
|
||||
description: 'Runs its outputs one after another.',
|
||||
inputs: [exec()],
|
||||
outputs: [exec('a', '1'), exec('b', '2'), exec('c', '3')],
|
||||
params: [],
|
||||
run: () => ({ next: ['a', 'b', 'c'] }),
|
||||
outputs: [outputAt(0), outputAt(1)],
|
||||
params: [
|
||||
{ key: COUNT, label: 'Outputs', kind: 'number', default: 3, min: FIXED, max: MAX, step: 1, decimals: 0 },
|
||||
],
|
||||
derivePorts: ({ params }) => ({
|
||||
outputs: Array.from({ length: sequenceCount(params) - FIXED }, (_, i) => outputAt(FIXED + i)),
|
||||
}),
|
||||
run: (_ctx, _inputs, params) => ({
|
||||
next: Array.from({ length: sequenceCount(params) }, (_, i) => sequenceKey(i)),
|
||||
}),
|
||||
};
|
||||
@@ -44,6 +44,7 @@ import { NumberLiteral } from './data/Number';
|
||||
import { ObjectByName } from './data/ObjectByName';
|
||||
import { Or } from './data/Or';
|
||||
import { Owner } from './data/Owner';
|
||||
import { PackStruct } from './data/PackStruct';
|
||||
import { Parent } from './data/Parent';
|
||||
import { Random } from './data/Random';
|
||||
import { Round } from './data/Round';
|
||||
@@ -57,6 +58,7 @@ import { ToNumber } from './data/ToNumber';
|
||||
import { ToText } from './data/ToText';
|
||||
import { Concat } from './data/Concat';
|
||||
import { Count } from './data/Count';
|
||||
import { ExpandStruct } from './data/ExpandStruct';
|
||||
import { Fill } from './data/Fill';
|
||||
import { IndexOf } from './data/IndexOf';
|
||||
import { ItemAt } from './data/ItemAt';
|
||||
@@ -157,6 +159,9 @@ export const BUILT_IN_NODES: readonly NodeDef[] = [
|
||||
SubList,
|
||||
Concat,
|
||||
IndexOf,
|
||||
// data: structs
|
||||
PackStruct,
|
||||
ExpandStruct,
|
||||
// data: vectors and input
|
||||
Length,
|
||||
Distance,
|
||||
|
||||
Reference in new issue
Block a user