Let a behavior be a node in another graph

A behavior that has a Start and an End node can now be dropped into any
other graph as a node of its own. What is declared on the start node
becomes the inputs of that node, what hangs on the end node its outputs;
the port list is derived from the sub graph, not copied, so declaring a
port in one graph grows the node in the other without touching it. The
node carries the behavior's name and the palette lists behaviors by
name, so a call is one grab instead of a node plus a dropdown.

An embedded behavior has no object and therefore no place for
variables: every call starts on the asset defaults and communicates
only through its ports. That is the conservative direction - state can
still be added later, and shared state already has a home in a behavior
object in the scene. Node ids are unique per graph only, so each call
gets its own pulse cache; without that, two graphs would collide.

Renaming a port used to silently drop the value typed on the calling
node. It is carried along now, project wide, like the edges.
This commit is contained in:
root committed 2026-09-09 20:01:49 +02:00
1 parent 64e034ffb9
commit 6702ff8abe
31 files changed
+1402 -107

No files matched your search

+26 -6
View File
@@ -81,14 +81,21 @@ export class NodeView extends Widget {
/** Data inputs that currently have a connection (their inline value is hidden). */
private connected = new Set<string>();
/**
* Not fixed at construction: a nested-behavior node's ports come from another graph, so the
* definition that applies to this node can change without the node itself changing.
*/
def: NodeDef;
constructor(
readonly node: GraphNode,
readonly def: NodeDef,
def: NodeDef,
) {
super();
this.def = def;
this.cursor = 'grab';
this.addChild(this.box, this.ports);
this.sync(node, { nodes: [node], edges: [] }, null, null);
this.sync(node, { nodes: [node], edges: [] }, null, null, def);
}
/** Ports sit on the left/right edges; widen the hit area so they can be grabbed. */
@@ -98,10 +105,23 @@ export class NodeView extends Widget {
}
/** `project` resolves asset references (Spawn, Is asset). */
sync(node: GraphNode, graph: Graph, asset: BehaviorAsset | null, project: Project | null): void {
this.paramLines = this.def.params.map(
(p) => `${p.label}: ${formatParam(p, node.params[p.key] ?? p.default, asset, project)}`,
);
sync(
node: GraphNode,
graph: Graph,
asset: BehaviorAsset | null,
project: Project | null,
def: NodeDef,
): void {
this.def = def;
this.paramLines = this.def.params
.map((p) => ({
label: p.label,
value: formatParam(p, node.params[p.key] ?? p.default, asset, project),
}))
// A setting the title already states is not worth a row: a nested-behavior node is titled
// with the behavior it runs, so "Behavior: Double" under "Double" says nothing new.
.filter((p) => p.value !== this.def.label)
.map((p) => `${p.label}: ${p.value}`);
this.connected = new Set(
inlineInputs(this.def)
.filter((p) => edgesInto(graph, node.id, p.key).length > 0)