diff --git a/daw-backend/src/audio/node_graph/nodes/script_node.rs b/daw-backend/src/audio/node_graph/nodes/script_node.rs index f4dcab9..f04f728 100644 --- a/daw-backend/src/audio/node_graph/nodes/script_node.rs +++ b/daw-backend/src/audio/node_graph/nodes/script_node.rs @@ -152,15 +152,17 @@ impl AudioNode for ScriptNode { fn set_parameter(&mut self, id: u32, value: f32) { let idx = id as usize; - if idx < self.vm.params.len() { - self.vm.params[idx] = value; + let params = self.vm.params_mut(); + if idx < params.len() { + params[idx] = value; } } fn get_parameter(&self, id: u32) -> f32 { let idx = id as usize; - if idx < self.vm.params.len() { - self.vm.params[idx] + let params = self.vm.params(); + if idx < params.len() { + params[idx] } else { 0.0 } diff --git a/lightningbeam-ui/beamdsp/src/codegen.rs b/lightningbeam-ui/beamdsp/src/codegen.rs index 602e3a1..1faa67a 100644 --- a/lightningbeam-ui/beamdsp/src/codegen.rs +++ b/lightningbeam-ui/beamdsp/src/codegen.rs @@ -3,19 +3,9 @@ use crate::error::CompileError; use crate::opcodes::OpCode; use crate::token::Span; use crate::ui_decl::{UiDeclaration, UiElement}; +use crate::validator::VType; use crate::vm::ScriptVM; -/// Type tracked during codegen to select typed opcodes -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum VType { - F32, - Int, - Bool, - ArrayF32, - ArrayInt, - Sample, -} - /// Where a named variable lives in the VM #[derive(Debug, Clone, Copy)] enum VarLoc { @@ -165,12 +155,39 @@ impl Compiler { } } - // Register params + self.register_params_and_state(script, true); + + // Compile process block + for stmt in &script.process { + self.compile_stmt(stmt)?; + } + + self.emit(OpCode::Halt); + Ok(()) + } + + /// Compile the draw block into separate bytecode (for the DrawVM) + fn compile_draw(&mut self, script: &Script) -> Result<(), CompileError> { + self.draw_context = true; + self.register_params_and_state(script, false); + + // Compile draw block + if let Some(draw) = &script.draw { + for stmt in draw { + self.compile_stmt(stmt)?; + } + } + + self.emit(OpCode::Halt); + Ok(()) + } + + /// Register params and state variables. If `include_samples` is true, also registers sample slots. + fn register_params_and_state(&mut self, script: &Script, include_samples: bool) { for (i, param) in script.params.iter().enumerate() { self.vars.push((param.name.clone(), VarLoc::Param(i as u16))); } - // Register state variables let mut scalar_idx: u16 = 0; let mut array_idx: u16 = 0; let mut sample_idx: u8 = 0; @@ -196,69 +213,13 @@ impl Compiler { self.vars.push((state.name.clone(), VarLoc::StateArray(array_idx, VType::Int))); array_idx += 1; } - StateType::Sample => { + StateType::Sample if include_samples => { self.vars.push((state.name.clone(), VarLoc::SampleSlot(sample_idx))); sample_idx += 1; } + StateType::Sample => {} } } - - // Compile process block - for stmt in &script.process { - self.compile_stmt(stmt)?; - } - - self.emit(OpCode::Halt); - Ok(()) - } - - /// Compile the draw block into separate bytecode (for the DrawVM) - fn compile_draw(&mut self, script: &Script) -> Result<(), CompileError> { - self.draw_context = true; - - // Register params (same as process) - for (i, param) in script.params.iter().enumerate() { - self.vars.push((param.name.clone(), VarLoc::Param(i as u16))); - } - - // Register state variables (draw gets its own copy) - let mut scalar_idx: u16 = 0; - let mut array_idx: u16 = 0; - for state in &script.state { - match &state.ty { - StateType::F32 => { - self.vars.push((state.name.clone(), VarLoc::StateScalar(scalar_idx, VType::F32))); - scalar_idx += 1; - } - StateType::Int => { - self.vars.push((state.name.clone(), VarLoc::StateScalar(scalar_idx, VType::Int))); - scalar_idx += 1; - } - StateType::Bool => { - self.vars.push((state.name.clone(), VarLoc::StateScalar(scalar_idx, VType::Bool))); - scalar_idx += 1; - } - StateType::ArrayF32(_) => { - self.vars.push((state.name.clone(), VarLoc::StateArray(array_idx, VType::F32))); - array_idx += 1; - } - StateType::ArrayInt(_) => { - self.vars.push((state.name.clone(), VarLoc::StateArray(array_idx, VType::Int))); - array_idx += 1; - } - StateType::Sample => {} // no samples in draw context - } - } - - // Compile draw block - if let Some(draw) = &script.draw { - for stmt in draw { - self.compile_stmt(stmt)?; - } - } - - self.emit(OpCode::Halt); - Ok(()) } fn compile_stmt(&mut self, stmt: &Stmt) -> Result<(), CompileError> { @@ -1149,12 +1110,13 @@ mod tests { // Draw VM should exist let mut dvm = draw_vm.expect("draw_vm should be Some"); - assert!(!dvm.bytecode.is_empty()); + assert!(dvm.has_bytecode()); // Execute should succeed without stack errors dvm.execute().unwrap(); // Should have produced draw commands assert_eq!(dvm.draw_commands.len(), 2); // fill_circle + stroke_arc + } } diff --git a/lightningbeam-ui/beamdsp/src/error.rs b/lightningbeam-ui/beamdsp/src/error.rs index 10c331e..3c472ef 100644 --- a/lightningbeam-ui/beamdsp/src/error.rs +++ b/lightningbeam-ui/beamdsp/src/error.rs @@ -40,8 +40,6 @@ pub enum ScriptError { ExecutionLimitExceeded, StackOverflow, StackUnderflow, - DivisionByZero, - IndexOutOfBounds { index: i32, len: usize }, InvalidOpcode(u8), } @@ -51,10 +49,6 @@ impl fmt::Display for ScriptError { ScriptError::ExecutionLimitExceeded => write!(f, "Execution limit exceeded (possible infinite loop)"), ScriptError::StackOverflow => write!(f, "Stack overflow"), ScriptError::StackUnderflow => write!(f, "Stack underflow"), - ScriptError::DivisionByZero => write!(f, "Division by zero"), - ScriptError::IndexOutOfBounds { index, len } => { - write!(f, "Index {} out of bounds (length {})", index, len) - } ScriptError::InvalidOpcode(op) => write!(f, "Invalid opcode: {}", op), } } diff --git a/lightningbeam-ui/beamdsp/src/vm.rs b/lightningbeam-ui/beamdsp/src/vm.rs index 925345c..084e1ea 100644 --- a/lightningbeam-ui/beamdsp/src/vm.rs +++ b/lightningbeam-ui/beamdsp/src/vm.rs @@ -39,24 +39,33 @@ impl Default for SampleSlot { } } -/// The BeamDSP virtual machine +/// Result of a single opcode step in VmCore +enum StepResult { + /// Opcode was handled, continue execution + Continue, + /// Hit a Halt instruction + Halt, + /// Opcode not handled by core — caller must handle it + Unhandled(OpCode), +} + +/// Shared VM state and opcode dispatch for arithmetic, logic, control flow, and math builtins. #[derive(Clone)] -pub struct ScriptVM { - pub bytecode: Vec, - pub constants_f32: Vec, - pub constants_i32: Vec, +struct VmCore { + bytecode: Vec, + constants_f32: Vec, + constants_i32: Vec, stack: Vec, sp: usize, locals: Vec, - pub params: Vec, - pub state_scalars: Vec, - pub state_arrays: Vec>, - pub sample_slots: Vec, + params: Vec, + state_scalars: Vec, + state_arrays: Vec>, instruction_limit: u64, } -impl ScriptVM { - pub fn new( +impl VmCore { + fn new( bytecode: Vec, constants_f32: Vec, constants_i32: Vec, @@ -64,7 +73,7 @@ impl ScriptVM { param_defaults: &[f32], num_state_scalars: usize, state_array_sizes: &[usize], - num_sample_slots: usize, + instruction_limit: u64, ) -> Self { let mut params = vec![0.0f32; num_params]; for (i, &d) in param_defaults.iter().enumerate() { @@ -72,7 +81,6 @@ impl ScriptVM { params[i] = d; } } - Self { bytecode, constants_f32, @@ -83,312 +91,209 @@ impl ScriptVM { params, state_scalars: vec![Value::default(); num_state_scalars], state_arrays: state_array_sizes.iter().map(|&sz| vec![0.0f32; sz]).collect(), - sample_slots: (0..num_sample_slots).map(|_| SampleSlot::default()).collect(), - instruction_limit: DEFAULT_INSTRUCTION_LIMIT, + instruction_limit, } } - /// Reset all state (scalars + arrays) to zero. Called on node reset. - pub fn reset_state(&mut self) { - for s in &mut self.state_scalars { - *s = Value::default(); - } - for arr in &mut self.state_arrays { - arr.fill(0.0); - } - } - - /// Execute the bytecode with the given I/O buffers - pub fn execute( - &mut self, - inputs: &[&[f32]], - outputs: &mut [&mut [f32]], - sample_rate: u32, - buffer_size: usize, - ) -> Result<(), ScriptError> { + /// Reset execution state (sp + locals) at the start of each execute() call. + fn reset_frame(&mut self) { self.sp = 0; - // Clear locals for l in &mut self.locals { *l = Value::default(); } + } - let mut pc: usize = 0; - let mut ic: u64 = 0; - let limit = self.instruction_limit; + /// Execute one opcode at `pc`. Returns the new `pc` and a `StepResult`. + /// Handles all opcodes shared between ScriptVM and DrawVM: + /// stack ops, locals, params, state, arrays, arithmetic, comparison, + /// logic, casts, control flow, and math builtins. + fn step(&mut self, pc: &mut usize) -> Result { + let op = self.bytecode[*pc]; + *pc += 1; - while pc < self.bytecode.len() { - ic += 1; - if ic > limit { - return Err(ScriptError::ExecutionLimitExceeded); + let Some(opcode) = OpCode::from_u8(op) else { + return Err(ScriptError::InvalidOpcode(op)); + }; + + match opcode { + OpCode::Halt => return Ok(StepResult::Halt), + + // Stack operations + OpCode::PushF32 => { + let idx = self.read_u16(pc) as usize; + self.push_f(self.constants_f32[idx])?; + } + OpCode::PushI32 => { + let idx = self.read_u16(pc) as usize; + self.push_i(self.constants_i32[idx])?; + } + OpCode::PushBool => { + let v = self.bytecode[*pc]; + *pc += 1; + self.push_b(v != 0)?; + } + OpCode::Pop => { self.pop()?; } + + // Locals + OpCode::LoadLocal => { + let idx = self.read_u16(pc) as usize; + self.push(self.locals[idx])?; + } + OpCode::StoreLocal => { + let idx = self.read_u16(pc) as usize; + self.locals[idx] = self.pop()?; } - let op = self.bytecode[pc]; - pc += 1; - - match OpCode::from_u8(op) { - Some(OpCode::Halt) => return Ok(()), - - Some(OpCode::PushF32) => { - let idx = self.read_u16(&mut pc) as usize; - let v = self.constants_f32[idx]; - self.push_f(v)?; - } - Some(OpCode::PushI32) => { - let idx = self.read_u16(&mut pc) as usize; - let v = self.constants_i32[idx]; - self.push_i(v)?; - } - Some(OpCode::PushBool) => { - let v = self.bytecode[pc]; - pc += 1; - self.push_b(v != 0)?; - } - Some(OpCode::Pop) => { - self.pop()?; - } - - // Locals - Some(OpCode::LoadLocal) => { - let idx = self.read_u16(&mut pc) as usize; - let v = self.locals[idx]; - self.push(v)?; - } - Some(OpCode::StoreLocal) => { - let idx = self.read_u16(&mut pc) as usize; - self.locals[idx] = self.pop()?; - } - - // Params - Some(OpCode::LoadParam) => { - let idx = self.read_u16(&mut pc) as usize; - let v = self.params[idx]; - self.push_f(v)?; - } - - // State scalars - Some(OpCode::LoadState) => { - let idx = self.read_u16(&mut pc) as usize; - let v = self.state_scalars[idx]; - self.push(v)?; - } - Some(OpCode::StoreState) => { - let idx = self.read_u16(&mut pc) as usize; - self.state_scalars[idx] = self.pop()?; - } - - // Input buffers - Some(OpCode::LoadInput) => { - let port = self.bytecode[pc] as usize; - pc += 1; - let idx = unsafe { self.pop()?.i } as usize; - let val = if port < inputs.len() && idx < inputs[port].len() { - inputs[port][idx] - } else { - 0.0 - }; - self.push_f(val)?; - } - - // Output buffers - Some(OpCode::StoreOutput) => { - let port = self.bytecode[pc] as usize; - pc += 1; - let val = unsafe { self.pop()?.f }; - let idx = unsafe { self.pop()?.i } as usize; - if port < outputs.len() && idx < outputs[port].len() { - outputs[port][idx] = val; - } - } - - // State arrays - Some(OpCode::LoadStateArray) => { - let arr_id = self.read_u16(&mut pc) as usize; - let idx = unsafe { self.pop()?.i }; - let val = if arr_id < self.state_arrays.len() { - let arr_len = self.state_arrays[arr_id].len(); - let idx = ((idx % arr_len as i32) + arr_len as i32) as usize % arr_len; - self.state_arrays[arr_id][idx] - } else { - 0.0 - }; - self.push_f(val)?; - } - Some(OpCode::StoreStateArray) => { - let arr_id = self.read_u16(&mut pc) as usize; - let val = unsafe { self.pop()?.f }; - let idx = unsafe { self.pop()?.i }; - if arr_id < self.state_arrays.len() { - let arr_len = self.state_arrays[arr_id].len(); - let idx = ((idx % arr_len as i32) + arr_len as i32) as usize % arr_len; - self.state_arrays[arr_id][idx] = val; - } - } - - // Sample access - Some(OpCode::SampleLen) => { - let slot = self.bytecode[pc] as usize; - pc += 1; - let len = if slot < self.sample_slots.len() { - self.sample_slots[slot].frame_count as i32 - } else { - 0 - }; - self.push_i(len)?; - } - Some(OpCode::SampleRead) => { - let slot = self.bytecode[pc] as usize; - pc += 1; - let idx = unsafe { self.pop()?.i } as usize; - let val = if slot < self.sample_slots.len() && idx < self.sample_slots[slot].data.len() { - self.sample_slots[slot].data[idx] - } else { - 0.0 - }; - self.push_f(val)?; - } - Some(OpCode::SampleRateOf) => { - let slot = self.bytecode[pc] as usize; - pc += 1; - let sr = if slot < self.sample_slots.len() { - self.sample_slots[slot].sample_rate as i32 - } else { - 0 - }; - self.push_i(sr)?; - } - - // Float arithmetic - Some(OpCode::AddF) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_f(a + b)?; } - Some(OpCode::SubF) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_f(a - b)?; } - Some(OpCode::MulF) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_f(a * b)?; } - Some(OpCode::DivF) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_f(if b.abs() > 1e-30 { a / b } else { 0.0 })?; } - Some(OpCode::ModF) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_f(if b.abs() > 1e-30 { a % b } else { 0.0 })?; } - Some(OpCode::NegF) => { let v = self.pop_f()?; self.push_f(-v)?; } - - // Int arithmetic - Some(OpCode::AddI) => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_i(a.wrapping_add(b))?; } - Some(OpCode::SubI) => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_i(a.wrapping_sub(b))?; } - Some(OpCode::MulI) => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_i(a.wrapping_mul(b))?; } - Some(OpCode::DivI) => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_i(if b != 0 { a / b } else { 0 })?; } - Some(OpCode::ModI) => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_i(if b != 0 { a % b } else { 0 })?; } - Some(OpCode::NegI) => { let v = self.pop_i()?; self.push_i(-v)?; } - - // Float comparison - Some(OpCode::EqF) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_b(a == b)?; } - Some(OpCode::NeF) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_b(a != b)?; } - Some(OpCode::LtF) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_b(a < b)?; } - Some(OpCode::GtF) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_b(a > b)?; } - Some(OpCode::LeF) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_b(a <= b)?; } - Some(OpCode::GeF) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_b(a >= b)?; } - - // Int comparison - Some(OpCode::EqI) => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_b(a == b)?; } - Some(OpCode::NeI) => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_b(a != b)?; } - Some(OpCode::LtI) => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_b(a < b)?; } - Some(OpCode::GtI) => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_b(a > b)?; } - Some(OpCode::LeI) => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_b(a <= b)?; } - Some(OpCode::GeI) => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_b(a >= b)?; } - - // Logical - Some(OpCode::And) => { let b = self.pop_b()?; let a = self.pop_b()?; self.push_b(a && b)?; } - Some(OpCode::Or) => { let b = self.pop_b()?; let a = self.pop_b()?; self.push_b(a || b)?; } - Some(OpCode::Not) => { let v = self.pop_b()?; self.push_b(!v)?; } - - // Casts - Some(OpCode::F32ToI32) => { let v = self.pop_f()?; self.push_i(v as i32)?; } - Some(OpCode::I32ToF32) => { let v = self.pop_i()?; self.push_f(v as f32)?; } - - // Control flow - Some(OpCode::Jump) => { - pc = self.read_u32(&mut pc) as usize; - } - Some(OpCode::JumpIfFalse) => { - let target = self.read_u32(&mut pc) as usize; - let cond = self.pop_b()?; - if !cond { - pc = target; - } - } - - // Math builtins - Some(OpCode::Sin) => { let v = self.pop_f()?; self.push_f(v.sin())?; } - Some(OpCode::Cos) => { let v = self.pop_f()?; self.push_f(v.cos())?; } - Some(OpCode::Tan) => { let v = self.pop_f()?; self.push_f(v.tan())?; } - Some(OpCode::Asin) => { let v = self.pop_f()?; self.push_f(v.asin())?; } - Some(OpCode::Acos) => { let v = self.pop_f()?; self.push_f(v.acos())?; } - Some(OpCode::Atan) => { let v = self.pop_f()?; self.push_f(v.atan())?; } - Some(OpCode::Atan2) => { let x = self.pop_f()?; let y = self.pop_f()?; self.push_f(y.atan2(x))?; } - Some(OpCode::Exp) => { let v = self.pop_f()?; self.push_f(v.exp())?; } - Some(OpCode::Log) => { let v = self.pop_f()?; self.push_f(v.ln())?; } - Some(OpCode::Log2) => { let v = self.pop_f()?; self.push_f(v.log2())?; } - Some(OpCode::Pow) => { let e = self.pop_f()?; let b = self.pop_f()?; self.push_f(b.powf(e))?; } - Some(OpCode::Sqrt) => { let v = self.pop_f()?; self.push_f(v.sqrt())?; } - Some(OpCode::Floor) => { let v = self.pop_f()?; self.push_f(v.floor())?; } - Some(OpCode::Ceil) => { let v = self.pop_f()?; self.push_f(v.ceil())?; } - Some(OpCode::Round) => { let v = self.pop_f()?; self.push_f(v.round())?; } - Some(OpCode::Trunc) => { let v = self.pop_f()?; self.push_f(v.trunc())?; } - Some(OpCode::Fract) => { let v = self.pop_f()?; self.push_f(v.fract())?; } - Some(OpCode::Abs) => { let v = self.pop_f()?; self.push_f(v.abs())?; } - Some(OpCode::Sign) => { let v = self.pop_f()?; self.push_f(v.signum())?; } - Some(OpCode::Clamp) => { - let hi = self.pop_f()?; - let lo = self.pop_f()?; - let v = self.pop_f()?; - self.push_f(v.clamp(lo, hi))?; - } - Some(OpCode::Min) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_f(a.min(b))?; } - Some(OpCode::Max) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_f(a.max(b))?; } - Some(OpCode::Mix) => { - let t = self.pop_f()?; - let b = self.pop_f()?; - let a = self.pop_f()?; - self.push_f(a + (b - a) * t)?; - } - Some(OpCode::Smoothstep) => { - let x = self.pop_f()?; - let e1 = self.pop_f()?; - let e0 = self.pop_f()?; - let t = ((x - e0) / (e1 - e0)).clamp(0.0, 1.0); - self.push_f(t * t * (3.0 - 2.0 * t))?; - } - Some(OpCode::IsNan) => { - let v = self.pop_f()?; - self.push_b(v.is_nan())?; - } - - // Array length - Some(OpCode::ArrayLen) => { - let arr_id = self.read_u16(&mut pc) as usize; - let len = if arr_id < self.state_arrays.len() { - self.state_arrays[arr_id].len() as i32 - } else { - 0 - }; - self.push_i(len)?; - } - - // Built-in constants - Some(OpCode::LoadSampleRate) => { - self.push_i(sample_rate as i32)?; - } - Some(OpCode::LoadBufferSize) => { - self.push_i(buffer_size as i32)?; - } - - // Draw/mouse opcodes are not valid in the audio ScriptVM - Some(OpCode::DrawFillCircle) | Some(OpCode::DrawStrokeCircle) | - Some(OpCode::DrawStrokeArc) | Some(OpCode::DrawLine) | - Some(OpCode::DrawFillRect) | Some(OpCode::DrawStrokeRect) | - Some(OpCode::MouseX) | Some(OpCode::MouseY) | Some(OpCode::MouseDown) | - Some(OpCode::StoreParam) => { - return Err(ScriptError::InvalidOpcode(op)); - } - - None => return Err(ScriptError::InvalidOpcode(op)), + // Params (read) + OpCode::LoadParam => { + let idx = self.read_u16(pc) as usize; + self.push_f(self.params[idx])?; } + + // State scalars + OpCode::LoadState => { + let idx = self.read_u16(pc) as usize; + self.push(self.state_scalars[idx])?; + } + OpCode::StoreState => { + let idx = self.read_u16(pc) as usize; + self.state_scalars[idx] = self.pop()?; + } + + // State arrays + OpCode::LoadStateArray => { + let arr_id = self.read_u16(pc) as usize; + let idx = unsafe { self.pop()?.i }; + let val = if arr_id < self.state_arrays.len() { + let arr_len = self.state_arrays[arr_id].len(); + let idx = ((idx % arr_len as i32) + arr_len as i32) as usize % arr_len; + self.state_arrays[arr_id][idx] + } else { + 0.0 + }; + self.push_f(val)?; + } + OpCode::StoreStateArray => { + let arr_id = self.read_u16(pc) as usize; + let val = unsafe { self.pop()?.f }; + let idx = unsafe { self.pop()?.i }; + if arr_id < self.state_arrays.len() { + let arr_len = self.state_arrays[arr_id].len(); + let idx = ((idx % arr_len as i32) + arr_len as i32) as usize % arr_len; + self.state_arrays[arr_id][idx] = val; + } + } + OpCode::ArrayLen => { + let arr_id = self.read_u16(pc) as usize; + let len = if arr_id < self.state_arrays.len() { + self.state_arrays[arr_id].len() as i32 + } else { + 0 + }; + self.push_i(len)?; + } + + // Float arithmetic + OpCode::AddF => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_f(a + b)?; } + OpCode::SubF => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_f(a - b)?; } + OpCode::MulF => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_f(a * b)?; } + OpCode::DivF => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_f(if b.abs() > 1e-30 { a / b } else { 0.0 })?; } + OpCode::ModF => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_f(if b.abs() > 1e-30 { a % b } else { 0.0 })?; } + OpCode::NegF => { let v = self.pop_f()?; self.push_f(-v)?; } + + // Int arithmetic + OpCode::AddI => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_i(a.wrapping_add(b))?; } + OpCode::SubI => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_i(a.wrapping_sub(b))?; } + OpCode::MulI => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_i(a.wrapping_mul(b))?; } + OpCode::DivI => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_i(if b != 0 { a / b } else { 0 })?; } + OpCode::ModI => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_i(if b != 0 { a % b } else { 0 })?; } + OpCode::NegI => { let v = self.pop_i()?; self.push_i(-v)?; } + + // Float comparison + OpCode::EqF => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_b(a == b)?; } + OpCode::NeF => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_b(a != b)?; } + OpCode::LtF => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_b(a < b)?; } + OpCode::GtF => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_b(a > b)?; } + OpCode::LeF => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_b(a <= b)?; } + OpCode::GeF => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_b(a >= b)?; } + + // Int comparison + OpCode::EqI => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_b(a == b)?; } + OpCode::NeI => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_b(a != b)?; } + OpCode::LtI => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_b(a < b)?; } + OpCode::GtI => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_b(a > b)?; } + OpCode::LeI => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_b(a <= b)?; } + OpCode::GeI => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_b(a >= b)?; } + + // Logical + OpCode::And => { let b = self.pop_b()?; let a = self.pop_b()?; self.push_b(a && b)?; } + OpCode::Or => { let b = self.pop_b()?; let a = self.pop_b()?; self.push_b(a || b)?; } + OpCode::Not => { let v = self.pop_b()?; self.push_b(!v)?; } + + // Casts + OpCode::F32ToI32 => { let v = self.pop_f()?; self.push_i(v as i32)?; } + OpCode::I32ToF32 => { let v = self.pop_i()?; self.push_f(v as f32)?; } + + // Control flow + OpCode::Jump => { + *pc = self.read_u32(pc) as usize; + } + OpCode::JumpIfFalse => { + let target = self.read_u32(pc) as usize; + let cond = self.pop_b()?; + if !cond { + *pc = target; + } + } + + // Math builtins + OpCode::Sin => { let v = self.pop_f()?; self.push_f(v.sin())?; } + OpCode::Cos => { let v = self.pop_f()?; self.push_f(v.cos())?; } + OpCode::Tan => { let v = self.pop_f()?; self.push_f(v.tan())?; } + OpCode::Asin => { let v = self.pop_f()?; self.push_f(v.asin())?; } + OpCode::Acos => { let v = self.pop_f()?; self.push_f(v.acos())?; } + OpCode::Atan => { let v = self.pop_f()?; self.push_f(v.atan())?; } + OpCode::Atan2 => { let x = self.pop_f()?; let y = self.pop_f()?; self.push_f(y.atan2(x))?; } + OpCode::Exp => { let v = self.pop_f()?; self.push_f(v.exp())?; } + OpCode::Log => { let v = self.pop_f()?; self.push_f(v.ln())?; } + OpCode::Log2 => { let v = self.pop_f()?; self.push_f(v.log2())?; } + OpCode::Pow => { let e = self.pop_f()?; let b = self.pop_f()?; self.push_f(b.powf(e))?; } + OpCode::Sqrt => { let v = self.pop_f()?; self.push_f(v.sqrt())?; } + OpCode::Floor => { let v = self.pop_f()?; self.push_f(v.floor())?; } + OpCode::Ceil => { let v = self.pop_f()?; self.push_f(v.ceil())?; } + OpCode::Round => { let v = self.pop_f()?; self.push_f(v.round())?; } + OpCode::Trunc => { let v = self.pop_f()?; self.push_f(v.trunc())?; } + OpCode::Fract => { let v = self.pop_f()?; self.push_f(v.fract())?; } + OpCode::Abs => { let v = self.pop_f()?; self.push_f(v.abs())?; } + OpCode::Sign => { let v = self.pop_f()?; self.push_f(v.signum())?; } + OpCode::Clamp => { + let hi = self.pop_f()?; + let lo = self.pop_f()?; + let v = self.pop_f()?; + self.push_f(v.clamp(lo, hi))?; + } + OpCode::Min => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_f(a.min(b))?; } + OpCode::Max => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_f(a.max(b))?; } + OpCode::Mix => { + let t = self.pop_f()?; + let b = self.pop_f()?; + let a = self.pop_f()?; + self.push_f(a + (b - a) * t)?; + } + OpCode::Smoothstep => { + let x = self.pop_f()?; + let e1 = self.pop_f()?; + let e0 = self.pop_f()?; + let t = ((x - e0) / (e1 - e0)).clamp(0.0, 1.0); + self.push_f(t * t * (3.0 - 2.0 * t))?; + } + OpCode::IsNan => { let v = self.pop_f()?; self.push_b(v.is_nan())?; } + + // VM-specific opcodes — caller must handle + other => return Ok(StepResult::Unhandled(other)), } - Ok(()) + Ok(StepResult::Continue) } // Stack helpers @@ -403,19 +308,11 @@ impl ScriptVM { } #[inline] - fn push_f(&mut self, v: f32) -> Result<(), ScriptError> { - self.push(Value { f: v }) - } - + fn push_f(&mut self, v: f32) -> Result<(), ScriptError> { self.push(Value { f: v }) } #[inline] - fn push_i(&mut self, v: i32) -> Result<(), ScriptError> { - self.push(Value { i: v }) - } - + fn push_i(&mut self, v: i32) -> Result<(), ScriptError> { self.push(Value { i: v }) } #[inline] - fn push_b(&mut self, v: bool) -> Result<(), ScriptError> { - self.push(Value { b: v }) - } + fn push_b(&mut self, v: bool) -> Result<(), ScriptError> { self.push(Value { b: v }) } #[inline] fn pop(&mut self) -> Result { @@ -427,19 +324,11 @@ impl ScriptVM { } #[inline] - fn pop_f(&mut self) -> Result { - Ok(unsafe { self.pop()?.f }) - } - + fn pop_f(&mut self) -> Result { Ok(unsafe { self.pop()?.f }) } #[inline] - fn pop_i(&mut self) -> Result { - Ok(unsafe { self.pop()?.i }) - } - + fn pop_i(&mut self) -> Result { Ok(unsafe { self.pop()?.i }) } #[inline] - fn pop_b(&mut self) -> Result { - Ok(unsafe { self.pop()?.b }) - } + fn pop_b(&mut self) -> Result { Ok(unsafe { self.pop()?.b }) } #[inline] fn read_u16(&self, pc: &mut usize) -> u16 { @@ -459,6 +348,159 @@ impl ScriptVM { } } +// ---- ScriptVM (runs on audio thread) ---- + +/// The BeamDSP virtual machine +#[derive(Clone)] +pub struct ScriptVM { + core: VmCore, + pub sample_slots: Vec, +} + +impl ScriptVM { + pub fn new( + bytecode: Vec, + constants_f32: Vec, + constants_i32: Vec, + num_params: usize, + param_defaults: &[f32], + num_state_scalars: usize, + state_array_sizes: &[usize], + num_sample_slots: usize, + ) -> Self { + Self { + core: VmCore::new( + bytecode, constants_f32, constants_i32, + num_params, param_defaults, num_state_scalars, state_array_sizes, + DEFAULT_INSTRUCTION_LIMIT, + ), + sample_slots: (0..num_sample_slots).map(|_| SampleSlot::default()).collect(), + } + } + + /// Access params for reading + pub fn params(&self) -> &[f32] { + &self.core.params + } + + /// Access params mutably (backend sets values from parameter changes) + pub fn params_mut(&mut self) -> &mut Vec { + &mut self.core.params + } + + /// Reset all state (scalars + arrays) to zero. Called on node reset. + pub fn reset_state(&mut self) { + for s in &mut self.core.state_scalars { + *s = Value::default(); + } + for arr in &mut self.core.state_arrays { + arr.fill(0.0); + } + } + + /// Execute the bytecode with the given I/O buffers + pub fn execute( + &mut self, + inputs: &[&[f32]], + outputs: &mut [&mut [f32]], + sample_rate: u32, + buffer_size: usize, + ) -> Result<(), ScriptError> { + self.core.reset_frame(); + + let mut pc: usize = 0; + let mut ic: u64 = 0; + let limit = self.core.instruction_limit; + + while pc < self.core.bytecode.len() { + ic += 1; + if ic > limit { + return Err(ScriptError::ExecutionLimitExceeded); + } + + match self.core.step(&mut pc)? { + StepResult::Continue => {} + StepResult::Halt => return Ok(()), + StepResult::Unhandled(opcode) => { + match opcode { + // Input buffers + OpCode::LoadInput => { + let port = self.core.bytecode[pc] as usize; + pc += 1; + let idx = unsafe { self.core.pop()?.i } as usize; + let val = if port < inputs.len() && idx < inputs[port].len() { + inputs[port][idx] + } else { + 0.0 + }; + self.core.push_f(val)?; + } + + // Output buffers + OpCode::StoreOutput => { + let port = self.core.bytecode[pc] as usize; + pc += 1; + let val = unsafe { self.core.pop()?.f }; + let idx = unsafe { self.core.pop()?.i } as usize; + if port < outputs.len() && idx < outputs[port].len() { + outputs[port][idx] = val; + } + } + + // Sample access + OpCode::SampleLen => { + let slot = self.core.bytecode[pc] as usize; + pc += 1; + let len = if slot < self.sample_slots.len() { + self.sample_slots[slot].frame_count as i32 + } else { + 0 + }; + self.core.push_i(len)?; + } + OpCode::SampleRead => { + let slot = self.core.bytecode[pc] as usize; + pc += 1; + let idx = unsafe { self.core.pop()?.i } as usize; + let val = if slot < self.sample_slots.len() && idx < self.sample_slots[slot].data.len() { + self.sample_slots[slot].data[idx] + } else { + 0.0 + }; + self.core.push_f(val)?; + } + OpCode::SampleRateOf => { + let slot = self.core.bytecode[pc] as usize; + pc += 1; + let sr = if slot < self.sample_slots.len() { + self.sample_slots[slot].sample_rate as i32 + } else { + 0 + }; + self.core.push_i(sr)?; + } + + // Built-in constants + OpCode::LoadSampleRate => { + self.core.push_i(sample_rate as i32)?; + } + OpCode::LoadBufferSize => { + self.core.push_i(buffer_size as i32)?; + } + + // Draw/mouse opcodes are not valid in the audio ScriptVM + _ => { + return Err(ScriptError::InvalidOpcode(opcode as u8)); + } + } + } + } + } + + Ok(()) + } +} + // ---- Draw VM (runs on UI thread, produces draw commands) ---- /// A draw command produced by the draw block @@ -483,18 +525,9 @@ pub struct MouseState { /// Lightweight VM for executing draw bytecode on the UI thread #[derive(Clone)] pub struct DrawVM { - pub bytecode: Vec, - pub constants_f32: Vec, - pub constants_i32: Vec, - stack: Vec, - sp: usize, - locals: Vec, - pub params: Vec, - pub state_scalars: Vec, - pub state_arrays: Vec>, + core: VmCore, pub draw_commands: Vec, pub mouse: MouseState, - instruction_limit: u64, } impl DrawVM { @@ -507,342 +540,135 @@ impl DrawVM { num_state_scalars: usize, state_array_sizes: &[usize], ) -> Self { - let mut params = vec![0.0f32; num_params]; - for (i, &d) in param_defaults.iter().enumerate() { - if i < params.len() { - params[i] = d; - } - } Self { - bytecode, - constants_f32, - constants_i32, - stack: vec![Value::default(); STACK_SIZE], - sp: 0, - locals: vec![Value::default(); MAX_LOCALS], - params, - state_scalars: vec![Value::default(); num_state_scalars], - state_arrays: state_array_sizes.iter().map(|&sz| vec![0.0f32; sz]).collect(), + core: VmCore::new( + bytecode, constants_f32, constants_i32, + num_params, param_defaults, num_state_scalars, state_array_sizes, + 1_000_000, // lower limit for draw (runs per frame) + ), draw_commands: Vec::new(), mouse: MouseState::default(), - instruction_limit: 1_000_000, // lower limit for draw (runs per frame) } } + /// Access params for reading/writing from the editor + pub fn params(&self) -> &[f32] { + &self.core.params + } + + /// Access params mutably (editor sets values from node inputs each frame) + pub fn params_mut(&mut self) -> &mut Vec { + &mut self.core.params + } + + /// Check if bytecode is non-empty + pub fn has_bytecode(&self) -> bool { + !self.core.bytecode.is_empty() + } + /// Execute the draw bytecode. Call once per frame. /// Draw commands accumulate in `self.draw_commands` (cleared at start). pub fn execute(&mut self) -> Result<(), ScriptError> { - self.sp = 0; + self.core.reset_frame(); self.draw_commands.clear(); - for l in &mut self.locals { - *l = Value::default(); - } let mut pc: usize = 0; let mut ic: u64 = 0; - let limit = self.instruction_limit; + let limit = self.core.instruction_limit; - while pc < self.bytecode.len() { + while pc < self.core.bytecode.len() { ic += 1; if ic > limit { return Err(ScriptError::ExecutionLimitExceeded); } - let op = self.bytecode[pc]; - pc += 1; + match self.core.step(&mut pc)? { + StepResult::Continue => {} + StepResult::Halt => return Ok(()), + StepResult::Unhandled(opcode) => { + match opcode { + // Draw commands + OpCode::DrawFillCircle => { + let color = self.core.pop_i()? as u32; + let r = self.core.pop_f()?; + let cy = self.core.pop_f()?; + let cx = self.core.pop_f()?; + self.draw_commands.push(DrawCommand::FillCircle { cx, cy, r, color }); + } + OpCode::DrawStrokeCircle => { + let width = self.core.pop_f()?; + let color = self.core.pop_i()? as u32; + let r = self.core.pop_f()?; + let cy = self.core.pop_f()?; + let cx = self.core.pop_f()?; + self.draw_commands.push(DrawCommand::StrokeCircle { cx, cy, r, color, width }); + } + OpCode::DrawStrokeArc => { + let width = self.core.pop_f()?; + let color = self.core.pop_i()? as u32; + let end_deg = self.core.pop_f()?; + let start_deg = self.core.pop_f()?; + let r = self.core.pop_f()?; + let cy = self.core.pop_f()?; + let cx = self.core.pop_f()?; + self.draw_commands.push(DrawCommand::StrokeArc { cx, cy, r, start_deg, end_deg, color, width }); + } + OpCode::DrawLine => { + let width = self.core.pop_f()?; + let color = self.core.pop_i()? as u32; + let y2 = self.core.pop_f()?; + let x2 = self.core.pop_f()?; + let y1 = self.core.pop_f()?; + let x1 = self.core.pop_f()?; + self.draw_commands.push(DrawCommand::Line { x1, y1, x2, y2, color, width }); + } + OpCode::DrawFillRect => { + let color = self.core.pop_i()? as u32; + let h = self.core.pop_f()?; + let w = self.core.pop_f()?; + let y = self.core.pop_f()?; + let x = self.core.pop_f()?; + self.draw_commands.push(DrawCommand::FillRect { x, y, w, h, color }); + } + OpCode::DrawStrokeRect => { + let width = self.core.pop_f()?; + let color = self.core.pop_i()? as u32; + let h = self.core.pop_f()?; + let w = self.core.pop_f()?; + let y = self.core.pop_f()?; + let x = self.core.pop_f()?; + self.draw_commands.push(DrawCommand::StrokeRect { x, y, w, h, color, width }); + } - match OpCode::from_u8(op) { - Some(OpCode::Halt) => return Ok(()), + // Mouse input + OpCode::MouseX => { self.core.push_f(self.mouse.x)?; } + OpCode::MouseY => { self.core.push_f(self.mouse.y)?; } + OpCode::MouseDown => { self.core.push_f(if self.mouse.down { 1.0 } else { 0.0 })?; } - Some(OpCode::PushF32) => { - let idx = self.read_u16(&mut pc) as usize; - self.push_f(self.constants_f32[idx])?; - } - Some(OpCode::PushI32) => { - let idx = self.read_u16(&mut pc) as usize; - self.push_i(self.constants_i32[idx])?; - } - Some(OpCode::PushBool) => { - let v = self.bytecode[pc]; - pc += 1; - self.push_b(v != 0)?; - } - Some(OpCode::Pop) => { self.pop()?; } + // Param write + OpCode::StoreParam => { + let idx = self.core.read_u16(&mut pc) as usize; + let val = self.core.pop_f()?; + if idx < self.core.params.len() { + self.core.params[idx] = val; + } + } - Some(OpCode::LoadLocal) => { - let idx = self.read_u16(&mut pc) as usize; - self.push(self.locals[idx])?; - } - Some(OpCode::StoreLocal) => { - let idx = self.read_u16(&mut pc) as usize; - self.locals[idx] = self.pop()?; - } - Some(OpCode::LoadParam) => { - let idx = self.read_u16(&mut pc) as usize; - self.push_f(self.params[idx])?; - } - Some(OpCode::LoadState) => { - let idx = self.read_u16(&mut pc) as usize; - self.push(self.state_scalars[idx])?; - } - Some(OpCode::StoreState) => { - let idx = self.read_u16(&mut pc) as usize; - self.state_scalars[idx] = self.pop()?; - } - Some(OpCode::LoadStateArray) => { - let arr_id = self.read_u16(&mut pc) as usize; - let idx = unsafe { self.pop()?.i }; - let val = if arr_id < self.state_arrays.len() { - let arr_len = self.state_arrays[arr_id].len(); - let idx = ((idx % arr_len as i32) + arr_len as i32) as usize % arr_len; - self.state_arrays[arr_id][idx] - } else { - 0.0 - }; - self.push_f(val)?; - } - Some(OpCode::StoreStateArray) => { - let arr_id = self.read_u16(&mut pc) as usize; - let val = unsafe { self.pop()?.f }; - let idx = unsafe { self.pop()?.i }; - if arr_id < self.state_arrays.len() { - let arr_len = self.state_arrays[arr_id].len(); - let idx = ((idx % arr_len as i32) + arr_len as i32) as usize % arr_len; - self.state_arrays[arr_id][idx] = val; + // Sample access not available in draw context + OpCode::SampleLen | OpCode::SampleRead | OpCode::SampleRateOf => { + pc += 1; // skip slot byte + self.core.push_i(0)?; + } + + // Audio I/O not available in draw context + _ => { + return Err(ScriptError::InvalidOpcode(opcode as u8)); + } } } - Some(OpCode::ArrayLen) => { - let arr_id = self.read_u16(&mut pc) as usize; - let len = if arr_id < self.state_arrays.len() { - self.state_arrays[arr_id].len() as i32 - } else { - 0 - }; - self.push_i(len)?; - } - - // Audio I/O not available in draw context - Some(OpCode::LoadInput) | Some(OpCode::StoreOutput) => { - return Err(ScriptError::InvalidOpcode(op)); - } - Some(OpCode::LoadSampleRate) | Some(OpCode::LoadBufferSize) => { - return Err(ScriptError::InvalidOpcode(op)); - } - // Sample access not available in draw context - Some(OpCode::SampleLen) | Some(OpCode::SampleRead) | Some(OpCode::SampleRateOf) => { - pc += 1; // skip slot byte - self.push_i(0)?; - } - - // Float arithmetic - Some(OpCode::AddF) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_f(a + b)?; } - Some(OpCode::SubF) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_f(a - b)?; } - Some(OpCode::MulF) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_f(a * b)?; } - Some(OpCode::DivF) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_f(if b.abs() > 1e-30 { a / b } else { 0.0 })?; } - Some(OpCode::ModF) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_f(if b.abs() > 1e-30 { a % b } else { 0.0 })?; } - Some(OpCode::NegF) => { let v = self.pop_f()?; self.push_f(-v)?; } - - // Int arithmetic - Some(OpCode::AddI) => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_i(a.wrapping_add(b))?; } - Some(OpCode::SubI) => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_i(a.wrapping_sub(b))?; } - Some(OpCode::MulI) => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_i(a.wrapping_mul(b))?; } - Some(OpCode::DivI) => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_i(if b != 0 { a / b } else { 0 })?; } - Some(OpCode::ModI) => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_i(if b != 0 { a % b } else { 0 })?; } - Some(OpCode::NegI) => { let v = self.pop_i()?; self.push_i(-v)?; } - - // Float comparison - Some(OpCode::EqF) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_b(a == b)?; } - Some(OpCode::NeF) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_b(a != b)?; } - Some(OpCode::LtF) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_b(a < b)?; } - Some(OpCode::GtF) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_b(a > b)?; } - Some(OpCode::LeF) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_b(a <= b)?; } - Some(OpCode::GeF) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_b(a >= b)?; } - - // Int comparison - Some(OpCode::EqI) => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_b(a == b)?; } - Some(OpCode::NeI) => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_b(a != b)?; } - Some(OpCode::LtI) => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_b(a < b)?; } - Some(OpCode::GtI) => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_b(a > b)?; } - Some(OpCode::LeI) => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_b(a <= b)?; } - Some(OpCode::GeI) => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_b(a >= b)?; } - - // Logical - Some(OpCode::And) => { let b = self.pop_b()?; let a = self.pop_b()?; self.push_b(a && b)?; } - Some(OpCode::Or) => { let b = self.pop_b()?; let a = self.pop_b()?; self.push_b(a || b)?; } - Some(OpCode::Not) => { let v = self.pop_b()?; self.push_b(!v)?; } - - // Casts - Some(OpCode::F32ToI32) => { let v = self.pop_f()?; self.push_i(v as i32)?; } - Some(OpCode::I32ToF32) => { let v = self.pop_i()?; self.push_f(v as f32)?; } - - // Control flow - Some(OpCode::Jump) => { - pc = self.read_u32(&mut pc) as usize; - } - Some(OpCode::JumpIfFalse) => { - let target = self.read_u32(&mut pc) as usize; - let cond = self.pop_b()?; - if !cond { - pc = target; - } - } - - // Math builtins - Some(OpCode::Sin) => { let v = self.pop_f()?; self.push_f(v.sin())?; } - Some(OpCode::Cos) => { let v = self.pop_f()?; self.push_f(v.cos())?; } - Some(OpCode::Tan) => { let v = self.pop_f()?; self.push_f(v.tan())?; } - Some(OpCode::Asin) => { let v = self.pop_f()?; self.push_f(v.asin())?; } - Some(OpCode::Acos) => { let v = self.pop_f()?; self.push_f(v.acos())?; } - Some(OpCode::Atan) => { let v = self.pop_f()?; self.push_f(v.atan())?; } - Some(OpCode::Atan2) => { let x = self.pop_f()?; let y = self.pop_f()?; self.push_f(y.atan2(x))?; } - Some(OpCode::Exp) => { let v = self.pop_f()?; self.push_f(v.exp())?; } - Some(OpCode::Log) => { let v = self.pop_f()?; self.push_f(v.ln())?; } - Some(OpCode::Log2) => { let v = self.pop_f()?; self.push_f(v.log2())?; } - Some(OpCode::Pow) => { let e = self.pop_f()?; let b = self.pop_f()?; self.push_f(b.powf(e))?; } - Some(OpCode::Sqrt) => { let v = self.pop_f()?; self.push_f(v.sqrt())?; } - Some(OpCode::Floor) => { let v = self.pop_f()?; self.push_f(v.floor())?; } - Some(OpCode::Ceil) => { let v = self.pop_f()?; self.push_f(v.ceil())?; } - Some(OpCode::Round) => { let v = self.pop_f()?; self.push_f(v.round())?; } - Some(OpCode::Trunc) => { let v = self.pop_f()?; self.push_f(v.trunc())?; } - Some(OpCode::Fract) => { let v = self.pop_f()?; self.push_f(v.fract())?; } - Some(OpCode::Abs) => { let v = self.pop_f()?; self.push_f(v.abs())?; } - Some(OpCode::Sign) => { let v = self.pop_f()?; self.push_f(v.signum())?; } - Some(OpCode::Clamp) => { - let hi = self.pop_f()?; - let lo = self.pop_f()?; - let v = self.pop_f()?; - self.push_f(v.clamp(lo, hi))?; - } - Some(OpCode::Min) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_f(a.min(b))?; } - Some(OpCode::Max) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_f(a.max(b))?; } - Some(OpCode::Mix) => { - let t = self.pop_f()?; - let b = self.pop_f()?; - let a = self.pop_f()?; - self.push_f(a + (b - a) * t)?; - } - Some(OpCode::Smoothstep) => { - let x = self.pop_f()?; - let e1 = self.pop_f()?; - let e0 = self.pop_f()?; - let t = ((x - e0) / (e1 - e0)).clamp(0.0, 1.0); - self.push_f(t * t * (3.0 - 2.0 * t))?; - } - Some(OpCode::IsNan) => { let v = self.pop_f()?; self.push_b(v.is_nan())?; } - - // Draw commands - Some(OpCode::DrawFillCircle) => { - let color = self.pop_i()? as u32; - let r = self.pop_f()?; - let cy = self.pop_f()?; - let cx = self.pop_f()?; - self.draw_commands.push(DrawCommand::FillCircle { cx, cy, r, color }); - } - Some(OpCode::DrawStrokeCircle) => { - let width = self.pop_f()?; - let color = self.pop_i()? as u32; - let r = self.pop_f()?; - let cy = self.pop_f()?; - let cx = self.pop_f()?; - self.draw_commands.push(DrawCommand::StrokeCircle { cx, cy, r, color, width }); - } - Some(OpCode::DrawStrokeArc) => { - let width = self.pop_f()?; - let color = self.pop_i()? as u32; - let end_deg = self.pop_f()?; - let start_deg = self.pop_f()?; - let r = self.pop_f()?; - let cy = self.pop_f()?; - let cx = self.pop_f()?; - self.draw_commands.push(DrawCommand::StrokeArc { cx, cy, r, start_deg, end_deg, color, width }); - } - Some(OpCode::DrawLine) => { - let width = self.pop_f()?; - let color = self.pop_i()? as u32; - let y2 = self.pop_f()?; - let x2 = self.pop_f()?; - let y1 = self.pop_f()?; - let x1 = self.pop_f()?; - self.draw_commands.push(DrawCommand::Line { x1, y1, x2, y2, color, width }); - } - Some(OpCode::DrawFillRect) => { - let color = self.pop_i()? as u32; - let h = self.pop_f()?; - let w = self.pop_f()?; - let y = self.pop_f()?; - let x = self.pop_f()?; - self.draw_commands.push(DrawCommand::FillRect { x, y, w, h, color }); - } - Some(OpCode::DrawStrokeRect) => { - let width = self.pop_f()?; - let color = self.pop_i()? as u32; - let h = self.pop_f()?; - let w = self.pop_f()?; - let y = self.pop_f()?; - let x = self.pop_f()?; - self.draw_commands.push(DrawCommand::StrokeRect { x, y, w, h, color, width }); - } - - // Mouse input - Some(OpCode::MouseX) => { self.push_f(self.mouse.x)?; } - Some(OpCode::MouseY) => { self.push_f(self.mouse.y)?; } - Some(OpCode::MouseDown) => { self.push_f(if self.mouse.down { 1.0 } else { 0.0 })?; } - - // Param write - Some(OpCode::StoreParam) => { - let idx = self.read_u16(&mut pc) as usize; - let val = self.pop_f()?; - if idx < self.params.len() { - self.params[idx] = val; - } - } - - None => return Err(ScriptError::InvalidOpcode(op)), } } Ok(()) } - - // Stack helpers (identical to ScriptVM) - #[inline] - fn push(&mut self, v: Value) -> Result<(), ScriptError> { - if self.sp >= STACK_SIZE { return Err(ScriptError::StackOverflow); } - self.stack[self.sp] = v; - self.sp += 1; - Ok(()) - } - #[inline] - fn push_f(&mut self, v: f32) -> Result<(), ScriptError> { self.push(Value { f: v }) } - #[inline] - fn push_i(&mut self, v: i32) -> Result<(), ScriptError> { self.push(Value { i: v }) } - #[inline] - fn push_b(&mut self, v: bool) -> Result<(), ScriptError> { self.push(Value { b: v }) } - #[inline] - fn pop(&mut self) -> Result { - if self.sp == 0 { return Err(ScriptError::StackUnderflow); } - self.sp -= 1; - Ok(self.stack[self.sp]) - } - #[inline] - fn pop_f(&mut self) -> Result { Ok(unsafe { self.pop()?.f }) } - #[inline] - fn pop_i(&mut self) -> Result { Ok(unsafe { self.pop()?.i }) } - #[inline] - fn pop_b(&mut self) -> Result { Ok(unsafe { self.pop()?.b }) } - #[inline] - fn read_u16(&self, pc: &mut usize) -> u16 { - let v = u16::from_le_bytes([self.bytecode[*pc], self.bytecode[*pc + 1]]); - *pc += 2; - v - } - #[inline] - fn read_u32(&self, pc: &mut usize) -> u32 { - let v = u32::from_le_bytes([ - self.bytecode[*pc], self.bytecode[*pc + 1], - self.bytecode[*pc + 2], self.bytecode[*pc + 3], - ]); - *pc += 4; - v - } } diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/actions.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/actions.rs index 1206cfc..f26fa26 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/actions.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/actions.rs @@ -177,8 +177,7 @@ impl AddNodeAction { .get(&self.layer_id) .ok_or("Track not found")?; - let BackendNodeId::Audio(node_idx) = backend_id; - controller.graph_remove_node(*track_id, node_idx.index() as u32); + controller.graph_remove_node(*track_id, backend_id.index()); } Ok(()) @@ -231,8 +230,7 @@ impl RemoveNodeAction { .get(&self.layer_id) .ok_or("Track not found")?; - let BackendNodeId::Audio(node_idx) = self.backend_node_id; - controller.graph_remove_node(*track_id, node_idx.index() as u32); + controller.graph_remove_node(*track_id, self.backend_node_id.index()); Ok(()) } @@ -341,14 +339,11 @@ impl ConnectAction { .get(&self.layer_id) .ok_or("Track not found")?; - let BackendNodeId::Audio(from_idx) = self.from_node; - let BackendNodeId::Audio(to_idx) = self.to_node; - controller.graph_connect( *track_id, - from_idx.index() as u32, + self.from_node.index(), self.from_port, - to_idx.index() as u32, + self.to_node.index(), self.to_port, ); @@ -370,14 +365,11 @@ impl ConnectAction { .get(&self.layer_id) .ok_or("Track not found")?; - let BackendNodeId::Audio(from_idx) = self.from_node; - let BackendNodeId::Audio(to_idx) = self.to_node; - controller.graph_disconnect( *track_id, - from_idx.index() as u32, + self.from_node.index(), self.from_port, - to_idx.index() as u32, + self.to_node.index(), self.to_port, ); @@ -433,14 +425,11 @@ impl DisconnectAction { .get(&self.layer_id) .ok_or("Track not found")?; - let BackendNodeId::Audio(from_idx) = self.from_node; - let BackendNodeId::Audio(to_idx) = self.to_node; - controller.graph_disconnect( *track_id, - from_idx.index() as u32, + self.from_node.index(), self.from_port, - to_idx.index() as u32, + self.to_node.index(), self.to_port, ); @@ -463,14 +452,11 @@ impl DisconnectAction { .get(&self.layer_id) .ok_or("Track not found")?; - let BackendNodeId::Audio(from_idx) = self.from_node; - let BackendNodeId::Audio(to_idx) = self.to_node; - controller.graph_connect( *track_id, - from_idx.index() as u32, + self.from_node.index(), self.from_port, - to_idx.index() as u32, + self.to_node.index(), self.to_port, ); @@ -522,14 +508,9 @@ impl SetParameterAction { .get(&self.layer_id) .ok_or("Track not found")?; - let BackendNodeId::Audio(node_idx) = self.backend_node_id; - - eprintln!("[DEBUG] Setting parameter: track {} node {} param {} = {}", - track_id, node_idx.index(), self.param_id, self.new_value); - controller.graph_set_parameter( *track_id, - node_idx.index() as u32, + self.backend_node_id.index(), self.param_id, self.new_value as f32, ); @@ -553,11 +534,9 @@ impl SetParameterAction { .get(&self.layer_id) .ok_or("Track not found")?; - let BackendNodeId::Audio(node_idx) = self.backend_node_id; - controller.graph_set_parameter( *track_id, - node_idx.index() as u32, + self.backend_node_id.index(), self.param_id, old_value as f32, ); diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/audio_backend.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/audio_backend.rs index 4a9b587..73d1571 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/audio_backend.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/audio_backend.rs @@ -52,12 +52,10 @@ impl GraphBackend for AudioGraphBackend { } fn remove_node(&mut self, backend_id: BackendNodeId) -> Result<(), String> { - let BackendNodeId::Audio(node_idx) = backend_id; - let mut controller = self.audio_controller.lock().unwrap(); - controller.graph_remove_node(self.track_id, node_idx.index() as u32); + controller.graph_remove_node(self.track_id, backend_id.index()); - self._node_index_to_stable.remove(&node_idx); + self._node_index_to_stable.remove(&NodeIndex::new(backend_id.index() as usize)); Ok(()) } @@ -69,15 +67,12 @@ impl GraphBackend for AudioGraphBackend { input_node: BackendNodeId, input_port: usize, ) -> Result<(), String> { - let BackendNodeId::Audio(from_idx) = output_node; - let BackendNodeId::Audio(to_idx) = input_node; - let mut controller = self.audio_controller.lock().unwrap(); controller.graph_connect( self.track_id, - from_idx.index() as u32, + output_node.index(), output_port, - to_idx.index() as u32, + input_node.index(), input_port, ); @@ -91,15 +86,12 @@ impl GraphBackend for AudioGraphBackend { input_node: BackendNodeId, input_port: usize, ) -> Result<(), String> { - let BackendNodeId::Audio(from_idx) = output_node; - let BackendNodeId::Audio(to_idx) = input_node; - let mut controller = self.audio_controller.lock().unwrap(); controller.graph_disconnect( self.track_id, - from_idx.index() as u32, + output_node.index(), output_port, - to_idx.index() as u32, + input_node.index(), input_port, ); @@ -112,12 +104,10 @@ impl GraphBackend for AudioGraphBackend { param_id: u32, value: f64, ) -> Result<(), String> { - let BackendNodeId::Audio(node_idx) = backend_id; - let mut controller = self.audio_controller.lock().unwrap(); controller.graph_set_parameter( self.track_id, - node_idx.index() as u32, + backend_id.index(), param_id, value as f32, ); @@ -172,12 +162,10 @@ impl GraphBackend for AudioGraphBackend { x: f32, y: f32, ) -> Result { - let BackendNodeId::Audio(allocator_idx) = voice_allocator_id; - let mut controller = self.audio_controller.lock().unwrap(); controller.graph_add_node_to_template( self.track_id, - allocator_idx.index() as u32, + voice_allocator_id.index(), node_type.to_string(), x, y, @@ -199,17 +187,13 @@ impl GraphBackend for AudioGraphBackend { input_node: BackendNodeId, input_port: usize, ) -> Result<(), String> { - let BackendNodeId::Audio(allocator_idx) = voice_allocator_id; - let BackendNodeId::Audio(from_idx) = output_node; - let BackendNodeId::Audio(to_idx) = input_node; - let mut controller = self.audio_controller.lock().unwrap(); controller.graph_connect_in_template( self.track_id, - allocator_idx.index() as u32, - from_idx.index() as u32, + voice_allocator_id.index(), + output_node.index(), output_port, - to_idx.index() as u32, + input_node.index(), input_port, ); diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/backend.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/backend.rs index e0ed183..e729bd0 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/backend.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/backend.rs @@ -13,6 +13,16 @@ pub enum BackendNodeId { // Future: Vfx(u32), } +impl BackendNodeId { + /// Get the backend node index as a u32 + pub fn index(self) -> u32 { + match self { + BackendNodeId::Audio(idx) => idx.index() as u32, + } + } + +} + /// Abstract backend for node graph operations /// /// Implementations: @@ -86,12 +96,14 @@ pub trait GraphBackend: Send { /// Serializable graph state (for presets and save/load) #[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(dead_code)] pub struct GraphState { pub nodes: Vec, pub connections: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(dead_code)] pub struct SerializedNode { pub id: u32, // Frontend node ID (stable) pub node_type: String, @@ -100,6 +112,7 @@ pub struct SerializedNode { } #[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(dead_code)] pub struct SerializedConnection { pub from_node: u32, pub from_port: usize, diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/graph_data.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/graph_data.rs index 36559aa..1f0da9e 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/graph_data.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/graph_data.rs @@ -168,6 +168,20 @@ pub struct NodeData { fn default_root_note() -> u8 { 69 } +impl NodeData { + pub fn new(template: NodeTemplate) -> Self { + Self { + template, + sample_display_name: None, + root_note: 69, + script_id: None, + ui_declaration: None, + sample_slot_names: Vec::new(), + script_sample_names: HashMap::new(), + } + } +} + /// Cached oscilloscope waveform data for rendering in node body pub struct OscilloscopeCache { pub audio: Vec, @@ -459,7 +473,7 @@ impl NodeTemplateTrait for NodeTemplate { } fn user_data(&self, _user_state: &mut Self::UserState) -> Self::NodeData { - NodeData { template: *self, sample_display_name: None, root_note: 69, script_id: None, ui_declaration: None, sample_slot_names: Vec::new(), script_sample_names: HashMap::new() } + NodeData::new(*self) } fn build_node( @@ -1209,8 +1223,6 @@ impl NodeDataTrait for NodeData { &[0,2,4,5,7,9,11], &[0,2,3,5,7,8,10], &[0,2,3,5,7,9,10], &[0,2,4,5,7,9,10], &[0,2,4,7,9], &[0,3,5,7,10], &[0,3,5,6,7,10], &[0,2,3,5,7,8,11], ]; - const NOTE_NAMES: &[&str] = &["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"]; - let row_to_note_name = |row: usize| -> String { let base = key_val as u16 + octave_val as u16 * 12; let midi_note = if is_diatonic { @@ -1374,8 +1386,9 @@ impl NodeDataTrait for NodeData { for (_name, input_id) in &node.inputs { if let ValueType::Float { value, backend_param_id: Some(pid), .. } = &_graph.get_input(*input_id).value { let idx = *pid as usize; - if idx < draw_vm.params.len() { - draw_vm.params[idx] = *value; + let params = draw_vm.params_mut(); + if idx < params.len() { + params[idx] = *value; } } } @@ -1447,7 +1460,7 @@ fn render_script_ui_elements( draw_vm.mouse.down = response.dragged() || response.drag_started(); // Save params before execution to detect changes - let params_before: Vec = draw_vm.params.clone(); + let params_before: Vec = draw_vm.params().to_vec(); // Execute draw block if let Err(e) = draw_vm.execute() { @@ -1528,7 +1541,7 @@ fn render_script_ui_elements( } // Detect param changes from draw block (e.g. knob drag) - for (i, (&before, &after)) in params_before.iter().zip(draw_vm.params.iter()).enumerate() { + for (i, (&before, &after)) in params_before.iter().zip(draw_vm.params().iter()).enumerate() { if (after - before).abs() > 1e-10 { pending_param_changes.push((node_id, i as u32, after)); } diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mod.rs index 4c51531..93c8184 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mod.rs @@ -6,7 +6,6 @@ pub mod actions; pub mod audio_backend; pub mod backend; pub mod graph_data; -pub mod node_types; use backend::{BackendNodeId, GraphBackend}; use graph_data::{AllNodeTemplates, SubgraphNodeTemplates, VoiceAllocatorNodeTemplates, DataType, GraphState, NodeData, NodeTemplate, ValueType}; @@ -88,7 +87,6 @@ pub struct NodeGraphPane { user_state: GraphState, /// Backend integration - #[allow(dead_code)] backend: Option>, /// Maps frontend node IDs to backend node IDs @@ -101,7 +99,6 @@ pub struct NodeGraphPane { track_id: Option, /// Pending action to execute - #[allow(dead_code)] pending_action: Option>, /// Track newly added nodes to update ID mappings after action execution @@ -177,50 +174,6 @@ impl NodeGraphPane { } } - #[allow(dead_code)] - pub fn with_track_id( - track_id: Uuid, - audio_controller: std::sync::Arc>, - backend_track_id: u32, - ) -> Self { - let backend = Box::new(audio_backend::AudioGraphBackend::new( - backend_track_id, - audio_controller, - )); - - let mut pane = Self { - state: GraphEditorState::new(1.0), - user_state: GraphState::default(), - backend: Some(backend), - node_id_map: HashMap::new(), - backend_to_frontend_map: HashMap::new(), - track_id: Some(track_id), - pending_action: None, - pending_node_addition: None, - parameter_values: HashMap::new(), - last_project_generation: 0, - dragging_node: None, - insert_target: None, - subgraph_stack: Vec::new(), - groups: Vec::new(), - next_group_id: 1, - group_placeholder_map: HashMap::new(), - renaming_group: None, - node_context_menu: None, - last_node_rects: HashMap::new(), - pending_script_resolutions: Vec::new(), - last_oscilloscope_poll: std::time::Instant::now(), - backend_track_id: Some(backend_track_id), - }; - - // Load existing graph from backend - if let Err(e) = pane.load_graph_from_backend() { - eprintln!("Failed to load graph from backend: {}", e); - } - - pane - } - /// Load the graph state from the backend and populate the frontend fn load_graph_from_backend(&mut self) -> Result<(), String> { let json = if let Some(backend) = &self.backend { @@ -487,9 +440,6 @@ impl NodeGraphPane { let to_backend = self.node_id_map.get(&to_node_id); if let (Some(&from_id), Some(&to_id)) = (from_backend, to_backend) { - let BackendNodeId::Audio(from_idx) = from_id; - let BackendNodeId::Audio(to_idx) = to_id; - if let Some(va_id) = self.va_context() { // Inside VA template if let Some(&backend_track_id) = shared.layer_to_track_map.get(&track_id) { @@ -497,8 +447,8 @@ impl NodeGraphPane { let mut controller = audio_controller.lock().unwrap(); controller.graph_connect_in_template( backend_track_id, va_id, - from_idx.index() as u32, from_port, - to_idx.index() as u32, to_port, + from_id.index(), from_port, + to_id.index(), to_port, ); } } @@ -532,9 +482,6 @@ impl NodeGraphPane { let to_backend = self.node_id_map.get(&to_node_id); if let (Some(&from_id), Some(&to_id)) = (from_backend, to_backend) { - let BackendNodeId::Audio(from_idx) = from_id; - let BackendNodeId::Audio(to_idx) = to_id; - if let Some(va_id) = self.va_context() { // Inside VA template if let Some(&backend_track_id) = shared.layer_to_track_map.get(&track_id) { @@ -542,8 +489,8 @@ impl NodeGraphPane { let mut controller = audio_controller.lock().unwrap(); controller.graph_disconnect_in_template( backend_track_id, va_id, - from_idx.index() as u32, from_port, - to_idx.index() as u32, to_port, + from_id.index(), from_port, + to_id.index(), to_port, ); } } @@ -572,15 +519,13 @@ impl NodeGraphPane { // Node was deleted if let Some(track_id) = self.track_id { if let Some(&backend_id) = self.node_id_map.get(&node_id) { - let BackendNodeId::Audio(node_idx) = backend_id; - if let Some(va_id) = self.va_context() { // Inside VA template if let Some(&backend_track_id) = shared.layer_to_track_map.get(&track_id) { if let Some(audio_controller) = &shared.audio_controller { let mut controller = audio_controller.lock().unwrap(); controller.graph_remove_node_from_template( - backend_track_id, va_id, node_idx.index() as u32, + backend_track_id, va_id, backend_id.index(), ); } } @@ -614,9 +559,7 @@ impl NodeGraphPane { // Sync updated position to backend if let Some(&backend_id) = self.node_id_map.get(&node) { if let Some(pos) = self.state.node_positions.get(node) { - let node_index = match backend_id { - BackendNodeId::Audio(idx) => idx.index() as u32, - }; + let node_index = backend_id.index(); if let Some(audio_controller) = &shared.audio_controller { if let Some(&backend_track_id) = self.track_id.and_then(|tid| shared.layer_to_track_map.get(&tid)) { let mut controller = audio_controller.lock().unwrap(); @@ -931,26 +874,18 @@ impl NodeGraphPane { fn check_parameter_changes(&mut self, shared: &mut crate::panes::SharedPaneState) { // Check all input parameters for value changes - let mut _checked_count = 0; - let mut _connection_only_count = 0; - let mut _non_float_count = 0; - for (input_id, input_param) in &self.state.graph.inputs { // Only check parameters that can have constant values (not ConnectionOnly) if matches!(input_param.kind, InputParamKind::ConnectionOnly) { - _connection_only_count += 1; continue; } // Get current value and backend param ID let (current_value, backend_param_id) = match &input_param.value { ValueType::Float { value, backend_param_id, .. } => { - _checked_count += 1; (*value, *backend_param_id) }, - other => { - _non_float_count += 1; - eprintln!("[DEBUG] Non-float parameter type: {:?}", std::mem::discriminant(other)); + _ => { continue; } }; @@ -972,8 +907,6 @@ impl NodeGraphPane { if let Some(&backend_id) = self.node_id_map.get(&node_id) { if let Some(param_id) = backend_param_id { - let BackendNodeId::Audio(node_idx) = backend_id; - if let Some(va_id) = self.va_context() { // Inside VA template — call template command directly if let Some(&backend_track_id) = shared.layer_to_track_map.get(&track_id) { @@ -981,7 +914,7 @@ impl NodeGraphPane { let mut controller = audio_controller.lock().unwrap(); controller.graph_set_parameter_in_template( backend_track_id, va_id, - node_idx.index() as u32, param_id, current_value, + backend_id.index(), param_id, current_value, ); } } @@ -1315,27 +1248,23 @@ impl NodeGraphPane { None => return, }; - let BackendNodeId::Audio(src_idx) = src_backend; - let BackendNodeId::Audio(dst_idx) = dst_backend; - let BackendNodeId::Audio(drag_idx) = drag_backend; - // Send commands to backend: disconnect old, connect source→drag, connect drag→dest { let mut controller = audio_controller.lock().unwrap(); controller.graph_disconnect( backend_track_id, - src_idx.index() as u32, src_port_idx, - dst_idx.index() as u32, dst_port_idx, + src_backend.index(), src_port_idx, + dst_backend.index(), dst_port_idx, ); controller.graph_connect( backend_track_id, - src_idx.index() as u32, src_port_idx, - drag_idx.index() as u32, drag_input_port_idx, + src_backend.index(), src_port_idx, + drag_backend.index(), drag_input_port_idx, ); controller.graph_connect( backend_track_id, - drag_idx.index() as u32, drag_output_port_idx, - dst_idx.index() as u32, dst_port_idx, + drag_backend.index(), drag_output_port_idx, + dst_backend.index(), dst_port_idx, ); } @@ -1394,12 +1323,11 @@ impl NodeGraphPane { // Load the subgraph state from backend match &context { SubgraphContext::VoiceAllocator { backend_id, .. } => { - let BackendNodeId::Audio(va_idx) = *backend_id; if let Some(track_id) = self.track_id { if let Some(&backend_track_id) = shared.layer_to_track_map.get(&track_id) { if let Some(audio_controller) = &shared.audio_controller { let mut controller = audio_controller.lock().unwrap(); - match controller.query_template_state(backend_track_id, va_idx.index() as u32) { + match controller.query_template_state(backend_track_id, backend_id.index()) { Ok(json) => { if let Err(e) = self.load_graph_from_json(&json) { eprintln!("Failed to load template state: {}", e); @@ -1608,8 +1536,7 @@ impl NodeGraphPane { fn va_context(&self) -> Option { for frame in self.subgraph_stack.iter().rev() { if let SubgraphContext::VoiceAllocator { backend_id, .. } = &frame.context { - let BackendNodeId::Audio(idx) = *backend_id; - return Some(idx.index() as u32); + return Some(backend_id.index()); } } None @@ -1664,7 +1591,7 @@ impl NodeGraphPane { // Collect selected backend IDs let selected_backend_ids: Vec = self.state.selected_nodes.iter() .filter_map(|fid| self.node_id_map.get(fid)) - .map(|bid| { let BackendNodeId::Audio(idx) = *bid; idx.index() as u32 }) + .map(|bid| bid.index()) .collect(); if selected_backend_ids.is_empty() { @@ -1689,9 +1616,9 @@ impl NodeGraphPane { if let (Some(from_fid), Some(to_fid)) = (from_node_fid, to_node_fid) { let from_bid = self.node_id_map.get(&from_fid) - .map(|b| { let BackendNodeId::Audio(idx) = *b; idx.index() as u32 }); + .map(|b| b.index()); let to_bid = self.node_id_map.get(&to_fid) - .map(|b| { let BackendNodeId::Audio(idx) = *b; idx.index() as u32 }); + .map(|b| b.index()); if let (Some(from_b), Some(to_b)) = (from_bid, to_bid) { let from_in_group = selected_set.contains(&from_b); @@ -1938,7 +1865,7 @@ impl NodeGraphPane { label: group.name.clone(), inputs: vec![], outputs: vec![], - user_data: NodeData { template: NodeTemplate::Group, sample_display_name: None, root_note: 69, script_id: None, ui_declaration: None, sample_slot_names: Vec::new(), script_sample_names: HashMap::new() }, + user_data: NodeData::new(NodeTemplate::Group), }); // Add dynamic input ports based on boundary inputs @@ -2010,7 +1937,7 @@ impl NodeGraphPane { label: "Group Input".to_string(), inputs: vec![], outputs: vec![], - user_data: NodeData { template: NodeTemplate::Group, sample_display_name: None, root_note: 69, script_id: None, ui_declaration: None, sample_slot_names: Vec::new(), script_sample_names: HashMap::new() }, + user_data: NodeData::new(NodeTemplate::Group), }); for bc in &scope_group.boundary_inputs { @@ -2057,7 +1984,7 @@ impl NodeGraphPane { label: "Group Output".to_string(), inputs: vec![], outputs: vec![], - user_data: NodeData { template: NodeTemplate::Group, sample_display_name: None, root_note: 69, script_id: None, ui_declaration: None, sample_slot_names: Vec::new(), script_sample_names: HashMap::new() }, + user_data: NodeData::new(NodeTemplate::Group), }); for bc in &scope_group.boundary_outputs { @@ -2254,7 +2181,7 @@ impl NodeGraphPane { label: label.to_string(), inputs: vec![], outputs: vec![], - user_data: NodeData { template: node_template, sample_display_name: None, root_note: 69, script_id: None, ui_declaration: None, sample_slot_names: Vec::new(), script_sample_names: HashMap::new() }, + user_data: NodeData::new(node_template), }); node_template.build_node(&mut self.state.graph, &mut self.user_state, frontend_id); @@ -2686,8 +2613,7 @@ impl crate::panes::PaneRenderer for NodeGraphPane { for (node_id, param_id, value) in changes { // Send to backend if let Some(backend_id) = self.node_id_map.get(&node_id) { - let BackendNodeId::Audio(node_idx) = backend_id; - controller.graph_set_parameter(backend_track_id, node_idx.index() as u32, param_id, value); + controller.graph_set_parameter(backend_track_id, backend_id.index(), param_id, value); } // Update frontend graph value let row_name = format!("Row{}", param_id - 7); @@ -2710,8 +2636,7 @@ impl crate::panes::PaneRenderer for NodeGraphPane { for (node_id, param_id, value) in changes { // Send to backend if let Some(backend_id) = self.node_id_map.get(&node_id) { - let BackendNodeId::Audio(node_idx) = backend_id; - controller.graph_set_parameter(backend_track_id, node_idx.index() as u32, param_id, value); + controller.graph_set_parameter(backend_track_id, backend_id.index(), param_id, value); } // Update frontend graph input port value if let Some(node) = self.state.graph.nodes.get(node_id) { @@ -2777,11 +2702,10 @@ impl crate::panes::PaneRenderer for NodeGraphPane { } if let Some(backend_track_id) = self.track_id.and_then(|tid| shared.layer_to_track_map.get(&tid).copied()) { if let Some(&backend_id) = self.node_id_map.get(&node_id) { - let BackendNodeId::Audio(node_idx) = backend_id; if let Some(controller_arc) = &shared.audio_controller { let mut controller = controller_arc.lock().unwrap(); controller.send_command(daw_backend::Command::GraphSetScript( - backend_track_id, node_idx.index() as u32, source, + backend_track_id, backend_id.index(), source, )); } } @@ -2830,11 +2754,10 @@ impl crate::panes::PaneRenderer for NodeGraphPane { } if let Some(backend_track_id) = self.track_id.and_then(|tid| shared.layer_to_track_map.get(&tid).copied()) { if let Some(&backend_id) = self.node_id_map.get(&node_id) { - let BackendNodeId::Audio(node_idx) = backend_id; if let Some(controller_arc) = &shared.audio_controller { let mut controller = controller_arc.lock().unwrap(); controller.send_command(daw_backend::Command::GraphSetScript( - backend_track_id, node_idx.index() as u32, source, + backend_track_id, backend_id.index(), source, )); } } @@ -2871,9 +2794,8 @@ impl crate::panes::PaneRenderer for NodeGraphPane { let mut controller = controller_arc.lock().unwrap(); for &node_id in &matching_nodes { if let Some(&backend_id) = self.node_id_map.get(&node_id) { - let BackendNodeId::Audio(node_idx) = backend_id; controller.send_command(daw_backend::Command::GraphSetScript( - backend_track_id, node_idx.index() as u32, source.clone(), + backend_track_id, backend_id.index(), source.clone(), )); } } @@ -2979,13 +2901,12 @@ impl crate::panes::PaneRenderer for NodeGraphPane { // Delete the node via the graph - queue the deletion if let Some(track_id) = self.track_id { if let Some(&backend_id) = self.node_id_map.get(&ctx_node_id) { - let BackendNodeId::Audio(node_idx) = backend_id; if let Some(va_id) = self.va_context() { if let Some(&backend_track_id) = shared.layer_to_track_map.get(&track_id) { if let Some(audio_controller) = &shared.audio_controller { let mut controller = audio_controller.lock().unwrap(); controller.graph_remove_node_from_template( - backend_track_id, va_id, node_idx.index() as u32, + backend_track_id, va_id, backend_id.index(), ); } } diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/node_types.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/node_types.rs deleted file mode 100644 index d345574..0000000 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/node_types.rs +++ /dev/null @@ -1,690 +0,0 @@ -#![allow(dead_code)] -//! Node Type Registry -//! -//! Defines metadata for all available node types - -use eframe::egui; -use std::collections::HashMap; - -/// Signal type for connections (matches daw_backend::SignalType) -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum DataType { - Audio, - Midi, - CV, -} - -impl DataType { - /// Get the color for this signal type - pub fn color(&self) -> egui::Color32 { - match self { - DataType::Audio => egui::Color32::from_rgb(33, 150, 243), // Blue (#2196F3) - DataType::Midi => egui::Color32::from_rgb(76, 175, 80), // Green (#4CAF50) - DataType::CV => egui::Color32::from_rgb(255, 152, 0), // Orange (#FF9800) - } - } -} - -/// Node category for organization -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum NodeCategory { - Inputs, - Generators, - Effects, - Utilities, - Outputs, -} - -impl NodeCategory { - pub fn display_name(&self) -> &'static str { - match self { - NodeCategory::Inputs => "Inputs", - NodeCategory::Generators => "Generators", - NodeCategory::Effects => "Effects", - NodeCategory::Utilities => "Utilities", - NodeCategory::Outputs => "Outputs", - } - } -} - -/// Port information -#[derive(Debug, Clone)] -pub struct PortInfo { - pub index: usize, - pub name: String, - pub signal_type: DataType, - pub description: String, -} - -/// Parameter units -#[derive(Debug, Clone, Copy)] -pub enum ParameterUnit { - Hz, - Percent, - Decibels, - Seconds, - Milliseconds, - Semitones, - None, -} - -impl ParameterUnit { - pub fn suffix(&self) -> &'static str { - match self { - ParameterUnit::Hz => " Hz", - ParameterUnit::Percent => "%", - ParameterUnit::Decibels => " dB", - ParameterUnit::Seconds => " s", - ParameterUnit::Milliseconds => " ms", - ParameterUnit::Semitones => " st", - ParameterUnit::None => "", - } - } -} - -/// Parameter information -#[derive(Debug, Clone)] -pub struct ParameterInfo { - pub id: u32, - pub name: String, - pub default: f64, - pub min: f64, - pub max: f64, - pub unit: ParameterUnit, - pub description: String, -} - -/// Node type metadata -#[derive(Debug, Clone)] -pub struct NodeTypeInfo { - pub id: String, - pub display_name: String, - pub category: NodeCategory, - pub inputs: Vec, - pub outputs: Vec, - pub parameters: Vec, - pub description: String, -} - -/// Registry of all available node types -pub struct NodeTypeRegistry { - types: HashMap, -} - -impl NodeTypeRegistry { - pub fn new() -> Self { - let mut types = HashMap::new(); - - // === INPUTS === - - types.insert( - "MidiInput".to_string(), - NodeTypeInfo { - id: "MidiInput".to_string(), - display_name: "MIDI Input".to_string(), - category: NodeCategory::Inputs, - inputs: vec![], - outputs: vec![PortInfo { - index: 0, - name: "MIDI".to_string(), - signal_type: DataType::Midi, - description: "MIDI output from connected device".to_string(), - }], - parameters: vec![], - description: "Receives MIDI from connected input devices".to_string(), - }, - ); - - types.insert( - "AudioInput".to_string(), - NodeTypeInfo { - id: "AudioInput".to_string(), - display_name: "Audio Input".to_string(), - category: NodeCategory::Inputs, - inputs: vec![], - outputs: vec![PortInfo { - index: 0, - name: "Audio".to_string(), - signal_type: DataType::Audio, - description: "Audio from microphone/line input".to_string(), - }], - parameters: vec![], - description: "Receives audio from connected input devices".to_string(), - }, - ); - - // === GENERATORS === - - types.insert( - "Oscillator".to_string(), - NodeTypeInfo { - id: "Oscillator".to_string(), - display_name: "Oscillator".to_string(), - category: NodeCategory::Generators, - inputs: vec![ - PortInfo { - index: 0, - name: "Freq".to_string(), - signal_type: DataType::CV, - description: "Frequency control (V/Oct)".to_string(), - }, - PortInfo { - index: 1, - name: "Sync".to_string(), - signal_type: DataType::CV, - description: "Hard sync input".to_string(), - }, - ], - outputs: vec![PortInfo { - index: 0, - name: "Out".to_string(), - signal_type: DataType::Audio, - description: "Audio output".to_string(), - }], - parameters: vec![ - ParameterInfo { - id: 0, - name: "Frequency".to_string(), - default: 440.0, - min: 20.0, - max: 20000.0, - unit: ParameterUnit::Hz, - description: "Base frequency".to_string(), - }, - ParameterInfo { - id: 1, - name: "Waveform".to_string(), - default: 0.0, - min: 0.0, - max: 3.0, - unit: ParameterUnit::None, - description: "0=Sine, 1=Saw, 2=Square, 3=Triangle".to_string(), - }, - ], - description: "Basic oscillator with multiple waveforms".to_string(), - }, - ); - - types.insert( - "Noise".to_string(), - NodeTypeInfo { - id: "Noise".to_string(), - display_name: "Noise".to_string(), - category: NodeCategory::Generators, - inputs: vec![], - outputs: vec![PortInfo { - index: 0, - name: "Out".to_string(), - signal_type: DataType::Audio, - description: "Noise output".to_string(), - }], - parameters: vec![ParameterInfo { - id: 0, - name: "Color".to_string(), - default: 0.0, - min: 0.0, - max: 2.0, - unit: ParameterUnit::None, - description: "0=White, 1=Pink, 2=Brown".to_string(), - }], - description: "Noise generator (white, pink, brown)".to_string(), - }, - ); - - // === EFFECTS === - - types.insert( - "Gain".to_string(), - NodeTypeInfo { - id: "Gain".to_string(), - display_name: "Gain".to_string(), - category: NodeCategory::Effects, - inputs: vec![ - PortInfo { - index: 0, - name: "In".to_string(), - signal_type: DataType::Audio, - description: "Audio input".to_string(), - }, - PortInfo { - index: 1, - name: "Gain".to_string(), - signal_type: DataType::CV, - description: "Gain control CV".to_string(), - }, - ], - outputs: vec![PortInfo { - index: 0, - name: "Out".to_string(), - signal_type: DataType::Audio, - description: "Gained audio output".to_string(), - }], - parameters: vec![ParameterInfo { - id: 0, - name: "Gain".to_string(), - default: 0.0, - min: -60.0, - max: 12.0, - unit: ParameterUnit::Decibels, - description: "Gain amount in dB".to_string(), - }], - description: "Amplifies or attenuates audio signal".to_string(), - }, - ); - - types.insert( - "Filter".to_string(), - NodeTypeInfo { - id: "Filter".to_string(), - display_name: "Filter".to_string(), - category: NodeCategory::Effects, - inputs: vec![ - PortInfo { - index: 0, - name: "In".to_string(), - signal_type: DataType::Audio, - description: "Audio input".to_string(), - }, - PortInfo { - index: 1, - name: "Cutoff".to_string(), - signal_type: DataType::CV, - description: "Cutoff frequency CV".to_string(), - }, - ], - outputs: vec![PortInfo { - index: 0, - name: "Out".to_string(), - signal_type: DataType::Audio, - description: "Filtered audio output".to_string(), - }], - parameters: vec![ - ParameterInfo { - id: 0, - name: "Cutoff".to_string(), - default: 1000.0, - min: 20.0, - max: 20000.0, - unit: ParameterUnit::Hz, - description: "Cutoff frequency".to_string(), - }, - ParameterInfo { - id: 1, - name: "Resonance".to_string(), - default: 0.0, - min: 0.0, - max: 1.0, - unit: ParameterUnit::None, - description: "Filter resonance".to_string(), - }, - ParameterInfo { - id: 2, - name: "Type".to_string(), - default: 0.0, - min: 0.0, - max: 3.0, - unit: ParameterUnit::None, - description: "0=LPF, 1=HPF, 2=BPF, 3=Notch".to_string(), - }, - ], - description: "Multi-mode filter (lowpass, highpass, bandpass, notch)".to_string(), - }, - ); - - types.insert( - "Echo".to_string(), - NodeTypeInfo { - id: "Echo".to_string(), - display_name: "Echo".to_string(), - category: NodeCategory::Effects, - inputs: vec![PortInfo { - index: 0, - name: "In".to_string(), - signal_type: DataType::Audio, - description: "Audio input".to_string(), - }], - outputs: vec![PortInfo { - index: 0, - name: "Out".to_string(), - signal_type: DataType::Audio, - description: "Echo audio output".to_string(), - }], - parameters: vec![ - ParameterInfo { - id: 0, - name: "Time".to_string(), - default: 250.0, - min: 1.0, - max: 2000.0, - unit: ParameterUnit::Milliseconds, - description: "Echo time".to_string(), - }, - ParameterInfo { - id: 1, - name: "Feedback".to_string(), - default: 0.3, - min: 0.0, - max: 0.95, - unit: ParameterUnit::None, - description: "Feedback amount".to_string(), - }, - ParameterInfo { - id: 2, - name: "Mix".to_string(), - default: 0.5, - min: 0.0, - max: 1.0, - unit: ParameterUnit::None, - description: "Dry/wet mix".to_string(), - }, - ], - description: "Echo effect with feedback".to_string(), - }, - ); - - // === UTILITIES === - - types.insert( - "ADSR".to_string(), - NodeTypeInfo { - id: "ADSR".to_string(), - display_name: "ADSR".to_string(), - category: NodeCategory::Utilities, - inputs: vec![PortInfo { - index: 0, - name: "Gate".to_string(), - signal_type: DataType::CV, - description: "Gate input (triggers envelope)".to_string(), - }], - outputs: vec![PortInfo { - index: 0, - name: "Out".to_string(), - signal_type: DataType::CV, - description: "Envelope CV output (0-1)".to_string(), - }], - parameters: vec![ - ParameterInfo { - id: 0, - name: "Attack".to_string(), - default: 10.0, - min: 0.1, - max: 2000.0, - unit: ParameterUnit::Milliseconds, - description: "Attack time".to_string(), - }, - ParameterInfo { - id: 1, - name: "Decay".to_string(), - default: 100.0, - min: 0.1, - max: 2000.0, - unit: ParameterUnit::Milliseconds, - description: "Decay time".to_string(), - }, - ParameterInfo { - id: 2, - name: "Sustain".to_string(), - default: 0.7, - min: 0.0, - max: 1.0, - unit: ParameterUnit::None, - description: "Sustain level".to_string(), - }, - ParameterInfo { - id: 3, - name: "Release".to_string(), - default: 200.0, - min: 0.1, - max: 5000.0, - unit: ParameterUnit::Milliseconds, - description: "Release time".to_string(), - }, - ], - description: "ADSR envelope generator".to_string(), - }, - ); - - types.insert( - "LFO".to_string(), - NodeTypeInfo { - id: "LFO".to_string(), - display_name: "LFO".to_string(), - category: NodeCategory::Utilities, - inputs: vec![], - outputs: vec![PortInfo { - index: 0, - name: "Out".to_string(), - signal_type: DataType::CV, - description: "LFO CV output".to_string(), - }], - parameters: vec![ - ParameterInfo { - id: 0, - name: "Rate".to_string(), - default: 1.0, - min: 0.01, - max: 20.0, - unit: ParameterUnit::Hz, - description: "LFO rate".to_string(), - }, - ParameterInfo { - id: 1, - name: "Waveform".to_string(), - default: 0.0, - min: 0.0, - max: 3.0, - unit: ParameterUnit::None, - description: "0=Sine, 1=Triangle, 2=Square, 3=Saw".to_string(), - }, - ], - description: "Low-frequency oscillator for modulation".to_string(), - }, - ); - - types.insert( - "Mixer".to_string(), - NodeTypeInfo { - id: "Mixer".to_string(), - display_name: "Mixer".to_string(), - category: NodeCategory::Utilities, - inputs: vec![ - PortInfo { - index: 0, - name: "In 1".to_string(), - signal_type: DataType::Audio, - description: "Audio input 1".to_string(), - }, - PortInfo { - index: 1, - name: "In 2".to_string(), - signal_type: DataType::Audio, - description: "Audio input 2".to_string(), - }, - PortInfo { - index: 2, - name: "In 3".to_string(), - signal_type: DataType::Audio, - description: "Audio input 3".to_string(), - }, - PortInfo { - index: 3, - name: "In 4".to_string(), - signal_type: DataType::Audio, - description: "Audio input 4".to_string(), - }, - ], - outputs: vec![PortInfo { - index: 0, - name: "Out".to_string(), - signal_type: DataType::Audio, - description: "Mixed audio output".to_string(), - }], - parameters: vec![ - ParameterInfo { - id: 0, - name: "Level 1".to_string(), - default: 1.0, - min: 0.0, - max: 1.0, - unit: ParameterUnit::None, - description: "Input 1 level".to_string(), - }, - ParameterInfo { - id: 1, - name: "Level 2".to_string(), - default: 1.0, - min: 0.0, - max: 1.0, - unit: ParameterUnit::None, - description: "Input 2 level".to_string(), - }, - ParameterInfo { - id: 2, - name: "Level 3".to_string(), - default: 1.0, - min: 0.0, - max: 1.0, - unit: ParameterUnit::None, - description: "Input 3 level".to_string(), - }, - ParameterInfo { - id: 3, - name: "Level 4".to_string(), - default: 1.0, - min: 0.0, - max: 1.0, - unit: ParameterUnit::None, - description: "Input 4 level".to_string(), - }, - ], - description: "4-channel audio mixer".to_string(), - }, - ); - - types.insert( - "Splitter".to_string(), - NodeTypeInfo { - id: "Splitter".to_string(), - display_name: "Splitter".to_string(), - category: NodeCategory::Utilities, - inputs: vec![PortInfo { - index: 0, - name: "In".to_string(), - signal_type: DataType::Audio, - description: "Audio input".to_string(), - }], - outputs: vec![ - PortInfo { - index: 0, - name: "Out 1".to_string(), - signal_type: DataType::Audio, - description: "Audio output 1".to_string(), - }, - PortInfo { - index: 1, - name: "Out 2".to_string(), - signal_type: DataType::Audio, - description: "Audio output 2".to_string(), - }, - PortInfo { - index: 2, - name: "Out 3".to_string(), - signal_type: DataType::Audio, - description: "Audio output 3".to_string(), - }, - PortInfo { - index: 3, - name: "Out 4".to_string(), - signal_type: DataType::Audio, - description: "Audio output 4".to_string(), - }, - ], - parameters: vec![], - description: "Splits one audio signal into four outputs".to_string(), - }, - ); - - types.insert( - "Constant".to_string(), - NodeTypeInfo { - id: "Constant".to_string(), - display_name: "Constant".to_string(), - category: NodeCategory::Utilities, - inputs: vec![], - outputs: vec![PortInfo { - index: 0, - name: "Out".to_string(), - signal_type: DataType::CV, - description: "Constant CV output".to_string(), - }], - parameters: vec![ParameterInfo { - id: 0, - name: "Value".to_string(), - default: 0.0, - min: -1.0, - max: 1.0, - unit: ParameterUnit::None, - description: "Constant value".to_string(), - }], - description: "Outputs a constant CV value".to_string(), - }, - ); - - // === OUTPUTS === - - types.insert( - "AudioOutput".to_string(), - NodeTypeInfo { - id: "AudioOutput".to_string(), - display_name: "Audio Output".to_string(), - category: NodeCategory::Outputs, - inputs: vec![ - PortInfo { - index: 0, - name: "Left".to_string(), - signal_type: DataType::Audio, - description: "Left channel input".to_string(), - }, - PortInfo { - index: 1, - name: "Right".to_string(), - signal_type: DataType::Audio, - description: "Right channel input".to_string(), - }, - ], - outputs: vec![], - parameters: vec![], - description: "Sends audio to the track output".to_string(), - }, - ); - - Self { types } - } - - pub fn get(&self, node_type: &str) -> Option<&NodeTypeInfo> { - self.types.get(node_type) - } - - pub fn get_by_category(&self, category: NodeCategory) -> Vec<&NodeTypeInfo> { - self.types - .values() - .filter(|info| info.category == category) - .collect() - } - - pub fn all_categories(&self) -> Vec { - vec![ - NodeCategory::Inputs, - NodeCategory::Generators, - NodeCategory::Effects, - NodeCategory::Utilities, - NodeCategory::Outputs, - ] - } -} - -impl Default for NodeTypeRegistry { - fn default() -> Self { - Self::new() - } -} diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/palette.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/palette.rs deleted file mode 100644 index c732e7f..0000000 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/palette.rs +++ /dev/null @@ -1,171 +0,0 @@ -//! Node Palette UI -//! -//! Left sidebar showing available node types organized by category - -use super::node_types::{NodeCategory, NodeTypeRegistry}; -use eframe::egui; - -/// Node palette state -pub struct NodePalette { - /// Node type registry - registry: NodeTypeRegistry, - - /// Category collapse states - collapsed_categories: std::collections::HashSet, - - /// Search filter text - search_filter: String, -} - -impl NodePalette { - pub fn new() -> Self { - Self { - registry: NodeTypeRegistry::new(), - collapsed_categories: std::collections::HashSet::new(), - search_filter: String::new(), - } - } - - /// Render the palette UI - /// - /// The `on_node_clicked` callback is called when the user clicks a node type to add it - pub fn render(&mut self, ui: &mut egui::Ui, rect: egui::Rect, mut on_node_clicked: F) - where - F: FnMut(&str), - { - // Draw background - ui.painter() - .rect_filled(rect, 0.0, egui::Color32::from_rgb(30, 30, 30)); - - // Create UI within the palette rect - ui.scope_builder(egui::UiBuilder::new().max_rect(rect), |ui| { - ui.vertical(|ui| { - ui.add_space(8.0); - - // Title - ui.heading("Node Palette"); - ui.add_space(4.0); - - // Search box - ui.horizontal(|ui| { - ui.label("Search:"); - ui.text_edit_singleline(&mut self.search_filter); - }); - - ui.add_space(8.0); - ui.separator(); - ui.add_space(8.0); - - // Scrollable node list - egui::ScrollArea::vertical() - .id_salt("node_palette_scroll") - .show(ui, |ui| { - self.render_categories(ui, &mut on_node_clicked); - }); - }); - }); - } - - fn render_categories(&mut self, ui: &mut egui::Ui, on_node_clicked: &mut F) - where - F: FnMut(&str), - { - let search_lower = self.search_filter.to_lowercase(); - - for category in self.registry.all_categories() { - // Get nodes in this category - let mut nodes = self.registry.get_by_category(category); - - // Filter by search text (node names only) - if !search_lower.is_empty() { - nodes.retain(|node| { - node.display_name.to_lowercase().contains(&search_lower) - }); - } - - // Skip empty categories - if nodes.is_empty() { - continue; - } - - // Sort nodes by name - nodes.sort_by(|a, b| a.display_name.cmp(&b.display_name)); - - // Render category header - let is_collapsed = self.collapsed_categories.contains(&category); - let arrow = if is_collapsed { ">" } else { "v" }; - let label = format!("{} {} ({})", arrow, category.display_name(), nodes.len()); - - let header_response = ui.selectable_label(false, label); - - // Toggle collapse on click - if header_response.clicked() { - if is_collapsed { - self.collapsed_categories.remove(&category); - } else { - self.collapsed_categories.insert(category); - } - } - - // Render nodes if not collapsed - if !is_collapsed { - ui.indent(category.display_name(), |ui| { - for node in nodes { - self.render_node_button(ui, node.id.as_str(), &node.display_name, on_node_clicked); - } - }); - } - - ui.add_space(4.0); - } - } - - fn render_node_button( - &self, - ui: &mut egui::Ui, - node_id: &str, - display_name: &str, - on_node_clicked: &mut F, - ) where - F: FnMut(&str), - { - // Use drag source to enable dragging - let drag_id = egui::Id::new(format!("node_palette_{}", node_id)); - let response = ui.dnd_drag_source( - drag_id, - node_id.to_string(), - |ui| { - let button = egui::Button::new(display_name) - .min_size(egui::vec2(ui.available_width() - 8.0, 24.0)) - .fill(egui::Color32::from_rgb(50, 50, 50)); - ui.add(button) - }, - ); - - // Handle click: detect clicks by checking if drag stopped with minimal movement - // dnd_drag_source always sets is_being_dragged=true on press, so we can't use that - if response.response.drag_stopped() { - // Check if this was actually a drag or just a click (minimal movement) - if let Some(start_pos) = response.response.interact_pointer_pos() { - if let Some(current_pos) = ui.input(|i| i.pointer.interact_pos()) { - let drag_distance = (current_pos - start_pos).length(); - if drag_distance < 5.0 { - // This was a click, not a drag - on_node_clicked(node_id); - } - } - } - } - - // Show tooltip with description - if let Some(node_info) = self.registry.get(node_id) { - response.response.on_hover_text(&node_info.description); - } - } -} - -impl Default for NodePalette { - fn default() -> Self { - Self::new() - } -}