From 4c76c62a3e9a0a4cafc4ce873af93f748cb0fbfe Mon Sep 17 00:00:00 2001 From: Max Richter Date: Wed, 21 Jan 2026 11:09:51 +0100 Subject: [PATCH 1/3] feat: add header element --- app/src/lib/graph-interface/graph/Graph.svelte | 2 +- app/src/lib/result-viewer/Viewer.svelte | 2 +- app/src/routes/+page.svelte | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/lib/graph-interface/graph/Graph.svelte b/app/src/lib/graph-interface/graph/Graph.svelte index 00da50c..052af80 100644 --- a/app/src/lib/graph-interface/graph/Graph.svelte +++ b/app/src/lib/graph-interface/graph/Graph.svelte @@ -105,7 +105,7 @@ onwheel={(ev) => mouseEvents.handleMouseScroll(ev)} bind:this={graphState.wrapper} class="graph-wrapper" - style="height: 100vh;" + style="height: 100%;" class:is-panning={graphState.isPanning} class:is-hovering={graphState.hoveredNodeId !== -1} aria-label="Graph" diff --git a/app/src/lib/result-viewer/Viewer.svelte b/app/src/lib/result-viewer/Viewer.svelte index b5735b9..e3b5791 100644 --- a/app/src/lib/result-viewer/Viewer.svelte +++ b/app/src/lib/result-viewer/Viewer.svelte @@ -95,7 +95,7 @@ {/if} -
+
-
+
Date: Wed, 21 Jan 2026 15:02:34 +0100 Subject: [PATCH 2/3] feat: initial working version of project manager --- .../graph-interface/graph-manager.svelte.ts | 3 +- .../lib/graph-interface/graph/Wrapper.svelte | 8 +- .../lib/project-manager/ProjectManager.svelte | 85 +++++++++++++++++ .../project-database.svelte.ts | 54 +++++++++++ .../project-manager/project-manager.svelte.ts | 93 +++++++++++++++++++ app/src/lib/sidebar/Panel.svelte | 3 - app/src/lib/sidebar/panels/GraphSource.svelte | 4 +- app/src/routes/+page.svelte | 49 +++++----- packages/types/src/types.ts | 2 +- packages/ui/src/lib/inputs/Search.svelte | 0 10 files changed, 267 insertions(+), 34 deletions(-) create mode 100644 app/src/lib/project-manager/ProjectManager.svelte create mode 100644 app/src/lib/project-manager/project-database.svelte.ts create mode 100644 app/src/lib/project-manager/project-manager.svelte.ts create mode 100644 packages/ui/src/lib/inputs/Search.svelte diff --git a/app/src/lib/graph-interface/graph-manager.svelte.ts b/app/src/lib/graph-interface/graph-manager.svelte.ts index 0f29368..2cff200 100644 --- a/app/src/lib/graph-interface/graph-manager.svelte.ts +++ b/app/src/lib/graph-interface/graph-manager.svelte.ts @@ -109,6 +109,7 @@ export class GraphManager extends EventEmitter<{ const serialized = { id: this.graph.id, settings: $state.snapshot(this.settings), + meta: this.graph.meta, nodes, edges }; @@ -304,7 +305,7 @@ export class GraphManager extends EventEmitter<{ this.status = 'loading'; this.id = graph.id; - logger.info('loading graph', $state.snapshot(graph)); + logger.info('loading graph', graph); const nodeIds = Array.from(new Set([...graph.nodes.map((n) => n.type)])); await this.registry.load(nodeIds); diff --git a/app/src/lib/graph-interface/graph/Wrapper.svelte b/app/src/lib/graph-interface/graph/Wrapper.svelte index 783b2cf..39af05e 100644 --- a/app/src/lib/graph-interface/graph/Wrapper.svelte +++ b/app/src/lib/graph-interface/graph/Wrapper.svelte @@ -11,7 +11,7 @@ import { setupKeymaps } from "../keymaps"; type Props = { - graph: Graph; + graph?: Graph; registry: NodeRegistry; settings?: Record; @@ -85,7 +85,11 @@ manager.on("save", (save) => onsave?.(save)); - manager.load(graph); + $effect(() => { + if (graph) { + manager.load(graph); + } + }); diff --git a/app/src/lib/project-manager/ProjectManager.svelte b/app/src/lib/project-manager/ProjectManager.svelte new file mode 100644 index 0000000..df09a0c --- /dev/null +++ b/app/src/lib/project-manager/ProjectManager.svelte @@ -0,0 +1,85 @@ + + +
+

Project

+ +
+ +{#if showNewProject} +
+ e.key === "Enter" && handleCreate()} + /> + +
+{/if} + +
+ {#if projectManager.loading} +

Loading...

+ {/if} + +
    + {#each projectManager.projects as project (project.id)} +
  • +
    projectManager.handleSelectProject(project.id!)} + role="button" + tabindex="0" + onkeydown={(e) => + e.key === "Enter" && + projectManager.handleSelectProject(project.id!)} + > +
    + {project.meta?.title || "Untitled"} + +
    +
    +
  • + {/each} +
+
diff --git a/app/src/lib/project-manager/project-database.svelte.ts b/app/src/lib/project-manager/project-database.svelte.ts new file mode 100644 index 0000000..4e852ef --- /dev/null +++ b/app/src/lib/project-manager/project-database.svelte.ts @@ -0,0 +1,54 @@ +import type { Graph } from '@nodarium/types'; +import { type IDBPDatabase, openDB } from 'idb'; + +export interface GraphDatabase { + projects: Graph; +} + +const DB_NAME = 'nodarium-graphs'; +const DB_VERSION = 1; +const STORE_NAME = 'graphs'; + +let dbPromise: Promise> | null = null; + +export function getDB() { + if (!dbPromise) { + dbPromise = openDB(DB_NAME, DB_VERSION, { + upgrade(db) { + if (!db.objectStoreNames.contains(STORE_NAME)) { + db.createObjectStore(STORE_NAME, { keyPath: 'id' }); + } + } + }); + } + return dbPromise; +} + +export async function getGraph(id: number): Promise { + const db = await getDB(); + return db.get(STORE_NAME, id); +} + +export async function saveGraph(graph: Graph): Promise { + const db = await getDB(); + graph.meta = { ...graph.meta, lastModified: new Date().toISOString() }; + console.log('SAVING GRAPH', { graph }); + await db.put(STORE_NAME, graph); + return graph; +} + +export async function deleteGraph(id: number): Promise { + const db = await getDB(); + await db.delete(STORE_NAME, id); + console.log('DELETE GRAPH', { id }); +} + +export async function getGraphs(): Promise { + const db = await getDB(); + return db.getAll(STORE_NAME); +} + +export async function clear(): Promise { + const db = await getDB(); + return db.clear(STORE_NAME); +} diff --git a/app/src/lib/project-manager/project-manager.svelte.ts b/app/src/lib/project-manager/project-manager.svelte.ts new file mode 100644 index 0000000..8af41d3 --- /dev/null +++ b/app/src/lib/project-manager/project-manager.svelte.ts @@ -0,0 +1,93 @@ +import * as templates from '$lib/graph-templates'; +import { localState } from '$lib/helpers/localState.svelte'; +import type { Graph } from '@nodarium/types'; +import * as db from './project-database.svelte'; + +export class ProjectManager { + public graph = $state(); + private projects = $state([]); + private activeProjectId = localState( + 'node.activeProjectId', + undefined + ); + public readonly loading = $derived(this.graph?.id !== this.activeProjectId.value); + + constructor() { + this.init(); + } + + async saveGraph(g: Graph) { + db.saveGraph(g); + } + + private async init() { + await db.getDB(); + this.projects = await db.getGraphs(); + + console.log('PM: INIT', { + projects: this.projects, + activeProjectId: this.activeProjectId.value + }); + + if (this.activeProjectId.value !== undefined) { + let loadedGraph = await db.getGraph(this.activeProjectId.value); + console.log('PM: LOAD ACTIVE PROJECT', { loadedGraph }); + if (loadedGraph) { + console.log('Load active project'); + this.graph = loadedGraph; + } + } + + if (!this.graph) { + console.log('Load first active project', { projectsAmount: this.projects.length }); + if (this.projects?.length && this.projects[0]?.id !== undefined) { + this.graph = this.projects[0]; + this.activeProjectId.value = this.graph.id; + } + } + + if (!this.graph) { + console.log('Create default project'); + this.handleCreateProject(); + } + } + + public handleCreateProject( + g: Graph = templates.defaultPlant as unknown as Graph, + title: string = 'New Project' + ) { + let id = g?.id || 0; + while (this.projects.find((p) => p.id === id)) { + id++; + } + + console.log('CREATE PROJECT', { id, title }); + + g.id = id; + if (!g.meta) g.meta = {}; + if (!g.meta.title) g.meta.title = title; + + db.saveGraph(g); + this.projects = [...this.projects, g]; + this.handleSelectProject(id); + } + + public async handleDeleteProject(projectId: number) { + await db.deleteGraph(projectId); + if (this.projects.length === 1) { + this.graph = undefined; + this.projects = []; + } else { + this.projects = this.projects.filter((p) => p.id !== projectId); + this.handleSelectProject(this.projects[0].id); + } + } + + public async handleSelectProject(id: number) { + if (this.activeProjectId.value !== id) { + const project = await db.getGraph(id); + this.graph = project; + this.activeProjectId.value = id; + } + } +} diff --git a/app/src/lib/sidebar/Panel.svelte b/app/src/lib/sidebar/Panel.svelte index 19d90c0..e079e9c 100644 --- a/app/src/lib/sidebar/Panel.svelte +++ b/app/src/lib/sidebar/Panel.svelte @@ -43,7 +43,4 @@ align-items: center; padding-left: 1em; } - h3 { - margin: 0px; - } diff --git a/app/src/lib/sidebar/panels/GraphSource.svelte b/app/src/lib/sidebar/panels/GraphSource.svelte index 37b3552..0866b3e 100644 --- a/app/src/lib/sidebar/panels/GraphSource.svelte +++ b/app/src/lib/sidebar/panels/GraphSource.svelte @@ -1,7 +1,7 @@
-  {convert(graph)}
+  {graph ? convert(graph) : 'No graph loaded'}
 
diff --git a/app/src/routes/+page.svelte b/app/src/routes/+page.svelte index 26a98cd..ea996ce 100644 --- a/app/src/routes/+page.svelte +++ b/app/src/routes/+page.svelte @@ -28,6 +28,8 @@ import BenchmarkPanel from "$lib/sidebar/panels/BenchmarkPanel.svelte"; import { debounceAsyncFunction } from "$lib/helpers"; import GraphSource from "$lib/sidebar/panels/GraphSource.svelte"; + import { ProjectManager } from "$lib/project-manager/project-manager.svelte"; + import ProjectManagerEl from "$lib/project-manager/ProjectManager.svelte"; let performanceStore = createPerformanceStore(); @@ -37,6 +39,7 @@ const runtimeCache = new MemoryRuntimeCache(); const memoryRuntime = new MemoryRuntimeExecutor(nodeRegistry, runtimeCache); memoryRuntime.perf = performanceStore; + const pm = new ProjectManager(); const runtime = $derived( appSettings.value.debug.useWorker ? workerRuntime : memoryRuntime, @@ -64,15 +67,6 @@ let activeNode = $state(undefined); let scene = $state(null!); - let graph = $state( - localStorage.getItem("graph") - ? JSON.parse(localStorage.getItem("graph")!) - : templates.defaultPlant, - ); - function handleSave(graph: Graph) { - localStorage.setItem("graph", JSON.stringify(graph)); - } - let graphInterface = $state>(null!); let viewerComponent = $state>(); const manager = $derived(graphInterface?.manager); @@ -172,7 +166,7 @@
-
+
- handleUpdate(result)} - onsave={(graph) => handleSave(graph)} - /> + {#if pm.graph} + pm.saveGraph(g)} + onresult={(result) => handleUpdate(result)} + /> + {/if} {/if} + + + Date: Wed, 21 Jan 2026 16:01:11 +0100 Subject: [PATCH 3/3] chore: remove some old console.logs --- app/src/lib/graph-interface/debug/index.ts | 2 +- .../graph-interface/graph-manager.svelte.ts | 15 ++++--- .../lib/graph-interface/graph/Wrapper.svelte | 6 --- app/src/lib/node-registry.ts | 1 - .../lib/project-manager/ProjectManager.svelte | 43 ++++++++++++++----- .../project-database.svelte.ts | 2 - .../project-manager/project-manager.svelte.ts | 16 ++----- app/src/lib/result-viewer/Scene.svelte | 7 --- app/src/routes/+page.svelte | 15 +++---- app/src/routes/dev/+page.svelte | 1 - packages/types/src/types.ts | 2 +- 11 files changed, 54 insertions(+), 56 deletions(-) diff --git a/app/src/lib/graph-interface/debug/index.ts b/app/src/lib/graph-interface/debug/index.ts index 34d66e6..cd95315 100644 --- a/app/src/lib/graph-interface/debug/index.ts +++ b/app/src/lib/graph-interface/debug/index.ts @@ -1,4 +1,5 @@ import type { Box } from '@nodarium/types'; +import type { Color } from 'three'; import { Vector3 } from 'three/src/math/Vector3.js'; import Component from './Debug.svelte'; import { lines, points, rects } from './store'; @@ -11,7 +12,6 @@ export function debugPosition(x: number, y: number) { } export function debugRect(rect: Box) { - console.log(rect); rects.update((r) => { r.push(rect); return r; diff --git a/app/src/lib/graph-interface/graph-manager.svelte.ts b/app/src/lib/graph-interface/graph-manager.svelte.ts index 2cff200..b47a664 100644 --- a/app/src/lib/graph-interface/graph-manager.svelte.ts +++ b/app/src/lib/graph-interface/graph-manager.svelte.ts @@ -109,7 +109,7 @@ export class GraphManager extends EventEmitter<{ const serialized = { id: this.graph.id, settings: $state.snapshot(this.settings), - meta: this.graph.meta, + meta: $state.snapshot(this.graph.meta), nodes, edges }; @@ -274,7 +274,7 @@ export class GraphManager extends EventEmitter<{ }) ); - const edges = graph.edges.map((edge) => { + this.edges = graph.edges.map((edge) => { const from = nodes.get(edge[0]); const to = nodes.get(edge[2]); if (!from || !to) { @@ -287,8 +287,6 @@ export class GraphManager extends EventEmitter<{ return [from, edge[1], to, edge[3]] as Edge; }); - this.edges = [...edges]; - this.nodes.clear(); for (const [id, node] of nodes) { this.nodes.set(id, node); @@ -305,7 +303,7 @@ export class GraphManager extends EventEmitter<{ this.status = 'loading'; this.id = graph.id; - logger.info('loading graph', graph); + logger.info('loading graph', { nodes: graph.nodes, edges: graph.edges, id: graph.id }); const nodeIds = Array.from(new Set([...graph.nodes.map((n) => n.type)])); await this.registry.load(nodeIds); @@ -645,6 +643,13 @@ export class GraphManager extends EventEmitter<{ 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; + } + this.emit('save', state); logger.log('saving graphs', state); } diff --git a/app/src/lib/graph-interface/graph/Wrapper.svelte b/app/src/lib/graph-interface/graph/Wrapper.svelte index 39af05e..3e08e8e 100644 --- a/app/src/lib/graph-interface/graph/Wrapper.svelte +++ b/app/src/lib/graph-interface/graph/Wrapper.svelte @@ -70,12 +70,6 @@ } }); - $effect(() => { - if (settingTypes && settings) { - manager.setSettings(settings); - } - }); - manager.on("settings", (_settings) => { settingTypes = { ...settingTypes, ..._settings.types }; settings = _settings.values; diff --git a/app/src/lib/node-registry.ts b/app/src/lib/node-registry.ts index 9594c61..b6d03e6 100644 --- a/app/src/lib/node-registry.ts +++ b/app/src/lib/node-registry.ts @@ -4,7 +4,6 @@ import path from 'path'; export async function getWasm(id: `${string}/${string}/${string}`) { const filePath = path.resolve(`./static/nodes/${id}`); - console.log({ filePath }); try { await fs.access(filePath); diff --git a/app/src/lib/project-manager/ProjectManager.svelte b/app/src/lib/project-manager/ProjectManager.svelte index df09a0c..efdf483 100644 --- a/app/src/lib/project-manager/ProjectManager.svelte +++ b/app/src/lib/project-manager/ProjectManager.svelte @@ -1,18 +1,32 @@ diff --git a/app/src/routes/+page.svelte b/app/src/routes/+page.svelte index ea996ce..6d89f4a 100644 --- a/app/src/routes/+page.svelte +++ b/app/src/routes/+page.svelte @@ -85,21 +85,16 @@ callback: () => randomGenerate(), }, ]); + let graphSettings = $state>({}); + let graphSettingTypes = $state({ + randomSeed: { type: "boolean", value: false }, + }); $effect(() => { - if (graphSettings) { + if (graphSettings && graphSettingTypes) { manager?.setSettings($state.snapshot(graphSettings)); } }); - type BooleanSchema = { - [key: string]: { - type: "boolean"; - value: false; - }; - }; - let graphSettingTypes = $state({ - randomSeed: { type: "boolean", value: false }, - }); async function update( g: Graph, diff --git a/app/src/routes/dev/+page.svelte b/app/src/routes/dev/+page.svelte index ebe4081..6166fa2 100644 --- a/app/src/routes/dev/+page.svelte +++ b/app/src/routes/dev/+page.svelte @@ -26,7 +26,6 @@ let nodeWasmWrapper = $state>(); async function fetchNodeData(nodeId?: NodeId) { - console.log("FETCHING", { nodeId }); nodeWasm = undefined; nodeInstance = undefined; diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index b627f2f..2a9167b 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -77,7 +77,7 @@ export type Socket = { export type Edge = [NodeInstance, number, NodeInstance, string]; export const GraphSchema = z.object({ - id: z.number().optional(), + id: z.number(), meta: z .object({ title: z.string().optional(), -- 2.49.1