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

+29 -1
View File
@@ -1,5 +1,15 @@
// Importing the graph package index is what registers the built-in nodes.
import { createEmptyGraph, type Graph, type GraphEdge, type GraphNode, nodeDef } from '../graph';
import {
createEmptyGraph,
DECLARED_PORT_KINDS,
type DeclaredPort,
type DeclaredPortKind,
type Graph,
type GraphEdge,
type GraphNode,
isValidPortName,
nodeDef,
} from '../graph';
import type { Asset, TextureFrame } from '../model/Asset';
import { isAssetType } from '../model/AssetTypes';
import { emptyEdit, type InstanceEdit, isEmptyEdit } from '../model/instance';
@@ -166,6 +176,22 @@ function normalizeVariables(raw: unknown): PropertyDef[] {
return defs;
}
function normalizePorts(raw: unknown): DeclaredPort[] {
const ports: DeclaredPort[] = [];
const seen = new Set<string>();
for (const item of Array.isArray(raw) ? raw : []) {
const p = asRecord(item);
const key = typeof p.key === 'string' ? p.key : '';
if (!isValidPortName(key) || seen.has(key)) continue;
seen.add(key);
const kind = (DECLARED_PORT_KINDS as readonly string[]).includes(String(p.kind))
? (p.kind as DeclaredPortKind)
: 'number';
ports.push({ key, kind });
}
return ports;
}
function normalizeGraph(raw: unknown): Graph {
const g = asRecord(raw);
const graph = createEmptyGraph();
@@ -182,6 +208,8 @@ function normalizeGraph(raw: unknown): Graph {
params[key] = value;
}
const node: GraphNode = { id, type, x: toNumber(n.x), y: toNumber(n.y), params };
const ports = normalizePorts(n.ports);
if (ports.length > 0) node.ports = ports;
graph.nodes.push(node);
}
const edgeIds = new Set<string>();