From e6c368afaa484cdacbed4702dee5c67a9d917b1c Mon Sep 17 00:00:00 2001 From: Max Richter Date: Fri, 8 May 2026 01:49:29 +0200 Subject: [PATCH] feat: update some nodes a bit --- Cargo.lock | 1 + .../lib/graph-interface/graph/drop.events.ts | 2 +- app/src/lib/result-viewer/debug.ts | 2 +- app/src/lib/runtime/runtime-executor.ts | 11 +- .../lib/runtime/worker-runtime-executor.ts | 4 +- app/src/routes/+page.svelte | 6 +- nodes/max/plantarium/gravity/src/input.json | 23 ++- nodes/max/plantarium/gravity/src/lib.rs | 140 ++++++++++++++---- nodes/max/plantarium/leaf/Cargo.toml | 1 + nodes/max/plantarium/leaf/src/input.json | 27 ++++ nodes/max/plantarium/leaf/src/lib.rs | 92 ++++++++++-- nodes/max/plantarium/noise/src/input.json | 9 +- nodes/max/plantarium/rotate/src/input.json | 2 +- 13 files changed, 262 insertions(+), 58 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 65725b6..ceeb0d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -66,6 +66,7 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" name = "leaf" version = "0.1.0" dependencies = [ + "glam", "nodarium_macros", "nodarium_utils", ] diff --git a/app/src/lib/graph-interface/graph/drop.events.ts b/app/src/lib/graph-interface/graph/drop.events.ts index a6c9896..636b357 100644 --- a/app/src/lib/graph-interface/graph/drop.events.ts +++ b/app/src/lib/graph-interface/graph/drop.events.ts @@ -1,5 +1,5 @@ -import { toast } from '@nodarium/ui'; import { GraphSchema, type NodeId } from '@nodarium/types'; +import { toast } from '@nodarium/ui'; import type { GraphManager } from '../graph-manager.svelte'; import type { GraphState } from '../graph-state.svelte'; diff --git a/app/src/lib/result-viewer/debug.ts b/app/src/lib/result-viewer/debug.ts index ea40319..5d5b50f 100644 --- a/app/src/lib/result-viewer/debug.ts +++ b/app/src/lib/result-viewer/debug.ts @@ -32,7 +32,7 @@ function writePath(scene: Group, data: Int32Array): Vector3[] { // Instanced spheres at points if (positions.length > 0) { - const sphereGeometry = new SphereGeometry(0.05, 8, 8); // keep low-poly + const sphereGeometry = new SphereGeometry(0.02, 8, 8); // keep low-poly const sphereMaterial = new MeshBasicMaterial({ color: 0xff0000, depthTest: false diff --git a/app/src/lib/runtime/runtime-executor.ts b/app/src/lib/runtime/runtime-executor.ts index 785990e..91ccbef 100644 --- a/app/src/lib/runtime/runtime-executor.ts +++ b/app/src/lib/runtime/runtime-executor.ts @@ -134,6 +134,14 @@ function getValue(input: NodeInput, value?: unknown) { return encodeFloat(value as number); } + if (input.type === 'select' && typeof value !== 'number') { + const index = input.options?.indexOf(value as string); + if (index === undefined || index < 0) { + throw new Error(`Unknown value ${value} for select input ${input.label}`); + } + return index; + } + if (Array.isArray(value)) { if (input.type === 'vec3' || input.type === 'shape') { return [ @@ -159,6 +167,8 @@ function getValue(input: NodeInput, value?: unknown) { return value; } + console.log({ input, value }); + throw new Error(`Unknown input type ${input.type}`); } @@ -312,7 +322,6 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor { continue; } - a = performance.now(); // Collect the inputs for the node diff --git a/app/src/lib/runtime/worker-runtime-executor.ts b/app/src/lib/runtime/worker-runtime-executor.ts index 6aba5b1..de37e12 100644 --- a/app/src/lib/runtime/worker-runtime-executor.ts +++ b/app/src/lib/runtime/worker-runtime-executor.ts @@ -12,8 +12,8 @@ export class WorkerRuntimeExecutor implements RuntimeExecutor { getPerformanceData() { return this.worker.getPerformanceData(); } - getDebugData() { - return this.worker.getDebugData(); + async getDebugData() { + return await this.worker.getDebugData(); } set useRuntimeCache(useCache: boolean) { this.worker.setUseRuntimeCache(useCache); diff --git a/app/src/routes/+page.svelte b/app/src/routes/+page.svelte index 15a8261..b061240 100644 --- a/app/src/routes/+page.svelte +++ b/app/src/routes/+page.svelte @@ -299,7 +299,11 @@ bind:showHelp={appSettings.value.nodeInterface.showHelp} bind:settings={graphSettings} bind:settingTypes={graphSettingTypes} - onsave={async (g) => { pendingSave = true; await pm.saveGraph(g); pendingSave = false; }} + onsave={async (g) => { + pendingSave = true; + await pm.saveGraph(g); + pendingSave = false; + }} onresult={(result) => handleUpdate(result as Graph)} /> {/key} diff --git a/nodes/max/plantarium/gravity/src/input.json b/nodes/max/plantarium/gravity/src/input.json index d731bf0..6d34ad7 100644 --- a/nodes/max/plantarium/gravity/src/input.json +++ b/nodes/max/plantarium/gravity/src/input.json @@ -13,19 +13,28 @@ "max": 1, "value": 1 }, - "curviness": { - "type": "float", - "hidden": true, - "min": 0, - "max": 1, - "value": 0.5 - }, "depth": { "type": "integer", "min": 1, "max": 10, "hidden": true, "value": 1 + }, + "elasticity": { + "type": "float", + "description": "How rigid the stem is. 0 = rope (uniform droop), 1 = stiff rod (only the tip bends).", + "min": 0, + "max": 1, + "step": 0.05, + "value": 0.3 + }, + "mode": { + "type": "select", + "internal": true, + "label": "Mode", + "options": ["closed-form", "chain"], + "hidden": true, + "description": "closed-form lerps each segment toward gravity; chain is a forward-kinematic cantilever where each segment rotates by an angle that grows along the stem." } } } diff --git a/nodes/max/plantarium/gravity/src/lib.rs b/nodes/max/plantarium/gravity/src/lib.rs index 4453934..26ecbaf 100644 --- a/nodes/max/plantarium/gravity/src/lib.rs +++ b/nodes/max/plantarium/gravity/src/lib.rs @@ -20,7 +20,11 @@ pub fn execute(input: &[i32]) -> Vec { let args = split_args(input); let plants = split_args(args[0]); - let depth = evaluate_int(args[3]); + let depth = evaluate_int(args[2]); + let elasticity = evaluate_float(args[3]).clamp(0.0, 1.0); + let mode = evaluate_int(args[4]); // 0 = closed-form, 1 = verlet + // 0 → sqrt (rope), 1 → ~4.5 (only the tip droops) + let bend_exponent = 0.5 + elasticity * 4.0; let mut max_depth = 0; for path_data in plants.iter() { @@ -42,50 +46,124 @@ pub fn execute(input: &[i32]) -> Vec { let mut output_data = path_data.clone(); let output = wrap_path_mut(&mut output_data); - let mut offset_vec = Vec3::ZERO; + if mode == 1 { + // Forward-kinematic cantilever chain. Each segment rotates around + // an axis perpendicular to (rest_dir, gravity) by an angle that + // grows with alpha along the stem. Positions are built from the + // anchored base outward, so segment lengths are preserved by + // construction (no iteration, no rescaling, no oscillation). - for i in 0..path.length - 1 { - let alpha = i as f32 / (path.length - 1) as f32; - let start_index = i * 4; + let raw_strength = evaluate_float(args[1]); + let gravity_dir = Vec3::new(0.0, -1.0, 0.0); - let start_point = Vec3::from_slice(&path.points[start_index..start_index + 3]); - let end_point = Vec3::from_slice(&path.points[start_index + 4..start_index + 7]); + // Tip bend angle in radians. PI/2 = horizontal tip at strength=1. + let max_angle = raw_strength * std::f32::consts::FRAC_PI_2; - let direction = end_point - start_point; + let original: Vec = (0..path.length) + .map(|i| { + let s = i * 4; + Vec3::from_slice(&path.points[s..s + 3]) + }) + .collect(); - let length = direction.length(); + let seg_lens: Vec = (0..path.length - 1) + .map(|i| (original[i + 1] - original[i]).length()) + .collect(); + let rest_dirs: Vec = (0..path.length - 1) + .map(|i| { + let d = original[i + 1] - original[i]; + let l = d.length(); + if l > 0.0001 { d / l } else { Vec3::Y } + }) + .collect(); - let curviness = evaluate_float(args[2]); - let strength = - evaluate_float(args[1]) / curviness.max(0.0001) * evaluate_float(args[1]); + let mut cur = vec![Vec3::ZERO; path.length]; + cur[0] = original[0]; - log!( - "length: {}, curviness: {}, strength: {}", - length, - curviness, - strength - ); + for i in 1..path.length { + let seg_idx = i - 1; + let alpha = if path.length > 2 { + seg_idx as f32 / (path.length - 2) as f32 + } else { + 1.0 + }; + let bend_angle = max_angle * alpha.powf(bend_exponent); - let down_point = Vec3::new(0.0, -length * strength, 0.0); + let rest_dir = rest_dirs[seg_idx]; + let mut bend_axis = rest_dir.cross(gravity_dir); + let axis_len = bend_axis.length(); + bend_axis = if axis_len > 0.0001 { + bend_axis / axis_len + } else { + // rest_dir parallel to gravity — pick an arbitrary + // perpendicular axis to break symmetry. + Vec3::X + }; - let mut mid_point = lerp_vec3(direction, down_point, curviness * alpha.sqrt()); + // Rodrigues' rotation formula + let (sin_a, cos_a) = bend_angle.sin_cos(); + let bent_dir = rest_dir * cos_a + + bend_axis.cross(rest_dir) * sin_a + + bend_axis * bend_axis.dot(rest_dir) * (1.0 - cos_a); - if mid_point[0] == 0.0 && mid_point[2] == 0.0 { - mid_point[0] += 0.0001; - mid_point[2] += 0.0001; + cur[i] = cur[i - 1] + bent_dir * seg_lens[seg_idx]; } - // Correct midpoint length - mid_point *= length / mid_point.length(); + for i in 0..path.length { + let s = i * 4; + output.points[s] = cur[i].x; + output.points[s + 1] = cur[i].y; + output.points[s + 2] = cur[i].z; + } + } else { + // Closed-form: per-segment lerp toward a downward vector + let mut offset_vec = Vec3::ZERO; - let final_end_point = start_point + mid_point; - let offset_end_point = end_point + offset_vec; + for i in 0..path.length - 1 { + let alpha = i as f32 / (path.length - 1) as f32; + let start_index = i * 4; - output.points[start_index + 4] = offset_end_point[0]; - output.points[start_index + 5] = offset_end_point[1]; - output.points[start_index + 6] = offset_end_point[2]; + let start_point = Vec3::from_slice(&path.points[start_index..start_index + 3]); + let end_point = + Vec3::from_slice(&path.points[start_index + 4..start_index + 7]); - offset_vec += final_end_point - end_point; + let direction = end_point - start_point; + + let length = direction.length(); + + let curviness = elasticity.max(0.0001); + let strength_arg = evaluate_float(args[1]) * 10.0; + let strength = strength_arg / curviness * strength_arg; + + log!( + "length: {}, curviness: {}, strength: {}", + length, + curviness, + strength + ); + + let down_point = Vec3::new(0.0, -length * strength, 0.0); + + let mut mid_point = + lerp_vec3(direction, down_point, curviness * alpha.powf(bend_exponent)); + + if mid_point[0] == 0.0 && mid_point[2] == 0.0 { + mid_point[0] += 0.0001; + mid_point[2] += 0.0001; + } + + // Correct midpoint length + mid_point *= length / mid_point.length(); + + let final_end_point = start_point + mid_point; + let offset_end_point = end_point + offset_vec; + + output.points[start_index + 4] = offset_end_point[0]; + output.points[start_index + 5] = offset_end_point[1]; + output.points[start_index + 6] = offset_end_point[2]; + + offset_vec += final_end_point - end_point; + } } output_data }) diff --git a/nodes/max/plantarium/leaf/Cargo.toml b/nodes/max/plantarium/leaf/Cargo.toml index b582166..800d006 100644 --- a/nodes/max/plantarium/leaf/Cargo.toml +++ b/nodes/max/plantarium/leaf/Cargo.toml @@ -8,5 +8,6 @@ edition = "2018" crate-type = ["cdylib", "rlib"] [dependencies] +glam = "0.30.10" nodarium_macros = { version = "0.1.0", path = "../../../../packages/macros" } nodarium_utils = { version = "0.1.0", path = "../../../../packages/utils" } diff --git a/nodes/max/plantarium/leaf/src/input.json b/nodes/max/plantarium/leaf/src/input.json index a3a94a8..eb8afbf 100644 --- a/nodes/max/plantarium/leaf/src/input.json +++ b/nodes/max/plantarium/leaf/src/input.json @@ -19,6 +19,33 @@ "max": 64, "value": 1, "hidden": true + }, + "yCurve": { + "type": "float", + "description": "Curl the leaf upward along its length (radians). 0 = flat, ~1.57 = 90° tip curl.", + "min": -3.14, + "max": 3.14, + "step": 0.05, + "value": 0, + "hidden": true + }, + "yTwist": { + "type": "float", + "description": "Twist around the leaf's spine. Combined with yCurve, produces a 3D spiral.", + "min": -6.28, + "max": 6.28, + "step": 0.05, + "value": 0, + "hidden": true + }, + "xCurve": { + "type": "float", + "description": "Curl each cross-section into an arc, mirrored around the midrib. 0 = flat, ~1.57 = U-shape.", + "min": -3.14, + "max": 3.14, + "step": 0.05, + "value": 0, + "hidden": true } } } diff --git a/nodes/max/plantarium/leaf/src/lib.rs b/nodes/max/plantarium/leaf/src/lib.rs index f48b3f1..5546216 100644 --- a/nodes/max/plantarium/leaf/src/lib.rs +++ b/nodes/max/plantarium/leaf/src/lib.rs @@ -1,6 +1,7 @@ use std::convert::TryInto; use std::f32::consts::PI; +use glam::Vec3; use nodarium_macros::nodarium_definition_file; use nodarium_macros::nodarium_execute; use nodarium_utils::encode_float; @@ -42,6 +43,9 @@ pub fn execute(input: &[i32]) -> Vec { let input_path = split_args(args[0])[0]; let size = evaluate_float(args[1]); let width_resolution = evaluate_int(args[2]).max(3) as usize; + let y_curve = evaluate_float(args[3]); + let y_twist = evaluate_float(args[4]); + let x_curve = evaluate_float(args[5]); let path_length = (input_path.len() - 4) / 2; let slice_count = path_length; @@ -93,27 +97,97 @@ pub fn execute(input: &[i32]) -> Vec { // Writing Positions let width = 50.0; + let leaf_length: f32 = 100.0; let mut positions = vec![[0.0f32; 3]; position_amount]; + + // Pre-compute a local frame (center, normal=local-Y, binormal=local-X) for + // each slice by walking the FK chain. At each step we bend around the + // current binormal (curls the leaf) and twist around the current tangent + // (rotates the bend plane → spiral). + let segs = (slice_count - 1).max(1) as f32; + let bend_per_step = y_curve / segs; + let twist_per_step = y_twist / segs; + + let mut centers: Vec = Vec::with_capacity(slice_count); + let mut frame_n: Vec = Vec::with_capacity(slice_count); + let mut frame_b: Vec = Vec::with_capacity(slice_count); + + let mut tangent = Vec3::new(0.0, 0.0, 1.0); + let mut normal = Vec3::new(0.0, 1.0, 0.0); + let mut binormal = Vec3::new(1.0, 0.0, 0.0); + + let pz_first = decode_float(input_path[2 + 1]); + let mut center = Vec3::new(0.0, 0.0, pz_first - leaf_length); + for i in 0..slice_count { - let ax = i as f32 / (slice_count -1) as f32; + centers.push(center); + frame_n.push(normal); + frame_b.push(binormal); + if i + 1 < slice_count { + let pz_curr = decode_float(input_path[2 + i * 2 + 1]); + let pz_next = decode_float(input_path[2 + (i + 1) * 2 + 1]); + let seg_len = pz_next - pz_curr; + + center = center + tangent * seg_len; + + // Bend around binormal — tilts tangent toward normal + let (sin_b, cos_b) = bend_per_step.sin_cos(); + let new_t = tangent * cos_b + normal * sin_b; + let new_n = -tangent * sin_b + normal * cos_b; + tangent = new_t; + normal = new_n; + + // Twist around tangent — rotates normal/binormal so the next bend + // happens in a rotated plane + let (sin_tw, cos_tw) = twist_per_step.sin_cos(); + let new_n2 = normal * cos_tw + binormal * sin_tw; + let new_b = -normal * sin_tw + binormal * cos_tw; + normal = new_n2; + binormal = new_b; + } + } + + for i in 0..slice_count { + let ax = i as f32 / segs; let px = decode_float(input_path[2 + i * 2 + 0]); - let pz = decode_float(input_path[2 + i * 2 + 1]); + let hw = width - px; // half-width at this slice + let c = centers[i]; + let n = frame_n[i]; + let b = frame_b[i]; for j in 0..width_resolution { let alpha = j as f32 / (width_resolution - 1) as f32; - let x = 2.0 * (-px * (alpha - 0.5) + alpha * width); - let py = calculate_y(alpha-0.5)*5.0*(ax*PI).sin(); - let pz_val = pz - 100.0; + // Signed cross-section parameter, -1 (left edge) → +1 (right edge) + let t = 2.0 * alpha - 1.0; + let py_local = calculate_y(alpha - 0.5) * 5.0 * (ax * PI).sin(); + + // X-curl: each cross-section traces a circular arc with curvature + // x_curve / hw. Because theta = x_curve * t is signed around the + // midrib, sin/cos give a mirrored arc (left and right edges curl + // the same direction). + let theta = x_curve * t; + let (sin_t, cos_t) = theta.sin_cos(); + let (b_arc, n_arc) = if x_curve.abs() < 0.0001 { + (t * hw, 0.0) + } else { + let r = hw / x_curve; + (r * sin_t, r * (1.0 - cos_t)) + }; + // Cross-section bulge follows the rotated local frame + let b_total = b_arc - py_local * sin_t; + let n_total = n_arc + py_local * cos_t; + + let world = c + b * b_total + n * n_total; let pos_idx = i * width_resolution + j; - positions[pos_idx] = [x - width, py, pz_val]; + positions[pos_idx] = [world.x, world.y, world.z]; let flat_idx = offset + pos_idx * 3; - out[flat_idx + 0] = encode_float((x - width) * size); - out[flat_idx + 1] = encode_float(py * size); - out[flat_idx + 2] = encode_float(pz_val * size); + out[flat_idx + 0] = encode_float(world.x * size); + out[flat_idx + 1] = encode_float(world.y * size); + out[flat_idx + 2] = encode_float(world.z * size); } } diff --git a/nodes/max/plantarium/noise/src/input.json b/nodes/max/plantarium/noise/src/input.json index d16d21a..bab3e7d 100644 --- a/nodes/max/plantarium/noise/src/input.json +++ b/nodes/max/plantarium/noise/src/input.json @@ -15,9 +15,9 @@ }, "strength": { "type": "float", - "min": 0.1, - "max": 10, - "value": 2 + "min": 0, + "max": 1, + "value": 0.5 }, "fixBottom": { "type": "float", @@ -56,7 +56,8 @@ "preserveLength": { "type": "boolean", "label": "Preserve length", - "value": true + "value": true, + "hidden": true } } } diff --git a/nodes/max/plantarium/rotate/src/input.json b/nodes/max/plantarium/rotate/src/input.json index 73101d9..c5f287f 100644 --- a/nodes/max/plantarium/rotate/src/input.json +++ b/nodes/max/plantarium/rotate/src/input.json @@ -29,7 +29,7 @@ "type": "boolean", "internal": true, "hidden": true, - "value": true, + "value": false, "description": "If multiple objects are connected, should we rotate them as one or spread them?" } }