Let a Sequence grow an output as you connect the last one

The Outputs setting is gone. A Sequence now keeps one free output below the
last connected one: nothing plugged in is one output, connecting the spare
grows a new one, unplugging the bottom edge shrinks it back.

It counts the last *connected* port rather than the number of edges, so
unplugging a middle one leaves a gap instead of silently dropping an edge.

Removing the setting also removes a bug it caused. Typing in the Outputs field
did nothing and the arrow keys did nothing, while dragging the value worked:
onInput fires per keystroke, the preview writes into node.params, that emits
'project', and Inspector.refreshValues() sees the node's signature change --
the port list is part of it -- so it rebuilds and destroys the row being typed
in. A drag survives because it keeps receiving globalpointermove on the old
widget. refreshValues() has no focus guard where refreshLive() does; the trap
is still there for the next typed field that changes a node's shape.

Ports that depend on the edges around a node mean derivePorts needs the graph,
not just the params, so resolveDef and portSignature take it too. And a node
whose ports are derived cannot know how many it has from its type, so run is
handed the definition resolved for it.
This commit is contained in:
root committed 2026-09-10 08:32:14 +02:00
1 parent b6a609ae24
commit 3c76106fd2
15 files changed
+159 -111

No files matched your search

+18 -10
View File
@@ -1,6 +1,7 @@
import { RemoveNodeCommand, SetNodeParamCommand } from '../../core/commands/graphCommands';
import {
callProblem,
type Graph,
edgesInto,
type GraphNode,
inlineInputDef,
@@ -25,14 +26,14 @@ export function nodeSignature(project: Project, assetId: string, nodeId: string)
const node = asset.graph.nodes.find((n) => n.id === nodeId);
if (!node) return 'missing';
const lookup = behaviorLookup(project);
const def = resolveDef(node, lookup);
const def = resolveDef(node, asset.graph, lookup);
const connected = def
? inlineInputs(def)
.filter((p) => edgesInto(asset.graph, node.id, p.key).length > 0)
.map((p) => p.key)
: [];
const declared = (node.ports ?? []).map((p) => `${p.key}:${p.kind}`).join(',');
return `${portSignature(node, lookup, assetId)}|${declared}|${connected.join(',')}|${asset.variables
return `${portSignature(node, asset.graph, lookup, assetId)}|${declared}|${connected.join(',')}|${asset.variables
.map((v) => v.key)
.join(',')}`;
}
@@ -42,7 +43,7 @@ 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 && resolveDef(node, behaviorLookup(host.state.project));
const def = node && resolveDef(node, asset.graph, behaviorLookup(host.state.project));
if (!node || !def) return;
addTitle(host, `${def.category.toUpperCase()} NODE`);
@@ -90,9 +91,9 @@ 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 && resolveDef(node, behaviorLookup(host.state.project));
if (!node || !def) return false;
const found = currentNode(host.state.project, assetId, nodeId);
const def = found && resolveDef(found.node, found.graph, behaviorLookup(host.state.project));
if (!found || !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);
@@ -100,7 +101,7 @@ export function refreshNodeSection(host: InspectorHost, assetId: string, nodeId:
scope === 'param'
? def.params.find((p) => p.key === name)?.default
: port && inlineInputDef(port).default;
row.setValue(node.params[name] ?? fallback ?? '');
row.setValue(found.node.params[name] ?? fallback ?? '');
}
return true;
}
@@ -117,7 +118,7 @@ function resolveParamDef(param: PropertyDef, asset: Asset): PropertyDef {
/** Previews straight into the node's params; the command records the value from before the preview. */
function paramHandlers(host: InspectorHost, assetId: string, nodeId: string, key: string): FieldHandlers {
return previewingHandlers<GraphNode>(host, {
target: () => currentNode(host.state.project, assetId, nodeId),
target: () => currentNode(host.state.project, assetId, nodeId)?.node,
read: (node) => node.params[key],
write: (node, value) => (node.params[key] = value),
remove: (node) => delete node.params[key],
@@ -125,7 +126,14 @@ function paramHandlers(host: InspectorHost, assetId: string, nodeId: string, key
});
}
function currentNode(project: Project, assetId: string, nodeId: string): GraphNode | undefined {
/** The node together with the graph it sits in, because its ports are derived from both. */
function currentNode(
project: Project,
assetId: string,
nodeId: string,
): { node: GraphNode; graph: Graph } | undefined {
const asset = findAsset(project, assetId);
return asset?.type === 'behavior' ? asset.graph.nodes.find((n) => n.id === nodeId) : undefined;
if (asset?.type !== 'behavior') return undefined;
const node = asset.graph.nodes.find((n) => n.id === nodeId);
return node && { node, graph: asset.graph };
}