3 Commits

Author SHA1 Message Date
9b94159f8e register works 2026-01-23 11:23:07 +01:00
aa4d7f73a8 setup zig node 2026-01-23 10:24:21 +01:00
1efb94b09c Add zig to flake packages 2026-01-23 09:07:05 +01:00
60 changed files with 960 additions and 2581 deletions

View File

@@ -1,9 +0,0 @@
[target.wasm32-unknown-unknown]
rustflags = [
"-C",
"link-arg=--import-memory",
"-C",
"link-arg=--initial-memory=67108864", # 64 MiB
"-C",
"link-arg=--max-memory=536870912", # 512 MiB
]

8
Cargo.lock generated
View File

@@ -24,14 +24,6 @@ dependencies = [
"nodarium_utils", "nodarium_utils",
] ]
[[package]]
name = "debug"
version = "0.1.0"
dependencies = [
"nodarium_macros",
"nodarium_utils",
]
[[package]] [[package]]
name = "float" name = "float"
version = "0.1.0" version = "0.1.0"

View File

@@ -6,7 +6,10 @@ members = [
"packages/types", "packages/types",
"packages/utils", "packages/utils",
] ]
exclude = ["nodes/max/plantarium/.template"] exclude = [
"nodes/max/plantarium/.template",
"nodes/max/plantarium/zig"
]
[profile.release] [profile.release]
lto = true lto = true

View File

@@ -2,17 +2,17 @@ Nodarium
<div align="center"> <div align="center">
<a href="https://nodes.max-richter.dev/"><h2 align="center">Nodarium</h2></a> <a href="https://nodes.max-richter.dev/"><h2 align="center">Nodarium</h2></a>
<p align="center"> <p align="center">
Nodarium is a WebAssembly based visual programming language. Nodarium is a WebAssembly based visual programming language.
</p> </p>
<img src=".github/graphics/nodes.svg" width="80%"/> <img src=".github/graphics/nodes.svg" width="80%"/>
</div> </div>
Currently this visual programming language is used to develop <https://nodes.max-richter.dev>, a procedural modelling tool for 3d-plants. Currently this visual programming language is used to develop https://nodes.max-richter.dev, a procedural modelling tool for 3d-plants.
# Table of contents # Table of contents
@@ -22,11 +22,12 @@ Currently this visual programming language is used to develop <https://nodes.max
# Developing # Developing
### Install prerequisites ### Install prerequisites:
- [Node.js](https://nodejs.org/en/download) - [Node.js](https://nodejs.org/en/download)
- [pnpm](https://pnpm.io/installation) - [pnpm](https://pnpm.io/installation)
- [rust](https://www.rust-lang.org/tools/install) - [rust](https://www.rust-lang.org/tools/install)
- wasm-pack
### Install dependencies ### Install dependencies

View File

@@ -1,783 +0,0 @@
# Shared Memory Refactor Plan
## Executive Summary
Migrate to a single shared `WebAssembly.Memory` instance imported by all nodes using `--import-memory`. The `#[nodarium_execute]` macro writes the function's return value directly to shared memory at the specified offset.
## Architecture Overview
```
┌─────────────────────────────────────────────────────────────────────┐
│ Shared WebAssembly.Memory │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ [Node A output] [Node B output] [Node C output] ... │ │
│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │
│ │ │ Vec<i32> │ │ Vec<i32> │ │ Vec<i32> │ │ │
│ │ │ 4 bytes │ │ 12 bytes │ │ 2KB │ │ │
│ │ └────────────┘ └────────────┘ └────────────┘ │ │
│ │ │ │
│ │ offset: 0 ────────────────────────────────────────────────► │ │
│ └───────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│ import { memory } from "env"
┌─────────────────────────┼─────────────────────────┐
│ │ │
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
│ Node A │ │ Node B │ │ Node C │
│ WASM │ │ WASM │ │ WASM │
└─────────┘ └─────────┘ └─────────┘
```
## Phase 1: Compilation Configuration
### 1.1 Cargo Config
```toml
# nodes/max/plantarium/box/.cargo/config.toml
[build]
rustflags = ["-C", "link-arg=--import-memory"]
```
Or globally in `Cargo.toml`:
```toml
[profile.release]
rustflags = ["-C", "link-arg=--import-memory"]
```
### 1.2 Import Memory Semantics
With `--import-memory`:
- Nodes **import** memory from the host (not export their own)
- All nodes receive the same `WebAssembly.Memory` instance
- Memory is read/write accessible from all modules
- No `memory.grow` needed (host manages allocation)
## Phase 2: Macro Design
### 2.1 Clean Node API
```rust
// input.json has 3 inputs: op_type, a, b
nodarium_definition_file!("src/input.json");
#[nodarium_execute]
pub fn execute(op_type: *i32, a: *i32, b: *i32) -> Vec<i32> {
// Read inputs directly from shared memory
let op = unsafe { *op_type };
let a_val = f32::from_bits(unsafe { *a } as u32);
let b_val = f32::from_bits(unsafe { *b } as u32);
let result = match op {
0 => a_val + b_val,
1 => a_val - b_val,
2 => a_val * b_val,
3 => a_val / b_val,
_ => 0.0,
};
// Return Vec<i32>, macro handles writing to shared memory
vec![result.to_bits()]
}
```
### 2.2 Macro Implementation
```rust
// packages/macros/src/lib.rs
#[proc_macro_attribute]
pub fn nodarium_execute(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input_fn = parse_macro_input!(item as syn::ItemFn);
let fn_name = &input_fn.sig.ident;
// Parse definition to get input count
let project_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let def: NodeDefinition = serde_json::from_str(&fs::read_to_string(
Path::new(&project_dir).join("src/input.json")
).unwrap()).unwrap();
let input_count = def.inputs.as_ref().map(|i| i.len()).unwrap_or(0);
// Validate signature
validate_signature(&input_fn, input_count);
// Generate wrapper
generate_execute_wrapper(input_fn, fn_name, input_count)
}
fn validate_signature(fn_sig: &syn::Signature, expected_inputs: usize) {
let param_count = fn_sig.inputs.len();
if param_count != expected_inputs {
panic!(
"Execute function has {} parameters but definition has {} inputs\n\
Definition inputs: {:?}\n\
Expected signature:\n\
pub fn execute({}) -> Vec<i32>",
param_count,
expected_inputs,
def.inputs.as_ref().map(|i| i.keys().collect::<Vec<_>>()),
(0..expected_inputs)
.map(|i| format!("arg{}: *const i32", i))
.collect::<Vec<_>>()
.join(", ")
);
}
// Verify return type is Vec<i32>
match &fn_sig.output {
syn::ReturnType::Type(_, ty) => {
if !matches!(&**ty, syn::Type::Path(tp) if tp.path.is_ident("Vec")) {
panic!("Execute function must return Vec<i32>");
}
}
syn::ReturnType::Default => {
panic!("Execute function must return Vec<i32>");
}
}
}
fn generate_execute_wrapper(
input_fn: syn::ItemFn,
fn_name: &syn::Ident,
input_count: usize,
) -> TokenStream {
let arg_names: Vec<_> = (0..input_count)
.map(|i| syn::Ident::new(&format!("arg{}", i), proc_macro2::Span::call_site()))
.collect();
let expanded = quote! {
#input_fn
#[no_mangle]
pub extern "C" fn execute(
output_pos: i32,
#( #arg_names: i32 ),*
) -> i32 {
extern "C" {
fn __nodarium_log(ptr: *const u8, len: usize);
fn __nodarium_log_panic(ptr: *const u8, len: usize);
}
// Setup panic hook
static SET_HOOK: std::sync::Once = std::sync::Once::new();
SET_HOOK.call_once(|| {
std::panic::set_hook(Box::new(|info| {
let msg = info.to_string();
unsafe { __nodarium_log_panic(msg.as_ptr(), msg.len()); }
}));
});
// Call user function
let result = #fn_name(
#( #arg_names as *const i32 ),*
);
// Write result directly to shared memory at output_pos
let len_bytes = result.len() * 4;
unsafe {
let src = result.as_ptr() as *const u8;
let dst = output_pos as *mut u8;
dst.copy_from_nonoverlapping(src, len_bytes);
}
// Forget the Vec to prevent deallocation (data is in shared memory now)
core::mem::forget(result);
len_bytes as i32
}
};
TokenStream::from(expanded)
}
```
### 2.3 Generated Assembly
The macro generates:
```asm
; Input: output_pos in register r0, arg0 in r1, arg1 in r2, arg2 in r3
execute:
; Call user function
bl user_execute ; returns pointer to Vec<i32> in r0
; Calculate byte length
ldr r4, [r0, #8] ; Vec::len field
lsl r4, r4, #2 ; len * 4 (i32 = 4 bytes)
; Copy Vec data to shared memory at output_pos
ldr r5, [r0, #0] ; Vec::ptr field
ldr r6, [r0, #4] ; capacity (unused)
; memcpy(dst=output_pos, src=r5, len=r4)
; (implemented via copy_from_nonoverlapping)
; Return length
mov r0, r4
bx lr
```
## Phase 3: Input Reading Helpers
```rust
// packages/utils/src/accessor.rs
/// Read i32 from shared memory
#[inline]
pub unsafe fn read_i32(ptr: *const i32) -> i32 {
*ptr
}
/// Read f32 from shared memory (stored as i32 bits)
#[inline]
pub unsafe fn read_f32(ptr: *const i32) -> f32 {
f32::from_bits(*ptr as u32)
}
/// Read boolean from shared memory
#[inline]
pub unsafe fn read_bool(ptr: *const i32) -> bool {
*ptr != 0
}
/// Read vec3 (3 f32s) from shared memory
#[inline]
pub unsafe fn read_vec3(ptr: *const i32) -> [f32; 3] {
let p = ptr as *const f32;
[p.read(), p.add(1).read(), p.add(2).read()]
}
/// Read slice from shared memory
#[inline]
pub unsafe fn read_i32_slice(ptr: *const i32, len: usize) -> &[i32] {
std::slice::from_raw_parts(ptr, len)
}
/// Read f32 slice from shared memory
#[inline]
pub unsafe fn read_f32_slice(ptr: *const i32, len: usize) -> &[f32] {
std::slice::from_raw_parts(ptr as *const f32, len)
}
/// Read with default value
#[inline]
pub unsafe fn read_f32_default(ptr: *const i32, default: f32) -> f32 {
if ptr.is_null() { default } else { read_f32(ptr) }
}
#[inline]
pub unsafe fn read_i32_default(ptr: *const i32, default: i32) -> i32 {
if ptr.is_null() { default } else { read_i32(ptr) }
}
```
## Phase 4: Node Implementation Examples
### 4.1 Math Node
```rust
// nodes/max/plantarium/math/src/lib.rs
nodarium_definition_file!("src/input.json");
#[nodarium_execute]
pub fn execute(op_type: *const i32, a: *const i32, b: *const i32) -> Vec<i32> {
use nodarium_utils::{read_i32, read_f32};
let op = unsafe { read_i32(op_type) };
let a_val = unsafe { read_f32(a) };
let b_val = unsafe { read_f32(b) };
let result = match op {
0 => a_val + b_val, // add
1 => a_val - b_val, // subtract
2 => a_val * b_val, // multiply
3 => a_val / b_val, // divide
_ => 0.0,
};
vec![result.to_bits()]
}
```
### 4.2 Vec3 Node
```rust
// nodes/max/plantarium/vec3/src/lib.rs
nodarium_definition_file!("src/input.json");
#[nodarium_execute]
pub fn execute(x: *const i32, y: *const i32, z: *const i32) -> Vec<i32> {
use nodarium_utils::read_f32;
let x_val = unsafe { read_f32(x) };
let y_val = unsafe { read_f32(y) };
let z_val = unsafe { read_f32(z) };
vec![x_val.to_bits(), y_val.to_bits(), z_val.to_bits()]
}
```
### 4.3 Box Node
```rust
// nodes/max/plantarium/box/src/lib.rs
nodarium_definition_file!("src/input.json");
#[nodarium_execute]
pub fn execute(size: *const i32) -> Vec<i32> {
use nodarium_utils::{read_f32, encode_float, calculate_normals};
let size = unsafe { read_f32(size) };
let p = encode_float(size);
let n = encode_float(-size);
let mut cube_geometry = vec![
1, // 1: geometry
8, // 8 vertices
12, // 12 faces
// Face indices
0, 1, 2, 0, 2, 3,
0, 3, 4, 4, 5, 0,
6, 1, 0, 5, 6, 0,
7, 2, 1, 6, 7, 1,
2, 7, 3, 3, 7, 4,
7, 6, 4, 4, 6, 5,
// Bottom plate
p, n, n, p, n, p, n, n, p, n, n, n,
// Top plate
n, p, n, p, p, n, p, p, p, n, p, p,
// Normals
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0,
];
calculate_normals(&mut cube_geometry);
cube_geometry
}
```
### 4.4 Stem Node
```rust
// nodes/max/plantarium/stem/src/lib.rs
nodarium_definition_file!("src/input.json");
#[nodarium_execute]
pub fn execute(
origin: *const i32,
amount: *const i32,
length: *const i32,
thickness: *const i32,
resolution: *const i32,
) -> Vec<i32> {
use nodarium_utils::{
read_vec3, read_i32, read_f32,
geometry::{create_multiple_paths, wrap_multiple_paths},
};
let origin = unsafe { read_vec3(origin) };
let amount = unsafe { read_i32(amount) } as usize;
let length = unsafe { read_f32(length) };
let thickness = unsafe { read_f32(thickness) };
let resolution = unsafe { read_i32(resolution) } as usize;
let mut stem_data = create_multiple_paths(amount, resolution, 1);
let mut stems = wrap_multiple_paths(&mut stem_data);
for stem in stems.iter_mut() {
let points = stem.get_points_mut();
for (i, point) in points.iter_mut().enumerate() {
let t = i as f32 / (resolution as f32 - 1.0);
point.x = origin[0];
point.y = origin[1] + t * length;
point.z = origin[2];
point.w = thickness * (1.0 - t);
}
}
stem_data
}
```
## Phase 5: Runtime Implementation
```typescript
// app/src/lib/runtime/memory-manager.ts
export const SHARED_MEMORY = new WebAssembly.Memory({
initial: 1024, // 64MB initial
maximum: 4096, // 256MB maximum
});
export class MemoryManager {
private offset: number = 0;
private readonly start: number = 0;
reset() {
this.offset = this.start;
}
alloc(bytes: number): number {
const pos = this.offset;
this.offset += bytes;
return pos;
}
readInt32(pos: number): number {
return new Int32Array(SHARED_MEMORY.buffer)[pos / 4];
}
readFloat32(pos: number): number {
return new Float32Array(SHARED_MEMORY.buffer)[pos / 4];
}
readBytes(pos: number, length: number): Uint8Array {
return new Uint8Array(SHARED_MEMORY.buffer, pos, length);
}
getInt32View(): Int32Array {
return new Int32Array(SHARED_MEMORY.buffer);
}
getFloat32View(): Float32Array {
return new Float32Array(SHARED_MEMORY.buffer);
}
getRemaining(): number {
return SHARED_MEMORY.buffer.byteLength - this.offset;
}
}
```
```typescript
// app/src/lib/runtime/imports.ts
import { SHARED_MEMORY } from "./memory-manager";
export function createImportObject(nodeId: string): WebAssembly.Imports {
return {
env: {
// Import shared memory
memory: SHARED_MEMORY,
// Logging
__nodarium_log: (ptr: number, len: number) => {
const msg = new TextDecoder().decode(
new Uint8Array(SHARED_MEMORY.buffer, ptr, len),
);
console.log(`[${nodeId}] ${msg}`);
},
__nodarium_log_panic: (ptr: number, len: number) => {
const msg = new TextDecoder().decode(
new Uint8Array(SHARED_MEMORY.buffer, ptr, len),
);
console.error(`[${nodeId}] PANIC: ${msg}`);
},
},
};
}
```
```typescript
// app/src/lib/runtime/executor.ts
import { SHARED_MEMORY } from "./memory-manager";
import { createImportObject } from "./imports";
export class SharedMemoryRuntimeExecutor implements RuntimeExecutor {
private memory: MemoryManager;
private results: Map<string, { pos: number; len: number }> = new Map();
private instances: Map<string, WebAssembly.Instance> = new Map();
constructor(private registry: NodeRegistry) {
this.memory = new MemoryManager();
}
async execute(graph: Graph, settings: Record<string, unknown>) {
this.memory.reset();
this.results.clear();
const [outputNode, nodes] = await this.addMetaData(graph);
const sortedNodes = nodes.sort((a, b) => b.depth - a.depth);
for (const node of sortedNodes) {
await this.executeNode(node, settings);
}
const result = this.results.get(outputNode.id);
const view = this.memory.getInt32View();
return view.subarray(result.pos / 4, result.pos / 4 + result.len / 4);
}
private async executeNode(
node: RuntimeNode,
settings: Record<string, unknown>,
) {
const def = this.definitionMap.get(node.type)!;
const inputs = def.inputs || {};
const inputNames = Object.keys(inputs);
const outputSize = this.estimateOutputSize(def);
const outputPos = this.memory.alloc(outputSize);
const args: number[] = [outputPos];
for (const inputName of inputNames) {
const inputDef = inputs[inputName];
const inputNode = node.state.inputNodes[inputName];
if (inputNode) {
const parentResult = this.results.get(inputNode.id)!;
args.push(parentResult.pos);
continue;
}
const valuePos = this.memory.alloc(16);
this.writeValue(
valuePos,
inputDef,
node.props?.[inputName] ??
settings[inputDef.setting ?? ""] ??
inputDef.value,
);
args.push(valuePos);
}
let instance = this.instances.get(node.type);
if (!instance) {
instance = await this.instantiateNode(node.type);
this.instances.set(node.type, instance);
}
const writtenLen = instance.exports.execute(...args);
this.results.set(node.id, { pos: outputPos, len: writtenLen });
}
private writeValue(pos: number, inputDef: NodeInput, value: unknown) {
const view = this.memory.getFloat32View();
const intView = this.memory.getInt32View();
switch (inputDef.type) {
case "float":
view[pos / 4] = value as number;
break;
case "integer":
case "select":
case "seed":
intView[pos / 4] = value as number;
break;
case "boolean":
intView[pos / 4] = value ? 1 : 0;
break;
case "vec3":
const arr = value as number[];
view[pos / 4] = arr[0];
view[pos / 4 + 1] = arr[1];
view[pos / 4 + 2] = arr[2];
break;
}
}
private estimateOutputSize(def: NodeDefinition): number {
const sizes: Record<string, number> = {
float: 16,
integer: 16,
boolean: 16,
vec3: 16,
geometry: 8192,
path: 4096,
};
return sizes[def.outputs?.[0] || "float"] || 64;
}
private async instantiateNode(
nodeType: string,
): Promise<WebAssembly.Instance> {
const wasmBytes = await this.fetchWasm(nodeType);
const module = await WebAssembly.compile(wasmBytes);
const importObject = createImportObject(nodeType);
return WebAssembly.instantiate(module, importObject);
}
}
```
## Phase 7: Execution Flow Visualization
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Execution Timeline │
└─────────────────────────────────────────────────────────────────────────────┘
Step 1: Setup
SHARED_MEMORY = new WebAssembly.Memory({ initial: 1024 })
memory.offset = 0
Step 2: Execute Node A (math with 3 inputs)
outputPos = memory.alloc(16) = 0
args = [0, ptr_to_op_type, ptr_to_a, ptr_to_b]
Node A reads:
*ptr_to_op_type → op
*ptr_to_a → a
*ptr_to_b → b
Node A returns: vec![result.to_bits()]
Macro writes result directly to SHARED_MEMORY[0..4]
Returns: 4
results['A'] = { pos: 0, len: 4 }
memory.offset = 4
Step 3: Execute Node B (stem with 5 inputs, input[0] from A)
outputPos = memory.alloc(4096) = 4
args = [4, results['A'].pos, ptr_to_amount, ptr_to_length, ...]
Node B reads:
*results['A'].pos → value from Node A
*ptr_to_amount → amount
...
Node B returns: stem_data Vec<i32> (1000 elements = 4000 bytes)
Macro writes stem_data directly to SHARED_MEMORY[4..4004]
Returns: 4000
results['B'] = { pos: 4, len: 4000 }
memory.offset = 4004
Step 4: Execute Node C (output, 1 input from B)
outputPos = memory.alloc(16) = 4004
args = [4004, results['B'].pos, results['B'].len]
Node C reads:
*results['B'].pos → stem geometry
Node C returns: vec![1] (identity)
Macro writes to SHARED_MEMORY[4004..4008]
results['C'] = { pos: 4004, len: 4 }
Final: Return SHARED_MEMORY[4004..4008] as geometry result
```
## Phase 6: Memory Growth Strategy
```typescript
class MemoryManager {
alloc(bytes: number): number {
const required = this.offset + bytes;
const currentBytes = SHARED_MEMORY.buffer.byteLength;
if (required > currentBytes) {
const pagesNeeded = Math.ceil((required - currentBytes) / 65536);
const success = SHARED_MEMORY.grow(pagesNeeded);
if (!success) {
throw new Error(`Out of memory: need ${bytes} bytes`);
}
this.int32View = new Int32Array(SHARED_MEMORY.buffer);
this.float32View = new Float32Array(SHARED_MEMORY.buffer);
}
const pos = this.offset;
this.offset += bytes;
return pos;
}
}
```
## Phase 8: Migration Checklist
### Build Configuration
- [ ] Add `--import-memory` to Rust flags in `Cargo.toml`
- [ ] Ensure no nodes export memory
### Runtime
- [ ] Create `SHARED_MEMORY` instance
- [ ] Implement `MemoryManager` with alloc/read/write
- [ ] Create import object factory
- [ ] Implement `SharedMemoryRuntimeExecutor`
### Macro
- [ ] Parse definition JSON
- [ ] Validate function signature (N params, Vec<i32> return)
- [ ] Generate wrapper that writes return value to `output_pos`
- [ ] Add panic hook
### Utilities
- [ ] `read_i32(ptr: *const i32) -> i32`
- [ ] `read_f32(ptr: *const i32) -> f32`
- [ ] `read_bool(ptr: *const i32) -> bool`
- [ ] `read_vec3(ptr: *const i32) -> [f32; 3]`
- [ ] `read_i32_slice(ptr: *const i32, len: usize) -> &[i32]`
### Nodes
- [ ] `float`, `integer`, `boolean` nodes
- [ ] `vec3` node
- [ ] `math` node
- [ ] `random` node
- [ ] `box` node
- [ ] `stem` node
- [ ] `branch` node
- [ ] `instance` node
- [ ] `output` node
## Phase 9: Before vs After
### Before (per-node memory)
```rust
#[nodarium_execute]
pub fn execute(input: &[i32]) -> Vec<i32> {
let args = split_args(input);
let a = evaluate_float(args[0]);
let b = evaluate_float(args[1]);
vec![(a + b).to_bits()]
}
```
### After (shared memory)
```rust
#[nodarium_execute]
pub fn execute(a: *const i32, b: *const i32) -> Vec<i32> {
use nodarium_utils::read_f32;
let a_val = unsafe { read_f32(a) };
let b_val = unsafe { read_f32(b) };
vec![(a_val + b_val).to_bits()]
}
```
**Key differences:**
- Parameters are input pointers, not a slice
- Use `read_f32` helper instead of `evaluate_float`
- Macro writes result directly to shared memory
- All nodes share the same memory import
## Phase 10: Benefits
| Aspect | Before | After |
| ----------------- | -------------- | -------------------- |
| Memory | N × ~1MB heaps | 1 × 64-256MB shared |
| Cross-node access | Copy via JS | Direct read |
| API | `&[i32]` slice | `*const i32` pointer |
| Validation | Runtime | Compile-time |

View File

@@ -1,227 +0,0 @@
# Nodarium - AI Coding Agent Summary
## Project Overview
Nodarium is a WebAssembly-based visual programming language used to build <https://nodes.max-richter.dev>, a procedural 3D plant modeling tool. The system allows users to create visual node graphs where each node is a compiled WebAssembly module.
## Technology Stack
**Frontend (SvelteKit):**
- Framework: SvelteKit with Svelte 5
- 3D Rendering: Three.js via Threlte
- Styling: Tailwind CSS 4
- Build Tool: Vite
- State Management: Custom store-client package
- WASM Integration: vite-plugin-wasm, comlink
**Backend/Core (Rust/WASM):**
- Language: Rust
- Output: WebAssembly (wasm32-unknown-unknown target)
- Build Tool: cargo
- Procedural Macros: custom macros package
**Package Management:**
- Node packages: pnpm workspace (v10.28.1)
- Rust packages: Cargo workspace
## Directory Structure
```
nodarium/
├── app/ # SvelteKit web application
│ ├── src/
│ │ ├── lib/ # App-specific components and utilities
│ │ ├── routes/ # SvelteKit routes (pages)
│ │ ├── app.css # Global styles
│ │ └── app.html # HTML template
│ ├── static/
│ │ └── nodes/ # Compiled WASM node files served statically
│ ├── package.json # App dependencies
│ ├── svelte.config.js # SvelteKit configuration
│ ├── vite.config.ts # Vite configuration
│ └── tsconfig.json # TypeScript configuration
├── packages/ # Shared workspace packages
│ ├── ui/ # Svelte UI component library (published as @nodarium/ui)
│ │ ├── src/ # UI components
│ │ ├── static/ # Static assets for UI
│ │ ├── dist/ # Built output
│ │ └── package.json
│ ├── registry/ # Node registry with IndexedDB persistence (@nodarium/registry)
│ │ └── src/
│ ├── types/ # Shared TypeScript types (@nodarium/types)
│ │ └── src/
│ ├── utils/ # Shared utilities (@nodarium/utils)
│ │ └── src/
│ └── macros/ # Rust procedural macros for node development
├── nodes/ # WebAssembly node packages (Rust)
│ └── max/plantarium/ # Plantarium nodes namespace
│ ├── box/ # Box geometry node
│ ├── branch/ # Branch generation node
│ ├── float/ # Float value node
│ ├── gravity/ # Gravity simulation node
│ ├── instance/ # Geometry instancing node
│ ├── math/ # Math operations node
│ ├── noise/ # Noise generation node
│ ├── output/ # Output node for results
│ ├── random/ # Random value node
│ ├── rotate/ # Rotation transformation node
│ ├── stem/ # Stem geometry node
│ ├── triangle/ # Triangle geometry node
│ ├── vec3/ # Vector3 manipulation node
│ └── .template/ # Node template for creating new nodes
├── docs/ # Documentation
│ ├── ARCHITECTURE.md # System architecture overview
│ ├── DEVELOPING_NODES.md # Guide for creating new nodes
│ ├── NODE_DEFINITION.md # Node definition schema
│ └── PLANTARIUM.md # Plantarium-specific documentation
├── Cargo.toml # Rust workspace configuration
├── package.json # Root npm scripts
├── pnpm-workspace.yaml # pnpm workspace configuration
├── pnpm-lock.yaml # Locked dependency versions
└── README.md # Project readme
```
## Node System Architecture
### What is a Node?
Nodes are WebAssembly modules that:
- Have a unique ID (e.g., `max/plantarium/stem`)
- Define inputs with types and default values
- Define outputs they produce
- Execute logic when called with arguments
### Node Definition Schema
Nodes are defined via `definition.json` embedded in each WASM module:
```json
{
"id": "namespace/category/node-name",
"outputs": ["geometry"],
"inputs": {
"height": { "type": "float", "value": 1.0 },
"radius": { "type": "float", "value": 0.1 }
}
}
```
For now the outputs are limited to a single output.
### Node Execution
Nodes receive serialized arguments and return serialized outputs. The `nodarium_utils` Rust crate provides helpers for:
- Parsing input arguments
- Creating geometry data
- Concatenating output vectors
### Node Registration
Nodes are:
1. Compiled to WASM files in `target/wasm32-unknown-unknown/release/`
2. Copied to `app/static/nodes/` for serving
3. Registered in the browser via IndexedDB using the registry package
## Key Dependencies
**Frontend:**
- `@sveltejs/kit` - Application framework
- `@threlte/core` & `@threlte/extras` - Three.js Svelte integration
- `three` - 3D graphics library
- `tailwindcss` - CSS framework
- `comlink` - WebWorker RPC
- `idb` - IndexedDB wrapper
- `wabt` - WebAssembly binary toolkit
**Rust/WASM:**
- Language: Rust (compiled with plain cargo)
- Output: WebAssembly (wasm32-unknown-unknown target)
- Generic WASM wrapper for language-agnostic node development
- `glam` - Math library (Vec2, Vec3, Mat4, etc.)
- `nodarium_macros` - Custom procedural macros
- `nodarium_utils` - Shared node utilities
## Build Commands
From root directory:
```bash
# Install dependencies
pnpm i
# Build all WASM nodes (compiles Rust, copies to app/static)
pnpm build:nodes
# Build the app (builds UI library + SvelteKit app)
pnpm build:app
# Full build (nodes + app)
pnpm build
# Development
pnpm dev # Run all dev commands in parallel
pnpm dev:nodes # Watch nodes/, auto-rebuild on changes
pnpm dev:app_ui # Watch app and UI package
pnpm dev_ui # Watch UI package only
```
## Workspace Packages
The project uses pnpm workspaces with the following packages:
| Package | Location | Purpose |
| ------------------ | ------------------ | ------------------------------ |
| @nodarium/app | app/ | Main SvelteKit application |
| @nodarium/ui | packages/ui/ | Reusable UI component library |
| @nodarium/registry | packages/registry/ | Node registry with persistence |
| @nodarium/types | packages/types/ | Shared TypeScript types |
| @nodarium/utils | packages/utils/ | Shared utilities |
| nodarium macros | packages/macros/ | Rust procedural macros |
## Configuration Files
- `.dprint.jsonc` - Dprint formatter configuration
- `svelte.config.js` - SvelteKit configuration (app and ui)
- `vite.config.ts` - Vite bundler configuration
- `tsconfig.json` - TypeScript configuration (app and packages)
- `Cargo.toml` - Rust workspace with member packages
- `flake.nix` - Nix development environment
## Development Workflow
### Adding a New Node
1. Copy the `.template` directory in `nodes/max/plantarium/` to create a new node directory
2. Define node in `src/definition.json`
3. Implement logic in `src/lib.rs`
4. Build with `cargo build --release --target wasm32-unknown-unknown`
5. Test by dragging onto the node graph
### Modifying UI Components
1. Changes to `packages/ui/` automatically rebuild with watch mode
2. App imports from `@nodarium/ui`
3. Run `pnpm dev:app_ui` for hot reload
## Important Notes for AI Agents
1. **WASM Compilation**: Nodes require `wasm32-unknown-unknown` target (`rustup target add wasm32-unknown-unknown`)
2. **Cross-Compilation**: WASM build happens on host, not in containers/VMs
3. **Static Serving**: Compiled WASM files must exist in `app/static/nodes/` before dev server runs
4. **Workspace Dependencies**: Use `workspace:*` protocol for internal packages
5. **Threlte Version**: Uses Threlte 8.x, not 7.x (important for 3D component APIs)
6. **Svelte 5**: Project uses Svelte 5 with runes (`$state`, `$derived`, `$effect`)
7. **Tailwind 4**: Uses Tailwind CSS v4 with `@tailwindcss/vite` plugin
8. **IndexedDB**: Registry uses IDB for persistent node storage in browser

View File

@@ -1,294 +0,0 @@
# Node Compilation and Runtime Execution
## Overview
Nodarium nodes are WebAssembly modules written in Rust. Each node is a compiled WASM binary that exposes a standardized C ABI interface. The system uses procedural macros to generate the necessary boilerplate for node definitions, memory management, and execution.
## Node Compilation
### 1. Node Definition (JSON)
Each node has a `src/input.json` file that defines:
```json
{
"id": "max/plantarium/stem",
"meta": { "description": "Creates a stem" },
"outputs": ["path"],
"inputs": {
"origin": { "type": "vec3", "value": [0, 0, 0], "external": true },
"amount": { "type": "integer", "value": 1, "min": 1, "max": 64 },
"length": { "type": "float", "value": 5 },
"thickness": { "type": "float", "value": 0.2 }
}
}
```
### 2. Procedural Macros
The `nodarium_macros` crate provides two procedural macros:
#### `#[nodarium_execute]`
Transforms a Rust function into a WASM-compatible entry point:
```rust
#[nodarium_execute]
pub fn execute(input: &[i32]) -> Vec<i32> {
// Node logic here
}
```
The macro generates:
- **C ABI wrapper**: Converts the WASM interface to a standard C FFI
- **`execute` function**: Takes `(ptr: *const i32, len: usize)` and returns `*mut i32`
- **Memory allocation**: `__alloc(len: usize) -> *mut i32` for buffer allocation
- **Memory deallocation**: `__free(ptr: *mut i32, len: usize)` for cleanup
- **Static output buffer**: `OUTPUT_BUFFER` for returning results
- **Panic hook**: Routes panics through `host_log_panic` for debugging
- **Internal logic wrapper**: Wraps the original function
#### `nodarium_definition_file!("path")`
Embeds the node definition JSON into the WASM binary:
```rust
nodarium_definition_file!("src/input.json");
```
Generates:
- **`DEFINITION_DATA`**: Static byte array in `nodarium_definition` section
- **`get_definition_ptr()`**: Returns pointer to definition data
- **`get_definition_len()`**: Returns length of definition data
### 3. Build Process
Nodes are compiled with:
```bash
cargo build --release --target wasm32-unknown-unknown
```
The resulting `.wasm` files are copied to `app/static/nodes/` for serving.
## Node Execution Runtime
### Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ WebWorker Thread │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ WorkerRuntimeExecutor ││
│ │ ┌───────────────────────────────────────────────────┐ ││
│ │ │ MemoryRuntimeExecutor ││
│ │ │ ┌─────────────────────────────────────────────┐ ││
│ │ │ │ Node Registry (WASM + Definitions) ││
│ │ │ └─────────────────────────────────────────────┘ ││
│ │ │ ┌─────────────────────────────────────────────┐ ││
│ │ │ │ Execution Engine (Bottom-Up Evaluation) ││
│ │ │ └─────────────────────────────────────────────┘ ││
│ │ └───────────────────────────────────────────────────┘ ││
│ └─────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘
```
### 1. MemoryRuntimeExecutor
The core execution engine in `runtime-executor.ts`:
#### Metadata Collection (`addMetaData`)
1. Load node definitions from registry
2. Build parent/child relationships from graph edges
3. Calculate execution depth via reverse BFS from output node
#### Node Sorting
Nodes are sorted by depth (highest depth first) for bottom-up execution:
```
Depth 3: n3 n6
Depth 2: n2 n4 n5
Depth 1: n1
Depth 0: Output
Execution order: n3, n6, n2, n4, n5, n1, Output
```
#### Input Collection
For each node, inputs are gathered from:
1. **Connected nodes**: Results from parent nodes in the graph
2. **Node props**: Values stored directly on the node instance
3. **Settings**: Global settings mapped via `setting` property
4. **Defaults**: Values from node definition
#### Input Encoding
Values are encoded as `Int32Array`:
- **Floats**: IEEE 754 bits cast to i32
- **Vectors**: `[0, count, v1, v2, v3, 1, 1]` (nested bracket format)
- **Booleans**: `0` or `1`
- **Integers**: Direct i32 value
#### Caching
Results are cached using:
```typescript
inputHash = `node-${node.id}-${fastHashArrayBuffer(encoded_inputs)}`
```
The cache uses LRU eviction (default size: 50 entries).
### 2. Execution Flow
```typescript
async execute(graph: Graph, settings) {
// 1. Load definitions and build node relationships
const [outputNode, nodes] = await this.addMetaData(graph);
// 2. Sort nodes by depth (bottom-up)
const sortedNodes = nodes.sort((a, b) => b.depth - a.depth);
// 3. Execute each node
for (const node of sortedNodes) {
const inputs = this.collectInputs(node, settings);
const encoded = concatEncodedArrays(inputs);
const result = nodeType.execute(encoded);
this.results[node.id] = result;
}
// 4. Return output node result
return this.results[outputNode.id];
}
```
### 3. Worker Isolation
`WorkerRuntimeExecutor` runs execution in a WebWorker via Comlink:
```typescript
class WorkerRuntimeExecutor implements RuntimeExecutor {
private worker = new ComlinkWorker(...);
async execute(graph, settings) {
return this.worker.executeGraph(graph, settings);
}
}
```
The worker backend (`worker-runtime-executor-backend.ts`):
- Creates a single `MemoryRuntimeExecutor` instance
- Manages caching state
- Collects performance metrics
### 4. Remote Execution (Optional)
`RemoteRuntimeExecutor` can execute graphs on a remote server:
```typescript
class RemoteRuntimeExecutor implements RuntimeExecutor {
async execute(graph, settings) {
const res = await fetch(this.url, {
method: "POST",
body: JSON.stringify({ graph, settings })
});
return new Int32Array(await res.arrayBuffer());
}
}
```
## Data Encoding Format
### Bracket Notation
Inputs and outputs use a nested bracket encoding:
```
[0, count, item1, item2, ..., 1, 1]
^ ^ items ^ ^
| | | |
| | | +-- closing bracket
| +-- number of items + 1 |
+-- opening bracket (0) +-- closing bracket (1)
```
### Example Encodings
**Float (5.0)**:
```typescript
encodeFloat(5.0) // → 1084227584 (IEEE 754 bits as i32)
```
**Vec3 ([1, 2, 3])**:
```typescript
[0, 4, encodeFloat(1), encodeFloat(2), encodeFloat(3), 1, 1]
```
**Nested Math Expression**:
```
[0, 3, 0, 2, 0, 3, 0, 0, 0, 3, 7549747, 127, 1, 1, ...]
```
### Decoding Utilities
From `packages/utils/src/tree.rs`:
- `split_args()`: Parses nested bracket arrays into segments
- `evaluate_float()`: Recursively evaluates and decodes float expressions
- `evaluate_int()`: Evaluates integer/math node expressions
- `evaluate_vec3()`: Decodes vec3 arrays
## Geometry Data Format
### Path Data
Paths represent procedural plant structures:
```
[0, count, [0, header_size, node_type, depth, x, y, z, w, ...], 1, 1]
```
Each point has 4 values: x, y, z position + thickness (w).
### Geometry Data
Meshes use a similar format with vertices and face indices.
## Performance Tracking
The runtime collects detailed performance metrics:
- `collect-metadata`: Time to build node graph
- `collected-inputs`: Time to gather inputs
- `encoded-inputs`: Time to encode inputs
- `hash-inputs`: Time to compute cache hash
- `cache-hit`: 1 if cache hit, 0 if miss
- `node/{node_type}`: Time per node execution
## Caching Strategy
### MemoryRuntimeCache
LRU cache implementation:
```typescript
class MemoryRuntimeCache {
private map = new Map<string, unknown>();
size: number = 50;
get(key) { /* move to front */ }
set(key, value) { /* evict oldest if at capacity */ }
}
```
### IndexDBCache
For persistence across sessions, the registry uses IndexedDB caching.
## Summary
The Nodarium node system works as follows:
1. **Compilation**: Rust functions are decorated with macros that generate C ABI WASM exports
2. **Registration**: Node definitions are embedded in WASM and loaded at runtime
3. **Graph Analysis**: Runtime builds node relationships and execution order
4. **Bottom-Up Execution**: Nodes execute from leaves to output
5. **Caching**: Results are cached per-node-inputs hash for performance
6. **Isolation**: Execution runs in a WebWorker to prevent main thread blocking

View File

@@ -39,6 +39,5 @@ server {
EOF EOF
COPY --from=builder /app/app/build /app COPY --from=builder /app/app/build /app
COPY --from=builder /app/packages/ui/build /app/ui
EXPOSE 80 EXPOSE 80

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));
export function areSocketsCompatible( 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('*') || inputs.includes(output); return inputs.includes(output);
} }
return inputs === output || inputs === '*'; return inputs === output;
} }
function areEdgesEqual(firstEdge: Edge, secondEdge: Edge) { function areEdgesEqual(firstEdge: Edge, secondEdge: Edge) {
@@ -268,7 +268,14 @@ 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) => {
return [node.id, node as NodeInstance]; const nodeType = this.registry.getNode(node.type);
const n = node as NodeInstance;
if (nodeType) {
n.state = {
type: nodeType
};
}
return [node.id, n];
}) })
); );
@@ -293,30 +300,6 @@ 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();
@@ -325,17 +308,26 @@ 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)]));
logger.info('loading graph', {
nodes: graph.nodes,
edges: graph.edges,
id: graph.id,
ids: nodeIds
});
await this.registry.load(nodeIds); await this.registry.load(nodeIds);
// Fetch all nodes from all collections of the loaded nodes
const allCollections = new Set<`${string}/${string}`>();
for (const id of nodeIds) {
const [user, collection] = id.split('/');
allCollections.add(`${user}/${collection}`);
}
for (const collection of allCollections) {
remoteRegistry
.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());
for (const node of this.graph.nodes) { for (const node of this.graph.nodes) {
@@ -392,9 +384,7 @@ 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() {
@@ -501,10 +491,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[i]; const [inputName, input] = inputs[0];
for (let o = 0; o < outputs.length; o++) { for (let o = 0; o < outputs.length; o++) {
const output = outputs[o]; const output = outputs[0];
if (input.type === output || input.type === '*') { if (input.type === output) {
return this.createEdge(from, o, to, inputName); return this.createEdge(from, o, to, inputName);
} }
} }
@@ -606,14 +596,11 @@ export class GraphManager extends EventEmitter<{
return; return;
} }
const fromType = from.state.type || this.registry.getNode(from.type);
const toType = to.state.type || this.registry.getNode(to.type);
// check if socket types match // check if socket types match
const fromSocketType = fromType?.outputs?.[fromSocket]; const fromSocketType = from.state?.type?.outputs?.[fromSocket];
const toSocketType = [toType?.inputs?.[toSocket]?.type]; const toSocketType = [to.state?.type?.inputs?.[toSocket]?.type];
if (toType?.inputs?.[toSocket]?.accepts) { if (to.state?.type?.inputs?.[toSocket]?.accepts) {
toSocketType.push(...(toType?.inputs?.[toSocket]?.accepts || [])); toSocketType.push(...(to?.state?.type?.inputs?.[toSocket]?.accepts || []));
} }
if (!areSocketsCompatible(fromSocketType, toSocketType)) { if (!areSocketsCompatible(fromSocketType, toSocketType)) {
@@ -736,9 +723,8 @@ export class GraphManager extends EventEmitter<{
} }
getPossibleSockets({ node, index }: Socket): [NodeInstance, string | number][] { getPossibleSockets({ node, index }: Socket): [NodeInstance, string | number][] {
const nodeType = this.registry.getNode(node.type); const nodeType = node?.state?.type;
if (!nodeType) return []; if (!nodeType) return [];
console.log({ index });
const sockets: [NodeInstance, string | number][] = []; const sockets: [NodeInstance, string | number][] = [];
@@ -753,7 +739,7 @@ export class GraphManager extends EventEmitter<{
const ownType = nodeType?.inputs?.[index].type; const ownType = nodeType?.inputs?.[index].type;
for (const node of nodes) { for (const node of nodes) {
const nodeType = this.registry.getNode(node.type); const nodeType = node?.state?.type;
const inputs = nodeType?.outputs; const inputs = nodeType?.outputs;
if (!inputs) continue; if (!inputs) continue;
for (let index = 0; index < inputs.length; index++) { for (let index = 0; index < inputs.length; index++) {
@@ -781,7 +767,7 @@ export class GraphManager extends EventEmitter<{
const ownType = nodeType.outputs?.[index]; const ownType = nodeType.outputs?.[index];
for (const node of nodes) { for (const node of nodes) {
const inputs = this.registry.getNode(node.type)?.inputs; const inputs = node?.state?.type?.inputs;
if (!inputs) continue; if (!inputs) continue;
for (const key in inputs) { for (const key in inputs) {
const otherType = [inputs[key].type]; const otherType = [inputs[key].type];
@@ -797,7 +783,6 @@ export class GraphManager extends EventEmitter<{
} }
} }
console.log(`Found ${sockets.length} possible sockets`, sockets);
return sockets; return sockets;
} }

View File

@@ -170,14 +170,11 @@ 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 inputs = node.state.type?.inputs || this.graph.registry.getNode(node.type)?.inputs const _index = Object.keys(node.state?.type?.inputs || {}).indexOf(index);
|| {}; 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;
} }
} }
@@ -253,7 +250,7 @@ export class GraphState {
let { node, index, position } = socket; let { node, index, position } = socket;
// if the socket is an input socket -> remove existing edges // remove existing edge
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

@@ -1,23 +1,23 @@
<script lang="ts"> <script lang="ts">
import type { Edge, NodeInstance } from "@nodarium/types"; import type { Edge, NodeInstance } from '@nodarium/types';
import { Canvas } from "@threlte/core"; import { Canvas } from '@threlte/core';
import { HTML } from "@threlte/extras"; import { HTML } from '@threlte/extras';
import { createKeyMap } from "../../helpers/createKeyMap"; import { createKeyMap } from '../../helpers/createKeyMap';
import Background from "../background/Background.svelte"; import Background from '../background/Background.svelte';
import AddMenu from "../components/AddMenu.svelte"; import AddMenu from '../components/AddMenu.svelte';
import BoxSelection from "../components/BoxSelection.svelte"; import BoxSelection from '../components/BoxSelection.svelte';
import Camera from "../components/Camera.svelte"; import Camera from '../components/Camera.svelte';
import HelpView from "../components/HelpView.svelte"; import HelpView from '../components/HelpView.svelte';
import Debug from "../debug/Debug.svelte"; import Debug from '../debug/Debug.svelte';
import EdgeEl from "../edges/Edge.svelte"; import EdgeEl from '../edges/Edge.svelte';
import { getGraphManager, getGraphState } from "../graph-state.svelte"; import { getGraphManager, getGraphState } from '../graph-state.svelte';
import NodeEl from "../node/Node.svelte"; import NodeEl from '../node/Node.svelte';
import { maxZoom, minZoom } from "./constants"; import { maxZoom, minZoom } from './constants';
import { FileDropEventManager } from "./drop.events"; import { FileDropEventManager } from './drop.events';
import { MouseEventManager } from "./mouse.events"; import { MouseEventManager } from './mouse.events';
const { const {
keymap, keymap
}: { }: {
keymap: ReturnType<typeof createKeyMap>; keymap: ReturnType<typeof createKeyMap>;
} = $props(); } = $props();
@@ -45,19 +45,18 @@
const newNode = graph.createNode({ const newNode = graph.createNode({
type: node.type, type: node.type,
position: node.position, position: node.position,
props: node.props, props: node.props
}); });
if (!newNode) return; if (!newNode) return;
if (graphState.activeSocket) { if (graphState.activeSocket) {
if (typeof graphState.activeSocket.index === "number") { if (typeof graphState.activeSocket.index === 'number') {
const socketType = const socketType = graphState.activeSocket.node.state?.type?.outputs?.[
graphState.activeSocket.node.state?.type?.outputs?.[ graphState.activeSocket.index
graphState.activeSocket.index ];
];
const input = Object.entries(newNode?.state?.type?.inputs || {}).find( const input = Object.entries(newNode?.state?.type?.inputs || {}).find(
(inp) => inp[1].type === socketType || inp[1].type === "*", (inp) => inp[1].type === socketType
); );
if (input) { if (input) {
@@ -65,14 +64,13 @@
graphState.activeSocket.node, graphState.activeSocket.node,
graphState.activeSocket.index, graphState.activeSocket.index,
newNode, newNode,
input[0], input[0]
); );
} }
} else { } else {
const socketType = const socketType = graphState.activeSocket.node.state?.type?.inputs?.[
graphState.activeSocket.node.state?.type?.inputs?.[ graphState.activeSocket.index
graphState.activeSocket.index ];
];
const output = newNode.state?.type?.outputs?.find((out) => { const output = newNode.state?.type?.outputs?.find((out) => {
if (socketType?.type === out) return true; if (socketType?.type === out) return true;
@@ -85,7 +83,7 @@
newNode, newNode,
output.indexOf(output), output.indexOf(output),
graphState.activeSocket.node, graphState.activeSocket.node,
graphState.activeSocket.index, graphState.activeSocket.index
); );
} }
} }
@@ -148,20 +146,18 @@
<BoxSelection <BoxSelection
cameraPosition={graphState.cameraPosition} cameraPosition={graphState.cameraPosition}
p1={{ p1={{
x: x: graphState.cameraPosition[0]
graphState.cameraPosition[0] + + (graphState.mouseDown[0] - graphState.width / 2)
(graphState.mouseDown[0] - graphState.width / 2) / / graphState.cameraPosition[2],
graphState.cameraPosition[2], y: graphState.cameraPosition[1]
y: + (graphState.mouseDown[1] - graphState.height / 2)
graphState.cameraPosition[1] + / graphState.cameraPosition[2]
(graphState.mouseDown[1] - graphState.height / 2) /
graphState.cameraPosition[2],
}} }}
p2={{ x: graphState.mousePosition[0], y: graphState.mousePosition[1] }} p2={{ x: graphState.mousePosition[0], y: graphState.mousePosition[1] }}
/> />
{/if} {/if}
{#if graph.status === "idle"} {#if graph.status === 'idle'}
{#if graphState.addMenuPosition} {#if graphState.addMenuPosition}
<AddMenu onnode={handleNodeCreation} /> <AddMenu onnode={handleNodeCreation} />
{/if} {/if}
@@ -208,9 +204,9 @@
{/each} {/each}
</div> </div>
</HTML> </HTML>
{:else if graph.status === "loading"} {:else if graph.status === 'loading'}
<span>Loading</span> <span>Loading</span>
{:else if graph.status === "error"} {:else if graph.status === 'error'}
<span>Error</span> <span>Error</span>
{/if} {/if}
</Canvas> </Canvas>

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 type { Graph } from '@nodarium/types'; import { create, type Delta } from "jsondiffpatch";
import { createLogger } from '@nodarium/utils'; import type { Graph } from "@nodarium/types";
import { create, type Delta } from 'jsondiffpatch'; import { clone } from "./helpers/index.js";
import { clone } from './helpers/index.js'; import { createLogger } from "@nodarium/utils";
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

@@ -2,7 +2,6 @@
import { getGraphState } from "../graph-state.svelte"; import { getGraphState } from "../graph-state.svelte";
import { createNodePath } from "../helpers/index.js"; import { createNodePath } from "../helpers/index.js";
import type { NodeInstance } from "@nodarium/types"; import type { NodeInstance } from "@nodarium/types";
import { appSettings } from "$lib/settings/app-settings.svelte";
const graphState = getGraphState(); const graphState = getGraphState();
@@ -44,11 +43,6 @@
<div class="wrapper" data-node-id={node.id} data-node-type={node.type}> <div class="wrapper" data-node-id={node.id} data-node-type={node.type}>
<div class="content"> <div class="content">
{#if appSettings.value.nodeInterface.showNodeIds}
<span class="bg-white text-black! mr-2 px-1 rounded-sm opacity-30"
>{node.id}</span
>
{/if}
{node.type.split("/").pop()} {node.type.split("/").pop()}
</div> </div>
<div <div

View File

@@ -20,10 +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( const wrapper = createWasmWrapper(wasmBytes);
wasmBytes.buffer,
new WebAssembly.Memory({ initial: 1024, maximum: 8192 })
);
return wrapper; return wrapper;
} }

View File

@@ -64,7 +64,6 @@
} }
export const update = function update(result: Int32Array) { export const update = function update(result: Int32Array) {
console.log({ result });
perf.addPoint("split-result"); perf.addPoint("split-result");
const inputs = splitNestedArray(result); const inputs = splitNestedArray(result);
perf.endPoint(); perf.endPoint();

View File

@@ -1,140 +1,96 @@
import { RemoteNodeRegistry } from '@nodarium/registry';
import type { import type {
Graph, Graph,
NodeDefinition, NodeDefinition,
NodeInput, NodeInput,
NodeRegistry, NodeRegistry,
RuntimeExecutor, RuntimeExecutor,
SyncCache SyncCache,
} from '@nodarium/types'; } from "@nodarium/types";
import { import {
concatEncodedArrays,
createLogger, createLogger,
createWasmWrapper,
encodeFloat, encodeFloat,
type PerformanceStore fastHashArrayBuffer,
} from '@nodarium/utils'; type PerformanceStore,
import type { RuntimeNode } from './types'; } from "@nodarium/utils";
import type { RuntimeNode } from "./types";
const log = createLogger('runtime-executor'); const log = createLogger("runtime-executor");
// log.mute(); // Keep logging enabled for debug info log.mute();
const remoteRegistry = new RemoteNodeRegistry(''); function getValue(input: NodeInput, value?: unknown) {
if (value === undefined && "value" in input) {
type WasmExecute = (outputPos: number, args: number[]) => number;
function getValue(input: NodeInput, value?: unknown): number | number[] | Int32Array {
if (value === undefined && 'value' in input) {
value = input.value; value = input.value;
} }
switch (input.type) { if (input.type === "float") {
case 'float': return encodeFloat(value as number);
return encodeFloat(value as number);
case 'select':
return (value as number) ?? 0;
case 'vec3': {
const arr = Array.isArray(value) ? value : [];
return [0, arr.length + 1, ...arr.map(v => encodeFloat(v)), 1, 1];
}
} }
if (Array.isArray(value)) { if (Array.isArray(value)) {
return [0, value.length + 1, ...value, 1, 1]; if (input.type === "vec3") {
return [
0,
value.length + 1,
...value.map((v) => encodeFloat(v)),
1,
1,
] as number[];
}
return [0, value.length + 1, ...value, 1, 1] as number[];
} }
if (typeof value === 'boolean') return value ? 1 : 0; if (typeof value === "boolean") {
if (typeof value === 'number') return value; return value ? 1 : 0;
if (value instanceof Int32Array) return value;
throw new Error(`Unsupported input type: ${input.type}`);
}
function compareInt32(a: Int32Array, b: Int32Array): boolean {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false;
} }
return true;
}
export type Pointer = { if (typeof value === "number") {
start: number; return value;
end: number; }
_title?: string;
}; if (value instanceof Int32Array) {
return value;
}
throw new Error(`Unknown input type ${input.type}`);
}
export class MemoryRuntimeExecutor implements RuntimeExecutor { export class MemoryRuntimeExecutor implements RuntimeExecutor {
private nodes = new Map<string, { definition: NodeDefinition; execute: WasmExecute }>(); private definitionMap: Map<string, NodeDefinition> = new Map();
private offset = 0; private seed = Math.floor(Math.random() * 100000000);
private isRunning = false;
private readonly memory = new WebAssembly.Memory({
initial: 4096,
maximum: 8192
});
private memoryView!: Int32Array;
results: Record<number, Pointer> = {};
inputPtrs: Record<number, Pointer[]> = {};
allPtrs: Pointer[] = [];
seed = 42424242;
perf?: PerformanceStore; perf?: PerformanceStore;
constructor( constructor(
private readonly registry: NodeRegistry, private registry: NodeRegistry,
public cache?: SyncCache<Int32Array> public cache?: SyncCache<Int32Array>,
) { ) {
this.cache = undefined; this.cache = undefined;
this.refreshView();
log.info('MemoryRuntimeExecutor initialized');
} }
private refreshView(): void {
this.memoryView = new Int32Array(this.memory.buffer);
log.info(`Memory view refreshed, length: ${this.memoryView.length}`);
}
public getMemory(): Int32Array {
return new Int32Array(this.memory.buffer);
}
private map = new Map<string, { definition: NodeDefinition; execute: WasmExecute }>();
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(n => n.type)); await this.registry.load(graph.nodes.map((node) => node.type));
log.info(`Loaded ${graph.nodes.length} node types from registry`);
for (const { type } of graph.nodes) { const typeMap = new Map<string, NodeDefinition>();
if (this.map.has(type)) continue; for (const node of graph.nodes) {
if (!typeMap.has(node.type)) {
const def = this.registry.getNode(type); const type = this.registry.getNode(node.type);
if (!def) continue; if (type) {
typeMap.set(node.type, type);
log.info(`Fetching WASM for node type: ${type}`); }
const buffer = await remoteRegistry.fetchArrayBuffer(`nodes/${type}.wasm`); }
const wrapper = createWasmWrapper(buffer, this.memory);
this.map.set(type, {
definition: def,
execute: wrapper.execute
});
log.info(`Node type ${type} loaded and wrapped`);
} }
return typeMap;
return this.map;
} }
private async addMetaData(graph: Graph) { private async addMetaData(graph: Graph) {
this.nodes = await this.getNodeDefinitions(graph); // First, lets check if all nodes have a definition
log.info(`Metadata added for ${this.nodes.size} nodes`); this.definitionMap = await this.getNodeDefinitions(graph);
const graphNodes = graph.nodes.map(node => { const graphNodes = graph.nodes.map(node => {
const n = node as RuntimeNode; const n = node as RuntimeNode;
@@ -142,180 +98,182 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
depth: 0, depth: 0,
children: [], children: [],
parents: [], parents: [],
inputNodes: {} inputNodes: {},
}; }
return n; return n
}); })
const outputNode = graphNodes.find(n => n.type.endsWith('/output') || n.type.endsWith('/debug'))
?? graphNodes[0];
const nodeMap = new Map(graphNodes.map(n => [n.id, n])); const outputNode = graphNodes.find((node) =>
node.type.endsWith("/output"),
for (const [parentId, , childId, childInput] of graph.edges) { );
const parent = nodeMap.get(parentId); if (!outputNode) {
const child = nodeMap.get(childId); throw new Error("No output node found");
if (!parent || !child) continue;
parent.state.children.push(child);
child.state.parents.push(parent);
child.state.inputNodes[childInput] = parent;
} }
const ordered: RuntimeNode[] = []; const nodeMap = new Map(
const stack = [outputNode]; graphNodes.map((node) => [node.id, node]),
);
// loop through all edges and assign the parent and child nodes to each node
for (const edge of graph.edges) {
const [parentId, _parentOutput, childId, childInput] = edge;
const parent = nodeMap.get(parentId);
const child = nodeMap.get(childId);
if (parent && child) {
parent.state.children.push(child);
child.state.parents.push(parent);
child.state.inputNodes[childInput] = parent;
}
}
const nodes = [];
// loop through all the nodes and assign each nodes its depth
const stack = [outputNode];
while (stack.length) { while (stack.length) {
const node = stack.pop()!; const node = stack.pop();
if (!node) continue;
for (const parent of node.state.parents) { for (const parent of node.state.parents) {
parent.state = parent.state || {};
parent.state.depth = node.state.depth + 1; parent.state.depth = node.state.depth + 1;
stack.push(parent); stack.push(parent);
} }
ordered.push(node); nodes.push(node);
} }
log.info(`Output node: ${outputNode.id}, total nodes ordered: ${ordered.length}`); return [outputNode, nodes] as const;
return [outputNode, ordered] as const;
} }
private writeToMemory(value: number | number[] | Int32Array, title?: string): Pointer { async execute(graph: Graph, settings: Record<string, unknown>) {
const start = this.offset; this.perf?.addPoint("runtime");
if (typeof value === 'number') { let a = performance.now();
this.memoryView[this.offset++] = value;
} else { // Then we add some metadata to the graph
this.memoryView.set(value, this.offset); const [outputNode, nodes] = await this.addMetaData(graph);
this.offset += value.length; let b = performance.now();
this.perf?.addPoint("collect-metadata", b - a);
/*
* Here we sort the nodes into buckets, which we then execute one by one
* +-b2-+-b1-+---b0---+
* | | | |
* | n3 | n2 | Output |
* | n6 | n4 | Level |
* | | n5 | |
* | | | |
* +----+----+--------+
*/
// we execute the nodes from the bottom up
const sortedNodes = nodes.sort(
(a, b) => (b.state?.depth || 0) - (a.state?.depth || 0),
);
// here we store the intermediate results of the nodes
const results: Record<string, Int32Array> = {};
if (settings["randomSeed"]) {
this.seed = Math.floor(Math.random() * 100000000);
} }
const ptr = { start, end: this.offset, _title: title }; for (const node of sortedNodes) {
this.allPtrs.push(ptr); const node_type = this.definitionMap.get(node.type)!;
log.info(`Memory written for ${title}: start=${ptr.start}, end=${ptr.end}`);
return ptr;
}
async execute(graph: Graph, settings: Record<string, unknown>): Promise<Int32Array> { if (!node_type || !node.state || !node_type.execute) {
if (this.isRunning) { log.warn(`Node ${node.id} has no definition`);
log.info('Executor is already running, skipping execution'); continue;
return undefined as unknown as Int32Array;
}
this.isRunning = true;
log.info('Execution started');
try {
this.offset = 0;
this.results = {};
this.inputPtrs = {};
this.allPtrs = [];
this.seed += 2;
this.refreshView();
const [outputNode, nodes] = await this.addMetaData(graph);
const sortedNodes = [...nodes].sort(
(a, b) => (b.state.depth ?? 0) - (a.state.depth ?? 0)
);
const seedPtr = this.writeToMemory(this.seed, 'seed');
const settingPtrs = new Map<string, Pointer>();
for (const [key, value] of Object.entries(settings)) {
const ptr = this.writeToMemory(value as number, `setting.${key}`);
settingPtrs.set(key, ptr);
} }
let lastNodePtr: Pointer | undefined = undefined; a = performance.now();
for (const node of sortedNodes) { // Collect the inputs for the node
const nodeType = this.nodes.get(node.type); const inputs = Object.entries(node_type.inputs || {}).map(
if (!nodeType) continue; ([key, input]) => {
if (input.type === "seed") {
return this.seed;
}
log.info(`Executing node: ${node.id} (type: ${node.type})`); // If the input is linked to a setting, we use that value
if (input.setting) {
return getValue(input, settings[input.setting]);
}
const inputs = Object.entries(nodeType.definition.inputs || {}).map( // check if the input is connected to another node
([key, input]) => { const inputNode = node.state.inputNodes[key];
if (input.type === 'seed') return seedPtr; if (inputNode) {
if (results[inputNode.id] === undefined) {
if (input.setting) { throw new Error(
const ptr = settingPtrs.get(input.setting); `Node ${node.type} is missing input from node ${inputNode.type}`,
if (!ptr) throw new Error(`Missing setting: ${input.setting}`);
return ptr;
}
const src = node.state.inputNodes[key];
if (src) {
const res = this.results[src.id];
if (!res) {
throw new Error(`Missing input from ${src.type}/${src.id}`);
}
return res;
}
if (node.props?.[key] !== undefined) {
return this.writeToMemory(
getValue(input, node.props[key]),
`${node.id}.${key}`
); );
} }
return results[inputNode.id];
return this.writeToMemory(getValue(input), `${node.id}.${key}`);
} }
);
this.inputPtrs[node.id] = inputs; // If the value is stored in the node itself, we use that value
const args = inputs.flatMap(p => [p.start * 4, p.end * 4]); if (node.props?.[key] !== undefined) {
return getValue(input, node.props[key]);
}
log.info(`Executing node ${node.type}/${node.id}`); return getValue(input);
const bytesWritten = nodeType.execute(this.offset * 4, args); },
if (bytesWritten === -1) { );
throw new Error(`Failed to execute node`); b = performance.now();
this.perf?.addPoint("collected-inputs", b - a);
try {
a = performance.now();
const encoded_inputs = concatEncodedArrays(inputs);
b = performance.now();
this.perf?.addPoint("encoded-inputs", b - a);
a = performance.now();
let inputHash = `node-${node.id}-${fastHashArrayBuffer(encoded_inputs)}`;
b = performance.now();
this.perf?.addPoint("hash-inputs", b - a);
let cachedValue = this.cache?.get(inputHash);
if (cachedValue !== undefined) {
log.log(`Using cached value for ${node_type.id || node.id}`);
this.perf?.addPoint("cache-hit", 1);
results[node.id] = cachedValue as Int32Array;
continue;
} }
this.refreshView(); this.perf?.addPoint("cache-hit", 0);
const outLen = bytesWritten >> 2; log.group(`executing ${node_type.id}-${node.id}`);
const outputStart = this.offset; log.log(`Inputs:`, inputs);
a = performance.now();
results[node.id] = node_type.execute(encoded_inputs);
log.log("Executed", node.type, node.id)
b = performance.now();
if ( if (this.cache && node.id !== outputNode.id) {
args.length === 2 this.cache.set(inputHash, results[node.id]);
&& inputs[0].end - inputs[0].start === outLen
&& compareInt32(
this.memoryView.slice(inputs[0].start, inputs[0].end),
this.memoryView.slice(outputStart, outputStart + outLen)
)
) {
this.results[node.id] = inputs[0];
log.info(`Node ${node.id} result reused input memory`);
} else {
this.results[node.id] = {
start: outputStart,
end: outputStart + outLen,
_title: `${node.id} ->`
};
this.offset += outLen;
lastNodePtr = this.results[node.id];
log.info(
`Node ${node.id} wrote result to memory: start=${outputStart}, end=${outputStart + outLen
}`
);
} }
this.perf?.addPoint("node/" + node_type.id, b - a);
log.log("Result:", results[node.id]);
log.groupEnd();
} catch (e) {
log.groupEnd();
log.error(`Error executing node ${node_type.id || node.id}`, e);
} }
const res = this.results[outputNode.id] ?? lastNodePtr;
if (!res) throw new Error('Output node produced no result');
log.info(`Execution finished, output pointer: start=${res.start}, end=${res.end}`);
this.refreshView();
return this.memoryView.slice(res.start, res.end);
} catch (e) {
log.info('Execution error:', e);
console.error(e);
} finally {
this.isRunning = false;
this.perf?.endPoint('runtime');
log.info('Executor state reset');
} }
// return the result of the parent of the output node
const res = results[outputNode.id];
if (this.cache) {
this.cache.size = sortedNodes.length * 2;
}
this.perf?.endPoint("runtime");
return res as unknown as Int32Array;
} }
getPerformanceData() { getPerformanceData() {

View File

@@ -1,147 +1,148 @@
import { localState } from '$lib/helpers/localState.svelte'; import { localState } from "$lib/helpers/localState.svelte";
const themes = [ const themes = [
'dark', "dark",
'light', "light",
'catppuccin', "catppuccin",
'solarized', "solarized",
'high-contrast', "high-contrast",
'nord', "nord",
'dracula' "dracula",
] as const; ] as const;
export const AppSettingTypes = { export const AppSettingTypes = {
theme: { theme: {
type: 'select', type: "select",
options: themes, options: themes,
label: 'Theme', label: "Theme",
value: themes[0] value: themes[0],
}, },
showGrid: { showGrid: {
type: 'boolean', type: "boolean",
label: 'Show Grid', label: "Show Grid",
value: true value: true,
}, },
centerCamera: { centerCamera: {
type: 'boolean', type: "boolean",
label: 'Center Camera', label: "Center Camera",
value: true value: true,
}, },
nodeInterface: { nodeInterface: {
title: 'Node Interface', title: "Node Interface",
showNodeGrid: { showNodeGrid: {
type: 'boolean', type: "boolean",
label: 'Show Grid', label: "Show Grid",
value: true value: true,
}, },
snapToGrid: { snapToGrid: {
type: 'boolean', type: "boolean",
label: 'Snap to Grid', label: "Snap to Grid",
value: true value: true,
}, },
showHelp: { showHelp: {
type: 'boolean', type: "boolean",
label: 'Show Help', label: "Show Help",
value: false value: false,
}, },
showNodeIds: {
type: 'boolean',
label: 'Show Node Ids',
value: false
}
}, },
debug: { debug: {
title: 'Debug', title: "Debug",
wireframe: { wireframe: {
type: 'boolean', type: "boolean",
label: 'Wireframe', label: "Wireframe",
value: false value: false,
}, },
useWorker: { useWorker: {
type: 'boolean', type: "boolean",
label: 'Execute in WebWorker', label: "Execute in WebWorker",
value: true value: true,
}, },
showIndices: { showIndices: {
type: 'boolean', type: "boolean",
label: 'Show Indices', label: "Show Indices",
value: false value: false,
}, },
showPerformancePanel: { showPerformancePanel: {
type: 'boolean', type: "boolean",
label: 'Show Performance Panel', label: "Show Performance Panel",
value: false value: false,
}, },
showBenchmarkPanel: { showBenchmarkPanel: {
type: 'boolean', type: "boolean",
label: 'Show Benchmark Panel', label: "Show Benchmark Panel",
value: false value: false,
}, },
showVertices: { showVertices: {
type: 'boolean', type: "boolean",
label: 'Show Vertices', label: "Show Vertices",
value: false value: false,
}, },
showStemLines: { showStemLines: {
type: 'boolean', type: "boolean",
label: 'Show Stem Lines', label: "Show Stem Lines",
value: false value: false,
}, },
showGraphJson: { showGraphJson: {
type: 'boolean', type: "boolean",
label: 'Show Graph Source', label: "Show Graph Source",
value: false value: false,
}, },
cache: { cache: {
title: 'Cache', title: "Cache",
useRuntimeCache: { useRuntimeCache: {
type: 'boolean', type: "boolean",
label: 'Node Results', label: "Node Results",
value: true value: true,
}, },
useRegistryCache: { useRegistryCache: {
type: 'boolean', type: "boolean",
label: 'Node Source', label: "Node Source",
value: true value: true,
} },
}, },
stressTest: { stressTest: {
title: 'Stress Test', title: "Stress Test",
amount: { amount: {
type: 'integer', type: "integer",
min: 2, min: 2,
max: 15, max: 15,
value: 4 value: 4,
}, },
loadGrid: { loadGrid: {
type: 'button', type: "button",
label: 'Load Grid' label: "Load Grid",
}, },
loadTree: { loadTree: {
type: 'button', type: "button",
label: 'Load Tree' label: "Load Tree",
}, },
lottaFaces: { lottaFaces: {
type: 'button', type: "button",
label: "Load 'lots of faces'" label: "Load 'lots of faces'",
}, },
lottaNodes: { lottaNodes: {
type: 'button', type: "button",
label: "Load 'lots of nodes'" label: "Load 'lots of nodes'",
}, },
lottaNodesAndFaces: { lottaNodesAndFaces: {
type: 'button', type: "button",
label: "Load 'lots of nodes and faces'" label: "Load 'lots of nodes and faces'",
} },
} },
} },
} as const; } as const;
type SettingsToStore<T> = T extends { value: infer V } ? V extends readonly string[] ? V[number] type SettingsToStore<T> =
T extends { value: infer V }
? V extends readonly string[]
? V[number]
: V : V
: T extends any[] ? {} : T extends any[]
: T extends object ? { ? {}
[K in keyof T as T[K] extends object ? K : never]: SettingsToStore<T[K]>; : T extends object
? {
[K in keyof T as T[K] extends object ? K : never]:
SettingsToStore<T[K]>
} }
: never; : never;
@@ -149,8 +150,8 @@ export function settingsToStore<T>(settings: T): SettingsToStore<T> {
const result = {} as any; const result = {} as any;
for (const key in settings) { for (const key in settings) {
const value = settings[key]; const value = settings[key];
if (value && typeof value === 'object') { if (value && typeof value === "object") {
if ('value' in value) { if ("value" in value) {
result[key] = value.value; result[key] = value.value;
} else { } else {
result[key] = settingsToStore(value); result[key] = settingsToStore(value);
@@ -161,8 +162,8 @@ export function settingsToStore<T>(settings: T): SettingsToStore<T> {
} }
export let appSettings = localState( export let appSettings = localState(
'app-settings', "app-settings",
settingsToStore(AppSettingTypes) settingsToStore(AppSettingTypes),
); );
$effect.root(() => { $effect.root(() => {
@@ -172,7 +173,7 @@ $effect.root(() => {
const newClassName = `theme-${theme}`; const newClassName = `theme-${theme}`;
if (classes) { if (classes) {
for (const className of classes) { for (const className of classes) {
if (className.startsWith('theme-') && className !== newClassName) { if (className.startsWith("theme-") && className !== newClassName) {
classes.remove(className); classes.remove(className);
} }
} }

View File

@@ -90,6 +90,11 @@
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 h-screen overflow-x-hidden"> <main class="w-screen overflow-x-hidden">
{@render children()} {@render children()}
</main> </main>

View File

@@ -1,226 +1,113 @@
<script lang="ts"> <script lang="ts">
import NodeHTML from "$lib/graph-interface/node/NodeHTML.svelte";
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 GraphInterface from "$lib/graph-interface"; import { IndexDBCache, RemoteNodeRegistry } from "@nodarium/registry";
import { RemoteNodeRegistry } from "@nodarium/registry"; import { type NodeId, type NodeInstance } from "@nodarium/types";
import { type Graph, type NodeInstance } from "@nodarium/types"; import Code from "./Code.svelte";
import Grid from "$lib/grid"; import Grid from "$lib/grid";
import { MemoryRuntimeExecutor, type Pointer } from "$lib/runtime";
import { decodeFloat } from "@nodarium/utils";
import { localState } from "$lib/helpers/localState.svelte";
import * as templates from "$lib/graph-templates";
import NestedSettings from "$lib/settings/NestedSettings.svelte";
import { import {
appSettings, concatEncodedArrays,
AppSettingTypes, createWasmWrapper,
} from "$lib/settings/app-settings.svelte"; encodeNestedArray,
} from "@nodarium/utils";
const nodeRegistry = new RemoteNodeRegistry(""); const registryCache = new IndexDBCache("node-registry");
const nodeRegistry = new RemoteNodeRegistry("", registryCache);
const runtimeExecutor = new MemoryRuntimeExecutor(nodeRegistry); let activeNode = localState<NodeId | undefined>(
"node.dev.activeNode",
let allPtrs = $state<Pointer[]>([]); undefined,
let activeNode = $state<NodeInstance>();
let isCalculating = $state<boolean>(false);
let windowHeight = $state(500);
const start = localState("nodes.dev.scroll", 0);
const rowHeight = 40;
const numRows = $derived(Math.floor(windowHeight / rowHeight));
let memory = $state<Int32Array>();
const visibleRows = $derived(
memory?.slice(start.value, start.value + numRows),
); );
const sortedPtrs = $derived.by(() => { let nodeWasm = $state<ArrayBuffer>();
const seen = new Set(); let nodeInstance = $state<NodeInstance>();
const _ptrs = [...allPtrs] let nodeWasmWrapper = $state<ReturnType<typeof createWasmWrapper>>();
.sort((a, b) => (a.start > b.start ? 1 : -1))
.filter((ptr) => { async function fetchNodeData(nodeId?: NodeId) {
const id = `${ptr.start}-${ptr.end}`; nodeWasm = undefined;
if (seen.has(id)) return false; nodeInstance = undefined;
seen.add(id);
return true; if (!nodeId) return;
});
if (!_ptrs) return []; const data = await nodeRegistry.fetchNodeDefinition(nodeId);
return _ptrs; 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);
}
$effect(() => {
fetchNodeData(activeNode.value);
}); });
const ptrs = $derived.by(() => { $effect(() => {
let out = []; if (nodeInstance?.props && nodeWasmWrapper) {
for (let i = 0; i < numRows; i++) { const keys = Object.keys(nodeInstance.state.type?.inputs || {});
let rowIndex = start.value + i; let ins = Object.values(nodeInstance.props) as number[];
const activePtr = sortedPtrs.find( if (keys[0] === "plant") {
(ptr) => ptr.start < rowIndex && ptr.end >= rowIndex, ins = [[0, 0, 0, 0, 0, 0, 0, 0], ...ins];
);
if (activePtr) {
out.push({
start: rowIndex,
end: rowIndex + 1,
_title: activePtr._title,
});
} }
const inputs = concatEncodedArrays(encodeNestedArray(ins));
nodeWasmWrapper?.execute(inputs);
} }
return out;
}); });
let graph = $state(
localStorage.getItem("nodes.dev.graph")
? JSON.parse(localStorage.getItem("nodes.dev.graph")!)
: templates.defaultPlant,
);
function handleSave(g: Graph) {
localStorage.setItem("nodes.dev.graph", JSON.stringify(g));
}
let graphSettings = $state<Record<string, any>>({});
let graphSettingTypes = $state({
randomSeed: { type: "boolean", value: false },
});
let calcTimeout: ReturnType<typeof setTimeout>;
async function handleResult(res?: Graph) {
console.clear();
isCalculating = true;
if (res) handleSave(res);
try {
await runtimeExecutor.execute(
res || graph,
$state.snapshot(graphSettings),
);
} catch (e) {
console.log(e);
}
memory = runtimeExecutor.getMemory();
allPtrs = runtimeExecutor.allPtrs;
clearTimeout(calcTimeout);
calcTimeout = setTimeout(() => {
isCalculating = false;
}, 500);
}
const rowIsFloat = localState<boolean[]>("node.dev.isFloat", []);
function decodeValue(value: number, isFloat?: boolean) {
return isFloat ? decodeFloat(value) : value;
}
</script> </script>
<svelte:window <div class="node-wrapper absolute bottom-8 left-8">
bind:innerHeight={windowHeight} {#if nodeInstance}
onkeydown={(ev) => ev.key === "r" && handleResult()} <NodeHTML inView position="relative" z={5} bind:node={nodeInstance} />
/> {/if}
</div>
<Grid.Row> <Grid.Row>
<Grid.Cell> <Grid.Cell>
{#if visibleRows?.length} <pre>
<table <code>
class="min-w-full select-none overflow-auto text-left text-sm flex-1" {JSON.stringify(nodeInstance?.props)}
onscroll={(e) => { </code>
const scrollTop = e.currentTarget.scrollTop; </pre>
start.value = Math.floor(scrollTop / rowHeight);
}}
>
<thead class="">
<tr>
<th class="px-4 py-2 border-b border-[var(--outline)]">i</th>
<th
class="px-4 py-2 border-b border-[var(--outline)] w-[50px]"
style:width="50px">Ptrs</th
>
<th class="px-4 py-2 border-b border-[var(--outline)]">Value</th>
<th class="px-4 py-2 border-b border-[var(--outline)]">Float</th>
</tr>
</thead>
<tbody
onscroll={(e) => {
const scrollTop = e.currentTarget.scrollTop;
start.value = Math.floor(scrollTop / rowHeight);
}}
>
{#each visibleRows as r, i}
{@const index = i + start.value}
{@const ptr = ptrs[i]}
<tr class="h-[40px] odd:bg-[var(--layer-1)]">
<td class="px-4 border-b border-[var(--outline)] w-8">{index}</td>
<td
class="border-b border-[var(--outline)] overflow-hidden text-ellipsis pl-2
{ptr?._title?.includes('->')
? 'bg-red-500'
: 'bg-blue-500'}"
style="width: 100px; min-width: 100px; max-width: 100px;"
>
{ptr?._title}
</td>
<td
class="px-4 border-b border-[var(--outline)] cursor-pointer text-blue-600 hover:text-blue-800"
onclick={() =>
(rowIsFloat.value[index] = !rowIsFloat.value[index])}
>
{decodeValue(r, rowIsFloat.value[index])}
</td>
<td class="px-4 border-b border-[var(--outline)] italic w-5">
<input
type="checkbox"
checked={rowIsFloat.value[index]}
onclick={() =>
(rowIsFloat.value[index] = !rowIsFloat.value[index])}
/>
</td>
</tr>
{/each}
</tbody>
</table>
<input
class="absolute bottom-4 left-4 bg-white"
bind:value={start.value}
min="0"
type="number"
step="1"
/>
{/if}
</Grid.Cell> </Grid.Cell>
<Grid.Cell> <Grid.Cell>
{#if isCalculating} <div class="h-screen w-[80vw] overflow-y-auto">
<span {#if nodeWasm}
class="opacity-50 top-4 left-4 i-[tabler--loader-2] w-10 h-10 absolute animate-spin z-100" <Code wasm={nodeWasm} />
></span> {/if}
{/if} </div>
<button
onclick={() => handleResult()}
class="flex items-center cursor-pointer absolute bottom-4 left-4 z-100"
>
Execute Graph (R)
</button>
<GraphInterface
{graph}
bind:activeNode
registry={nodeRegistry}
bind:settings={graphSettings}
bind:settingTypes={graphSettingTypes}
onsave={(g) => handleSave(g)}
onresult={(res) => handleResult(res)}
/>
</Grid.Cell> </Grid.Cell>
</Grid.Row> </Grid.Row>
<Sidebar> <Sidebar>
<Panel id="general" title="General" icon="i-[tabler--settings]">
<NestedSettings
id="general"
bind:value={appSettings.value}
type={AppSettingTypes}
/>
</Panel>
<Panel <Panel
id="node-store" id="node-store"
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

@@ -1,74 +0,0 @@
{
"settings": {
"resolution.circle": 26,
"resolution.curve": 39
},
"nodes": [
{
"id": 9,
"position": [
225,
65
],
"type": "max/plantarium/output",
"props": {
"out": 0
}
},
{
"id": 10,
"position": [
200,
60
],
"type": "max/plantarium/math",
"props": {
"op_type": 3,
"a": 2,
"b": 0.38
}
},
{
"id": 11,
"position": [
175,
60
],
"type": "max/plantarium/float",
"props": {
"value": 0.66
}
},
{
"id": 12,
"position": [
175,
80
],
"type": "max/plantarium/float",
"props": {
"value": 1
}
}
],
"edges": [
[
11,
0,
10,
"a"
],
[
12,
0,
10,
"b"
],
[
10,
0,
9,
"out"
]
]
}

View File

@@ -4,19 +4,20 @@ This guide will help you developing your first Nodarium Node written in Rust. As
## Prerequesites ## Prerequesites
You need to have [Rust](https://www.rust-lang.org/tools/install) installed. Rust is the language we are going to develop our node in and cargo compiles our rust code into webassembly. You need to have [Rust](https://www.rust-lang.org/tools/install) and [wasm-pack](https://rustwasm.github.io/docs/wasm-pack/) installed. Rust is the language we are going to develop our node in and wasm-pack helps us compile our rust code into a webassembly file.
```bash ```bash
# install rust # install rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# install wasm-pack
cargo install wasm-pack
``` ```
## Clone Template ## Clone Template
```bash ```bash
# copy the template directory wasm-pack new my-new-node --template https://github.com/jim-fx/nodarium_template
cp -r nodes/max/plantarium/.template nodes/max/plantarium/my-new-node cd my-new-node
cd nodes/max/plantarium/my-new-node
``` ```
## Setup Definition ## Setup Definition

View File

@@ -4,7 +4,7 @@
}; };
outputs = {nixpkgs, ...}: let outputs = {nixpkgs, ...}: let
systems = ["aarch64-darwin" "x86_64-linux"]; systems = ["aarch64-darwin" "x86_64-darwin" "aarch64-linux" "x86_64-linux"];
eachSystem = function: eachSystem = function:
nixpkgs.lib.genAttrs systems (system: nixpkgs.lib.genAttrs systems (system:
function { function {
@@ -19,14 +19,15 @@
pkgs.nodejs_24 pkgs.nodejs_24
pkgs.pnpm_10 pkgs.pnpm_10
# wasm/rust stuff # wasm stuff
pkgs.rustc pkgs.rustc
pkgs.cargo pkgs.cargo
pkgs.rust-analyzer pkgs.rust-analyzer
pkgs.rustfmt pkgs.rustfmt
pkgs.wasm-bindgen-cli pkgs.binaryen
pkgs.wasm-pack
pkgs.lld pkgs.lld
pkgs.zig
pkgs.zls
# frontend # frontend
pkgs.vscode-langservers-extracted pkgs.vscode-langservers-extracted
@@ -35,6 +36,10 @@
pkgs.tailwindcss-language-server pkgs.tailwindcss-language-server
pkgs.svelte-language-server pkgs.svelte-language-server
]; ];
shellHook = ''
unset ZIG_GLOBAL_CACHE_DIR
'';
}; };
}); });
}; };

View File

@@ -1,18 +1,20 @@
use nodarium_macros::nodarium_definition_file; use nodarium_macros::nodarium_definition_file;
use nodarium_macros::nodarium_execute; use nodarium_macros::nodarium_execute;
use nodarium_utils::{ use nodarium_utils::{
encode_float, evaluate_float, geometry::calculate_normals, wrap_arg, encode_float, evaluate_float, geometry::calculate_normals,log,
read_i32_slice split_args, wrap_arg,
}; };
nodarium_definition_file!("src/input.json"); nodarium_definition_file!("src/input.json");
#[nodarium_execute] #[nodarium_execute]
pub fn execute(size: (i32, i32)) -> Vec<i32> { pub fn execute(input: &[i32]) -> Vec<i32> {
let args = read_i32_slice(size); let args = split_args(input);
let size = evaluate_float(&args); log!("WASM(cube): input: {:?} -> {:?}", input, args);
let size = evaluate_float(args[0]);
let p = encode_float(size); let p = encode_float(size);
let n = encode_float(-size); let n = encode_float(-size);
@@ -75,6 +77,8 @@ pub fn execute(size: (i32, i32)) -> Vec<i32> {
let res = wrap_arg(&cube_geometry); let res = wrap_arg(&cube_geometry);
log!("WASM(box): output: {:?}", res);
res res
} }

View File

@@ -1,6 +1,5 @@
use nodarium_macros::nodarium_definition_file; use nodarium_macros::nodarium_definition_file;
use nodarium_macros::nodarium_execute; use nodarium_macros::nodarium_execute;
use nodarium_utils::read_i32_slice;
use nodarium_utils::{ use nodarium_utils::{
concat_arg_vecs, evaluate_float, evaluate_int, concat_arg_vecs, evaluate_float, evaluate_int,
geometry::{ geometry::{
@@ -14,25 +13,15 @@ use std::f32::consts::PI;
nodarium_definition_file!("src/input.json"); nodarium_definition_file!("src/input.json");
#[nodarium_execute] #[nodarium_execute]
pub fn execute( pub fn execute(input: &[i32]) -> Vec<i32> {
path: (i32, i32), let args = split_args(input);
length: (i32, i32),
thickness: (i32, i32), let paths = split_args(args[0]);
offset_single: (i32, i32),
lowest_branch: (i32, i32),
highest_branch: (i32, i32),
depth: (i32, i32),
amount: (i32, i32),
resolution_curve: (i32, i32),
rotation: (i32, i32),
) -> Vec<i32> {
let arg = read_i32_slice(path);
let paths = split_args(arg.as_slice());
let mut output: Vec<Vec<i32>> = Vec::new(); let mut output: Vec<Vec<i32>> = Vec::new();
let resolution = evaluate_int(read_i32_slice(resolution_curve).as_slice()).max(4) as usize; let resolution = evaluate_int(args[8]).max(4) as usize;
let depth = evaluate_int(read_i32_slice(depth).as_slice()); let depth = evaluate_int(args[6]);
let mut max_depth = 0; let mut max_depth = 0;
for path_data in paths.iter() { for path_data in paths.iter() {
@@ -51,18 +40,18 @@ pub fn execute(
let path = wrap_path(path_data); let path = wrap_path(path_data);
let branch_amount = evaluate_int(read_i32_slice(amount).as_slice()).max(1); let branch_amount = evaluate_int(args[7]).max(1);
let lowest_branch = evaluate_float(read_i32_slice(lowest_branch).as_slice()); let lowest_branch = evaluate_float(args[4]);
let highest_branch = evaluate_float(read_i32_slice(highest_branch).as_slice()); let highest_branch = evaluate_float(args[5]);
for i in 0..branch_amount { for i in 0..branch_amount {
let a = i as f32 / (branch_amount - 1).max(1) as f32; let a = i as f32 / (branch_amount - 1).max(1) as f32;
let length = evaluate_float(read_i32_slice(length).as_slice()); let length = evaluate_float(args[1]);
let thickness = evaluate_float(read_i32_slice(thickness).as_slice()); let thickness = evaluate_float(args[2]);
let offset_single = if i % 2 == 0 { let offset_single = if i % 2 == 0 {
evaluate_float(read_i32_slice(offset_single).as_slice()) evaluate_float(args[3])
} else { } else {
0.0 0.0
}; };
@@ -76,8 +65,7 @@ pub fn execute(
root_alpha + (offset_single - 0.5) * 6.0 / resolution as f32, root_alpha + (offset_single - 0.5) * 6.0 / resolution as f32,
); );
let rotation_angle = let rotation_angle = (evaluate_float(args[9]) * PI / 180.0) * i as f32;
(evaluate_float(read_i32_slice(rotation).as_slice()) * PI / 180.0) * i as f32;
// check if diration contains NaN // check if diration contains NaN
if orthogonal[0].is_nan() || orthogonal[1].is_nan() || orthogonal[2].is_nan() { if orthogonal[0].is_nan() || orthogonal[1].is_nan() || orthogonal[2].is_nan() {

View File

@@ -1,6 +0,0 @@
/target
**/*.rs.bk
Cargo.lock
bin/
pkg/
wasm-pack.log

View File

@@ -1,12 +0,0 @@
[package]
name = "debug"
version = "0.1.0"
authors = ["Max Richter <jim-x@web.de>"]
edition = "2018"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" }
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }

View File

@@ -1,22 +0,0 @@
{
"id": "max/plantarium/debug",
"outputs": [],
"inputs": {
"input": {
"type": "float",
"accepts": [
"*"
],
"external": true
},
"type": {
"type": "select",
"options": [
"float",
"vec3",
"geometry"
],
"internal": true
}
}
}

View File

@@ -1,25 +0,0 @@
use nodarium_macros::nodarium_definition_file;
use nodarium_macros::nodarium_execute;
use nodarium_utils::encode_float;
use nodarium_utils::evaluate_float;
use nodarium_utils::evaluate_vec3;
use nodarium_utils::read_i32;
use nodarium_utils::read_i32_slice;
nodarium_definition_file!("src/input.json");
#[nodarium_execute]
pub fn execute(input: (i32, i32), input_type: (i32, i32)) -> Vec<i32> {
let inp = read_i32_slice(input);
let t = read_i32(input_type.0);
if t == 0 {
let f = evaluate_float(inp.as_slice());
return vec![encode_float(f)];
}
if t == 1 {
let f = evaluate_vec3(inp.as_slice());
return vec![encode_float(f[0]), encode_float(f[1]), encode_float(f[2])];
}
return inp;
}

View File

@@ -8,5 +8,5 @@ edition = "2018"
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
[dependencies] [dependencies]
nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" }
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" } nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }
nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" }

View File

@@ -1,10 +1,9 @@
use nodarium_macros::nodarium_definition_file; use nodarium_macros::nodarium_definition_file;
use nodarium_macros::nodarium_execute; use nodarium_macros::nodarium_execute;
use nodarium_utils::read_i32;
nodarium_definition_file!("src/input.json"); nodarium_definition_file!("src/input.json");
#[nodarium_execute] #[nodarium_execute]
pub fn execute(a: (i32, i32)) -> Vec<i32> { pub fn execute(args: &[i32]) -> Vec<i32> {
vec![read_i32(a.0)] args.into()
} }

View File

@@ -1,7 +1,6 @@
use glam::Vec3; use glam::Vec3;
use nodarium_macros::nodarium_definition_file; use nodarium_macros::nodarium_definition_file;
use nodarium_macros::nodarium_execute; use nodarium_macros::nodarium_execute;
use nodarium_utils::read_i32_slice;
use nodarium_utils::{ use nodarium_utils::{
concat_args, evaluate_float, evaluate_int, concat_args, evaluate_float, evaluate_int,
geometry::{wrap_path, wrap_path_mut}, geometry::{wrap_path, wrap_path_mut},
@@ -15,17 +14,13 @@ fn lerp_vec3(a: Vec3, b: Vec3, t: f32) -> Vec3 {
} }
#[nodarium_execute] #[nodarium_execute]
pub fn execute( pub fn execute(input: &[i32]) -> Vec<i32> {
plant: (i32, i32),
strength: (i32, i32),
curviness: (i32, i32),
depth: (i32, i32),
) -> Vec<i32> {
reset_call_count(); reset_call_count();
let arg = read_i32_slice(plant); let args = split_args(input);
let plants = split_args(arg.as_slice());
let depth = evaluate_int(read_i32_slice(depth).as_slice()); let plants = split_args(args[0]);
let depth = evaluate_int(args[3]);
let mut max_depth = 0; let mut max_depth = 0;
for path_data in plants.iter() { for path_data in plants.iter() {
@@ -60,9 +55,9 @@ pub fn execute(
let length = direction.length(); let length = direction.length();
let str = evaluate_float(read_i32_slice(strength).as_slice()); let curviness = evaluate_float(args[2]);
let curviness = evaluate_float(read_i32_slice(curviness).as_slice()); let strength =
let strength = str / curviness.max(0.0001) * str; evaluate_float(args[1]) / curviness.max(0.0001) * evaluate_float(args[1]);
log!( log!(
"length: {}, curviness: {}, strength: {}", "length: {}, curviness: {}, strength: {}",

View File

@@ -1,29 +1,23 @@
use glam::{Mat4, Quat, Vec3}; use glam::{Mat4, Quat, Vec3};
use nodarium_macros::nodarium_definition_file;
use nodarium_macros::nodarium_execute; use nodarium_macros::nodarium_execute;
use nodarium_utils::read_i32_slice; use nodarium_macros::nodarium_definition_file;
use nodarium_utils::{ use nodarium_utils::{
concat_args, evaluate_float, evaluate_int, concat_args, evaluate_float, evaluate_int,
geometry::{create_instance_data, wrap_geometry_data, wrap_instance_data, wrap_path}, geometry::{
create_instance_data, wrap_geometry_data, wrap_instance_data, wrap_path,
},
log, split_args, log, split_args,
}; };
nodarium_definition_file!("src/input.json"); nodarium_definition_file!("src/input.json");
#[nodarium_execute] #[nodarium_execute]
pub fn execute( pub fn execute(input: &[i32]) -> Vec<i32> {
plant: (i32, i32), let args = split_args(input);
geometry: (i32, i32), let mut inputs = split_args(args[0]);
amount: (i32, i32),
lowest_instance: (i32, i32),
highest_instance: (i32, i32),
depth: (i32, i32),
) -> Vec<i32> {
let arg = read_i32_slice(plant);
let mut inputs = split_args(arg.as_slice());
log!("WASM(instance): inputs: {:?}", inputs); log!("WASM(instance): inputs: {:?}", inputs);
let mut geo_data = read_i32_slice(geometry); let mut geo_data = args[1].to_vec();
let geo = wrap_geometry_data(&mut geo_data); let geo = wrap_geometry_data(&mut geo_data);
let mut transforms: Vec<Mat4> = Vec::new(); let mut transforms: Vec<Mat4> = Vec::new();
@@ -36,17 +30,17 @@ pub fn execute(
max_depth = max_depth.max(path_data[3]); max_depth = max_depth.max(path_data[3]);
} }
let depth = evaluate_int(read_i32_slice(depth).as_slice()); let depth = evaluate_int(args[5]);
for path_data in inputs.iter() { for path_data in inputs.iter() {
if path_data[3] < (max_depth - depth + 1) { if path_data[3] < (max_depth - depth + 1) {
continue; continue;
} }
let amount = evaluate_int(read_i32_slice(amount).as_slice()); let amount = evaluate_int(args[2]);
let lowest_instance = evaluate_float(read_i32_slice(lowest_instance).as_slice()); let lowest_instance = evaluate_float(args[3]);
let highest_instance = evaluate_float(read_i32_slice(highest_instance).as_slice()); let highest_instance = evaluate_float(args[4]);
let path = wrap_path(path_data); let path = wrap_path(path_data);

View File

@@ -8,5 +8,5 @@ edition = "2018"
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
[dependencies] [dependencies]
nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" }
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" } nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }
nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" }

View File

@@ -1,15 +1,13 @@
use nodarium_macros::nodarium_definition_file; use nodarium_macros::nodarium_definition_file;
use nodarium_macros::nodarium_execute; use nodarium_macros::nodarium_execute;
use nodarium_utils::log; use nodarium_utils::{
use nodarium_utils::{concat_arg_vecs, read_i32_slice}; concat_args, split_args
};
nodarium_definition_file!("src/input.json");
#[nodarium_execute] #[nodarium_execute]
pub fn execute(op_type: (i32, i32), a: (i32, i32), b: (i32, i32)) -> Vec<i32> { pub fn execute(args: &[i32]) -> Vec<i32> {
log!("math.op {:?}", op_type); let args = split_args(args);
let op = read_i32_slice(op_type); concat_args(vec![&[0], args[0], args[1], args[2]])
let a_val = read_i32_slice(a);
let b_val = read_i32_slice(b);
concat_arg_vecs(vec![vec![0], op, a_val, b_val])
} }
nodarium_definition_file!("src/input.json");

View File

@@ -1,8 +1,7 @@
use nodarium_macros::nodarium_definition_file; use nodarium_macros::nodarium_definition_file;
use nodarium_macros::nodarium_execute; use nodarium_macros::nodarium_execute;
use nodarium_utils::read_i32_slice;
use nodarium_utils::{ use nodarium_utils::{
concat_args, evaluate_float, evaluate_int, evaluate_vec3, geometry::wrap_path_mut, read_i32, concat_args, evaluate_float, evaluate_int, evaluate_vec3, geometry::wrap_path_mut,
reset_call_count, split_args, reset_call_count, split_args,
}; };
use noise::{HybridMulti, MultiFractal, NoiseFn, OpenSimplex}; use noise::{HybridMulti, MultiFractal, NoiseFn, OpenSimplex};
@@ -14,31 +13,23 @@ fn lerp(a: f32, b: f32, t: f32) -> f32 {
} }
#[nodarium_execute] #[nodarium_execute]
pub fn execute( pub fn execute(input: &[i32]) -> Vec<i32> {
plant: (i32, i32),
scale: (i32, i32),
strength: (i32, i32),
fix_bottom: (i32, i32),
seed: (i32, i32),
directional_strength: (i32, i32),
depth: (i32, i32),
octaves: (i32, i32),
) -> Vec<i32> {
reset_call_count(); reset_call_count();
let arg = read_i32_slice(plant); let args = split_args(input);
let plants = split_args(arg.as_slice());
let scale = (evaluate_float(read_i32_slice(scale).as_slice()) * 0.1) as f64;
let strength = evaluate_float(read_i32_slice(strength).as_slice());
let fix_bottom = evaluate_float(read_i32_slice(fix_bottom).as_slice());
let seed = read_i32(seed.0); let plants = split_args(args[0]);
let scale = (evaluate_float(args[1]) * 0.1) as f64;
let strength = evaluate_float(args[2]);
let fix_bottom = evaluate_float(args[3]);
let directional_strength = evaluate_vec3(read_i32_slice(directional_strength).as_slice()); let seed = args[4][0];
let depth = evaluate_int(read_i32_slice(depth).as_slice()); let directional_strength = evaluate_vec3(args[5]);
let octaves = evaluate_int(read_i32_slice(octaves).as_slice()); let depth = evaluate_int(args[6]);
let octaves = evaluate_int(args[7]);
let noise_x: HybridMulti<OpenSimplex> = let noise_x: HybridMulti<OpenSimplex> =
HybridMulti::new(seed as u32 + 1).set_octaves(octaves as usize); HybridMulti::new(seed as u32 + 1).set_octaves(octaves as usize);

View File

@@ -8,5 +8,5 @@ edition = "2018"
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
[dependencies] [dependencies]
nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" }
nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" } nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" }
nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" }

View File

@@ -5,7 +5,7 @@
"input": { "input": {
"type": "path", "type": "path",
"accepts": [ "accepts": [
"*" "geometry"
], ],
"external": true "external": true
}, },

View File

@@ -1,11 +1,44 @@
use nodarium_macros::nodarium_definition_file; use nodarium_macros::nodarium_definition_file;
use nodarium_macros::nodarium_execute; use nodarium_macros::nodarium_execute;
use nodarium_utils::read_i32_slice; use nodarium_utils::{
concat_args, evaluate_int,
geometry::{extrude_path, wrap_path},
log, split_args,
};
nodarium_definition_file!("src/input.json"); nodarium_definition_file!("src/inputs.json");
#[nodarium_execute] #[nodarium_execute]
pub fn execute(input: (i32, i32), _res: (i32, i32)) -> Vec<i32> { pub fn execute(input: &[i32]) -> Vec<i32> {
let inp = read_i32_slice(input); log!("WASM(output): input: {:?}", input);
return inp;
let args = split_args(input);
log!("WASM(output) args: {:?}", args);
assert_eq!(args.len(), 2, "Expected 2 arguments, got {}", args.len());
let inputs = split_args(args[0]);
let resolution = evaluate_int(args[1]) as usize;
log!("inputs: {}, resolution: {}", inputs.len(), resolution);
let mut output: Vec<Vec<i32>> = Vec::new();
for arg in inputs {
let arg_type = arg[2];
log!("arg_type: {}, \n {:?}", arg_type, arg,);
if arg_type == 0 {
// if this is path we need to extrude it
output.push(arg.to_vec());
let path_data = wrap_path(arg);
let geometry = extrude_path(path_data, resolution);
output.push(geometry);
continue;
}
output.push(arg.to_vec());
}
concat_args(output.iter().map(|v| v.as_slice()).collect())
} }

View File

@@ -1,16 +1,11 @@
use nodarium_macros::nodarium_definition_file; use nodarium_macros::nodarium_definition_file;
use nodarium_macros::nodarium_execute; use nodarium_macros::nodarium_execute;
use nodarium_utils::concat_arg_vecs; use nodarium_utils::{concat_args, split_args};
use nodarium_utils::read_i32_slice;
nodarium_definition_file!("src/input.json"); nodarium_definition_file!("src/definition.json");
#[nodarium_execute] #[nodarium_execute]
pub fn execute(min: (i32, i32), max: (i32, i32), seed: (i32, i32)) -> Vec<i32> { pub fn execute(args: &[i32]) -> Vec<i32> {
concat_arg_vecs(vec![ let args = split_args(args);
vec![1], concat_args(vec![&[1], args[0], args[1], args[2]])
read_i32_slice(min),
read_i32_slice(max),
read_i32_slice(seed),
])
} }

View File

@@ -1,26 +1,23 @@
use glam::{Mat4, Vec3}; use glam::{Mat4, Vec3};
use nodarium_macros::nodarium_definition_file; use nodarium_macros::nodarium_definition_file;
use nodarium_macros::nodarium_execute; use nodarium_macros::nodarium_execute;
use nodarium_utils::read_i32_slice;
use nodarium_utils::{ use nodarium_utils::{
concat_args, evaluate_float, evaluate_int, geometry::wrap_path_mut, log, split_args, concat_args, evaluate_float, evaluate_int, geometry::wrap_path_mut, log,
split_args,
}; };
nodarium_definition_file!("src/input.json"); nodarium_definition_file!("src/input.json");
#[nodarium_execute] #[nodarium_execute]
pub fn execute( pub fn execute(input: &[i32]) -> Vec<i32> {
plant: (i32, i32),
axis: (i32, i32),
angle: (i32, i32),
spread: (i32, i32),
) -> Vec<i32> {
log!("DEBUG args: {:?}", plant);
let arg = read_i32_slice(plant); log!("DEBUG args: {:?}", input);
let plants = split_args(arg.as_slice());
let axis = evaluate_int(read_i32_slice(axis).as_slice()); // 0 =x, 1 = y, 2 = z let args = split_args(input);
let spread = evaluate_int(read_i32_slice(spread).as_slice());
let plants = split_args(args[0]);
let axis = evaluate_int(args[1]); // 0 =x, 1 = y, 2 = z
let spread = evaluate_int(args[3]);
let output: Vec<Vec<i32>> = plants let output: Vec<Vec<i32>> = plants
.iter() .iter()
@@ -35,7 +32,7 @@ pub fn execute(
let path = wrap_path_mut(&mut path_data); let path = wrap_path_mut(&mut path_data);
let angle = evaluate_float(read_i32_slice(angle).as_slice()); let angle = evaluate_float(args[2]);
let origin = [path.points[0], path.points[1], path.points[2]]; let origin = [path.points[0], path.points[1], path.points[2]];

View File

@@ -3,29 +3,30 @@ use nodarium_macros::nodarium_execute;
use nodarium_utils::{ use nodarium_utils::{
evaluate_float, evaluate_int, evaluate_vec3, evaluate_float, evaluate_int, evaluate_vec3,
geometry::{create_multiple_paths, wrap_multiple_paths}, geometry::{create_multiple_paths, wrap_multiple_paths},
log, reset_call_count, log, reset_call_count, split_args,
read_i32_slice, read_i32,
}; };
nodarium_definition_file!("src/input.json"); nodarium_definition_file!("src/input.json");
#[nodarium_execute] #[nodarium_execute]
pub fn execute(origin: (i32, i32), _amount: (i32,i32), length: (i32, i32), thickness: (i32, i32), resolution_curve: (i32, i32)) -> Vec<i32> { pub fn execute(input: &[i32]) -> Vec<i32> {
reset_call_count(); reset_call_count();
let amount = evaluate_int(read_i32_slice(_amount).as_slice()) as usize; let args = split_args(input);
let path_resolution = read_i32(resolution_curve.0) as usize;
log!("stem args: amount={:?}", amount); let amount = evaluate_int(args[1]) as usize;
let path_resolution = evaluate_int(args[4]) as usize;
log!("stem args: {:?}", args);
let mut stem_data = create_multiple_paths(amount, path_resolution, 1); let mut stem_data = create_multiple_paths(amount, path_resolution, 1);
let mut stems = wrap_multiple_paths(&mut stem_data); let mut stems = wrap_multiple_paths(&mut stem_data);
for stem in stems.iter_mut() { for stem in stems.iter_mut() {
let origin = evaluate_vec3(read_i32_slice(origin).as_slice()); let origin = evaluate_vec3(args[0]);
let length = evaluate_float(read_i32_slice(length).as_slice()); let length = evaluate_float(args[2]);
let thickness = evaluate_float(read_i32_slice(thickness).as_slice()); let thickness = evaluate_float(args[3]);
let amount_points = stem.points.len() / 4; let amount_points = stem.points.len() / 4;
for i in 0..amount_points { for i in 0..amount_points {

View File

@@ -1,48 +1,45 @@
use nodarium_macros::nodarium_definition_file; use nodarium_macros::nodarium_definition_file;
use nodarium_macros::nodarium_execute; use nodarium_macros::nodarium_execute;
use nodarium_utils::read_i32_slice; use nodarium_utils::{
use nodarium_utils::{decode_float, encode_float, evaluate_int, log, wrap_arg}; decode_float, encode_float, evaluate_int, split_args, wrap_arg, log
};
nodarium_definition_file!("src/input.json"); nodarium_definition_file!("src/input.json");
#[nodarium_execute] #[nodarium_execute]
pub fn execute(size: (i32, i32)) -> Vec<i32> { pub fn execute(input: &[i32]) -> Vec<i32> {
let size = evaluate_int(read_i32_slice(size).as_slice());
let args = split_args(input);
let size = evaluate_int(args[0]);
let decoded = decode_float(size); let decoded = decode_float(size);
let negative_size = encode_float(-decoded); let negative_size = encode_float(-decoded);
log!("WASM(triangle): input: {:?} -> {}", size, decoded); log!("WASM(triangle): input: {:?} -> {}", args[0],decoded);
// [[1,3, x, y, z, x, y,z,x,y,z]]; // [[1,3, x, y, z, x, y,z,x,y,z]];
wrap_arg(&[ wrap_arg(&[
1, // 1: geometry 1, // 1: geometry
3, // 3 vertices 3, // 3 vertices
1, // 1 face 1, // 1 face
// this are the indeces for the face // this are the indeces for the face
0, 0, 2, 1,
2,
1,
// //
negative_size, // x -> point 1 negative_size, // x -> point 1
0, // y 0, // y
0, // z 0, // z
// //
size, // x -> point 2 size, // x -> point 2
0, // y 0, // y
0, // z 0, // z
// //
0, // x -> point 3 0, // x -> point 3
0, // y 0, // y
size, // z size, // z
// this is the normal for the single face 1065353216 == 1.0f encoded is i32 // this is the normal for the single face 1065353216 == 1.0f encoded is i32
0, 0, 1065353216, 0,
1065353216, 0, 1065353216, 0,
0, 0, 1065353216, 0,
0,
1065353216,
0,
0,
1065353216,
0,
]) ])
} }

View File

@@ -1,17 +1,13 @@
use nodarium_macros::nodarium_definition_file; use nodarium_macros::nodarium_definition_file;
use nodarium_macros::nodarium_execute; use nodarium_macros::nodarium_execute;
use nodarium_utils::concat_args; use nodarium_utils::{concat_args, log, split_args};
use nodarium_utils::log;
use nodarium_utils::read_i32_slice;
nodarium_definition_file!("src/input.json"); nodarium_definition_file!("src/input.json");
#[nodarium_execute] #[nodarium_execute]
pub fn execute(x: (i32, i32), y: (i32, i32), z: (i32, i32)) -> Vec<i32> { pub fn execute(input: &[i32]) -> Vec<i32> {
log!("vec3 x: {:?}", x); let args = split_args(input);
concat_args(vec![ log!("vec3 input: {:?}", input);
read_i32_slice(x).as_slice(), log!("vec3 args: {:?}", args);
read_i32_slice(y).as_slice(), concat_args(args)
read_i32_slice(z).as_slice(),
])
} }

2
nodes/max/plantarium/zig/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.zig-cache/
zig-out/

View File

@@ -0,0 +1,19 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.resolveTargetQuery(.{ .os_tag = .freestanding, .abi = .none, .cpu_arch = .wasm32 });
const release = b.option(bool, "release", "To build a wasm release") orelse false;
const exe = b.addExecutable(.{
.name = "zig",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = if (release) .ReleaseSmall else .Debug,
}),
});
exe.rdynamic = true;
exe.entry = .disabled;
b.installArtifact(exe);
}

View File

@@ -0,0 +1,81 @@
.{
// This is the default name used by packages depending on this one. For
// example, when a user runs `zig fetch --save <url>`, this field is used
// as the key in the `dependencies` table. Although the user can choose a
// different name, most users will stick with this provided value.
//
// It is redundant to include "zig" in this name because it is already
// within the Zig package namespace.
.name = .math,
// This is a [Semantic Version](https://semver.org/).
// In a future version of Zig it will be used for package deduplication.
.version = "0.0.0",
// Together with name, this represents a globally unique package
// identifier. This field is generated by the Zig toolchain when the
// package is first created, and then *never changes*. This allows
// unambiguous detection of one package being an updated version of
// another.
//
// When forking a Zig project, this id should be regenerated (delete the
// field and run `zig build`) if the upstream project is still maintained.
// Otherwise, the fork is *hostile*, attempting to take control over the
// original project's identity. Thus it is recommended to leave the comment
// on the following line intact, so that it shows up in code reviews that
// modify the field.
.fingerprint = 0xa927044d8d610b01, // Changing this has security and trust implications.
// Tracks the earliest Zig version that the package considers to be a
// supported use case.
.minimum_zig_version = "0.15.2",
// This field is optional.
// Each dependency must either provide a `url` and `hash`, or a `path`.
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
// Once all dependencies are fetched, `zig build` no longer requires
// internet connectivity.
.dependencies = .{
// See `zig fetch --save <url>` for a command-line interface for adding dependencies.
//.example = .{
// // When updating this field to a new URL, be sure to delete the corresponding
// // `hash`, otherwise you are communicating that you expect to find the old hash at
// // the new URL. If the contents of a URL change this will result in a hash mismatch
// // which will prevent zig from using it.
// .url = "https://example.com/foo.tar.gz",
//
// // This is computed from the file contents of the directory of files that is
// // obtained after fetching `url` and applying the inclusion rules given by
// // `paths`.
// //
// // This field is the source of truth; packages do not come from a `url`; they
// // come from a `hash`. `url` is just one of many possible mirrors for how to
// // obtain a package matching this `hash`.
// //
// // Uses the [multihash](https://multiformats.io/multihash/) format.
// .hash = "...",
//
// // When this is provided, the package is found in a directory relative to the
// // build root. In this case the package's hash is irrelevant and therefore not
// // computed. This field and `url` are mutually exclusive.
// .path = "foo",
//
// // When this is set to `true`, a package is declared to be lazily
// // fetched. This makes the dependency only get fetched if it is
// // actually used.
// .lazy = false,
//},
},
// Specifies the set of files and directories that are included in this package.
// Only files and directories listed here are included in the `hash` that
// is computed for this package. Only files listed here will remain on disk
// when using the zig package manager. As a rule of thumb, one should list
// files required for compilation plus any license(s).
// Paths are relative to the build root. Use the empty string (`""`) to refer to
// the build root itself.
// A directory listed here means that all files within, recursively, are included.
.paths = .{
"build.zig",
"build.zig.zon",
"src",
// For example...
//"LICENSE",
//"README.md",
},
}

View File

@@ -0,0 +1,27 @@
{
"id": "max/nodarium/zig",
"outputs": [
"float"
],
"inputs": {
"op_type": {
"label": "type",
"type": "select",
"options": [
"add",
"subtract",
"multiply",
"divide"
],
"internal": true
},
"a": {
"type": "float",
"value": 2
},
"b": {
"type": "float",
"value": 2
}
}
}

View File

@@ -0,0 +1,29 @@
const std = @import("std");
const def = @embedFile("input.json");
export fn execute(ptr: *anyopaque, len: c_int) c_int {
_ = ptr; // autofix
_ = len; // autofix
return 0;
}
export fn __alloc(len: c_int) ?*anyopaque {
if (len < 0) return null;
const mem = std.heap.wasm_allocator.alloc(u8, @intCast(len)) catch return null;
return mem.ptr;
}
export fn __free(ptr: *anyopaque, len: c_int) void {
if (len < 1) return;
const mem: [*]u8 = @ptrCast(@alignCast(ptr));
std.heap.wasm_allocator.free(mem[0..@intCast(len)]);
}
export fn getDefinitionPtr() *const anyopaque {
return def.ptr;
}
export fn getDefinitionLen() usize {
return def.len;
}

View File

@@ -5,8 +5,6 @@
"build:story": "pnpm -r --filter 'ui' story:build", "build:story": "pnpm -r --filter 'ui' story:build",
"build:app": "BASE_PATH=/ui pnpm -r --filter 'ui' build && pnpm -r --filter 'app' build", "build:app": "BASE_PATH=/ui pnpm -r --filter 'ui' build && pnpm -r --filter 'app' build",
"build:nodes": "cargo build --workspace --target wasm32-unknown-unknown --release && rm -rf ./app/static/nodes/max/plantarium/ && mkdir -p ./app/static/nodes/max/plantarium/ && cp -R ./target/wasm32-unknown-unknown/release/*.wasm ./app/static/nodes/max/plantarium/", "build:nodes": "cargo build --workspace --target wasm32-unknown-unknown --release && rm -rf ./app/static/nodes/max/plantarium/ && mkdir -p ./app/static/nodes/max/plantarium/ && cp -R ./target/wasm32-unknown-unknown/release/*.wasm ./app/static/nodes/max/plantarium/",
"build:nodes:debug": "cargo build --workspace --target wasm32-unknown-unknown && rm -rf ./app/static/nodes/max/plantarium/ && mkdir -p ./app/static/nodes/max/plantarium/ && cp -R ./target/wasm32-unknown-unknown/release/*.wasm ./app/static/nodes/max/plantarium/",
"build:node": "cargo build --package float --package output --package math --package nodarium_macros --package nodarium_utils --target wasm32-unknown-unknown --release && rm -rf ./app/static/nodes/max/plantarium/ && mkdir -p ./app/static/nodes/max/plantarium/ && cp -R ./target/wasm32-unknown-unknown/release/*.wasm ./app/static/nodes/max/plantarium/",
"build:deploy": "pnpm build", "build:deploy": "pnpm build",
"dev:nodes": "chokidar './nodes/**' --initial -i '/pkg/' -c 'pnpm build:nodes'", "dev:nodes": "chokidar './nodes/**' --initial -i '/pkg/' -c 'pnpm build:nodes'",
"dev:app_ui": "pnpm -r --parallel --filter 'app' --filter './packages/ui' dev", "dev:app_ui": "pnpm -r --parallel --filter 'app' --filter './packages/ui' dev",

View File

@@ -6,7 +6,6 @@ use std::env;
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
use syn::parse_macro_input; use syn::parse_macro_input;
use syn::spanned::Spanned;
fn add_line_numbers(input: String) -> String { fn add_line_numbers(input: String) -> String {
return input return input
@@ -17,177 +16,86 @@ fn add_line_numbers(input: String) -> String {
.join("\n"); .join("\n");
} }
fn read_node_definition(file_path: &Path) -> NodeDefinition {
let project_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let full_path = Path::new(&project_dir).join(file_path);
let json_content = fs::read_to_string(&full_path).unwrap_or_else(|err| {
panic!(
"Failed to read JSON file at '{}/{}': {}",
project_dir,
file_path.to_string_lossy(),
err
)
});
serde_json::from_str(&json_content).unwrap_or_else(|err| {
panic!(
"JSON file contains invalid JSON: \n{} \n{}",
err,
add_line_numbers(json_content.clone())
)
})
}
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn nodarium_execute(_attr: TokenStream, item: TokenStream) -> TokenStream { pub fn nodarium_execute(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input_fn = parse_macro_input!(item as syn::ItemFn); let input_fn = parse_macro_input!(item as syn::ItemFn);
let fn_name = &input_fn.sig.ident; let _fn_name = &input_fn.sig.ident;
let fn_vis = &input_fn.vis; let _fn_vis = &input_fn.vis;
let fn_body = &input_fn.block; let fn_body = &input_fn.block;
let inner_fn_name = syn::Ident::new(&format!("__nodarium_inner_{}", fn_name), fn_name.span());
let def: NodeDefinition = read_node_definition(Path::new("src/input.json")); let first_arg_ident = if let Some(syn::FnArg::Typed(pat_type)) = input_fn.sig.inputs.first() {
if let syn::Pat::Ident(pat_ident) = &*pat_type.pat {
let input_count = def.inputs.as_ref().map(|i| i.len()).unwrap_or(0); &pat_ident.ident
} else {
validate_signature(&input_fn.sig, input_count, &def); panic!("Expected a simple identifier for the first argument");
}
let input_param_names: Vec<_> = input_fn } else {
.sig panic!("The execute function must have at least one argument (the input slice)");
.inputs };
.iter()
.filter_map(|arg| {
if let syn::FnArg::Typed(pat_type) = arg {
if let syn::Pat::Ident(pat_ident) = &*pat_type.pat {
Some(pat_ident.ident.clone())
} else {
None
}
} else {
None
}
})
.collect();
let param_count = input_fn.sig.inputs.len();
let total_c_params = param_count * 2;
let arg_names: Vec<_> = (0..total_c_params)
.map(|i| syn::Ident::new(&format!("arg{}", i), input_fn.sig.span()))
.collect();
let mut tuple_args = Vec::new();
for i in 0..param_count {
let start_name = &arg_names[i * 2];
let end_name = &arg_names[i * 2 + 1];
let tuple_arg = quote! {
(#start_name, #end_name)
};
tuple_args.push(tuple_arg);
}
// We create a wrapper that handles the C ABI and pointer math
let expanded = quote! { let expanded = quote! {
extern "C" { extern "C" {
fn __nodarium_log(ptr: *const u8, len: usize); fn host_log_panic(ptr: *const u8, len: usize);
fn __nodarium_log_panic(ptr: *const u8, len: usize); fn host_log(ptr: *const u8, len: usize);
} }
#fn_vis fn #inner_fn_name(#( #input_param_names: (i32, i32) ),*) -> Vec<i32> { fn setup_panic_hook() {
#fn_body static SET_HOOK: std::sync::Once = std::sync::Once::new();
SET_HOOK.call_once(|| {
std::panic::set_hook(Box::new(|info| {
let msg = info.to_string();
unsafe { host_log_panic(msg.as_ptr(), msg.len()); }
}));
});
}
#[no_mangle]
pub extern "C" fn __alloc(len: usize) -> *mut i32 {
let mut buf = Vec::with_capacity(len);
let ptr = buf.as_mut_ptr();
std::mem::forget(buf);
ptr
} }
#[no_mangle] #[no_mangle]
#fn_vis extern "C" fn execute(output_pos: i32, #( #arg_names: i32 ),*) -> i32 { pub extern "C" fn __free(ptr: *mut i32, len: usize) {
nodarium_utils::log!("before_fn");
let result = #inner_fn_name(
#( #tuple_args ),*
);
nodarium_utils::log!("after_fn");
let len_bytes = result.len() * 4;
unsafe { unsafe {
let src = result.as_ptr() as *const u8; let _ = Vec::from_raw_parts(ptr, 0, len);
let dst = output_pos as *mut u8;
// nodarium_utils::log!("writing output_pos={:?} src={:?} len_bytes={:?}", output_pos, src, len_bytes);
dst.copy_from_nonoverlapping(src, len_bytes);
} }
}
len_bytes as i32 static mut OUTPUT_BUFFER: Vec<i32> = Vec::new();
#[no_mangle]
pub extern "C" fn execute(ptr: *const i32, len: usize) -> *mut i32 {
setup_panic_hook();
// 1. Convert raw pointer to slice
let input = unsafe { core::slice::from_raw_parts(ptr, len) };
// 2. Call the logic (which we define below)
let result_data: Vec<i32> = internal_logic(input);
// 3. Use the static buffer for the result
let result_len = result_data.len();
unsafe {
OUTPUT_BUFFER.clear();
OUTPUT_BUFFER.reserve(result_len + 1);
OUTPUT_BUFFER.push(result_len as i32);
OUTPUT_BUFFER.extend(result_data);
OUTPUT_BUFFER.as_mut_ptr()
}
}
fn internal_logic(#first_arg_ident: &[i32]) -> Vec<i32> {
#fn_body
} }
}; };
TokenStream::from(expanded) TokenStream::from(expanded)
} }
fn validate_signature(fn_sig: &syn::Signature, expected_inputs: usize, def: &NodeDefinition) {
let param_count = fn_sig.inputs.len();
let expected_params = expected_inputs;
if param_count != expected_params {
panic!(
"Execute function has {} parameters but definition has {} inputs\n\
Definition inputs: {:?}\n\
Expected signature:\n\
pub fn execute({}) -> Vec<i32>",
param_count,
expected_inputs,
def.inputs
.as_ref()
.map(|i| i.keys().collect::<Vec<_>>())
.unwrap_or_default(),
(0..expected_inputs)
.map(|i| format!("arg{}: (i32, i32)", i))
.collect::<Vec<_>>()
.join(", ")
);
}
for (i, arg) in fn_sig.inputs.iter().enumerate() {
match arg {
syn::FnArg::Typed(pat_type) => {
let type_str = quote! { #pat_type.ty }.to_string();
let clean_type = type_str
.trim()
.trim_start_matches("_")
.trim_end_matches(".ty")
.trim()
.to_string();
if !clean_type.contains("(") && !clean_type.contains(",") {
panic!(
"Parameter {} has type '{}' but should be a tuple (i32, i32) representing (start, end) positions in memory",
i,
clean_type
);
}
}
syn::FnArg::Receiver(_) => {
panic!("Execute function cannot have 'self' parameter");
}
}
}
match &fn_sig.output {
syn::ReturnType::Type(_, ty) => {
let is_vec = match &**ty {
syn::Type::Path(tp) => tp
.path
.segments
.first()
.map(|seg| seg.ident == "Vec")
.unwrap_or(false),
_ => false,
};
if !is_vec {
panic!("Execute function must return Vec<i32>");
}
}
syn::ReturnType::Default => {
panic!("Execute function must return Vec<i32>");
}
}
}
#[proc_macro] #[proc_macro]
pub fn nodarium_definition_file(input: TokenStream) -> TokenStream { pub fn nodarium_definition_file(input: TokenStream) -> TokenStream {
let path_lit = syn::parse_macro_input!(input as syn::LitStr); let path_lit = syn::parse_macro_input!(input as syn::LitStr);
@@ -197,26 +105,30 @@ pub fn nodarium_definition_file(input: TokenStream) -> TokenStream {
let full_path = Path::new(&project_dir).join(&file_path); let full_path = Path::new(&project_dir).join(&file_path);
let json_content = fs::read_to_string(&full_path).unwrap_or_else(|err| { let json_content = fs::read_to_string(&full_path).unwrap_or_else(|err| {
panic!( panic!("Failed to read JSON file at '{}/{}': {}", project_dir, file_path, err)
"Failed to read JSON file at '{}/{}': {}",
project_dir, file_path, err
)
}); });
let _: NodeDefinition = serde_json::from_str(&json_content).unwrap_or_else(|err| { let _: NodeDefinition = serde_json::from_str(&json_content).unwrap_or_else(|err| {
panic!( panic!("JSON file contains invalid JSON: \n{} \n{}", err, add_line_numbers(json_content.clone()))
"JSON file contains invalid JSON: \n{} \n{}",
err,
add_line_numbers(json_content.clone())
)
}); });
// We use the span from the input path literal
let bytes = syn::LitByteStr::new(json_content.as_bytes(), path_lit.span()); let bytes = syn::LitByteStr::new(json_content.as_bytes(), path_lit.span());
let len = json_content.len(); let len = json_content.len();
let expanded = quote! { let expanded = quote! {
#[link_section = "nodarium_definition"] #[link_section = "nodarium_definition"]
static DEFINITION_DATA: [u8; #len] = *#bytes; static DEFINITION_DATA: [u8; #len] = *#bytes;
#[no_mangle]
pub extern "C" fn get_definition_ptr() -> *const u8 {
DEFINITION_DATA.as_ptr()
}
#[no_mangle]
pub extern "C" fn get_definition_len() -> usize {
DEFINITION_DATA.len()
}
}; };
TokenStream::from(expanded) TokenStream::from(expanded)

View File

@@ -2,7 +2,6 @@ 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';
@@ -13,7 +12,6 @@ log.mute();
export class RemoteNodeRegistry implements NodeRegistry { export class RemoteNodeRegistry implements NodeRegistry {
status: 'loading' | 'ready' | 'error' = 'loading'; status: 'loading' | 'ready' | 'error' = 'loading';
private nodes: Map<string, NodeDefinition> = new Map(); private nodes: Map<string, NodeDefinition> = new Map();
private memory = new WebAssembly.Memory({ initial: 1024, maximum: 8192 });
constructor( constructor(
private url: string, private url: string,
@@ -128,10 +126,7 @@ export class RemoteNodeRegistry implements NodeRegistry {
} }
async register(wasmBuffer: ArrayBuffer) { async register(wasmBuffer: ArrayBuffer) {
const wrapper = createWasmWrapper( const wrapper = createWasmWrapper(wasmBuffer);
wasmBuffer,
this.memory
);
const definition = NodeDefinitionSchema.safeParse(wrapper.get_definition()); const definition = NodeDefinitionSchema.safeParse(wrapper.get_definition());
@@ -143,7 +138,10 @@ export class RemoteNodeRegistry implements NodeRegistry {
this.cache.set(definition.data.id, wasmBuffer); this.cache.set(definition.data.id, wasmBuffer);
} }
let node = { ...definition.data, execute: wrapper.execute }; let node = {
...definition.data,
execute: wrapper.execute
};
this.nodes.set(definition.data.id, node); this.nodes.set(definition.data.id, node);
@@ -155,13 +153,6 @@ export class RemoteNodeRegistry implements NodeRegistry {
} }
getAllNodes() { getAllNodes() {
const allNodes = [...this.nodes.values()]; return [...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,78 +9,72 @@ const DefaultOptionsSchema = z.object({
accepts: z accepts: z
.array( .array(
z.union([ z.union([
z.literal('*'), 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([
@@ -93,7 +87,6 @@ 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>;

View File

@@ -21,7 +21,7 @@ export type NodeRuntimeState = {
parents?: NodeInstance[]; parents?: NodeInstance[];
children?: NodeInstance[]; children?: NodeInstance[];
inputNodes?: Record<string, NodeInstance>; inputNodes?: Record<string, NodeInstance>;
type?: NodeDefinition; // we should probably remove this and rely on registry.getNode(nodeType) type?: NodeDefinition;
downX?: number; downX?: number;
downY?: number; downY?: number;
x?: number; x?: number;
@@ -65,7 +65,7 @@ export const NodeSchema = z.object({
export type SerializedNode = z.infer<typeof NodeSchema>; export type SerializedNode = z.infer<typeof NodeSchema>;
export type NodeDefinition = z.infer<typeof NodeDefinitionSchema> & { export type NodeDefinition = z.infer<typeof NodeDefinitionSchema> & {
execute(outputPos: number, args: number[]): number; execute(input: Int32Array): Int32Array;
}; };
export type Socket = { export type Socket = {

View File

@@ -9,10 +9,6 @@ description = "A collection of utilities for Nodarium"
[lib] [lib]
crate-type = ["rlib"] crate-type = ["rlib"]
[features]
default = ["std"]
std = []
[dependencies] [dependencies]
glam = "0.30.10" glam = "0.30.10"
noise = "0.9.0" noise = "0.9.0"

View File

@@ -10,50 +10,6 @@ pub fn decode_float(bits: i32) -> f32 {
f32::from_bits(bits) f32::from_bits(bits)
} }
#[inline]
pub fn read_i32(ptr: i32) -> i32 {
unsafe {
let _ptr = ptr as *const i32;
*_ptr
}
}
#[inline]
pub fn read_f32(ptr: i32) -> f32 {
unsafe {
let _ptr = ptr as *const i32;
f32::from_bits(*_ptr as u32)
}
}
#[inline]
pub fn read_i32_slice(range: (i32, i32)) -> Vec<i32> {
let (start, end) = range;
assert!(end >= start);
let byte_len = (end - start) as usize;
assert!(byte_len % 4 == 0);
unsafe {
let ptr = start as *const i32;
let len = byte_len / 4;
std::slice::from_raw_parts(ptr, len).to_vec()
}
}
#[inline]
pub fn read_f32_slice(range: (i32, i32)) -> Vec<f32> {
let (start, end) = range;
assert!(end >= start);
let byte_len = (end - start) as usize;
assert!(byte_len % 4 == 0);
unsafe {
let ptr = start as *const f32;
let len = byte_len / 4;
std::slice::from_raw_parts(ptr, len).to_vec()
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@@ -8,7 +8,7 @@ pub mod geometry;
extern "C" { extern "C" {
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
pub fn __nodarium_log(ptr: *const u8, len: usize); pub fn host_log(ptr: *const u8, len: usize);
} }
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
@@ -18,7 +18,7 @@ macro_rules! log {
let msg = std::format!($($t)*); let msg = std::format!($($t)*);
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
unsafe { unsafe {
$crate::__nodarium_log(msg.as_ptr(), msg.len()); $crate::host_log(msg.as_ptr(), msg.len());
} }
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
println!("{}", msg); println!("{}", msg);

View File

@@ -1,47 +1,66 @@
interface NodariumExports extends WebAssembly.Exports { interface NodariumExports extends WebAssembly.Exports {
memory: WebAssembly.Memory; memory: WebAssembly.Memory;
execute: (outputPos: number, ...args: number[]) => number; execute: (ptr: number, len: number) => number;
__free: (ptr: number, len: number) => void;
__alloc: (len: number) => number;
getDefinitionPtr: () => number;
getDefinitionLen: () => number;
} }
export function createWasmWrapper(buffer: ArrayBuffer, memory: WebAssembly.Memory) { export function createWasmWrapper(buffer: ArrayBuffer) {
let exports: NodariumExports; let exports: NodariumExports;
const importObject = { const importObject = {
env: { env: {
memory: memory, host_log_panic: (ptr: number, len: number) => {
__nodarium_log_panic: (ptr: number, len: number) => {
if (!exports) return; if (!exports) return;
const view = new Uint8Array(memory.buffer, ptr, len); const view = new Uint8Array(exports.memory.buffer, ptr, len);
console.error('WASM PANIC:', new TextDecoder().decode(view)); console.error("RUST PANIC:", new TextDecoder().decode(view));
}, },
__nodarium_log: (ptr: number, len: number) => { host_log: (ptr: number, len: number) => {
if (!exports) return; if (!exports) return;
const view = new Uint8Array(memory.buffer, ptr, len); const view = new Uint8Array(exports.memory.buffer, ptr, len);
console.log('WASM:', new TextDecoder().decode(view)); console.log("RUST:", new TextDecoder().decode(view));
} },
} },
}; };
const module = new WebAssembly.Module(buffer); const module = new WebAssembly.Module(buffer);
const instance = new WebAssembly.Instance(module, importObject); const instance = new WebAssembly.Instance(module, importObject);
exports = instance.exports as NodariumExports; exports = instance.exports as NodariumExports;
function execute(outputPos: number, args: number[]): number { function execute(args: Int32Array) {
try { const inPtr = exports.__alloc(args.length);
return exports.execute(outputPos, ...args); new Int32Array(exports.memory.buffer).set(args, inPtr / 4);
} catch (e) {
console.log(e); const outPtr = exports.execute(inPtr, args.length);
return -1;
} const i32Result = new Int32Array(exports.memory.buffer);
const outLen = i32Result[outPtr / 4];
const out = i32Result.slice(outPtr / 4 + 1, outPtr / 4 + 1 + outLen);
exports.__free(inPtr, args.length);
return out;
} }
function get_definition() { function get_definition() {
const sections = WebAssembly.Module.customSections(module, 'nodarium_definition'); const decoder = new TextDecoder();
const sections = WebAssembly.Module.customSections(
module,
"nodarium_definition",
);
if (sections.length > 0) { if (sections.length > 0) {
const decoder = new TextDecoder();
const jsonString = decoder.decode(sections[0]); const jsonString = decoder.decode(sections[0]);
return JSON.parse(jsonString); return JSON.parse(jsonString);
} }
const ptr = exports.getDefinitionPtr();
const len = exports.getDefinitionLen();
const view = new Uint8Array(exports.memory.buffer, ptr, len);
const jsonString = decoder.decode(view);
return JSON.parse(jsonString);
} }
return { execute, get_definition }; return { execute, get_definition };