Fix findUsages/undo/node-lookup bugs, split Viewport and Editor, share command and inspector helpers

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-08 09:22:49 +02:00
1 parent 1eb28314cf
commit f140c2fd77
58 files changed
+1969 -1171

No files matched your search

+31 -29
View File
@@ -1,12 +1,13 @@
import { edgesInto, findNode, type GraphEdge, type GraphNode, portsCompatible } from '../graph/Graph';
import { requireNodeDef } from '../graph/NodeRegistry';
import { edgesInto, edgesOfNode, findNode, type GraphEdge, type GraphNode, portsCompatible } from '../graph/Graph';
import { nodeDef } from '../graph/NodeRegistry';
import { newId } from '../ids';
import { type PropertyBag, type PropertyValue, setBagValue } from '../model/Properties';
import type { PropertyBag, PropertyValue } from '../model/Properties';
import type { Project } from '../model/Project';
import { coerceValue, defaultsOf } from '../properties/PropertySchema';
import { type Removed, removeWhere, restoreAt } from '../util/list';
import { type Removed, removeById, removeFirst, removeWhere, restoreAt } from '../util/list';
import { type Command, CommandError } from './Command';
import { requireBehavior, requireNode } from './require';
import { requireBehavior, requireNode, requireNodeDef } from './require';
import { SetBagValueCommand } from './SetBagValueCommand';
/** Adds a node of the given type at graph coordinates. Params start at their defaults. */
export class AddNodeCommand implements Command {
@@ -14,18 +15,21 @@ export class AddNodeCommand implements Command {
readonly node: GraphNode;
constructor(private readonly assetId: string, type: string, x: number, y: number, params: PropertyBag = {}) {
const def = requireNodeDef(type);
this.node = { id: newId(), type, x: Math.round(x), y: Math.round(y), params: { ...defaultsOf(def.params), ...params } };
this.label = `Add ${def.label}`;
// An unknown type is rejected in `execute`, not here: constructors run outside the
// editor's error handling, so throwing here would crash instead of showing a notice.
const def = nodeDef(type);
this.node = { id: newId(), type, x: Math.round(x), y: Math.round(y), params: { ...defaultsOf(def?.params ?? []), ...params } };
this.label = def ? `Add ${def.label}` : `Add ${type}`;
}
execute(project: Project): void {
requireNodeDef(this.node.type);
requireBehavior(project, this.assetId).graph.nodes.push(this.node);
}
undo(project: Project): void {
const graph = requireBehavior(project, this.assetId).graph;
graph.nodes = graph.nodes.filter((n) => n.id !== this.node.id);
graph.nodes = removeById(graph.nodes, this.node.id);
}
}
@@ -38,12 +42,11 @@ export class RemoveNodeCommand implements Command {
execute(project: Project): void {
const graph = requireBehavior(project, this.assetId).graph;
const index = graph.nodes.findIndex((n) => n.id === this.nodeId);
const node = graph.nodes[index];
const node = removeFirst(graph.nodes, (n) => n.id === this.nodeId);
if (!node) throw new CommandError(`Node ${this.nodeId} not found`);
const edges = removeWhere(graph.edges, (e) => e.from.node === this.nodeId || e.to.node === this.nodeId);
graph.nodes.splice(index, 1);
this.removed = { node: { item: node, index }, edges };
// `edgesOfNode` reads the edge list only, so it still finds them after the node is gone.
const touching = edgesOfNode(graph, this.nodeId);
this.removed = { node, edges: removeWhere(graph.edges, (e) => touching.includes(e)) };
}
undo(project: Project): void {
@@ -79,24 +82,23 @@ export class MoveNodeCommand implements Command {
}
/** Sets a node param or the inline value of an unconnected data input. */
export class SetNodeParamCommand implements Command {
readonly label: string;
private previous: PropertyValue | undefined;
constructor(private readonly assetId: string, private readonly nodeId: string, private readonly key: string, private readonly value: PropertyValue) {
this.label = `Set ${key}`;
export class SetNodeParamCommand extends SetBagValueCommand<GraphNode> {
constructor(private readonly assetId: string, private readonly nodeId: string, key: string, private readonly value: PropertyValue) {
super(key, `Set ${key}`);
}
execute(project: Project): void {
const node = requireNode(project, this.assetId, this.nodeId);
protected override resolveTarget(project: Project): GraphNode {
return requireNode(project, this.assetId, this.nodeId);
}
protected override bagOf(node: GraphNode): PropertyBag {
return node.params;
}
protected override computeNext(node: GraphNode): PropertyValue {
const param = requireNodeDef(node.type).params.find((p) => p.key === this.key);
this.previous = node.params[this.key];
// Inline input values are stored as-is; declared params are coerced against their definition.
node.params[this.key] = param && param.kind !== 'enum' ? coerceValue(param, this.value) : this.value;
}
undo(project: Project): void {
setBagValue(requireNode(project, this.assetId, this.nodeId).params, this.key, this.previous);
return param && param.kind !== 'enum' ? coerceValue(param, this.value) : this.value;
}
}
@@ -131,7 +133,7 @@ export class ConnectCommand implements Command {
undo(project: Project): void {
const graph = requireBehavior(project, this.assetId).graph;
graph.edges = graph.edges.filter((e) => e.id !== this.edge.id);
graph.edges = removeById(graph.edges, this.edge.id);
restoreAt(graph.edges, this.replaced);
}
}