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:
1 parent
64e034ffb9
commit
6702ff8abe
31 files changed
+1402
-107
No files matched your search
@@ -0,0 +1,80 @@
|
||||
import type { PropertyBag } from '../model/Properties';
|
||||
import { type DeclaredPort, declaredPortType, type Graph, type GraphNode } from './Graph';
|
||||
import { nodeDef, type NodeDef, type PortDef } from './NodeRegistry';
|
||||
|
||||
export const BEHAVIOR_START = 'BehaviorStart';
|
||||
export const BEHAVIOR_END = 'BehaviorEnd';
|
||||
export const RUN_BEHAVIOR = 'RunBehavior';
|
||||
|
||||
export const BEHAVIOR_PARAM = 'behavior';
|
||||
|
||||
/** What running another behavior's graph in place needs: its nodes, its variable defaults, its name. */
|
||||
export interface BehaviorProgram {
|
||||
name: string;
|
||||
graph: Graph;
|
||||
defaults: PropertyBag;
|
||||
}
|
||||
|
||||
export type BehaviorLookup = (assetId: string) => BehaviorProgram | null;
|
||||
|
||||
export const NO_BEHAVIORS: BehaviorLookup = () => null;
|
||||
|
||||
/** Identifier-like, and not `exec`: that key belongs to the fixed flow port of every terminal node. */
|
||||
export function isValidPortName(name: string): boolean {
|
||||
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) && name !== 'exec';
|
||||
}
|
||||
|
||||
/** The single start (or end) node of a graph; a second one is ignored. */
|
||||
export function terminalNode(graph: Graph, type: string): GraphNode | undefined {
|
||||
return graph.nodes.find((n) => n.type === type);
|
||||
}
|
||||
|
||||
export function isTerminalType(type: string): boolean {
|
||||
return type === BEHAVIOR_START || type === BEHAVIOR_END;
|
||||
}
|
||||
|
||||
export function isTerminalNode(node: GraphNode): boolean {
|
||||
return isTerminalType(node.type);
|
||||
}
|
||||
|
||||
export function portsOf(port: DeclaredPort): PortDef {
|
||||
return { key: port.key, label: port.key, type: declaredPortType(port.kind) };
|
||||
}
|
||||
|
||||
function declaredOf(graph: Graph | undefined, type: string): PortDef[] {
|
||||
const node = graph && terminalNode(graph, type);
|
||||
return (node?.ports ?? []).map(portsOf);
|
||||
}
|
||||
|
||||
/**
|
||||
* The node definition as it applies to *this* node. Identical to the registered one for every fixed
|
||||
* type; the three nested-behavior types grow ports from a declaration instead – the start and end
|
||||
* node from their own, the call node from those of the behavior it names.
|
||||
*/
|
||||
export function resolveDef(node: GraphNode, lookup: BehaviorLookup): NodeDef | undefined {
|
||||
const def = nodeDef(node.type);
|
||||
if (!def) return undefined;
|
||||
const own = (node.ports ?? []).map(portsOf);
|
||||
if (node.type === BEHAVIOR_START) return { ...def, outputs: [...def.outputs, ...own] };
|
||||
if (node.type === BEHAVIOR_END) return { ...def, inputs: [...def.inputs, ...own] };
|
||||
if (node.type !== RUN_BEHAVIOR) return def;
|
||||
const program = lookup(String(node.params[BEHAVIOR_PARAM] ?? ''));
|
||||
return {
|
||||
...def,
|
||||
// The graph reads as what it does, not as a row of identical "Run behavior" boxes.
|
||||
label: program ? program.name : def.label,
|
||||
inputs: [...def.inputs, ...declaredOf(program?.graph, BEHAVIOR_START)],
|
||||
outputs: [...def.outputs, ...declaredOf(program?.graph, BEHAVIOR_END)],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Everything that changes a node's ports, as one string. Views and the inspector rebuild when it
|
||||
* changes – a port list that is derived rather than fixed has no other way of being noticed.
|
||||
*/
|
||||
export function portSignature(node: GraphNode, lookup: BehaviorLookup): string {
|
||||
const def = resolveDef(node, lookup);
|
||||
if (!def) return 'missing';
|
||||
const list = (ports: PortDef[]) => ports.map((p) => `${p.key}:${p.type}`).join(',');
|
||||
return `${node.type}|${def.label}|${list(def.inputs)}|${list(def.outputs)}`;
|
||||
}
|
||||
Reference in new issue
Block a user