diff --git a/app/src/lib/graph-interface/graph-manager.svelte.ts b/app/src/lib/graph-interface/graph-manager.svelte.ts index 0b3da39..30be17b 100644 --- a/app/src/lib/graph-interface/graph-manager.svelte.ts +++ b/app/src/lib/graph-interface/graph-manager.svelte.ts @@ -1,4 +1,4 @@ -import { clone } from '$lib/helpers'; +import { clone, debounce } from '$lib/helpers'; import throttle from '$lib/helpers/throttle'; import { RemoteNodeRegistry } from '$lib/node-registry/index'; import type { @@ -309,17 +309,18 @@ export class GraphManager extends EventEmitter<{ this.nodes.set(n.id, n); } - this.edges = graph.edges.map((edge) => { + this.edges = graph.edges.flatMap((edge) => { const from = this.nodes.get(edge[0]); const to = this.nodes.get(edge[2]); if (!from || !to) { - throw new Error('Edge references non-existing node'); + log.warn('Dropping orphaned edge', edge); + return []; } from.state.children = from.state.children || []; from.state.children.push(to); to.state.parents = to.state.parents || []; to.state.parents.push(from); - return [from, edge[1], to, edge[3]] as Edge; + return [[from, edge[1], to, edge[3]] as Edge]; }); this.execute(); @@ -657,9 +658,9 @@ export class GraphManager extends EventEmitter<{ const inputs = Object.entries(to.state?.type?.inputs ?? {}); const outputs = from.state?.type?.outputs ?? []; for (let i = 0; i < inputs.length; i++) { - const [inputName, input] = inputs[0]; + const [inputName, input] = inputs[i]; for (let o = 0; o < outputs.length; o++) { - const output = outputs[0]; + const output = outputs[o]; if (input.type === output) { return this.createEdge(from, o, to, inputName); } @@ -1239,20 +1240,18 @@ export class GraphManager extends EventEmitter<{ this.save(); } + private _emitSave = debounce(() => { + if (this.nodes.size === 0 && this.edges.length === 0) return; + const state = this.serialize(); + this.emit('save', state); + log.log('saving graphs', state); + }, 300); + save() { if (this.currentUndoGroup) return; - const state = this.serialize(); - this.history.save(state); - - // This is some stupid race condition where the graph-manager emits a save event - // when the graph is not fully loaded - if (this.nodes.size === 0 && this.edges.length === 0) { - return; - } - - const fullState = this.serialize(); - this.emit('save', fullState); - log.log('saving graphs', fullState); + // History snapshot is immediate; the IDB emit is debounced. + this.history.save(this.serialize()); + this._emitSave(); } getParentsOfNode(node: NodeInstance) { diff --git a/app/src/lib/graph-interface/graph-state.svelte.ts b/app/src/lib/graph-interface/graph-state.svelte.ts index 0a6fdc8..b800d1a 100644 --- a/app/src/lib/graph-interface/graph-state.svelte.ts +++ b/app/src/lib/graph-interface/graph-state.svelte.ts @@ -1,4 +1,4 @@ -import { animate, lerp } from '$lib/helpers'; +import { animate, debounce, lerp } from '$lib/helpers'; import type { NodeInstance, SerializedEdge, SerializedNode, Socket } from '@nodarium/types'; import { getContext, setContext } from 'svelte'; import { SvelteMap, SvelteSet } from 'svelte/reactivity'; @@ -62,12 +62,20 @@ export class GraphState { colors = new ColorGenerator(predefinedColors); constructor(private graph: GraphManager) { + const saveCameraPosition = debounce(() => { + localStorage.setItem( + 'cameraPosition', + `[${this.cameraPosition[0]},${this.cameraPosition[1]},${this.cameraPosition[2]}]` + ); + }, 500); + $effect.root(() => { $effect(() => { - localStorage.setItem( - 'cameraPosition', - `[${this.cameraPosition[0]},${this.cameraPosition[1]},${this.cameraPosition[2]}]` - ); + // Read values to subscribe to reactivity, then flush lazily. + void this.cameraPosition[0]; + void this.cameraPosition[1]; + void this.cameraPosition[2]; + saveCameraPosition(); }); }); const storedPosition = localStorage.getItem('cameraPosition'); @@ -157,6 +165,27 @@ export class GraphState { this.edges.delete(edgeId); } + private _dirtyPositions = new Set(); + private _positionFlushPending = false; + + private _flushPositions() { + for (const node of this._dirtyPositions) { + if (node.state['x'] !== undefined && node.state['y'] !== undefined) { + if (node.state.ref) { + node.state.ref.style.setProperty('--nx', `${node.state.x * 10}px`); + node.state.ref.style.setProperty('--ny', `${node.state.y * 10}px`); + } + } else { + if (node.state.ref) { + node.state.ref.style.setProperty('--nx', `${node.position[0] * 10}px`); + node.state.ref.style.setProperty('--ny', `${node.position[1] * 10}px`); + } + } + } + this._dirtyPositions.clear(); + this._positionFlushPending = false; + } + updateNodePosition(node: NodeInstance) { if ( node.state.x === node.position[0] @@ -166,16 +195,10 @@ export class GraphState { delete node.state.y; } - if (node.state['x'] !== undefined && node.state['y'] !== undefined) { - if (node.state.ref) { - node.state.ref.style.setProperty('--nx', `${node.state.x * 10}px`); - node.state.ref.style.setProperty('--ny', `${node.state.y * 10}px`); - } - } else { - if (node.state.ref) { - node.state.ref.style.setProperty('--nx', `${node.position[0] * 10}px`); - node.state.ref.style.setProperty('--ny', `${node.position[1] * 10}px`); - } + this._dirtyPositions.add(node); + if (!this._positionFlushPending) { + this._positionFlushPending = true; + requestAnimationFrame(() => this._flushPositions()); } }