diff --git a/app/src/lib/runtime/runtime-executor.test.ts b/app/src/lib/runtime/runtime-executor.test.ts index 7549e0a..6bfa647 100644 --- a/app/src/lib/runtime/runtime-executor.test.ts +++ b/app/src/lib/runtime/runtime-executor.test.ts @@ -1,13 +1,23 @@ +import type { Graph } from '@nodarium/types'; import { describe, expect, it } from 'vitest'; import { expandGroups } from './runtime-executor'; -import type { Graph } from '@nodarium/types'; // Helpers to build minimal serialized nodes/edges function node(id: number, type: string, props?: Record) { - 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]; } @@ -41,8 +51,8 @@ describe('expandGroups', () => { node(3, 'test/node/input') ], edges: [ - edge(1, 0, groupNodeId, 'input_0'), // A → group - edge(groupNodeId, 0, 3, 'value') // group → C + edge(1, 0, groupNodeId, 'input_0'), // A → group + edge(groupNodeId, 0, 3, 'value') // group → C ], groups: [{ id: groupId, @@ -52,8 +62,8 @@ describe('expandGroups', () => { node(7, '__internal/group/output') ], edges: [ - edge(6, 0, 2, 'input'), // inputBoundary → B - edge(2, 0, 7, 'Out') // B → outputBoundary + edge(6, 0, 2, 'input'), // inputBoundary → B + edge(2, 0, 7, 'Out') // B → outputBoundary ], inputs: { input_0: { type: 'float' } }, outputs: [{ type: 'float', label: 'Output 0' }] @@ -69,7 +79,7 @@ describe('expandGroups', () => { expect(ids).toContain(3); // C expect(result.nodes.length).toBe(3); // A, B(remapped), C - expect(result.edges).toContainEqual(edge(1, 0, remappedB, 'input')); // A → B + expect(result.edges).toContainEqual(edge(1, 0, remappedB, 'input')); // A → B expect(result.edges).toContainEqual(edge(remappedB, 0, 3, 'value')); // B → C expect(result.edges.length).toBe(2); }); @@ -96,14 +106,14 @@ describe('expandGroups', () => { id: groupId, nodes: [ node(3, '__internal/group/input'), - node(1, 'test/node/output'), // B - node(2, 'test/node/output'), // D + node(1, 'test/node/output'), // B + node(2, 'test/node/output'), // D node(4, '__internal/group/output') ], edges: [ - edge(3, 0, 1, 'input'), // inputBoundary → B - edge(1, 0, 2, 'input'), // B → D (internal) - edge(2, 0, 4, 'Out') // D → outputBoundary + edge(3, 0, 1, 'input'), // inputBoundary → B + edge(1, 0, 2, 'input'), // B → D (internal) + edge(2, 0, 4, 'Out') // D → outputBoundary ], inputs: { input_0: { type: 'float' } }, outputs: [{ type: 'float' }] @@ -116,9 +126,9 @@ describe('expandGroups', () => { expect(result.nodes.map(n => n.id)).toContain(remappedB); expect(result.nodes.map(n => n.id)).toContain(remappedD); - expect(result.edges).toContainEqual(edge(0, 0, remappedB, 'input')); // A → B + expect(result.edges).toContainEqual(edge(0, 0, remappedB, 'input')); // A → B expect(result.edges).toContainEqual(edge(remappedB, 0, remappedD, 'input')); // B → D (internal) - expect(result.edges).toContainEqual(edge(remappedD, 0, 9, 'value')); // D → C + expect(result.edges).toContainEqual(edge(remappedD, 0, 9, 'value')); // D → C expect(result.edges.length).toBe(3); }); diff --git a/docs/LLM.md b/docs/LLM.md index 983cd3d..48b1b83 100644 --- a/docs/LLM.md +++ b/docs/LLM.md @@ -47,6 +47,7 @@ User Interaction ``` **Event flow:** + 1. User edits graph → GraphManager mutates state 2. GraphManager emits `save` → ProjectManager persists to IndexDB 3. GraphManager emits `result` → Runtime executes graph → Viewer updates @@ -55,26 +56,26 @@ User Interaction ## Critical Files -| File | Role | -|------|------| -| `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-state.svelte.ts` | UI state: camera, selection, mouse, clipboard, groupSelectedNodes | -| `app/src/lib/graph-interface/graph/Graph.svelte` | Canvas renderer | -| `app/src/lib/graph-interface/node/Node.svelte` | 3D mesh node (Three.js shader) | -| `app/src/lib/graph-interface/node/NodeHTML.svelte` | HTML overlay: labels + parameters | -| `app/src/lib/graph-interface/node/NodeHeader.svelte` | Node title bar | -| `app/src/lib/graph-interface/keymaps.ts` | Keyboard shortcuts | -| `app/src/lib/graph-interface/helpers/nodeHelpers.ts` | Node height calculations | -| `app/src/lib/graph-interface/graph/colors.svelte.ts` | Socket type → color mapping | -| `app/src/lib/runtime/runtime-executor.ts` | Executes nodes in DAG order; expandGroups() | -| `app/src/lib/node-registry/index.ts` | RemoteNodeRegistry entry | -| `app/src/lib/node-registry/groupNode.ts` | Built-in group node definition | -| `app/src/lib/node-registry/debugNode.ts` | Built-in debug node | -| `app/src/lib/sidebar/panels/ActiveNodeSettings.svelte` | Per-node settings panel | -| `packages/types/src/types.ts` | Graph, NodeInstance, NodeDefinition, Edge, GroupDefinition | -| `packages/types/src/inputs.ts` | NodeInput union types (float, vec3, geometry, path, …) | -| `packages/utils/src/wasm.ts` | createWasmWrapper() — wraps WASM bytes into a NodeDefinition | +| File | Role | +| ------------------------------------------------------ | --------------------------------------------------------------------- | +| `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-state.svelte.ts` | UI state: camera, selection, mouse, clipboard, groupSelectedNodes | +| `app/src/lib/graph-interface/graph/Graph.svelte` | Canvas renderer | +| `app/src/lib/graph-interface/node/Node.svelte` | 3D mesh node (Three.js shader) | +| `app/src/lib/graph-interface/node/NodeHTML.svelte` | HTML overlay: labels + parameters | +| `app/src/lib/graph-interface/node/NodeHeader.svelte` | Node title bar | +| `app/src/lib/graph-interface/keymaps.ts` | Keyboard shortcuts | +| `app/src/lib/graph-interface/helpers/nodeHelpers.ts` | Node height calculations | +| `app/src/lib/graph-interface/graph/colors.svelte.ts` | Socket type → color mapping | +| `app/src/lib/runtime/runtime-executor.ts` | Executes nodes in DAG order; expandGroups() | +| `app/src/lib/node-registry/index.ts` | RemoteNodeRegistry entry | +| `app/src/lib/node-registry/groupNode.ts` | Built-in group node definition | +| `app/src/lib/node-registry/debugNode.ts` | Built-in debug node | +| `app/src/lib/sidebar/panels/ActiveNodeSettings.svelte` | Per-node settings panel | +| `packages/types/src/types.ts` | Graph, NodeInstance, NodeDefinition, Edge, GroupDefinition | +| `packages/types/src/inputs.ts` | NodeInput union types (float, vec3, geometry, path, …) | +| `packages/utils/src/wasm.ts` | createWasmWrapper() — wraps WASM bytes into a NodeDefinition | --- @@ -83,54 +84,56 @@ User Interaction ```typescript // 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 = { - id: number - type: NodeId - position: [number, number] - props?: Record // current parameter values - meta?: { title?: string; lastModified?: string } - state: NodeRuntimeState // runtime-only, NOT serialized -} + id: number; + type: NodeId; + position: [number, number]; + props?: Record; // current parameter values + meta?: { title?: string; lastModified?: string }; + state: NodeRuntimeState; // runtime-only, NOT serialized +}; type NodeRuntimeState = { - type?: NodeDefinition // resolved definition - parents?: NodeInstance[] - children?: NodeInstance[] - x?: number; y?: number // interpolated position - mesh?: Mesh // Three.js mesh reference - ref?: HTMLElement -} + type?: NodeDefinition; // resolved definition + parents?: NodeInstance[]; + children?: NodeInstance[]; + x?: number; + y?: number; // interpolated position + mesh?: Mesh; // Three.js mesh reference + ref?: HTMLElement; +}; type NodeDefinition = { - id: NodeId - inputs?: Record - outputs?: string[] // output type names - meta?: { title?: string; description?: string } - execute(input: Int32Array): Int32Array // WASM function -} + id: NodeId; + inputs?: Record; + outputs?: string[]; // output type names + meta?: { title?: string; description?: string }; + execute(input: Int32Array): Int32Array; // WASM function +}; // Edge: [fromNode, outputIndex, toNode, inputSocketName] -type Edge = [NodeInstance, number, NodeInstance, string] +type Edge = [NodeInstance, number, NodeInstance, string]; type Graph = { - nodes: NodeInstance[] - edges: [number, number, number, string][] // serialized (IDs, not refs) - settings: Record - groups: GroupDefinition[] -} + nodes: NodeInstance[]; + edges: [number, number, number, string][]; // serialized (IDs, not refs) + settings: Record; + groups: GroupDefinition[]; +}; type GroupDefinition = { - id: number - nodes: NodeInstance[] - edges: Edge[] - inputs?: Record - outputs?: string[] -} + id: number; + nodes: NodeInstance[]; + edges: Edge[]; + inputs?: Record; + outputs?: string[]; +}; ``` ### NodeInput socket types + `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). @@ -140,34 +143,43 @@ Each input can have: `value` (default), `label`, `hidden`, `external`, `setting` ## Patterns & Conventions ### Svelte 5 reactivity + The codebase uses Svelte 5 runes throughout — `$state`, `$derived`, `$effect`. Collections use `SvelteMap` and `SvelteSet` (from `svelte/reactivity`) instead of plain Map/Set so that mutations trigger reactive updates. ### 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. ### 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). ### Socket compatibility + ```typescript areSocketsCompatible(outputType: string, inputType: string | string[]): boolean // '*' wildcard matches any type; 'geometry' accepts ['geometry', 'instances'] ``` ### WASM execution interface + Every node exposes a single function: `execute(input: Int32Array): Int32Array`. Data encoding (Plantarium): + - `[0, stemDepth, ...x,y,z,thickness]` — path - `[1, vertexCount, faceCount, ...faces, ...vertices, ...normals]` — geometry - `[2, vertexCount, faceCount, instanceCount, stemDepth, ...]` — instances ### 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. ### History + Every mutation goes through `HistoryManager`. Call `this.history.save(this.serialize())` before mutations; undo/redo replays jsondiffpatch deltas. ### 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`. --- @@ -178,13 +190,13 @@ Group selected nodes with **Ctrl+G**. A `GroupDefinition` is stored in `Graph.gr **Known gaps as of 2026-05-03:** -| Issue | Location | -|-------|----------| -| `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.inputs` as array; schema defines it as `Record` | Same mismatch | -| `enterGroupNode()` is a stub — no group navigation | `graph-state.svelte.ts` | -| `serialize()` writes parent-graph edges into group instead of group-internal edges | `graph-manager.svelte.ts` | +| Issue | Location | +| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | +| `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.inputs` as array; schema defines it as `Record` | Same mismatch | +| `enterGroupNode()` is a stub — no group navigation | `graph-state.svelte.ts` | +| `serialize()` writes parent-graph edges into group instead of group-internal edges | `graph-manager.svelte.ts` | --- diff --git a/packages/ui/src/lib/JsonViewer.svelte b/packages/ui/src/lib/JsonViewer.svelte index 804f109..1c3c5ec 100644 --- a/packages/ui/src/lib/JsonViewer.svelte +++ b/packages/ui/src/lib/JsonViewer.svelte @@ -1,4 +1,4 @@ -