fix: pasting nodes

This commit is contained in:
2026-05-07 17:39:58 +02:00
parent 4aff3874d3
commit 9a7a7166b7
4 changed files with 37 additions and 30 deletions
@@ -742,25 +742,26 @@ export class GraphManager extends EventEmitter<{
return id; return id;
} }
createGraph(nodes: NodeInstance[], edges: [number, number, number, string][]) { createGraph(nodes: SerializedNode[], edges: [number, number, number, string][]) {
// map old ids to new ids // map old ids to new ids
const idMap = new SvelteMap<number, number>(); const idMap = new SvelteMap<number, number>();
let startId = this.createNodeId(); let startId = this.createNodeId();
nodes = nodes.map((node) => { const instances: NodeInstance[] = nodes.map((node) => {
const id = startId++; const id = startId++;
idMap.set(node.id, id); idMap.set(node.id, id);
const type = this.registry.getNode(node.type); const type = this.registry.getNode(node.type);
if (!type && !node.type.startsWith('__internal/')) { if (!type && !node.type.startsWith('__internal/')) {
throw new Error(`Node type not found: ${node.type}`); throw new Error(`Node type not found: ${node.type}`);
} }
return { ...node, id, tmp: { type } }; const registryType = this.registry.getNode(node.type);
return { ...node, id, state: { type: registryType } };
}); });
const _edges = edges.map((edge) => { const _edges = edges.map((edge) => {
const from = nodes.find((n) => n.id === idMap.get(edge[0])); const from = instances.find((n) => n.id === idMap.get(edge[0]));
const to = nodes.find((n) => n.id === idMap.get(edge[2])); const to = instances.find((n) => n.id === idMap.get(edge[2]));
if (!from || !to) { if (!from || !to) {
throw new Error('Edge references non-existing node'); throw new Error('Edge references non-existing node');
@@ -775,14 +776,15 @@ export class GraphManager extends EventEmitter<{
return [from, edge[1], to, edge[3]] as Edge; return [from, edge[1], to, edge[3]] as Edge;
}); });
for (const node of nodes) { for (const node of instances) {
this.nodes.set(node.id, node); const n = $state(node);
this.nodes.set(node.id, n);
} }
this.edges.push(..._edges); this.edges.push(..._edges);
this.save(); this.save();
return nodes; return instances;
} }
getUnusedGroups() { getUnusedGroups() {
@@ -1,11 +1,16 @@
import { animate, lerp } from '$lib/helpers'; import { animate, lerp } from '$lib/helpers';
import type { NodeInstance, Socket } from '@nodarium/types'; import type { NodeInstance, SerializedEdge, SerializedNode, Socket } from '@nodarium/types';
import { getContext, setContext } from 'svelte'; import { getContext, setContext } from 'svelte';
import { SvelteMap, SvelteSet } from 'svelte/reactivity'; import { SvelteMap, SvelteSet } from 'svelte/reactivity';
import type { OrthographicCamera, Vector3 } from 'three'; import type { OrthographicCamera, Vector3 } from 'three';
import type { GraphManager } from './graph-manager.svelte'; import type { GraphManager } from './graph-manager.svelte';
import { ColorGenerator } from './graph/colors'; import { ColorGenerator } from './graph/colors';
import { getNodeHeight, getParameterHeight } from './helpers/nodeHelpers'; import {
getNodeHeight,
getParameterHeight,
serializeEdge,
serializeNode
} from './helpers/nodeHelpers';
const graphStateKey = Symbol('graph-state'); const graphStateKey = Symbol('graph-state');
export function getGraphState() { export function getGraphState() {
@@ -95,8 +100,8 @@ export class GraphState {
cameraPosition: [number, number, number] = $state([140, 100, 3.5]); cameraPosition: [number, number, number] = $state([140, 100, 3.5]);
clipboard: null | { clipboard: null | {
nodes: NodeInstance[]; nodes: SerializedNode[];
edges: [number, number, number, string][]; edges: SerializedEdge[];
} = null; } = null;
cameraBounds = $derived([ cameraBounds = $derived([
@@ -190,12 +195,10 @@ export class GraphState {
if (this.activeNodeId === -1 && !this.selectedNodes?.size) { if (this.activeNodeId === -1 && !this.selectedNodes?.size) {
return; return;
} }
let nodes = [ const ids = new SvelteSet([this.activeNodeId, ...(this.selectedNodes?.values() || [])]);
this.activeNodeId, let nodes = [...ids]
...(this.selectedNodes?.values() || [])
]
.map((id) => this.graph.getNode(id)) .map((id) => this.graph.getNode(id))
.filter(b => !!b); .filter((b): b is NodeInstance => !!b);
const edges = this.graph.getEdgesBetweenNodes(nodes); const edges = this.graph.getEdgesBetweenNodes(nodes);
nodes = nodes.map((node) => ({ nodes = nodes.map((node) => ({
@@ -203,13 +206,12 @@ export class GraphState {
position: [ position: [
this.mousePosition[0] - node.position[0], this.mousePosition[0] - node.position[0],
this.mousePosition[1] - node.position[1] this.mousePosition[1] - node.position[1]
], ]
tmp: undefined
})); }));
this.clipboard = { this.clipboard = {
nodes: nodes, nodes: nodes.map(n => serializeNode(n)),
edges: edges edges: edges.map(e => serializeEdge(e))
}; };
} }
@@ -255,13 +257,16 @@ export class GraphState {
pasteNodes() { pasteNodes() {
if (!this.clipboard) return; if (!this.clipboard) return;
const nodes = this.clipboard.nodes // Create fresh node objects — never mutate clipboard so repeat pastes work correctly.
.map((node) => { // State is also spread (with cleared parents/children) so createGraph's mutations
node.position[0] = this.mousePosition[0] - node.position[0]; // don't corrupt the clipboard's stored state references.
node.position[1] = this.mousePosition[1] - node.position[1]; const nodes = this.clipboard.nodes.map((node) => ({
return node; ...node,
}) position: [
.filter(Boolean) as NodeInstance[]; this.mousePosition[0] - node.position[0],
this.mousePosition[1] - node.position[1]
] as [number, number]
}));
const newNodes = this.graph.createGraph(nodes, this.clipboard.edges); const newNodes = this.graph.createGraph(nodes, this.clipboard.edges);
this.selectedNodes.clear(); this.selectedNodes.clear();
@@ -228,7 +228,7 @@
style:transform={`scale(${graphState.cameraPosition[2] * 0.1})`} style:transform={`scale(${graphState.cameraPosition[2] * 0.1})`}
class:hovering-sockets={graphState.activeSocket} class:hovering-sockets={graphState.activeSocket}
> >
{#each graph.nodeArray as node, index (node.id)} {#each graph.nodeArray as node, index (node)}
<NodeEl <NodeEl
bind:node={graph.nodeArray[index]} bind:node={graph.nodeArray[index]}
inView={node ? graphState.isNodeInView(node) : false} inView={node ? graphState.isNodeInView(node) : false}
@@ -8,7 +8,7 @@
graph graph
? { ? {
...graph, ...graph,
nodes: graph.nodes.map((n: object) => ({ ...n, tmp: undefined, state: undefined })) nodes: graph.nodes.map((n: object) => ({ ...n, state: undefined }))
} }
: null : null
); );