From 9a7a7166b79ab4c980a1830b2359556fb479d41d Mon Sep 17 00:00:00 2001 From: Max Richter Date: Thu, 7 May 2026 17:39:58 +0200 Subject: [PATCH] fix: pasting nodes --- .../graph-interface/graph-manager.svelte.ts | 18 ++++---- .../lib/graph-interface/graph-state.svelte.ts | 45 ++++++++++--------- .../lib/graph-interface/graph/Graph.svelte | 2 +- app/src/lib/sidebar/panels/GraphSource.svelte | 2 +- 4 files changed, 37 insertions(+), 30 deletions(-) diff --git a/app/src/lib/graph-interface/graph-manager.svelte.ts b/app/src/lib/graph-interface/graph-manager.svelte.ts index fd889f3..0b3da39 100644 --- a/app/src/lib/graph-interface/graph-manager.svelte.ts +++ b/app/src/lib/graph-interface/graph-manager.svelte.ts @@ -742,25 +742,26 @@ export class GraphManager extends EventEmitter<{ return id; } - createGraph(nodes: NodeInstance[], edges: [number, number, number, string][]) { + createGraph(nodes: SerializedNode[], edges: [number, number, number, string][]) { // map old ids to new ids const idMap = new SvelteMap(); let startId = this.createNodeId(); - nodes = nodes.map((node) => { + const instances: NodeInstance[] = nodes.map((node) => { const id = startId++; idMap.set(node.id, id); const type = this.registry.getNode(node.type); if (!type && !node.type.startsWith('__internal/')) { 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 from = nodes.find((n) => n.id === idMap.get(edge[0])); - const to = nodes.find((n) => n.id === idMap.get(edge[2])); + const from = instances.find((n) => n.id === idMap.get(edge[0])); + const to = instances.find((n) => n.id === idMap.get(edge[2])); if (!from || !to) { 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; }); - for (const node of nodes) { - this.nodes.set(node.id, node); + for (const node of instances) { + const n = $state(node); + this.nodes.set(node.id, n); } this.edges.push(..._edges); this.save(); - return nodes; + return instances; } getUnusedGroups() { diff --git a/app/src/lib/graph-interface/graph-state.svelte.ts b/app/src/lib/graph-interface/graph-state.svelte.ts index db7bcfc..0a6fdc8 100644 --- a/app/src/lib/graph-interface/graph-state.svelte.ts +++ b/app/src/lib/graph-interface/graph-state.svelte.ts @@ -1,11 +1,16 @@ 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 { SvelteMap, SvelteSet } from 'svelte/reactivity'; import type { OrthographicCamera, Vector3 } from 'three'; import type { GraphManager } from './graph-manager.svelte'; import { ColorGenerator } from './graph/colors'; -import { getNodeHeight, getParameterHeight } from './helpers/nodeHelpers'; +import { + getNodeHeight, + getParameterHeight, + serializeEdge, + serializeNode +} from './helpers/nodeHelpers'; const graphStateKey = Symbol('graph-state'); export function getGraphState() { @@ -95,8 +100,8 @@ export class GraphState { cameraPosition: [number, number, number] = $state([140, 100, 3.5]); clipboard: null | { - nodes: NodeInstance[]; - edges: [number, number, number, string][]; + nodes: SerializedNode[]; + edges: SerializedEdge[]; } = null; cameraBounds = $derived([ @@ -190,12 +195,10 @@ export class GraphState { if (this.activeNodeId === -1 && !this.selectedNodes?.size) { return; } - let nodes = [ - this.activeNodeId, - ...(this.selectedNodes?.values() || []) - ] + const ids = new SvelteSet([this.activeNodeId, ...(this.selectedNodes?.values() || [])]); + let nodes = [...ids] .map((id) => this.graph.getNode(id)) - .filter(b => !!b); + .filter((b): b is NodeInstance => !!b); const edges = this.graph.getEdgesBetweenNodes(nodes); nodes = nodes.map((node) => ({ @@ -203,13 +206,12 @@ export class GraphState { position: [ this.mousePosition[0] - node.position[0], this.mousePosition[1] - node.position[1] - ], - tmp: undefined + ] })); this.clipboard = { - nodes: nodes, - edges: edges + nodes: nodes.map(n => serializeNode(n)), + edges: edges.map(e => serializeEdge(e)) }; } @@ -255,13 +257,16 @@ export class GraphState { pasteNodes() { if (!this.clipboard) return; - const nodes = this.clipboard.nodes - .map((node) => { - node.position[0] = this.mousePosition[0] - node.position[0]; - node.position[1] = this.mousePosition[1] - node.position[1]; - return node; - }) - .filter(Boolean) as NodeInstance[]; + // Create fresh node objects — never mutate clipboard so repeat pastes work correctly. + // State is also spread (with cleared parents/children) so createGraph's mutations + // don't corrupt the clipboard's stored state references. + const nodes = this.clipboard.nodes.map((node) => ({ + ...node, + position: [ + this.mousePosition[0] - node.position[0], + this.mousePosition[1] - node.position[1] + ] as [number, number] + })); const newNodes = this.graph.createGraph(nodes, this.clipboard.edges); this.selectedNodes.clear(); diff --git a/app/src/lib/graph-interface/graph/Graph.svelte b/app/src/lib/graph-interface/graph/Graph.svelte index 89e2fab..b57dfbe 100644 --- a/app/src/lib/graph-interface/graph/Graph.svelte +++ b/app/src/lib/graph-interface/graph/Graph.svelte @@ -228,7 +228,7 @@ style:transform={`scale(${graphState.cameraPosition[2] * 0.1})`} class:hovering-sockets={graphState.activeSocket} > - {#each graph.nodeArray as node, index (node.id)} + {#each graph.nodeArray as node, index (node)} ({ ...n, tmp: undefined, state: undefined })) + nodes: graph.nodes.map((n: object) => ({ ...n, state: undefined })) } : null );