Replace the five variable nodes with one Variables node
A single node now lists any number of variables. Every entry is an output that reads the current value whenever it is pulled; an entry switched to Set is also an input and is written when the flow arrives. Exec pins only exist while at least one entry writes, and all inputs are read before any write, so one node can swap two variables. Get/Set variable, Get/Set variables and Variable are gone. Old project files are converted on load: each old node becomes a Variables node with the same variables, edges and typed values. A written port without a typed value now shows the zero of its kind on the node body, matching what the inspector shows and what the run writes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
854ae2e756
commit
7c63c884ef
40 files changed
+895
-590
No files matched your search
@@ -32,9 +32,7 @@ interface PortSite {
|
||||
* the behavior – facing the other way, because what a start node hands out the caller passes in.
|
||||
*/
|
||||
function portSites(project: Project, assetId: string, node: GraphNode): PortSite[] {
|
||||
const asset = requireBehavior(project, assetId);
|
||||
const asInput = requireNodeDef(node.type).declaresPorts === 'inputs';
|
||||
const sites: PortSite[] = [{ graph: asset.graph, nodeId: node.id, asInput }];
|
||||
const sites = ownSites(requireBehavior(project, assetId).graph, node);
|
||||
if (!isTerminalNode(node)) return sites;
|
||||
for (const other of project.assets) {
|
||||
if (other.type !== 'behavior') continue;
|
||||
@@ -46,6 +44,37 @@ function portSites(project: Project, assetId: string, node: GraphNode): PortSite
|
||||
return sites;
|
||||
}
|
||||
|
||||
function ownSites(graph: Graph, node: GraphNode): PortSite[] {
|
||||
const side = requireNodeDef(node.type).declaresPorts;
|
||||
const site = (asInput: boolean): PortSite => ({ graph, nodeId: node.id, asInput });
|
||||
return side === 'both' ? [site(false), site(true)] : [site(side === 'inputs')];
|
||||
}
|
||||
|
||||
type EdgeTest = { graph: Graph; test: (edge: GraphEdge) => boolean };
|
||||
type DroppedEdges = { graph: Graph; entries: Removed<GraphEdge>[] };
|
||||
|
||||
/** One pass per edge list: a second pass over the same list would be restored at shifted indices. */
|
||||
function dropEdges(tests: readonly EdgeTest[]): DroppedEdges[] {
|
||||
const byGraph = new Map<Graph, EdgeTest['test'][]>();
|
||||
for (const { graph, test } of tests) byGraph.set(graph, [...(byGraph.get(graph) ?? []), test]);
|
||||
return [...byGraph].map(([graph, all]) => ({
|
||||
graph,
|
||||
entries: removeWhere(graph.edges, (edge) => all.some((test) => test(edge))),
|
||||
}));
|
||||
}
|
||||
|
||||
function restoreEdges(dropped: readonly DroppedEdges[]): void {
|
||||
for (const { graph, entries } of dropped) restoreAt(graph.edges, entries);
|
||||
}
|
||||
|
||||
/** A node declaring both sides has its flow pins only while one of its ports writes. */
|
||||
function flowEdgesOnceSilent(graph: Graph, node: GraphNode): EdgeTest[] {
|
||||
if (requireNodeDef(node.type).declaresPorts !== 'both' || (node.ports ?? []).some((p) => p.write))
|
||||
return [];
|
||||
const onFlow = (end: { node: string; port: string }) => end.node === node.id && end.port === 'exec';
|
||||
return [{ graph, test: (edge) => onFlow(edge.to) || onFlow(edge.from) }];
|
||||
}
|
||||
|
||||
function edgesOnPort(site: PortSite, key: string): (edge: GraphEdge) => boolean {
|
||||
return (edge) =>
|
||||
site.asInput
|
||||
@@ -122,7 +151,7 @@ export class AddPortCommand extends AddPortsCommand {
|
||||
export class RemovePortCommand implements Command {
|
||||
readonly label: string;
|
||||
private removed?: Removed<DeclaredPort>;
|
||||
private edges: { graph: Graph; entries: Removed<GraphEdge>[] }[] = [];
|
||||
private edges: DroppedEdges[] = [];
|
||||
private values: { site: PortSite; value: PropertyValue }[] = [];
|
||||
|
||||
constructor(
|
||||
@@ -141,10 +170,11 @@ export class RemovePortCommand implements Command {
|
||||
if (!removed) throw new CommandError(`Port "${this.key}" not found`);
|
||||
this.removed = removed;
|
||||
node.ports = ports;
|
||||
this.edges = sites.map((site) => ({
|
||||
graph: site.graph,
|
||||
entries: removeWhere(site.graph.edges, edgesOnPort(site, this.key)),
|
||||
}));
|
||||
const graph = requireBehavior(project, this.assetId).graph;
|
||||
this.edges = dropEdges([
|
||||
...sites.map((site) => ({ graph: site.graph, test: edgesOnPort(site, this.key) })),
|
||||
...flowEdgesOnceSilent(graph, node),
|
||||
]);
|
||||
this.values = [];
|
||||
for (const site of sites) {
|
||||
const target = site.asInput ? nodeAt(site) : undefined;
|
||||
@@ -161,7 +191,7 @@ export class RemovePortCommand implements Command {
|
||||
const ports = [...(node.ports ?? [])];
|
||||
restoreAt(ports, [this.removed]);
|
||||
node.ports = ports;
|
||||
for (const { graph, entries } of this.edges) restoreAt(graph.edges, entries);
|
||||
restoreEdges(this.edges);
|
||||
for (const { site, value } of this.values) {
|
||||
const target = nodeAt(site);
|
||||
if (target) target.params[this.key] = value;
|
||||
@@ -169,6 +199,62 @@ export class RemovePortCommand implements Command {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches whether a port of a node declaring both sides is written. Switched off, its input goes
|
||||
* with its edge and typed-in value, and the last one off takes the flow pins and their edges along.
|
||||
*/
|
||||
export class SetPortWriteCommand implements Command {
|
||||
readonly label: string;
|
||||
private edges: DroppedEdges[] = [];
|
||||
private value: PropertyValue | undefined;
|
||||
private was = false;
|
||||
|
||||
constructor(
|
||||
private readonly assetId: string,
|
||||
private readonly nodeId: string,
|
||||
private readonly key: string,
|
||||
private readonly write: boolean,
|
||||
) {
|
||||
this.label = write ? `Set ${key}` : `Stop setting ${key}`;
|
||||
}
|
||||
|
||||
execute(project: Project): void {
|
||||
const node = requireNode(project, this.assetId, this.nodeId);
|
||||
if (requireNodeDef(node.type).declaresPorts !== 'both')
|
||||
throw new CommandError('This node does not write its ports');
|
||||
const port = (node.ports ?? []).find((p) => p.key === this.key);
|
||||
if (!port) throw new CommandError(`Port "${this.key}" not found`);
|
||||
this.was = port.write === true;
|
||||
this.mark(node, this.write);
|
||||
this.edges = [];
|
||||
this.value = undefined;
|
||||
if (this.write) return;
|
||||
const graph = requireBehavior(project, this.assetId).graph;
|
||||
const input: PortSite = { graph, nodeId: node.id, asInput: true };
|
||||
this.edges = dropEdges([
|
||||
{ graph, test: edgesOnPort(input, this.key) },
|
||||
...flowEdgesOnceSilent(graph, node),
|
||||
]);
|
||||
this.value = node.params[this.key];
|
||||
delete node.params[this.key];
|
||||
}
|
||||
|
||||
undo(project: Project): void {
|
||||
const node = requireNode(project, this.assetId, this.nodeId);
|
||||
this.mark(node, this.was);
|
||||
restoreEdges(this.edges);
|
||||
if (this.value !== undefined) node.params[this.key] = this.value;
|
||||
}
|
||||
|
||||
private mark(node: GraphNode, write: boolean): void {
|
||||
node.ports = (node.ports ?? []).map((port) => {
|
||||
if (port.key !== this.key) return port;
|
||||
const { write: _, ...plain } = port;
|
||||
return write ? { ...plain, write: true } : plain;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Rewrites the edges and inline values pointing at the port too, in every graph that has any. */
|
||||
export class RenamePortCommand implements Command {
|
||||
readonly label: string;
|
||||
@@ -208,11 +294,7 @@ function renamePortOn(graph: Graph, node: GraphNode, from: string, to: string):
|
||||
const ports = node.ports ?? [];
|
||||
if (from === to || !ports.some((p) => p.key === from)) return;
|
||||
node.ports = ports.map((p) => (p.key === from ? { ...p, key: to } : p));
|
||||
carryPort(
|
||||
{ graph, nodeId: node.id, asInput: requireNodeDef(node.type).declaresPorts === 'inputs' },
|
||||
from,
|
||||
to,
|
||||
);
|
||||
for (const site of ownSites(graph, node)) carryPort(site, from, to);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -138,8 +138,6 @@ export class RenameVariableCommand implements Command {
|
||||
bag[to] = bag[from]!;
|
||||
delete bag[from];
|
||||
}
|
||||
// Every node that picks a variable stores it under the param key `variable`.
|
||||
for (const node of asset.graph.nodes) if (node.params.variable === from) node.params.variable = to;
|
||||
renameVariablePorts(asset.graph, from, to);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user