Code cleanup

This commit is contained in:
Skyler Lehmkuhl 2026-02-19 11:19:44 -05:00
parent a98b59a6d3
commit c344e11e42
11 changed files with 597 additions and 1764 deletions

View File

@ -152,15 +152,17 @@ impl AudioNode for ScriptNode {
fn set_parameter(&mut self, id: u32, value: f32) { fn set_parameter(&mut self, id: u32, value: f32) {
let idx = id as usize; let idx = id as usize;
if idx < self.vm.params.len() { let params = self.vm.params_mut();
self.vm.params[idx] = value; if idx < params.len() {
params[idx] = value;
} }
} }
fn get_parameter(&self, id: u32) -> f32 { fn get_parameter(&self, id: u32) -> f32 {
let idx = id as usize; let idx = id as usize;
if idx < self.vm.params.len() { let params = self.vm.params();
self.vm.params[idx] if idx < params.len() {
params[idx]
} else { } else {
0.0 0.0
} }

View File

@ -3,19 +3,9 @@ use crate::error::CompileError;
use crate::opcodes::OpCode; use crate::opcodes::OpCode;
use crate::token::Span; use crate::token::Span;
use crate::ui_decl::{UiDeclaration, UiElement}; use crate::ui_decl::{UiDeclaration, UiElement};
use crate::validator::VType;
use crate::vm::ScriptVM; 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 /// Where a named variable lives in the VM
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
enum VarLoc { 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() { for (i, param) in script.params.iter().enumerate() {
self.vars.push((param.name.clone(), VarLoc::Param(i as u16))); self.vars.push((param.name.clone(), VarLoc::Param(i as u16)));
} }
// Register state variables
let mut scalar_idx: u16 = 0; let mut scalar_idx: u16 = 0;
let mut array_idx: u16 = 0; let mut array_idx: u16 = 0;
let mut sample_idx: u8 = 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))); self.vars.push((state.name.clone(), VarLoc::StateArray(array_idx, VType::Int)));
array_idx += 1; array_idx += 1;
} }
StateType::Sample => { StateType::Sample if include_samples => {
self.vars.push((state.name.clone(), VarLoc::SampleSlot(sample_idx))); self.vars.push((state.name.clone(), VarLoc::SampleSlot(sample_idx)));
sample_idx += 1; 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> { fn compile_stmt(&mut self, stmt: &Stmt) -> Result<(), CompileError> {
@ -1149,12 +1110,13 @@ mod tests {
// Draw VM should exist // Draw VM should exist
let mut dvm = draw_vm.expect("draw_vm should be Some"); 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 // Execute should succeed without stack errors
dvm.execute().unwrap(); dvm.execute().unwrap();
// Should have produced draw commands // Should have produced draw commands
assert_eq!(dvm.draw_commands.len(), 2); // fill_circle + stroke_arc assert_eq!(dvm.draw_commands.len(), 2); // fill_circle + stroke_arc
} }
} }

View File

@ -40,8 +40,6 @@ pub enum ScriptError {
ExecutionLimitExceeded, ExecutionLimitExceeded,
StackOverflow, StackOverflow,
StackUnderflow, StackUnderflow,
DivisionByZero,
IndexOutOfBounds { index: i32, len: usize },
InvalidOpcode(u8), InvalidOpcode(u8),
} }
@ -51,10 +49,6 @@ impl fmt::Display for ScriptError {
ScriptError::ExecutionLimitExceeded => write!(f, "Execution limit exceeded (possible infinite loop)"), ScriptError::ExecutionLimitExceeded => write!(f, "Execution limit exceeded (possible infinite loop)"),
ScriptError::StackOverflow => write!(f, "Stack overflow"), ScriptError::StackOverflow => write!(f, "Stack overflow"),
ScriptError::StackUnderflow => write!(f, "Stack underflow"), 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), ScriptError::InvalidOpcode(op) => write!(f, "Invalid opcode: {}", op),
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -177,8 +177,7 @@ impl AddNodeAction {
.get(&self.layer_id) .get(&self.layer_id)
.ok_or("Track not found")?; .ok_or("Track not found")?;
let BackendNodeId::Audio(node_idx) = backend_id; controller.graph_remove_node(*track_id, backend_id.index());
controller.graph_remove_node(*track_id, node_idx.index() as u32);
} }
Ok(()) Ok(())
@ -231,8 +230,7 @@ impl RemoveNodeAction {
.get(&self.layer_id) .get(&self.layer_id)
.ok_or("Track not found")?; .ok_or("Track not found")?;
let BackendNodeId::Audio(node_idx) = self.backend_node_id; controller.graph_remove_node(*track_id, self.backend_node_id.index());
controller.graph_remove_node(*track_id, node_idx.index() as u32);
Ok(()) Ok(())
} }
@ -341,14 +339,11 @@ impl ConnectAction {
.get(&self.layer_id) .get(&self.layer_id)
.ok_or("Track not found")?; .ok_or("Track not found")?;
let BackendNodeId::Audio(from_idx) = self.from_node;
let BackendNodeId::Audio(to_idx) = self.to_node;
controller.graph_connect( controller.graph_connect(
*track_id, *track_id,
from_idx.index() as u32, self.from_node.index(),
self.from_port, self.from_port,
to_idx.index() as u32, self.to_node.index(),
self.to_port, self.to_port,
); );
@ -370,14 +365,11 @@ impl ConnectAction {
.get(&self.layer_id) .get(&self.layer_id)
.ok_or("Track not found")?; .ok_or("Track not found")?;
let BackendNodeId::Audio(from_idx) = self.from_node;
let BackendNodeId::Audio(to_idx) = self.to_node;
controller.graph_disconnect( controller.graph_disconnect(
*track_id, *track_id,
from_idx.index() as u32, self.from_node.index(),
self.from_port, self.from_port,
to_idx.index() as u32, self.to_node.index(),
self.to_port, self.to_port,
); );
@ -433,14 +425,11 @@ impl DisconnectAction {
.get(&self.layer_id) .get(&self.layer_id)
.ok_or("Track not found")?; .ok_or("Track not found")?;
let BackendNodeId::Audio(from_idx) = self.from_node;
let BackendNodeId::Audio(to_idx) = self.to_node;
controller.graph_disconnect( controller.graph_disconnect(
*track_id, *track_id,
from_idx.index() as u32, self.from_node.index(),
self.from_port, self.from_port,
to_idx.index() as u32, self.to_node.index(),
self.to_port, self.to_port,
); );
@ -463,14 +452,11 @@ impl DisconnectAction {
.get(&self.layer_id) .get(&self.layer_id)
.ok_or("Track not found")?; .ok_or("Track not found")?;
let BackendNodeId::Audio(from_idx) = self.from_node;
let BackendNodeId::Audio(to_idx) = self.to_node;
controller.graph_connect( controller.graph_connect(
*track_id, *track_id,
from_idx.index() as u32, self.from_node.index(),
self.from_port, self.from_port,
to_idx.index() as u32, self.to_node.index(),
self.to_port, self.to_port,
); );
@ -522,14 +508,9 @@ impl SetParameterAction {
.get(&self.layer_id) .get(&self.layer_id)
.ok_or("Track not found")?; .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( controller.graph_set_parameter(
*track_id, *track_id,
node_idx.index() as u32, self.backend_node_id.index(),
self.param_id, self.param_id,
self.new_value as f32, self.new_value as f32,
); );
@ -553,11 +534,9 @@ impl SetParameterAction {
.get(&self.layer_id) .get(&self.layer_id)
.ok_or("Track not found")?; .ok_or("Track not found")?;
let BackendNodeId::Audio(node_idx) = self.backend_node_id;
controller.graph_set_parameter( controller.graph_set_parameter(
*track_id, *track_id,
node_idx.index() as u32, self.backend_node_id.index(),
self.param_id, self.param_id,
old_value as f32, old_value as f32,
); );

View File

@ -52,12 +52,10 @@ impl GraphBackend for AudioGraphBackend {
} }
fn remove_node(&mut self, backend_id: BackendNodeId) -> Result<(), String> { 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(); 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(()) Ok(())
} }
@ -69,15 +67,12 @@ impl GraphBackend for AudioGraphBackend {
input_node: BackendNodeId, input_node: BackendNodeId,
input_port: usize, input_port: usize,
) -> Result<(), String> { ) -> Result<(), String> {
let BackendNodeId::Audio(from_idx) = output_node;
let BackendNodeId::Audio(to_idx) = input_node;
let mut controller = self.audio_controller.lock().unwrap(); let mut controller = self.audio_controller.lock().unwrap();
controller.graph_connect( controller.graph_connect(
self.track_id, self.track_id,
from_idx.index() as u32, output_node.index(),
output_port, output_port,
to_idx.index() as u32, input_node.index(),
input_port, input_port,
); );
@ -91,15 +86,12 @@ impl GraphBackend for AudioGraphBackend {
input_node: BackendNodeId, input_node: BackendNodeId,
input_port: usize, input_port: usize,
) -> Result<(), String> { ) -> Result<(), String> {
let BackendNodeId::Audio(from_idx) = output_node;
let BackendNodeId::Audio(to_idx) = input_node;
let mut controller = self.audio_controller.lock().unwrap(); let mut controller = self.audio_controller.lock().unwrap();
controller.graph_disconnect( controller.graph_disconnect(
self.track_id, self.track_id,
from_idx.index() as u32, output_node.index(),
output_port, output_port,
to_idx.index() as u32, input_node.index(),
input_port, input_port,
); );
@ -112,12 +104,10 @@ impl GraphBackend for AudioGraphBackend {
param_id: u32, param_id: u32,
value: f64, value: f64,
) -> Result<(), String> { ) -> Result<(), String> {
let BackendNodeId::Audio(node_idx) = backend_id;
let mut controller = self.audio_controller.lock().unwrap(); let mut controller = self.audio_controller.lock().unwrap();
controller.graph_set_parameter( controller.graph_set_parameter(
self.track_id, self.track_id,
node_idx.index() as u32, backend_id.index(),
param_id, param_id,
value as f32, value as f32,
); );
@ -172,12 +162,10 @@ impl GraphBackend for AudioGraphBackend {
x: f32, x: f32,
y: f32, y: f32,
) -> Result<BackendNodeId, String> { ) -> Result<BackendNodeId, String> {
let BackendNodeId::Audio(allocator_idx) = voice_allocator_id;
let mut controller = self.audio_controller.lock().unwrap(); let mut controller = self.audio_controller.lock().unwrap();
controller.graph_add_node_to_template( controller.graph_add_node_to_template(
self.track_id, self.track_id,
allocator_idx.index() as u32, voice_allocator_id.index(),
node_type.to_string(), node_type.to_string(),
x, x,
y, y,
@ -199,17 +187,13 @@ impl GraphBackend for AudioGraphBackend {
input_node: BackendNodeId, input_node: BackendNodeId,
input_port: usize, input_port: usize,
) -> Result<(), String> { ) -> 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(); let mut controller = self.audio_controller.lock().unwrap();
controller.graph_connect_in_template( controller.graph_connect_in_template(
self.track_id, self.track_id,
allocator_idx.index() as u32, voice_allocator_id.index(),
from_idx.index() as u32, output_node.index(),
output_port, output_port,
to_idx.index() as u32, input_node.index(),
input_port, input_port,
); );

View File

@ -13,6 +13,16 @@ pub enum BackendNodeId {
// Future: Vfx(u32), // 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 /// Abstract backend for node graph operations
/// ///
/// Implementations: /// Implementations:
@ -86,12 +96,14 @@ pub trait GraphBackend: Send {
/// Serializable graph state (for presets and save/load) /// Serializable graph state (for presets and save/load)
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(dead_code)]
pub struct GraphState { pub struct GraphState {
pub nodes: Vec<SerializedNode>, pub nodes: Vec<SerializedNode>,
pub connections: Vec<SerializedConnection>, pub connections: Vec<SerializedConnection>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(dead_code)]
pub struct SerializedNode { pub struct SerializedNode {
pub id: u32, // Frontend node ID (stable) pub id: u32, // Frontend node ID (stable)
pub node_type: String, pub node_type: String,
@ -100,6 +112,7 @@ pub struct SerializedNode {
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(dead_code)]
pub struct SerializedConnection { pub struct SerializedConnection {
pub from_node: u32, pub from_node: u32,
pub from_port: usize, pub from_port: usize,

View File

@ -168,6 +168,20 @@ pub struct NodeData {
fn default_root_note() -> u8 { 69 } 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 /// Cached oscilloscope waveform data for rendering in node body
pub struct OscilloscopeCache { pub struct OscilloscopeCache {
pub audio: Vec<f32>, pub audio: Vec<f32>,
@ -459,7 +473,7 @@ impl NodeTemplateTrait for NodeTemplate {
} }
fn user_data(&self, _user_state: &mut Self::UserState) -> Self::NodeData { 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( 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,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], &[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 row_to_note_name = |row: usize| -> String {
let base = key_val as u16 + octave_val as u16 * 12; let base = key_val as u16 + octave_val as u16 * 12;
let midi_note = if is_diatonic { let midi_note = if is_diatonic {
@ -1374,8 +1386,9 @@ impl NodeDataTrait for NodeData {
for (_name, input_id) in &node.inputs { for (_name, input_id) in &node.inputs {
if let ValueType::Float { value, backend_param_id: Some(pid), .. } = &_graph.get_input(*input_id).value { if let ValueType::Float { value, backend_param_id: Some(pid), .. } = &_graph.get_input(*input_id).value {
let idx = *pid as usize; let idx = *pid as usize;
if idx < draw_vm.params.len() { let params = draw_vm.params_mut();
draw_vm.params[idx] = *value; 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(); draw_vm.mouse.down = response.dragged() || response.drag_started();
// Save params before execution to detect changes // Save params before execution to detect changes
let params_before: Vec<f32> = draw_vm.params.clone(); let params_before: Vec<f32> = draw_vm.params().to_vec();
// Execute draw block // Execute draw block
if let Err(e) = draw_vm.execute() { 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) // 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 { if (after - before).abs() > 1e-10 {
pending_param_changes.push((node_id, i as u32, after)); pending_param_changes.push((node_id, i as u32, after));
} }

View File

@ -6,7 +6,6 @@ pub mod actions;
pub mod audio_backend; pub mod audio_backend;
pub mod backend; pub mod backend;
pub mod graph_data; pub mod graph_data;
pub mod node_types;
use backend::{BackendNodeId, GraphBackend}; use backend::{BackendNodeId, GraphBackend};
use graph_data::{AllNodeTemplates, SubgraphNodeTemplates, VoiceAllocatorNodeTemplates, DataType, GraphState, NodeData, NodeTemplate, ValueType}; use graph_data::{AllNodeTemplates, SubgraphNodeTemplates, VoiceAllocatorNodeTemplates, DataType, GraphState, NodeData, NodeTemplate, ValueType};
@ -88,7 +87,6 @@ pub struct NodeGraphPane {
user_state: GraphState, user_state: GraphState,
/// Backend integration /// Backend integration
#[allow(dead_code)]
backend: Option<Box<dyn GraphBackend>>, backend: Option<Box<dyn GraphBackend>>,
/// Maps frontend node IDs to backend node IDs /// Maps frontend node IDs to backend node IDs
@ -101,7 +99,6 @@ pub struct NodeGraphPane {
track_id: Option<Uuid>, track_id: Option<Uuid>,
/// Pending action to execute /// Pending action to execute
#[allow(dead_code)]
pending_action: Option<Box<dyn lightningbeam_core::action::Action>>, pending_action: Option<Box<dyn lightningbeam_core::action::Action>>,
/// Track newly added nodes to update ID mappings after action execution /// 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<std::sync::Mutex<daw_backend::EngineController>>,
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 /// Load the graph state from the backend and populate the frontend
fn load_graph_from_backend(&mut self) -> Result<(), String> { fn load_graph_from_backend(&mut self) -> Result<(), String> {
let json = if let Some(backend) = &self.backend { 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); let to_backend = self.node_id_map.get(&to_node_id);
if let (Some(&from_id), Some(&to_id)) = (from_backend, to_backend) { 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() { if let Some(va_id) = self.va_context() {
// Inside VA template // Inside VA template
if let Some(&backend_track_id) = shared.layer_to_track_map.get(&track_id) { 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(); let mut controller = audio_controller.lock().unwrap();
controller.graph_connect_in_template( controller.graph_connect_in_template(
backend_track_id, va_id, backend_track_id, va_id,
from_idx.index() as u32, from_port, from_id.index(), from_port,
to_idx.index() as u32, to_port, to_id.index(), to_port,
); );
} }
} }
@ -532,9 +482,6 @@ impl NodeGraphPane {
let to_backend = self.node_id_map.get(&to_node_id); let to_backend = self.node_id_map.get(&to_node_id);
if let (Some(&from_id), Some(&to_id)) = (from_backend, to_backend) { 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() { if let Some(va_id) = self.va_context() {
// Inside VA template // Inside VA template
if let Some(&backend_track_id) = shared.layer_to_track_map.get(&track_id) { 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(); let mut controller = audio_controller.lock().unwrap();
controller.graph_disconnect_in_template( controller.graph_disconnect_in_template(
backend_track_id, va_id, backend_track_id, va_id,
from_idx.index() as u32, from_port, from_id.index(), from_port,
to_idx.index() as u32, to_port, to_id.index(), to_port,
); );
} }
} }
@ -572,15 +519,13 @@ impl NodeGraphPane {
// Node was deleted // Node was deleted
if let Some(track_id) = self.track_id { if let Some(track_id) = self.track_id {
if let Some(&backend_id) = self.node_id_map.get(&node_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() { if let Some(va_id) = self.va_context() {
// Inside VA template // Inside VA template
if let Some(&backend_track_id) = shared.layer_to_track_map.get(&track_id) { if let Some(&backend_track_id) = shared.layer_to_track_map.get(&track_id) {
if let Some(audio_controller) = &shared.audio_controller { if let Some(audio_controller) = &shared.audio_controller {
let mut controller = audio_controller.lock().unwrap(); let mut controller = audio_controller.lock().unwrap();
controller.graph_remove_node_from_template( 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 // Sync updated position to backend
if let Some(&backend_id) = self.node_id_map.get(&node) { if let Some(&backend_id) = self.node_id_map.get(&node) {
if let Some(pos) = self.state.node_positions.get(node) { if let Some(pos) = self.state.node_positions.get(node) {
let node_index = match backend_id { let node_index = backend_id.index();
BackendNodeId::Audio(idx) => idx.index() as u32,
};
if let Some(audio_controller) = &shared.audio_controller { 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)) { 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(); let mut controller = audio_controller.lock().unwrap();
@ -931,26 +874,18 @@ impl NodeGraphPane {
fn check_parameter_changes(&mut self, shared: &mut crate::panes::SharedPaneState) { fn check_parameter_changes(&mut self, shared: &mut crate::panes::SharedPaneState) {
// Check all input parameters for value changes // 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 { for (input_id, input_param) in &self.state.graph.inputs {
// Only check parameters that can have constant values (not ConnectionOnly) // Only check parameters that can have constant values (not ConnectionOnly)
if matches!(input_param.kind, InputParamKind::ConnectionOnly) { if matches!(input_param.kind, InputParamKind::ConnectionOnly) {
_connection_only_count += 1;
continue; continue;
} }
// Get current value and backend param ID // Get current value and backend param ID
let (current_value, backend_param_id) = match &input_param.value { let (current_value, backend_param_id) = match &input_param.value {
ValueType::Float { value, backend_param_id, .. } => { ValueType::Float { value, backend_param_id, .. } => {
_checked_count += 1;
(*value, *backend_param_id) (*value, *backend_param_id)
}, },
other => { _ => {
_non_float_count += 1;
eprintln!("[DEBUG] Non-float parameter type: {:?}", std::mem::discriminant(other));
continue; continue;
} }
}; };
@ -972,8 +907,6 @@ impl NodeGraphPane {
if let Some(&backend_id) = self.node_id_map.get(&node_id) { if let Some(&backend_id) = self.node_id_map.get(&node_id) {
if let Some(param_id) = backend_param_id { if let Some(param_id) = backend_param_id {
let BackendNodeId::Audio(node_idx) = backend_id;
if let Some(va_id) = self.va_context() { if let Some(va_id) = self.va_context() {
// Inside VA template — call template command directly // Inside VA template — call template command directly
if let Some(&backend_track_id) = shared.layer_to_track_map.get(&track_id) { 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(); let mut controller = audio_controller.lock().unwrap();
controller.graph_set_parameter_in_template( controller.graph_set_parameter_in_template(
backend_track_id, va_id, 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, 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 // Send commands to backend: disconnect old, connect source→drag, connect drag→dest
{ {
let mut controller = audio_controller.lock().unwrap(); let mut controller = audio_controller.lock().unwrap();
controller.graph_disconnect( controller.graph_disconnect(
backend_track_id, backend_track_id,
src_idx.index() as u32, src_port_idx, src_backend.index(), src_port_idx,
dst_idx.index() as u32, dst_port_idx, dst_backend.index(), dst_port_idx,
); );
controller.graph_connect( controller.graph_connect(
backend_track_id, backend_track_id,
src_idx.index() as u32, src_port_idx, src_backend.index(), src_port_idx,
drag_idx.index() as u32, drag_input_port_idx, drag_backend.index(), drag_input_port_idx,
); );
controller.graph_connect( controller.graph_connect(
backend_track_id, backend_track_id,
drag_idx.index() as u32, drag_output_port_idx, drag_backend.index(), drag_output_port_idx,
dst_idx.index() as u32, dst_port_idx, dst_backend.index(), dst_port_idx,
); );
} }
@ -1394,12 +1323,11 @@ impl NodeGraphPane {
// Load the subgraph state from backend // Load the subgraph state from backend
match &context { match &context {
SubgraphContext::VoiceAllocator { backend_id, .. } => { SubgraphContext::VoiceAllocator { backend_id, .. } => {
let BackendNodeId::Audio(va_idx) = *backend_id;
if let Some(track_id) = self.track_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(&backend_track_id) = shared.layer_to_track_map.get(&track_id) {
if let Some(audio_controller) = &shared.audio_controller { if let Some(audio_controller) = &shared.audio_controller {
let mut controller = audio_controller.lock().unwrap(); 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) => { Ok(json) => {
if let Err(e) = self.load_graph_from_json(&json) { if let Err(e) = self.load_graph_from_json(&json) {
eprintln!("Failed to load template state: {}", e); eprintln!("Failed to load template state: {}", e);
@ -1608,8 +1536,7 @@ impl NodeGraphPane {
fn va_context(&self) -> Option<u32> { fn va_context(&self) -> Option<u32> {
for frame in self.subgraph_stack.iter().rev() { for frame in self.subgraph_stack.iter().rev() {
if let SubgraphContext::VoiceAllocator { backend_id, .. } = &frame.context { if let SubgraphContext::VoiceAllocator { backend_id, .. } = &frame.context {
let BackendNodeId::Audio(idx) = *backend_id; return Some(backend_id.index());
return Some(idx.index() as u32);
} }
} }
None None
@ -1664,7 +1591,7 @@ impl NodeGraphPane {
// Collect selected backend IDs // Collect selected backend IDs
let selected_backend_ids: Vec<u32> = self.state.selected_nodes.iter() let selected_backend_ids: Vec<u32> = self.state.selected_nodes.iter()
.filter_map(|fid| self.node_id_map.get(fid)) .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(); .collect();
if selected_backend_ids.is_empty() { 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) { if let (Some(from_fid), Some(to_fid)) = (from_node_fid, to_node_fid) {
let from_bid = self.node_id_map.get(&from_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) 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) { if let (Some(from_b), Some(to_b)) = (from_bid, to_bid) {
let from_in_group = selected_set.contains(&from_b); let from_in_group = selected_set.contains(&from_b);
@ -1938,7 +1865,7 @@ impl NodeGraphPane {
label: group.name.clone(), label: group.name.clone(),
inputs: vec![], inputs: vec![],
outputs: 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 // Add dynamic input ports based on boundary inputs
@ -2010,7 +1937,7 @@ impl NodeGraphPane {
label: "Group Input".to_string(), label: "Group Input".to_string(),
inputs: vec![], inputs: vec![],
outputs: 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 { for bc in &scope_group.boundary_inputs {
@ -2057,7 +1984,7 @@ impl NodeGraphPane {
label: "Group Output".to_string(), label: "Group Output".to_string(),
inputs: vec![], inputs: vec![],
outputs: 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 { for bc in &scope_group.boundary_outputs {
@ -2254,7 +2181,7 @@ impl NodeGraphPane {
label: label.to_string(), label: label.to_string(),
inputs: vec![], inputs: vec![],
outputs: 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); 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 { for (node_id, param_id, value) in changes {
// Send to backend // Send to backend
if let Some(backend_id) = self.node_id_map.get(&node_id) { 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, backend_id.index(), param_id, value);
controller.graph_set_parameter(backend_track_id, node_idx.index() as u32, param_id, value);
} }
// Update frontend graph value // Update frontend graph value
let row_name = format!("Row{}", param_id - 7); 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 { for (node_id, param_id, value) in changes {
// Send to backend // Send to backend
if let Some(backend_id) = self.node_id_map.get(&node_id) { 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, backend_id.index(), param_id, value);
controller.graph_set_parameter(backend_track_id, node_idx.index() as u32, param_id, value);
} }
// Update frontend graph input port value // Update frontend graph input port value
if let Some(node) = self.state.graph.nodes.get(node_id) { 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_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) { 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 { if let Some(controller_arc) = &shared.audio_controller {
let mut controller = controller_arc.lock().unwrap(); let mut controller = controller_arc.lock().unwrap();
controller.send_command(daw_backend::Command::GraphSetScript( 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_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) { 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 { if let Some(controller_arc) = &shared.audio_controller {
let mut controller = controller_arc.lock().unwrap(); let mut controller = controller_arc.lock().unwrap();
controller.send_command(daw_backend::Command::GraphSetScript( 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(); let mut controller = controller_arc.lock().unwrap();
for &node_id in &matching_nodes { for &node_id in &matching_nodes {
if let Some(&backend_id) = self.node_id_map.get(&node_id) { 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( 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 // Delete the node via the graph - queue the deletion
if let Some(track_id) = self.track_id { if let Some(track_id) = self.track_id {
if let Some(&backend_id) = self.node_id_map.get(&ctx_node_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(va_id) = self.va_context() {
if let Some(&backend_track_id) = shared.layer_to_track_map.get(&track_id) { if let Some(&backend_track_id) = shared.layer_to_track_map.get(&track_id) {
if let Some(audio_controller) = &shared.audio_controller { if let Some(audio_controller) = &shared.audio_controller {
let mut controller = audio_controller.lock().unwrap(); let mut controller = audio_controller.lock().unwrap();
controller.graph_remove_node_from_template( controller.graph_remove_node_from_template(
backend_track_id, va_id, node_idx.index() as u32, backend_track_id, va_id, backend_id.index(),
); );
} }
} }

View File

@ -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<PortInfo>,
pub outputs: Vec<PortInfo>,
pub parameters: Vec<ParameterInfo>,
pub description: String,
}
/// Registry of all available node types
pub struct NodeTypeRegistry {
types: HashMap<String, NodeTypeInfo>,
}
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<NodeCategory> {
vec![
NodeCategory::Inputs,
NodeCategory::Generators,
NodeCategory::Effects,
NodeCategory::Utilities,
NodeCategory::Outputs,
]
}
}
impl Default for NodeTypeRegistry {
fn default() -> Self {
Self::new()
}
}

View File

@ -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<NodeCategory>,
/// 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<F>(&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<F>(&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<F>(
&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()
}
}