feat: add "*"/any type input for dev page

This commit is contained in:
Max Richter
2026-01-22 15:54:08 +01:00
parent 6f5c5bb46e
commit 841b447ac3
13 changed files with 273 additions and 220 deletions

View File

@@ -25,14 +25,14 @@ const clone = 'structuredClone' in self
? self.structuredClone ? self.structuredClone
: (args: any) => JSON.parse(JSON.stringify(args)); : (args: any) => JSON.parse(JSON.stringify(args));
function areSocketsCompatible( export function areSocketsCompatible(
output: string | undefined, output: string | undefined,
inputs: string | (string | undefined)[] | undefined inputs: string | (string | undefined)[] | undefined
) { ) {
if (Array.isArray(inputs) && output) { if (Array.isArray(inputs) && output) {
return inputs.includes(output); return inputs.includes('*') || inputs.includes(output);
} }
return inputs === output; return inputs === output || inputs === '*';
} }
function areEdgesEqual(firstEdge: Edge, secondEdge: Edge) { function areEdgesEqual(firstEdge: Edge, secondEdge: Edge) {
@@ -268,14 +268,7 @@ export class GraphManager extends EventEmitter<{
private _init(graph: Graph) { private _init(graph: Graph) {
const nodes = new Map( const nodes = new Map(
graph.nodes.map((node) => { graph.nodes.map((node) => {
const nodeType = this.registry.getNode(node.type); return [node.id, node as NodeInstance];
const n = node as NodeInstance;
if (nodeType) {
n.state = {
type: nodeType
};
}
return [node.id, n];
}) })
); );
@@ -300,6 +293,30 @@ export class GraphManager extends EventEmitter<{
this.execute(); this.execute();
} }
private async loadAllCollections() {
// Fetch all nodes from all collections of the loaded nodes
const nodeIds = Array.from(new Set([...this.graph.nodes.map((n) => n.type)]));
const allCollections = new Set<`${string}/${string}`>();
for (const id of nodeIds) {
const [user, collection] = id.split('/');
allCollections.add(`${user}/${collection}`);
}
const allCollectionIds = await Promise
.all([...allCollections]
.map(async (collection) =>
remoteRegistry
.fetchCollection(collection)
.then((collection: { nodes: { id: NodeId }[] }) => {
return collection.nodes.map(n => n.id.replace(/\.wasm$/, '') as NodeId);
})
));
const missingNodeIds = [...new Set(allCollectionIds.flat())];
this.registry.load(missingNodeIds);
}
async load(graph: Graph) { async load(graph: Graph) {
const a = performance.now(); const a = performance.now();
@@ -308,25 +325,16 @@ export class GraphManager extends EventEmitter<{
this.status = 'loading'; this.status = 'loading';
this.id = graph.id; this.id = graph.id;
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)])); const nodeIds = Array.from(new Set([...graph.nodes.map((n) => n.type)]));
await this.registry.load(nodeIds);
// Fetch all nodes from all collections of the loaded nodes logger.info('loading graph', {
const allCollections = new Set<`${string}/${string}`>(); nodes: graph.nodes,
for (const id of nodeIds) { edges: graph.edges,
const [user, collection] = id.split('/'); id: graph.id,
allCollections.add(`${user}/${collection}`); ids: nodeIds
} });
for (const collection of allCollections) {
remoteRegistry await this.registry.load(nodeIds);
.fetchCollection(collection)
.then((collection: { nodes: { id: NodeId }[] }) => {
const ids = collection.nodes.map((n) => n.id);
return this.registry.load(ids);
});
}
logger.info('loaded node types', this.registry.getAllNodes()); logger.info('loaded node types', this.registry.getAllNodes());
@@ -384,7 +392,9 @@ export class GraphManager extends EventEmitter<{
this.loaded = true; this.loaded = true;
logger.log(`Graph loaded in ${performance.now() - a}ms`); logger.log(`Graph loaded in ${performance.now() - a}ms`);
setTimeout(() => this.execute(), 100); setTimeout(() => this.execute(), 100);
this.loadAllCollections(); // lazily load all nodes from all collections
} }
getAllNodes() { getAllNodes() {
@@ -491,10 +501,10 @@ export class GraphManager extends EventEmitter<{
const inputs = Object.entries(to.state?.type?.inputs ?? {}); const inputs = Object.entries(to.state?.type?.inputs ?? {});
const outputs = from.state?.type?.outputs ?? []; const outputs = from.state?.type?.outputs ?? [];
for (let i = 0; i < inputs.length; i++) { 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++) { for (let o = 0; o < outputs.length; o++) {
const output = outputs[0]; const output = outputs[o];
if (input.type === output) { if (input.type === output || input.type === '*') {
return this.createEdge(from, o, to, inputName); return this.createEdge(from, o, to, inputName);
} }
} }
@@ -724,6 +734,7 @@ export class GraphManager extends EventEmitter<{
getPossibleSockets({ node, index }: Socket): [NodeInstance, string | number][] { getPossibleSockets({ node, index }: Socket): [NodeInstance, string | number][] {
const nodeType = node?.state?.type; const nodeType = node?.state?.type;
console.log({ node: $state.snapshot(node), index, nodeType });
if (!nodeType) return []; if (!nodeType) return [];
const sockets: [NodeInstance, string | number][] = []; const sockets: [NodeInstance, string | number][] = [];
@@ -783,6 +794,7 @@ export class GraphManager extends EventEmitter<{
} }
} }
console.log(`Found ${sockets.length} possible sockets`, sockets);
return sockets; return sockets;
} }

View File

@@ -58,7 +58,9 @@ export class GraphState {
wrapper = $state<HTMLDivElement>(null!); wrapper = $state<HTMLDivElement>(null!);
rect: DOMRect = $derived( rect: DOMRect = $derived(
(this.wrapper && this.width && this.height) ? this.wrapper.getBoundingClientRect() : new DOMRect(0, 0, 0, 0) (this.wrapper && this.width && this.height)
? this.wrapper.getBoundingClientRect()
: new DOMRect(0, 0, 0, 0)
); );
camera = $state<OrthographicCamera>(null!); camera = $state<OrthographicCamera>(null!);
@@ -168,11 +170,14 @@ export class GraphState {
(node?.state?.y ?? node.position[1]) + 2.5 + 10 * index (node?.state?.y ?? node.position[1]) + 2.5 + 10 * index
]; ];
} else { } else {
const _index = Object.keys(node.state?.type?.inputs || {}).indexOf(index); const inputs = node.state.type?.inputs || this.graph.registry.getNode(node.type)?.inputs
return [ || {};
const _index = Object.keys(inputs).indexOf(index);
const pos = [
node?.state?.x ?? node.position[0], node?.state?.x ?? node.position[0],
(node?.state?.y ?? node.position[1]) + 10 + 10 * _index (node?.state?.y ?? node.position[1]) + 10 + 10 * _index
]; ] as [number, number];
return pos;
} }
} }
@@ -248,7 +253,7 @@ export class GraphState {
let { node, index, position } = socket; let { node, index, position } = socket;
// remove existing edge // if the socket is an input socket -> remove existing edges
if (typeof index === 'string') { if (typeof index === 'string') {
const edges = this.graph.getEdgesToNode(node); const edges = this.graph.getEdgesToNode(node);
for (const edge of edges) { for (const edge of edges) {

View File

@@ -57,7 +57,7 @@
]; ];
const input = Object.entries(newNode?.state?.type?.inputs || {}).find( const input = Object.entries(newNode?.state?.type?.inputs || {}).find(
(inp) => inp[1].type === socketType, (inp) => inp[1].type === socketType || inp[1].type === "*",
); );
if (input) { if (input) {

View File

@@ -29,11 +29,11 @@
let { let {
graph, graph,
registry, registry,
settings = $bindable(),
activeNode = $bindable(), activeNode = $bindable(),
showGrid = $bindable(true), showGrid = $bindable(true),
snapToGrid = $bindable(true), snapToGrid = $bindable(true),
showHelp = $bindable(false), showHelp = $bindable(false),
settings = $bindable(),
settingTypes = $bindable(), settingTypes = $bindable(),
onsave, onsave,
onresult, onresult,

View File

@@ -1,21 +1,21 @@
import { create, type Delta } from "jsondiffpatch"; import type { Graph } from '@nodarium/types';
import type { Graph } from "@nodarium/types"; import { createLogger } from '@nodarium/utils';
import { clone } from "./helpers/index.js"; import { create, type Delta } from 'jsondiffpatch';
import { createLogger } from "@nodarium/utils"; import { clone } from './helpers/index.js';
const diff = create({ const diff = create({
objectHash: function (obj, index) { objectHash: function (obj, index) {
if (obj === null) return obj; if (obj === null) return obj;
if ("id" in obj) return obj.id as string; if ('id' in obj) return obj.id as string;
if ("_id" in obj) return obj._id as string; if ('_id' in obj) return obj._id as string;
if (Array.isArray(obj)) { if (Array.isArray(obj)) {
return obj.join("-"); return obj.join('-');
} }
return "$$index:" + index; return '$$index:' + index;
}, }
}); });
const log = createLogger("history"); const log = createLogger('history');
log.mute(); log.mute();
export class HistoryManager { export class HistoryManager {
@@ -26,7 +26,7 @@ export class HistoryManager {
private opts = { private opts = {
debounce: 400, debounce: 400,
maxHistory: 100, maxHistory: 100
}; };
constructor({ maxHistory = 100, debounce = 100 } = {}) { constructor({ maxHistory = 100, debounce = 100 } = {}) {
@@ -40,12 +40,12 @@ export class HistoryManager {
if (!this.state) { if (!this.state) {
this.state = clone(state); this.state = clone(state);
this.initialState = this.state; this.initialState = this.state;
log.log("initial state saved"); log.log('initial state saved');
} else { } else {
const newState = state; const newState = state;
const delta = diff.diff(this.state, newState); const delta = diff.diff(this.state, newState);
if (delta) { if (delta) {
log.log("saving state"); log.log('saving state');
// Add the delta to history // Add the delta to history
if (this.index < this.history.length - 1) { if (this.index < this.history.length - 1) {
// Clear the history after the current index if new changes are made // Clear the history after the current index if new changes are made
@@ -61,7 +61,7 @@ export class HistoryManager {
} }
this.state = newState; this.state = newState;
} else { } else {
log.log("no changes"); log.log('no changes');
} }
} }
} }
@@ -75,7 +75,7 @@ export class HistoryManager {
undo() { undo() {
if (this.index === -1 && this.initialState) { if (this.index === -1 && this.initialState) {
log.log("reached start, loading initial state"); log.log('reached start, loading initial state');
return clone(this.initialState); return clone(this.initialState);
} else { } else {
const delta = this.history[this.index]; const delta = this.history[this.index];
@@ -95,7 +95,7 @@ export class HistoryManager {
this.state = nextState; this.state = nextState;
return clone(nextState); return clone(nextState);
} else { } else {
log.log("reached end"); log.log('reached end');
} }
} }
} }

View File

@@ -20,7 +20,7 @@ export async function getNodeWasm(id: `${string}/${string}/${string}`) {
const wasmBytes = await getWasm(id); const wasmBytes = await getWasm(id);
if (!wasmBytes) return null; if (!wasmBytes) return null;
const wrapper = createWasmWrapper(wasmBytes); const wrapper = createWasmWrapper(wasmBytes.buffer);
return wrapper; return wrapper;
} }

View File

@@ -4,47 +4,47 @@ import type {
NodeInput, NodeInput,
NodeRegistry, NodeRegistry,
RuntimeExecutor, RuntimeExecutor,
SyncCache, SyncCache
} from "@nodarium/types"; } from '@nodarium/types';
import { import {
concatEncodedArrays, concatEncodedArrays,
createLogger, createLogger,
encodeFloat, encodeFloat,
fastHashArrayBuffer, fastHashArrayBuffer,
type PerformanceStore, type PerformanceStore
} from "@nodarium/utils"; } from '@nodarium/utils';
import type { RuntimeNode } from "./types"; import type { RuntimeNode } from './types';
const log = createLogger("runtime-executor"); const log = createLogger('runtime-executor');
log.mute(); log.mute();
function getValue(input: NodeInput, value?: unknown) { function getValue(input: NodeInput, value?: unknown) {
if (value === undefined && "value" in input) { if (value === undefined && 'value' in input) {
value = input.value; value = input.value;
} }
if (input.type === "float") { if (input.type === 'float') {
return encodeFloat(value as number); return encodeFloat(value as number);
} }
if (Array.isArray(value)) { if (Array.isArray(value)) {
if (input.type === "vec3") { if (input.type === 'vec3') {
return [ return [
0, 0,
value.length + 1, value.length + 1,
...value.map((v) => encodeFloat(v)), ...value.map((v) => encodeFloat(v)),
1, 1,
1, 1
] as number[]; ] as number[];
} }
return [0, value.length + 1, ...value, 1, 1] as number[]; return [0, value.length + 1, ...value, 1, 1] as number[];
} }
if (typeof value === "boolean") { if (typeof value === 'boolean') {
return value ? 1 : 0; return value ? 1 : 0;
} }
if (typeof value === "number") { if (typeof value === 'number') {
return value; return value;
} }
@@ -52,6 +52,7 @@ function getValue(input: NodeInput, value?: unknown) {
return value; return value;
} }
console.error({ input, value });
throw new Error(`Unknown input type ${input.type}`); throw new Error(`Unknown input type ${input.type}`);
} }
@@ -62,16 +63,18 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
perf?: PerformanceStore; perf?: PerformanceStore;
private results: Record<string, Int32Array> = {};
constructor( constructor(
private registry: NodeRegistry, private registry: NodeRegistry,
public cache?: SyncCache<Int32Array>, public cache?: SyncCache<Int32Array>
) { ) {
this.cache = undefined; this.cache = undefined;
} }
private async getNodeDefinitions(graph: Graph) { private async getNodeDefinitions(graph: Graph) {
if (this.registry.status !== "ready") { if (this.registry.status !== 'ready') {
throw new Error("Node registry is not ready"); throw new Error('Node registry is not ready');
} }
await this.registry.load(graph.nodes.map((node) => node.type)); await this.registry.load(graph.nodes.map((node) => node.type));
@@ -98,21 +101,18 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
depth: 0, depth: 0,
children: [], children: [],
parents: [], parents: [],
inputNodes: {}, inputNodes: {}
} };
return n return n;
}) });
const outputNode = graphNodes.find((node) => node.type.endsWith('/output'));
const outputNode = graphNodes.find((node) =>
node.type.endsWith("/output"),
);
if (!outputNode) { if (!outputNode) {
throw new Error("No output node found"); throw new Error('No output node found');
} }
const nodeMap = new Map( const nodeMap = new Map(
graphNodes.map((node) => [node.id, node]), graphNodes.map((node) => [node.id, node])
); );
// loop through all edges and assign the parent and child nodes to each node // loop through all edges and assign the parent and child nodes to each node
@@ -146,7 +146,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
} }
async execute(graph: Graph, settings: Record<string, unknown>) { async execute(graph: Graph, settings: Record<string, unknown>) {
this.perf?.addPoint("runtime"); this.perf?.addPoint('runtime');
let a = performance.now(); let a = performance.now();
@@ -154,7 +154,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
const [outputNode, nodes] = await this.addMetaData(graph); const [outputNode, nodes] = await this.addMetaData(graph);
let b = performance.now(); let b = performance.now();
this.perf?.addPoint("collect-metadata", b - a); this.perf?.addPoint('collect-metadata', b - a);
/* /*
* Here we sort the nodes into buckets, which we then execute one by one * Here we sort the nodes into buckets, which we then execute one by one
@@ -169,13 +169,13 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
// we execute the nodes from the bottom up // we execute the nodes from the bottom up
const sortedNodes = nodes.sort( const sortedNodes = nodes.sort(
(a, b) => (b.state?.depth || 0) - (a.state?.depth || 0), (a, b) => (b.state?.depth || 0) - (a.state?.depth || 0)
); );
// here we store the intermediate results of the nodes // here we store the intermediate results of the nodes
const results: Record<string, Int32Array> = {}; this.results = {};
if (settings["randomSeed"]) { if (settings['randomSeed']) {
this.seed = Math.floor(Math.random() * 100000000); this.seed = Math.floor(Math.random() * 100000000);
} }
@@ -192,7 +192,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
// Collect the inputs for the node // Collect the inputs for the node
const inputs = Object.entries(node_type.inputs || {}).map( const inputs = Object.entries(node_type.inputs || {}).map(
([key, input]) => { ([key, input]) => {
if (input.type === "seed") { if (input.type === 'seed') {
return this.seed; return this.seed;
} }
@@ -204,12 +204,12 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
// check if the input is connected to another node // check if the input is connected to another node
const inputNode = node.state.inputNodes[key]; const inputNode = node.state.inputNodes[key];
if (inputNode) { if (inputNode) {
if (results[inputNode.id] === undefined) { if (this.results[inputNode.id] === undefined) {
throw new Error( throw new Error(
`Node ${node.type} is missing input from node ${inputNode.type}`, `Node ${node.type} is missing input from node ${inputNode.type}`
); );
} }
return results[inputNode.id]; return this.results[inputNode.id];
} }
// If the value is stored in the node itself, we use that value // If the value is stored in the node itself, we use that value
@@ -218,45 +218,45 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
} }
return getValue(input); return getValue(input);
}, }
); );
b = performance.now(); b = performance.now();
this.perf?.addPoint("collected-inputs", b - a); this.perf?.addPoint('collected-inputs', b - a);
try { try {
a = performance.now(); a = performance.now();
const encoded_inputs = concatEncodedArrays(inputs); const encoded_inputs = concatEncodedArrays(inputs);
b = performance.now(); b = performance.now();
this.perf?.addPoint("encoded-inputs", b - a); this.perf?.addPoint('encoded-inputs', b - a);
a = performance.now(); a = performance.now();
let inputHash = `node-${node.id}-${fastHashArrayBuffer(encoded_inputs)}`; let inputHash = `node-${node.id}-${fastHashArrayBuffer(encoded_inputs)}`;
b = performance.now(); b = performance.now();
this.perf?.addPoint("hash-inputs", b - a); this.perf?.addPoint('hash-inputs', b - a);
let cachedValue = this.cache?.get(inputHash); let cachedValue = this.cache?.get(inputHash);
if (cachedValue !== undefined) { if (cachedValue !== undefined) {
log.log(`Using cached value for ${node_type.id || node.id}`); log.log(`Using cached value for ${node_type.id || node.id}`);
this.perf?.addPoint("cache-hit", 1); this.perf?.addPoint('cache-hit', 1);
results[node.id] = cachedValue as Int32Array; this.results[node.id] = cachedValue as Int32Array;
continue; continue;
} }
this.perf?.addPoint("cache-hit", 0); this.perf?.addPoint('cache-hit', 0);
log.group(`executing ${node_type.id}-${node.id}`); log.group(`executing ${node_type.id}-${node.id}`);
log.log(`Inputs:`, inputs); log.log(`Inputs:`, inputs);
a = performance.now(); a = performance.now();
results[node.id] = node_type.execute(encoded_inputs); this.results[node.id] = node_type.execute(encoded_inputs);
log.log("Executed", node.type, node.id) log.log('Executed', node.type, node.id);
b = performance.now(); b = performance.now();
if (this.cache && node.id !== outputNode.id) { if (this.cache && node.id !== outputNode.id) {
this.cache.set(inputHash, results[node.id]); this.cache.set(inputHash, this.results[node.id]);
} }
this.perf?.addPoint("node/" + node_type.id, b - a); this.perf?.addPoint('node/' + node_type.id, b - a);
log.log("Result:", results[node.id]); log.log('Result:', this.results[node.id]);
log.groupEnd(); log.groupEnd();
} catch (e) { } catch (e) {
log.groupEnd(); log.groupEnd();
@@ -265,17 +265,21 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
} }
// return the result of the parent of the output node // return the result of the parent of the output node
const res = results[outputNode.id]; const res = this.results[outputNode.id];
if (this.cache) { if (this.cache) {
this.cache.size = sortedNodes.length * 2; this.cache.size = sortedNodes.length * 2;
} }
this.perf?.endPoint("runtime"); this.perf?.endPoint('runtime');
return res as unknown as Int32Array; return res as unknown as Int32Array;
} }
getIntermediateResults() {
return this.results;
}
getPerformanceData() { getPerformanceData() {
return this.perf?.get(); return this.perf?.get();
} }

View File

@@ -90,11 +90,6 @@
let graphSettingTypes = $state({ let graphSettingTypes = $state({
randomSeed: { type: "boolean", value: false }, randomSeed: { type: "boolean", value: false },
}); });
$effect(() => {
if (graphSettings && graphSettingTypes) {
manager?.setSettings($state.snapshot(graphSettings));
}
});
async function update( async function update(
g: Graph, g: Graph,

View File

@@ -3,6 +3,6 @@
const { children } = $props<{ children?: Snippet }>(); const { children } = $props<{ children?: Snippet }>();
</script> </script>
<main class="w-screen overflow-x-hidden"> <main class="w-screen h-screen overflow-x-hidden">
{@render children()} {@render children()}
</main> </main>

View File

@@ -1,88 +1,73 @@
<script lang="ts"> <script lang="ts">
import NodeHTML from "$lib/graph-interface/node/NodeHTML.svelte"; import * as templates from "$lib/graph-templates";
import { localState } from "$lib/helpers/localState.svelte";
import Panel from "$lib/sidebar/Panel.svelte"; import Panel from "$lib/sidebar/Panel.svelte";
import Sidebar from "$lib/sidebar/Sidebar.svelte"; import Sidebar from "$lib/sidebar/Sidebar.svelte";
import { IndexDBCache, RemoteNodeRegistry } from "@nodarium/registry"; import GraphInterface from "$lib/graph-interface";
import { type NodeId, type NodeInstance } from "@nodarium/types"; import { RemoteNodeRegistry } from "@nodarium/registry";
import Code from "./Code.svelte"; import { type Graph } from "@nodarium/types";
import Grid from "$lib/grid"; import Grid from "$lib/grid";
import { import { MemoryRuntimeExecutor } from "$lib/runtime";
concatEncodedArrays, import devPlant from "./dev-graph.json";
createWasmWrapper, import { decodeNestedArray } from "@nodarium/utils";
encodeNestedArray,
} from "@nodarium/utils";
const registryCache = new IndexDBCache("node-registry"); let result = $state<Int32Array>();
const nodeRegistry = new RemoteNodeRegistry("", registryCache);
let activeNode = localState<NodeId | undefined>( const nodeRegistry = new RemoteNodeRegistry("");
"node.dev.activeNode", nodeRegistry.overwriteNode("max/plantarium/output", {
undefined, id: "max/plantarium/output",
); meta: {
title: "Debug View",
let nodeWasm = $state<ArrayBuffer>(); description: "",
let nodeInstance = $state<NodeInstance>(); },
let nodeWasmWrapper = $state<ReturnType<typeof createWasmWrapper>>(); inputs: {
out: {
async function fetchNodeData(nodeId?: NodeId) { type: "*",
nodeWasm = undefined;
nodeInstance = undefined;
if (!nodeId) return;
const data = await nodeRegistry.fetchNodeDefinition(nodeId);
nodeWasm = await nodeRegistry.fetchArrayBuffer("nodes/" + nodeId + ".wasm");
nodeInstance = {
id: 0,
type: nodeId,
position: [0, 0] as [number, number],
props: {},
state: {
type: data,
}, },
}; },
nodeWasmWrapper = createWasmWrapper(nodeWasm); execute(input: Int32Array) {
result = input;
return input;
},
});
const runtimeExecutor = new MemoryRuntimeExecutor(nodeRegistry);
let graph = $state(
localStorage.getItem("nodes.dev.graph")
? JSON.parse(localStorage.getItem("nodes.dev.graph")!)
: devPlant,
);
function handleSave(graph: Graph) {
localStorage.setItem("nodes.dev.graph", JSON.stringify(graph));
} }
$effect(() => { let graphSettings = $state<Record<string, any>>({});
fetchNodeData(activeNode.value); let graphSettingTypes = $state({
randomSeed: { type: "boolean", value: false },
}); });
$effect(() => { async function handleResult(res: unknown) {
if (nodeInstance?.props && nodeWasmWrapper) { const result = await runtimeExecutor.execute(graph, graphSettings);
const keys = Object.keys(nodeInstance.state.type?.inputs || {}); console.log({ res, result });
let ins = Object.values(nodeInstance.props) as number[]; }
if (keys[0] === "plant") {
ins = [[0, 0, 0, 0, 0, 0, 0, 0], ...ins];
}
const inputs = concatEncodedArrays(encodeNestedArray(ins));
nodeWasmWrapper?.execute(inputs);
}
});
</script> </script>
<div class="node-wrapper absolute bottom-8 left-8">
{#if nodeInstance}
<NodeHTML inView position="relative" z={5} bind:node={nodeInstance} />
{/if}
</div>
<Grid.Row> <Grid.Row>
<Grid.Cell> <Grid.Cell>
<pre> {#if result}
<code> <pre><code>{JSON.stringify(decodeNestedArray(result))}</code></pre>
{JSON.stringify(nodeInstance?.props)} {/if}
</code>
</pre>
</Grid.Cell> </Grid.Cell>
<Grid.Cell> <Grid.Cell>
<div class="h-screen w-[80vw] overflow-y-auto"> <GraphInterface
{#if nodeWasm} {graph}
<Code wasm={nodeWasm} /> registry={nodeRegistry}
{/if} bind:settings={graphSettings}
</div> bind:settingTypes={graphSettingTypes}
onsave={(g) => handleSave(g)}
onresult={(result) => handleResult(result)}
/>
</Grid.Cell> </Grid.Cell>
</Grid.Row> </Grid.Row>
@@ -92,22 +77,7 @@
classes="text-green-400" classes="text-green-400"
title="Node Store" title="Node Store"
icon="i-[tabler--database]" icon="i-[tabler--database]"
> ></Panel>
<div class="p-4 flex flex-col gap-2">
{#await nodeRegistry.fetchCollection("max/plantarium")}
<p>Loading Nodes...</p>
{:then result}
{#each result.nodes as n}
<button
class="cursor-pointer p-2 bg-layer-1 {activeNode.value === n.id
? 'outline outline-offset-1'
: ''}"
onclick={() => (activeNode.value = n.id)}>{n.id}</button
>
{/each}
{/await}
</div>
</Panel>
</Sidebar> </Sidebar>
<style> <style>

View File

@@ -0,0 +1,53 @@
{
"settings": {
"resolution.circle": 26,
"resolution.curve": 39
},
"nodes": [
{
"id": 9,
"position": [
220,
80
],
"type": "max/plantarium/output",
"props": {}
},
{
"id": 10,
"position": [
195,
80
],
"type": "max/plantarium/math",
"props": {
"op_type": 0,
"a": 2,
"b": 2
}
},
{
"id": 11,
"position": [
170,
80
],
"type": "max/plantarium/float",
"props": {
"value": 0.1
}
},
{
"id": 12,
"position": [
170,
100
],
"type": "max/plantarium/float",
"props": {
"value": 0.1
}
}
],
"edges": []
}

View File

@@ -2,6 +2,7 @@ import {
type AsyncCache, type AsyncCache,
type NodeDefinition, type NodeDefinition,
NodeDefinitionSchema, NodeDefinitionSchema,
type NodeId,
type NodeRegistry type NodeRegistry
} from '@nodarium/types'; } from '@nodarium/types';
import { createLogger, createWasmWrapper } from '@nodarium/utils'; import { createLogger, createWasmWrapper } from '@nodarium/utils';
@@ -153,6 +154,13 @@ export class RemoteNodeRegistry implements NodeRegistry {
} }
getAllNodes() { getAllNodes() {
return [...this.nodes.values()]; const allNodes = [...this.nodes.values()];
log.info('getting all nodes', allNodes);
return allNodes;
}
async overwriteNode(nodeId: NodeId, node: NodeDefinition) {
log.info('Overwritten node', { nodeId, node });
this.nodes.set(nodeId, node);
} }
} }

View File

@@ -1,4 +1,4 @@
import { z } from "zod"; import { z } from 'zod';
const DefaultOptionsSchema = z.object({ const DefaultOptionsSchema = z.object({
internal: z.boolean().optional(), internal: z.boolean().optional(),
@@ -9,72 +9,77 @@ const DefaultOptionsSchema = z.object({
accepts: z accepts: z
.array( .array(
z.union([ z.union([
z.literal("float"), z.literal('float'),
z.literal("integer"), z.literal('integer'),
z.literal("boolean"), z.literal('boolean'),
z.literal("select"), z.literal('select'),
z.literal("seed"), z.literal('seed'),
z.literal("vec3"), z.literal('vec3'),
z.literal("geometry"), z.literal('geometry'),
z.literal("path"), z.literal('path')
]), ])
) )
.optional(), .optional(),
hidden: z.boolean().optional(), hidden: z.boolean().optional()
}); });
export const NodeInputFloatSchema = z.object({ export const NodeInputFloatSchema = z.object({
...DefaultOptionsSchema.shape, ...DefaultOptionsSchema.shape,
type: z.literal("float"), type: z.literal('float'),
element: z.literal("slider").optional(), element: z.literal('slider').optional(),
value: z.number().optional(), value: z.number().optional(),
min: z.number().optional(), min: z.number().optional(),
max: z.number().optional(), max: z.number().optional(),
step: z.number().optional(), step: z.number().optional()
}); });
export const NodeInputIntegerSchema = z.object({ export const NodeInputIntegerSchema = z.object({
...DefaultOptionsSchema.shape, ...DefaultOptionsSchema.shape,
type: z.literal("integer"), type: z.literal('integer'),
element: z.literal("slider").optional(), element: z.literal('slider').optional(),
value: z.number().optional(), value: z.number().optional(),
min: z.number().optional(), min: z.number().optional(),
max: z.number().optional(), max: z.number().optional()
}); });
export const NodeInputBooleanSchema = z.object({ export const NodeInputBooleanSchema = z.object({
...DefaultOptionsSchema.shape, ...DefaultOptionsSchema.shape,
type: z.literal("boolean"), type: z.literal('boolean'),
value: z.boolean().optional(), value: z.boolean().optional()
}); });
export const NodeInputSelectSchema = z.object({ export const NodeInputSelectSchema = z.object({
...DefaultOptionsSchema.shape, ...DefaultOptionsSchema.shape,
type: z.literal("select"), type: z.literal('select'),
options: z.array(z.string()).optional(), options: z.array(z.string()).optional(),
value: z.string().optional(), value: z.string().optional()
}); });
export const NodeInputSeedSchema = z.object({ export const NodeInputSeedSchema = z.object({
...DefaultOptionsSchema.shape, ...DefaultOptionsSchema.shape,
type: z.literal("seed"), type: z.literal('seed'),
value: z.number().optional(), value: z.number().optional()
}); });
export const NodeInputVec3Schema = z.object({ export const NodeInputVec3Schema = z.object({
...DefaultOptionsSchema.shape, ...DefaultOptionsSchema.shape,
type: z.literal("vec3"), type: z.literal('vec3'),
value: z.array(z.number()).optional(), value: z.array(z.number()).optional()
}); });
export const NodeInputGeometrySchema = z.object({ export const NodeInputGeometrySchema = z.object({
...DefaultOptionsSchema.shape, ...DefaultOptionsSchema.shape,
type: z.literal("geometry"), type: z.literal('geometry')
}); });
export const NodeInputPathSchema = z.object({ export const NodeInputPathSchema = z.object({
...DefaultOptionsSchema.shape, ...DefaultOptionsSchema.shape,
type: z.literal("path"), type: z.literal('path')
});
export const NodeInputAnySchema = z.object({
...DefaultOptionsSchema.shape,
type: z.literal('*')
}); });
export const NodeInputSchema = z.union([ export const NodeInputSchema = z.union([
@@ -87,6 +92,7 @@ export const NodeInputSchema = z.union([
NodeInputVec3Schema, NodeInputVec3Schema,
NodeInputGeometrySchema, NodeInputGeometrySchema,
NodeInputPathSchema, NodeInputPathSchema,
NodeInputAnySchema
]); ]);
export type NodeInput = z.infer<typeof NodeInputSchema>; export type NodeInput = z.infer<typeof NodeInputSchema>;