Code cleanup
This commit is contained in:
parent
a98b59a6d3
commit
c344e11e42
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<BackendNodeId, String> {
|
||||
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,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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<SerializedNode>,
|
||||
pub connections: Vec<SerializedConnection>,
|
||||
}
|
||||
|
||||
#[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,
|
||||
|
|
|
|||
|
|
@ -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<f32>,
|
||||
|
|
@ -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<f32> = draw_vm.params.clone();
|
||||
let params_before: Vec<f32> = 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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Box<dyn GraphBackend>>,
|
||||
|
||||
/// Maps frontend node IDs to backend node IDs
|
||||
|
|
@ -101,7 +99,6 @@ pub struct NodeGraphPane {
|
|||
track_id: Option<Uuid>,
|
||||
|
||||
/// Pending action to execute
|
||||
#[allow(dead_code)]
|
||||
pending_action: Option<Box<dyn lightningbeam_core::action::Action>>,
|
||||
|
||||
/// 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
|
||||
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<u32> {
|
||||
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<u32> = 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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue