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
@@ -1,26 +1,40 @@
|
||||
import { RemoveNodeCommand, SetNodeParamCommand } from '../../core/commands/graphCommands';
|
||||
import { edgesInto, type GraphNode, inlineInputDef, inlineInputs, nodeDef } from '../../core/graph';
|
||||
import {
|
||||
edgesInto,
|
||||
type GraphNode,
|
||||
inlineInputDef,
|
||||
inlineInputs,
|
||||
isTerminalNode,
|
||||
portSignature,
|
||||
resolveDef,
|
||||
} from '../../core/graph';
|
||||
import type { Asset } from '../../core/model/Asset';
|
||||
import { behaviorLookup } from '../../core/model/behaviorPrograms';
|
||||
import { findAsset, type Project } from '../../core/model/Project';
|
||||
import type { PropertyDef } from '../../core/properties/PropertySchema';
|
||||
import { Button, Label } from '../../ui';
|
||||
import { createFieldRow, type FieldHandlers } from './fieldFactory';
|
||||
import { addGap, addNote, addTitle, type InspectorHost } from './host';
|
||||
import { buildPortSection } from './portSection';
|
||||
import { previewingHandlers } from './previewHandlers';
|
||||
|
||||
/** Structure-defining facts of a node selection: type, connected inputs, declared variables. */
|
||||
/** Structure-defining facts of a node selection: ports, connected inputs, declared variables. */
|
||||
export function nodeSignature(project: Project, assetId: string, nodeId: string): string {
|
||||
const asset = findAsset(project, assetId);
|
||||
if (!asset || asset.type !== 'behavior') return 'missing';
|
||||
const node = asset.graph.nodes.find((n) => n.id === nodeId);
|
||||
if (!node) return 'missing';
|
||||
const def = nodeDef(node.type);
|
||||
const lookup = behaviorLookup(project);
|
||||
const def = resolveDef(node, lookup);
|
||||
const connected = def
|
||||
? inlineInputs(def)
|
||||
.filter((p) => edgesInto(asset.graph, node.id, p.key).length > 0)
|
||||
.map((p) => p.key)
|
||||
: [];
|
||||
return `${node.type}|${connected.join(',')}|${asset.variables.map((v) => v.key).join(',')}`;
|
||||
const declared = (node.ports ?? []).map((p) => `${p.key}:${p.kind}`).join(',');
|
||||
return `${portSignature(node, lookup)}|${declared}|${connected.join(',')}|${asset.variables
|
||||
.map((v) => v.key)
|
||||
.join(',')}`;
|
||||
}
|
||||
|
||||
/** Description, settings (params) and the inline values of unconnected data inputs. */
|
||||
@@ -28,12 +42,13 @@ export function buildNodeSection(host: InspectorHost, assetId: string, nodeId: s
|
||||
const asset = findAsset(host.state.project, assetId);
|
||||
if (!asset || asset.type !== 'behavior') return;
|
||||
const node = asset.graph.nodes.find((n) => n.id === nodeId);
|
||||
const def = node && nodeDef(node.type);
|
||||
const def = node && resolveDef(node, behaviorLookup(host.state.project));
|
||||
if (!node || !def) return;
|
||||
|
||||
addTitle(host, `${def.category.toUpperCase()} NODE`);
|
||||
host.body.add(new Label(def.label, { style: 'bodyBold' }));
|
||||
addNote(host, def.description);
|
||||
if (isTerminalNode(node)) buildPortSection(host, assetId, node);
|
||||
|
||||
if (def.params.length > 0) addTitle(host, 'SETTINGS');
|
||||
for (const param of def.params) {
|
||||
@@ -74,14 +89,15 @@ export function buildNodeSection(host: InspectorHost, assetId: string, nodeId: s
|
||||
/** Pushes the node's current params into the rows. False when the node is gone (rebuild). */
|
||||
export function refreshNodeSection(host: InspectorHost, assetId: string, nodeId: string): boolean {
|
||||
const node = currentNode(host.state.project, assetId, nodeId);
|
||||
const def = node && nodeDef(node.type);
|
||||
const def = node && resolveDef(node, behaviorLookup(host.state.project));
|
||||
if (!node || !def) return false;
|
||||
for (const [key, row] of host.fields) {
|
||||
const [scope, name] = key.split(':') as ['param' | 'input', string];
|
||||
const port = def.inputs.find((p) => p.key === name);
|
||||
const fallback =
|
||||
scope === 'param'
|
||||
? def.params.find((p) => p.key === name)?.default
|
||||
: inlineInputDef(def.inputs.find((p) => p.key === name)!).default;
|
||||
: port && inlineInputDef(port).default;
|
||||
row.setValue(node.params[name] ?? fallback ?? '');
|
||||
}
|
||||
return true;
|
||||
|
||||
Reference in new issue
Block a user