feat: debounce cameraPosition saving

This commit is contained in:
2026-05-07 21:11:33 +02:00
parent 5fa9d36b34
commit a039bddba1
2 changed files with 55 additions and 33 deletions
@@ -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) {
@@ -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) {
$effect.root(() => {
$effect(() => {
const saveCameraPosition = debounce(() => {
localStorage.setItem(
'cameraPosition',
`[${this.cameraPosition[0]},${this.cameraPosition[1]},${this.cameraPosition[2]}]`
);
}, 500);
$effect.root(() => {
$effect(() => {
// 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,15 +165,11 @@ export class GraphState {
this.edges.delete(edgeId);
}
updateNodePosition(node: NodeInstance) {
if (
node.state.x === node.position[0]
&& node.state.y === node.position[1]
) {
delete node.state.x;
delete node.state.y;
}
private _dirtyPositions = new Set<NodeInstance>();
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`);
@@ -178,6 +182,25 @@ export class GraphState {
}
}
}
this._dirtyPositions.clear();
this._positionFlushPending = false;
}
updateNodePosition(node: NodeInstance) {
if (
node.state.x === node.position[0]
&& node.state.y === node.position[1]
) {
delete node.state.x;
delete node.state.y;
}
this._dirtyPositions.add(node);
if (!this._positionFlushPending) {
this._positionFlushPending = true;
requestAnimationFrame(() => this._flushPositions());
}
}
getSnapLevel() {
const z = this.cameraPosition[2];