chore: pnpm format
📊 Benchmark the Runtime / benchmark (pull_request) Successful in 1m2s
🚀 Lint & Test & Deploy / quality (pull_request) Successful in 2m30s
🚀 Lint & Test & Deploy / test-unit (pull_request) Successful in 32s
🚀 Lint & Test & Deploy / test-e2e (pull_request) Failing after 33s
🚀 Lint & Test & Deploy / deploy (pull_request) Has been skipped

This commit is contained in:
2026-05-04 15:00:40 +02:00
parent e695c76490
commit d4910aba8c
4 changed files with 105 additions and 83 deletions
+13 -3
View File
@@ -1,13 +1,23 @@
import type { Graph } from '@nodarium/types';
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { expandGroups } from './runtime-executor'; import { expandGroups } from './runtime-executor';
import type { Graph } from '@nodarium/types';
// Helpers to build minimal serialized nodes/edges // Helpers to build minimal serialized nodes/edges
function node(id: number, type: string, props?: Record<string, number>) { function node(id: number, type: string, props?: Record<string, number>) {
return { id, type: type as Graph['nodes'][0]['type'], position: [0, 0] as [number, number], ...(props ? { props } : {}) }; return {
id,
type: type as Graph['nodes'][0]['type'],
position: [0, 0] as [number, number],
...(props ? { props } : {})
};
} }
function edge(from: number, fromSocket: number, to: number, toSocket: string): [number, number, number, string] { function edge(
from: number,
fromSocket: number,
to: number,
toSocket: string
): [number, number, number, string] {
return [from, fromSocket, to, toSocket]; return [from, fromSocket, to, toSocket];
} }
+47 -35
View File
@@ -47,6 +47,7 @@ User Interaction
``` ```
**Event flow:** **Event flow:**
1. User edits graph → GraphManager mutates state 1. User edits graph → GraphManager mutates state
2. GraphManager emits `save` → ProjectManager persists to IndexDB 2. GraphManager emits `save` → ProjectManager persists to IndexDB
3. GraphManager emits `result` → Runtime executes graph → Viewer updates 3. GraphManager emits `result` → Runtime executes graph → Viewer updates
@@ -56,7 +57,7 @@ User Interaction
## Critical Files ## Critical Files
| File | Role | | File | Role |
|------|------| | ------------------------------------------------------ | --------------------------------------------------------------------- |
| `app/src/routes/+page.svelte` | Wires all systems; creates GraphManager, runtime, registry | | `app/src/routes/+page.svelte` | Wires all systems; creates GraphManager, runtime, registry |
| `app/src/lib/graph-interface/graph-manager.svelte.ts` | Central graph logic: createNode, createEdge, serialize, load, history | | `app/src/lib/graph-interface/graph-manager.svelte.ts` | Central graph logic: createNode, createEdge, serialize, load, history |
| `app/src/lib/graph-interface/graph-state.svelte.ts` | UI state: camera, selection, mouse, clipboard, groupSelectedNodes | | `app/src/lib/graph-interface/graph-state.svelte.ts` | UI state: camera, selection, mouse, clipboard, groupSelectedNodes |
@@ -83,54 +84,56 @@ User Interaction
```typescript ```typescript
// packages/types/src/types.ts // packages/types/src/types.ts
type NodeId = `${string}/${string}/${string}` // e.g. "max/plantarium/stem" type NodeId = `${string}/${string}/${string}`; // e.g. "max/plantarium/stem"
type NodeInstance = { type NodeInstance = {
id: number id: number;
type: NodeId type: NodeId;
position: [number, number] position: [number, number];
props?: Record<string, number | number[]> // current parameter values props?: Record<string, number | number[]>; // current parameter values
meta?: { title?: string; lastModified?: string } meta?: { title?: string; lastModified?: string };
state: NodeRuntimeState // runtime-only, NOT serialized state: NodeRuntimeState; // runtime-only, NOT serialized
} };
type NodeRuntimeState = { type NodeRuntimeState = {
type?: NodeDefinition // resolved definition type?: NodeDefinition; // resolved definition
parents?: NodeInstance[] parents?: NodeInstance[];
children?: NodeInstance[] children?: NodeInstance[];
x?: number; y?: number // interpolated position x?: number;
mesh?: Mesh // Three.js mesh reference y?: number; // interpolated position
ref?: HTMLElement mesh?: Mesh; // Three.js mesh reference
} ref?: HTMLElement;
};
type NodeDefinition = { type NodeDefinition = {
id: NodeId id: NodeId;
inputs?: Record<string, NodeInput> inputs?: Record<string, NodeInput>;
outputs?: string[] // output type names outputs?: string[]; // output type names
meta?: { title?: string; description?: string } meta?: { title?: string; description?: string };
execute(input: Int32Array): Int32Array // WASM function execute(input: Int32Array): Int32Array; // WASM function
} };
// Edge: [fromNode, outputIndex, toNode, inputSocketName] // Edge: [fromNode, outputIndex, toNode, inputSocketName]
type Edge = [NodeInstance, number, NodeInstance, string] type Edge = [NodeInstance, number, NodeInstance, string];
type Graph = { type Graph = {
nodes: NodeInstance[] nodes: NodeInstance[];
edges: [number, number, number, string][] // serialized (IDs, not refs) edges: [number, number, number, string][]; // serialized (IDs, not refs)
settings: Record<string, unknown> settings: Record<string, unknown>;
groups: GroupDefinition[] groups: GroupDefinition[];
} };
type GroupDefinition = { type GroupDefinition = {
id: number id: number;
nodes: NodeInstance[] nodes: NodeInstance[];
edges: Edge[] edges: Edge[];
inputs?: Record<string, NodeInput> inputs?: Record<string, NodeInput>;
outputs?: string[] outputs?: string[];
} };
``` ```
### NodeInput socket types ### NodeInput socket types
`float` | `integer` | `boolean` | `select` | `seed` | `vec3` | `geometry` | `path` | `shape` | `color` | `*` (wildcard) `float` | `integer` | `boolean` | `select` | `seed` | `vec3` | `geometry` | `path` | `shape` | `color` | `*` (wildcard)
Each input can have: `value` (default), `label`, `hidden`, `external`, `setting` (link to graph setting), `accepts` (extra compatible types). Each input can have: `value` (default), `label`, `hidden`, `external`, `setting` (link to graph setting), `accepts` (extra compatible types).
@@ -140,34 +143,43 @@ Each input can have: `value` (default), `label`, `hidden`, `external`, `setting`
## Patterns & Conventions ## Patterns & Conventions
### Svelte 5 reactivity ### Svelte 5 reactivity
The codebase uses Svelte 5 runes throughout — `$state`, `$derived`, `$effect`. Collections use `SvelteMap<K,V>` and `SvelteSet<T>` (from `svelte/reactivity`) instead of plain Map/Set so that mutations trigger reactive updates. The codebase uses Svelte 5 runes throughout — `$state`, `$derived`, `$effect`. Collections use `SvelteMap<K,V>` and `SvelteSet<T>` (from `svelte/reactivity`) instead of plain Map/Set so that mutations trigger reactive updates.
### Context API ### Context API
`GraphManager` and `GraphState` are provided via Svelte context (`setContext` / `getContext`) inside `GraphInterface`. All child components (Node, Edge, etc.) consume them via context rather than props. `GraphManager` and `GraphState` are provided via Svelte context (`setContext` / `getContext`) inside `GraphInterface`. All child components (Node, Edge, etc.) consume them via context rather than props.
### Edge representation ### Edge representation
In memory, edges are `[NodeInstance, outputIndex, NodeInstance, inputSocketName]` — direct object references for fast traversal. On serialization (`Graph.edges`), they become `[nodeId, outputIndex, nodeId, inputSocketName]` (plain IDs). In memory, edges are `[NodeInstance, outputIndex, NodeInstance, inputSocketName]` — direct object references for fast traversal. On serialization (`Graph.edges`), they become `[nodeId, outputIndex, nodeId, inputSocketName]` (plain IDs).
### Socket compatibility ### Socket compatibility
```typescript ```typescript
areSocketsCompatible(outputType: string, inputType: string | string[]): boolean areSocketsCompatible(outputType: string, inputType: string | string[]): boolean
// '*' wildcard matches any type; 'geometry' accepts ['geometry', 'instances'] // '*' wildcard matches any type; 'geometry' accepts ['geometry', 'instances']
``` ```
### WASM execution interface ### WASM execution interface
Every node exposes a single function: `execute(input: Int32Array): Int32Array`. Every node exposes a single function: `execute(input: Int32Array): Int32Array`.
Data encoding (Plantarium): Data encoding (Plantarium):
- `[0, stemDepth, ...x,y,z,thickness]` — path - `[0, stemDepth, ...x,y,z,thickness]` — path
- `[1, vertexCount, faceCount, ...faces, ...vertices, ...normals]` — geometry - `[1, vertexCount, faceCount, ...faces, ...vertices, ...normals]` — geometry
- `[2, vertexCount, faceCount, instanceCount, stemDepth, ...]` — instances - `[2, vertexCount, faceCount, instanceCount, stemDepth, ...]` — instances
### Event emitter ### Event emitter
`GraphManager extends EventEmitter<{ save, result, settings }>`. Subscribe with `manager.on('result', cb)`. Used to decouple the editor UI from runtime execution and persistence. `GraphManager extends EventEmitter<{ save, result, settings }>`. Subscribe with `manager.on('result', cb)`. Used to decouple the editor UI from runtime execution and persistence.
### History ### History
Every mutation goes through `HistoryManager`. Call `this.history.save(this.serialize())` before mutations; undo/redo replays jsondiffpatch deltas. Every mutation goes through `HistoryManager`. Call `this.history.save(this.serialize())` before mutations; undo/redo replays jsondiffpatch deltas.
### Internal node IDs ### Internal node IDs
Built-in nodes use the `__internal/` namespace: `__internal/group/instance`, `__internal/node/debug`. Virtual boundary nodes use `__virtual/`: `__virtual/group/input`, `__virtual/group/output`. Built-in nodes use the `__internal/` namespace: `__internal/group/instance`, `__internal/node/debug`. Virtual boundary nodes use `__virtual/`: `__virtual/group/input`, `__virtual/group/output`.
--- ---
@@ -179,7 +191,7 @@ Group selected nodes with **Ctrl+G**. A `GroupDefinition` is stored in `Graph.gr
**Known gaps as of 2026-05-03:** **Known gaps as of 2026-05-03:**
| Issue | Location | | Issue | Location |
|-------|----------| | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| `createGroupNode()` called but not defined | `graph-state.svelte.ts:334` → missing in `graph-manager.svelte.ts` | | `createGroupNode()` called but not defined | `graph-state.svelte.ts:334` → missing in `graph-manager.svelte.ts` |
| Runtime expects `group.graph.nodes/edges`; schema has flat `nodes/edges` | `runtime-executor.ts` vs `types.ts` | | Runtime expects `group.graph.nodes/edges`; schema has flat `nodes/edges` | `runtime-executor.ts` vs `types.ts` |
| Runtime expects `group.inputs` as array; schema defines it as `Record<string, NodeInput>` | Same mismatch | | Runtime expects `group.inputs` as array; schema defines it as `Record<string, NodeInput>` | Same mismatch |
+7 -5
View File
@@ -1,4 +1,4 @@
<script module> <script module lang="ts">
import { SvelteMap } from 'svelte/reactivity'; import { SvelteMap } from 'svelte/reactivity';
const cache = new SvelteMap<string, Record<string, boolean>>(); const cache = new SvelteMap<string, Record<string, boolean>>();
@@ -60,7 +60,7 @@
} }
return [] as [string, unknown][]; return [] as [string, unknown][];
}); });
const showKeys = $derived(!isArr || typeof items[0]?.[1] === "object") const showKeys = $derived(!isArr || typeof items[0]?.[1] === 'object');
function toggle(next: boolean) { function toggle(next: boolean) {
open = next; open = next;
@@ -92,8 +92,10 @@
<button <button
class="text-text hover:bg-layer-3 cursor-pointer" class="text-text hover:bg-layer-3 cursor-pointer"
title="Copy value" title="Copy value"
onclick={() => navigator.clipboard.writeText(JSON.stringify({[key]: value}, null, 2))} onclick={() => navigator.clipboard.writeText(JSON.stringify({ [key]: value }, null, 2))}
>{key}</button><span class="text-text/40">: </span> >
{key}
</button><span class="text-text/40">: </span>
{/if} {/if}
{#if isExpandable} {#if isExpandable}
@@ -111,7 +113,7 @@
<div> <div>
<JsonViewer <JsonViewer
value={v} value={v}
key={showKeys ? k : undefined } key={showKeys ? k : undefined}
depth={depth + 1} depth={depth + 1}
path={path ? `${path}/${k}` : k} path={path ? `${path}/${k}` : k}
/>{#if i < items.length - 1}<span class="text-text/20">,</span>{/if} />{#if i < items.length - 1}<span class="text-text/20">,</span>{/if}
@@ -10,9 +10,7 @@
let { options = [], value = $bindable(0), id = '' }: Props = $props(); let { options = [], value = $bindable(0), id = '' }: Props = $props();
const normalized = $derived( const normalized = $derived(
options.map((opt, i) => options.map((opt, i) => typeof opt === 'string' ? { value: i, label: opt } : opt)
typeof opt === 'string' ? { value: i, label: opt } : opt
)
); );
</script> </script>