Use egui_node_graph2 for node graph

This commit is contained in:
Skyler Lehmkuhl 2025-12-16 10:14:34 -05:00
parent fa7bae12a6
commit c58192a7da
4 changed files with 410 additions and 323 deletions

View File

@ -10,10 +10,10 @@ members = [
# Note: Upgraded from 0.29 to 0.31 to fix Linux IME/keyboard input issues
# See: https://github.com/emilk/egui/pull/5198
# Upgraded to 0.33 for shader editor (egui_code_editor) and continued bug fixes
egui = "0.33"
eframe = { version = "0.33", default-features = true, features = ["wgpu"] }
egui_extras = { version = "0.33", features = ["image", "svg", "syntect"] }
egui-wgpu = "0.33"
egui = "0.33.3"
eframe = { version = "0.33.3", default-features = true, features = ["wgpu"] }
egui_extras = { version = "0.33.3", features = ["image", "svg", "syntect"] }
egui-wgpu = "0.33.3"
egui_code_editor = "0.2"
# GPU Rendering

View File

@ -15,7 +15,7 @@ eframe = { workspace = true }
egui_extras = { workspace = true }
egui-wgpu = { workspace = true }
egui_code_editor = { workspace = true }
egui-snarl = "0.9"
egui_node_graph2 = { git = "https://github.com/PVDoriginal/egui_node_graph2" }
# GPU
wgpu = { workspace = true }

View File

@ -1,207 +1,306 @@
//! Graph Data Types for egui-snarl
//! Graph Data Types for egui_node_graph2
//!
//! Node definitions and viewer implementation for audio/MIDI node graph
//! Node definitions and trait implementations for audio/MIDI node graph
use super::backend::BackendNodeId;
use super::node_types::DataType as SignalType;
use eframe::egui;
use egui_snarl::ui::{PinInfo, SnarlStyle, SnarlViewer};
use egui_snarl::{InPin, NodeId, OutPin, Snarl};
use egui_node_graph2::*;
use serde::{Deserialize, Serialize};
/// Audio/MIDI node types
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum AudioNode {
/// Oscillator generator
Oscillator {
frequency: f32,
waveform: String,
},
/// Noise generator
Noise {
color: String,
},
/// Audio filter
Filter {
cutoff: f32,
resonance: f32,
},
/// Gain/volume control
Gain {
gain: f32,
},
/// ADSR envelope
Adsr {
attack: f32,
decay: f32,
sustain: f32,
release: f32,
},
/// LFO modulator
Lfo {
frequency: f32,
waveform: String,
},
/// Audio output
AudioOutput,
/// MIDI input
/// Signal types for audio node graph
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum DataType {
Audio,
Midi,
CV,
}
/// Node templates - types of nodes that can be created
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum NodeTemplate {
// Inputs
MidiInput,
AudioInput,
// Generators
Oscillator,
Noise,
// Effects
Filter,
Gain,
// Utilities
Adsr,
Lfo,
// Outputs
AudioOutput,
}
impl AudioNode {
/// Get the display name for this node type
pub fn type_name(&self) -> &'static str {
/// Custom node data - empty for now, can be extended
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NodeData;
/// Custom graph state - can track selected nodes, etc.
#[derive(Default)]
pub struct GraphState {
pub active_node: Option<NodeId>,
}
/// User response type (empty for now)
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UserResponse {}
impl UserResponseTrait for UserResponse {}
/// Value types for inline parameters
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum ValueType {
Float { value: f32 },
String { value: String },
}
impl Default for ValueType {
fn default() -> Self {
ValueType::Float { value: 0.0 }
}
}
// Implement DataTypeTrait for our signal types
impl DataTypeTrait<GraphState> for DataType {
fn data_type_color(&self, _user_state: &mut GraphState) -> egui::Color32 {
match self {
AudioNode::Oscillator { .. } => "Oscillator",
AudioNode::Noise { .. } => "Noise",
AudioNode::Filter { .. } => "Filter",
AudioNode::Gain { .. } => "Gain",
AudioNode::Adsr { .. } => "ADSR",
AudioNode::Lfo { .. } => "LFO",
AudioNode::AudioOutput => "Audio Output",
AudioNode::MidiInput => "MIDI Input",
DataType::Audio => egui::Color32::from_rgb(100, 150, 255), // Blue
DataType::Midi => egui::Color32::from_rgb(100, 255, 100), // Green
DataType::CV => egui::Color32::from_rgb(255, 150, 100), // Orange
}
}
/// Get the signal type for an output pin
fn output_type(&self, _pin: usize) -> SignalType {
fn name(&self) -> std::borrow::Cow<'_, str> {
match self {
AudioNode::MidiInput => SignalType::Midi,
AudioNode::Lfo { .. } => SignalType::CV,
AudioNode::Adsr { .. } => SignalType::CV,
_ => SignalType::Audio,
}
}
/// Get the signal type for an input pin
fn input_type(&self, pin: usize) -> SignalType {
match self {
AudioNode::Filter { .. } => {
if pin == 0 {
SignalType::Audio
} else {
SignalType::CV
}
}
AudioNode::Gain { .. } => {
if pin == 0 {
SignalType::Audio
} else {
SignalType::CV
}
}
_ => SignalType::Audio,
DataType::Audio => "Audio".into(),
DataType::Midi => "MIDI".into(),
DataType::CV => "CV".into(),
}
}
}
/// Viewer implementation for audio node graph
pub struct AudioNodeViewer;
// Implement NodeTemplateTrait for our node types
impl NodeTemplateTrait for NodeTemplate {
type NodeData = NodeData;
type DataType = DataType;
type ValueType = ValueType;
type UserState = GraphState;
type CategoryType = &'static str;
impl SnarlViewer<AudioNode> for AudioNodeViewer {
fn title(&mut self, node: &AudioNode) -> String {
node.type_name().to_string()
}
fn inputs(&mut self, node: &AudioNode) -> usize {
match node {
AudioNode::Oscillator { .. } => 1, // FM input
AudioNode::Noise { .. } => 0,
AudioNode::Filter { .. } => 2, // Audio + cutoff CV
AudioNode::Gain { .. } => 2, // Audio + gain CV
AudioNode::Adsr { .. } => 1, // Gate/trigger
AudioNode::Lfo { .. } => 0,
AudioNode::AudioOutput => 1,
AudioNode::MidiInput => 0,
fn node_finder_label(&self, _user_state: &mut Self::UserState) -> std::borrow::Cow<'_, str> {
match self {
NodeTemplate::MidiInput => "MIDI Input".into(),
NodeTemplate::AudioInput => "Audio Input".into(),
NodeTemplate::Oscillator => "Oscillator".into(),
NodeTemplate::Noise => "Noise".into(),
NodeTemplate::Filter => "Filter".into(),
NodeTemplate::Gain => "Gain".into(),
NodeTemplate::Adsr => "ADSR".into(),
NodeTemplate::Lfo => "LFO".into(),
NodeTemplate::AudioOutput => "Audio Output".into(),
}
}
fn outputs(&mut self, node: &AudioNode) -> usize {
match node {
AudioNode::AudioOutput => 0,
_ => 1,
fn node_finder_categories(&self, _user_state: &mut Self::UserState) -> Vec<&'static str> {
match self {
NodeTemplate::MidiInput | NodeTemplate::AudioInput => vec!["Inputs"],
NodeTemplate::Oscillator | NodeTemplate::Noise => vec!["Generators"],
NodeTemplate::Filter | NodeTemplate::Gain => vec!["Effects"],
NodeTemplate::Adsr | NodeTemplate::Lfo => vec!["Utilities"],
NodeTemplate::AudioOutput => vec!["Outputs"],
}
}
fn show_input(
&mut self,
pin: &InPin,
ui: &mut egui::Ui,
snarl: &mut Snarl<AudioNode>,
) -> PinInfo {
let node = &snarl[pin.id.node];
let signal_type = node.input_type(pin.id.input);
ui.label(match pin.id.input {
0 => match node {
AudioNode::Oscillator { .. } => "FM",
AudioNode::Filter { .. } => "In",
AudioNode::Gain { .. } => "In",
AudioNode::Adsr { .. } => "Gate",
AudioNode::AudioOutput => "In",
_ => "In",
},
1 => match node {
AudioNode::Filter { .. } => "Cutoff",
AudioNode::Gain { .. } => "Gain",
_ => "In",
},
_ => "In",
});
PinInfo::square().with_fill(signal_type.color())
fn node_graph_label(&self, user_state: &mut Self::UserState) -> String {
self.node_finder_label(user_state).into()
}
fn show_output(
&mut self,
pin: &OutPin,
ui: &mut egui::Ui,
snarl: &mut Snarl<AudioNode>,
) -> PinInfo {
let node = &snarl[pin.id.node];
let signal_type = node.output_type(pin.id.output);
ui.label("Out");
PinInfo::square().with_fill(signal_type.color())
fn user_data(&self, _user_state: &mut Self::UserState) -> Self::NodeData {
NodeData
}
fn connect(&mut self, from: &OutPin, to: &InPin, snarl: &mut Snarl<AudioNode>) {
let from_node = &snarl[from.id.node];
let to_node = &snarl[to.id.node];
let from_type = from_node.output_type(from.id.output);
let to_type = to_node.input_type(to.id.input);
// Only allow connections between compatible signal types
if from_type == to_type {
// Disconnect existing connection to this input
for remote_out in snarl.in_pin(to.id).remotes.iter().copied().collect::<Vec<_>>() {
snarl.disconnect(remote_out, to.id);
}
// Create new connection
snarl.connect(from.id, to.id);
}
}
fn has_graph_menu(&mut self, _pos: egui::Pos2, _snarl: &mut Snarl<AudioNode>) -> bool {
false // We use the palette instead
}
fn has_node_menu(&mut self, _node: &AudioNode) -> bool {
true
}
fn show_node_menu(
&mut self,
node: NodeId,
_inputs: &[InPin],
_outputs: &[OutPin],
ui: &mut egui::Ui,
snarl: &mut Snarl<AudioNode>,
fn build_node(
&self,
graph: &mut Graph<Self::NodeData, Self::DataType, Self::ValueType>,
_user_state: &mut Self::UserState,
node_id: NodeId,
) {
if ui.button("Remove").clicked() {
snarl.remove_node(node);
ui.close_menu();
match self {
NodeTemplate::Oscillator => {
// FM input
graph.add_input_param(
node_id,
"FM".into(),
DataType::Audio,
ValueType::Float { value: 0.0 },
InputParamKind::ConnectionOnly,
true,
);
// Frequency parameter
graph.add_input_param(
node_id,
"Freq".into(),
DataType::CV,
ValueType::Float { value: 440.0 },
InputParamKind::ConstantOnly,
true,
);
// Audio output
graph.add_output_param(node_id, "Out".into(), DataType::Audio);
}
NodeTemplate::Noise => {
graph.add_output_param(node_id, "Out".into(), DataType::Audio);
}
NodeTemplate::Filter => {
graph.add_input_param(
node_id,
"In".into(),
DataType::Audio,
ValueType::Float { value: 0.0 },
InputParamKind::ConnectionOnly,
true,
);
graph.add_input_param(
node_id,
"Cutoff".into(),
DataType::CV,
ValueType::Float { value: 1000.0 },
InputParamKind::ConnectionOrConstant,
true,
);
graph.add_output_param(node_id, "Out".into(), DataType::Audio);
}
NodeTemplate::Gain => {
graph.add_input_param(
node_id,
"In".into(),
DataType::Audio,
ValueType::Float { value: 0.0 },
InputParamKind::ConnectionOnly,
true,
);
graph.add_input_param(
node_id,
"Gain".into(),
DataType::CV,
ValueType::Float { value: 1.0 },
InputParamKind::ConnectionOrConstant,
true,
);
graph.add_output_param(node_id, "Out".into(), DataType::Audio);
}
NodeTemplate::Adsr => {
graph.add_input_param(
node_id,
"Gate".into(),
DataType::Midi,
ValueType::Float { value: 0.0 },
InputParamKind::ConnectionOnly,
true,
);
graph.add_output_param(node_id, "Out".into(), DataType::CV);
}
NodeTemplate::Lfo => {
graph.add_output_param(node_id, "Out".into(), DataType::CV);
}
NodeTemplate::AudioOutput => {
graph.add_input_param(
node_id,
"In".into(),
DataType::Audio,
ValueType::Float { value: 0.0 },
InputParamKind::ConnectionOnly,
true,
);
}
NodeTemplate::AudioInput => {
graph.add_output_param(node_id, "Out".into(), DataType::Audio);
}
NodeTemplate::MidiInput => {
graph.add_output_param(node_id, "Out".into(), DataType::Midi);
}
}
}
}
// Implement WidgetValueTrait for parameter editing
impl WidgetValueTrait for ValueType {
type Response = UserResponse;
type UserState = GraphState;
type NodeData = NodeData;
fn value_widget(
&mut self,
param_name: &str,
_node_id: NodeId,
ui: &mut egui::Ui,
_user_state: &mut Self::UserState,
_node_data: &Self::NodeData,
) -> Vec<Self::Response> {
match self {
ValueType::Float { value } => {
ui.horizontal(|ui| {
ui.label(param_name);
ui.add(egui::DragValue::new(value).speed(0.1));
});
}
ValueType::String { value } => {
ui.horizontal(|ui| {
ui.label(param_name);
ui.text_edit_singleline(value);
});
}
}
vec![]
}
}
// Implement NodeDataTrait for custom node UI (optional)
impl NodeDataTrait for NodeData {
type Response = UserResponse;
type UserState = GraphState;
type DataType = DataType;
type ValueType = ValueType;
fn bottom_ui(
&self,
ui: &mut egui::Ui,
_node_id: NodeId,
_graph: &Graph<NodeData, DataType, ValueType>,
_user_state: &mut Self::UserState,
) -> Vec<NodeResponse<Self::Response, NodeData>>
where
Self::Response: UserResponseTrait,
{
// No custom UI for now
ui.label("");
vec![]
}
}
// Iterator for all node templates
pub struct AllNodeTemplates;
impl NodeTemplateIter for AllNodeTemplates {
type Item = NodeTemplate;
fn all_kinds(&self) -> Vec<Self::Item> {
vec![
NodeTemplate::MidiInput,
NodeTemplate::AudioInput,
NodeTemplate::Oscillator,
NodeTemplate::Noise,
NodeTemplate::Filter,
NodeTemplate::Gain,
NodeTemplate::Adsr,
NodeTemplate::Lfo,
NodeTemplate::AudioOutput,
]
}
}

View File

@ -7,32 +7,22 @@ pub mod audio_backend;
pub mod backend;
pub mod graph_data;
pub mod node_types;
pub mod palette;
use backend::{BackendNodeId, GraphBackend};
use graph_data::{AudioNode, AudioNodeViewer};
use node_types::NodeTypeRegistry;
use palette::NodePalette;
use graph_data::{AllNodeTemplates, DataType, GraphState, NodeData, NodeTemplate, ValueType};
use super::NodePath;
use eframe::egui;
use egui_snarl::ui::{SnarlWidget, SnarlStyle, BackgroundPattern, Grid};
use egui_snarl::Snarl;
use egui_node_graph2::*;
use std::collections::HashMap;
use uuid::Uuid;
/// Node graph pane with egui-snarl integration
/// Node graph pane with egui_node_graph2 integration
pub struct NodeGraphPane {
/// The graph structure
snarl: Snarl<AudioNode>,
/// The graph editor state
state: GraphEditorState<NodeData, DataType, ValueType, NodeTemplate, GraphState>,
/// Node viewer for rendering
viewer: AudioNodeViewer,
/// Node palette (left sidebar)
palette: NodePalette,
/// Node type registry
node_registry: NodeTypeRegistry,
/// User state for the graph
user_state: GraphState,
/// Backend integration
#[allow(dead_code)]
@ -40,7 +30,7 @@ pub struct NodeGraphPane {
/// Maps frontend node IDs to backend node IDs
#[allow(dead_code)]
node_id_map: HashMap<egui_snarl::NodeId, BackendNodeId>,
node_id_map: HashMap<NodeId, BackendNodeId>,
/// Track ID this graph belongs to
#[allow(dead_code)]
@ -49,38 +39,26 @@ pub struct NodeGraphPane {
/// Pending action to execute
#[allow(dead_code)]
pending_action: Option<Box<dyn lightningbeam_core::action::Action>>,
/// Counter for offsetting clicked nodes
click_node_offset: f32,
}
impl NodeGraphPane {
pub fn new() -> Self {
let mut snarl = Snarl::new();
// Add a test node to verify rendering works
snarl.insert_node(
egui::pos2(300.0, 200.0),
AudioNode::Oscillator {
frequency: 440.0,
waveform: "sine".to_string(),
},
);
let state = GraphEditorState::new(1.0);
Self {
snarl,
viewer: AudioNodeViewer,
palette: NodePalette::new(),
node_registry: NodeTypeRegistry::new(),
state,
user_state: GraphState::default(),
backend: None,
node_id_map: HashMap::new(),
track_id: None,
pending_action: None,
click_node_offset: 0.0,
}
}
pub fn with_track_id(track_id: Uuid, audio_controller: std::sync::Arc<std::sync::Mutex<daw_backend::EngineController>>) -> Self {
pub fn with_track_id(
track_id: Uuid,
audio_controller: std::sync::Arc<std::sync::Mutex<daw_backend::EngineController>>,
) -> Self {
// Get backend track ID (placeholder - would need actual mapping)
let backend_track_id = 0;
@ -90,15 +68,68 @@ impl NodeGraphPane {
));
Self {
snarl: Snarl::new(),
viewer: AudioNodeViewer,
palette: NodePalette::new(),
node_registry: NodeTypeRegistry::new(),
state: GraphEditorState::new(1.0),
user_state: GraphState::default(),
backend: Some(backend),
node_id_map: HashMap::new(),
track_id: Some(track_id),
pending_action: None,
click_node_offset: 0.0,
}
}
fn draw_dot_grid_background(
ui: &mut egui::Ui,
rect: egui::Rect,
bg_color: egui::Color32,
dot_color: egui::Color32,
pan_zoom: &egui_node_graph2::PanZoom,
) {
let painter = ui.painter();
// Draw background
painter.rect_filled(rect, 0.0, bg_color);
// Draw grid dots with pan/zoom transform
let grid_spacing = 20.0;
let dot_radius = 1.0 * pan_zoom.zoom;
// Get pan offset and zoom
let pan = pan_zoom.pan;
let zoom = pan_zoom.zoom;
// Calculate zoom center (same as nodes - they zoom relative to viewport center)
let half_size = rect.size() / 2.0;
let zoom_center = rect.min.to_vec2() + half_size - pan;
// Calculate grid bounds in graph space
// Screen to graph: (screen_pos - zoom_center) / zoom
let graph_min = egui::pos2(
(rect.min.x - zoom_center.x) / zoom,
(rect.min.y - zoom_center.y) / zoom,
);
let graph_max = egui::pos2(
(rect.max.x - zoom_center.x) / zoom,
(rect.max.y - zoom_center.y) / zoom,
);
let start_x = (graph_min.x / grid_spacing).floor() * grid_spacing;
let start_y = (graph_min.y / grid_spacing).floor() * grid_spacing;
let mut y = start_y;
while y < graph_max.y {
let mut x = start_x;
while x < graph_max.x {
// Transform to screen space: graph_pos * zoom + zoom_center
let screen_pos = egui::pos2(
x * zoom + zoom_center.x,
y * zoom + zoom_center.y,
);
if rect.contains(screen_pos) {
painter.circle_filled(screen_pos, dot_radius, dot_color);
}
x += grid_spacing;
}
y += grid_spacing;
}
}
}
@ -109,70 +140,71 @@ impl crate::panes::PaneRenderer for NodeGraphPane {
ui: &mut egui::Ui,
rect: egui::Rect,
_path: &NodePath,
_shared: &mut crate::panes::SharedPaneState,
shared: &mut crate::panes::SharedPaneState,
) {
// Use a horizontal layout for palette + graph
// Get colors from theme
let bg_style = shared.theme.style(".node-graph-background", ui.ctx());
let grid_style = shared.theme.style(".node-graph-grid", ui.ctx());
let bg_color = bg_style.background_color.unwrap_or(egui::Color32::from_gray(45));
let grid_color = grid_style.background_color.unwrap_or(egui::Color32::from_gray(55));
// Allocate the rect and render the graph editor within it
ui.allocate_ui_at_rect(rect, |ui| {
ui.horizontal(|ui| {
// Track clicked node from palette
let mut clicked_node: Option<String> = None;
// Check for scroll input to override library's default zoom behavior
let scroll_delta = ui.input(|i| i.smooth_scroll_delta);
let modifiers = ui.input(|i| i.modifiers);
let has_scroll = scroll_delta != egui::Vec2::ZERO;
let has_ctrl = modifiers.ctrl || modifiers.command;
// Left panel: Node palette (fixed width)
ui.allocate_ui_with_layout(
egui::vec2(200.0, rect.height()),
egui::Layout::top_down(egui::Align::Min),
|ui| {
let palette_rect = ui.available_rect_before_wrap();
self.palette.render(ui, palette_rect, |node_type| {
clicked_node = Some(node_type.to_string());
});
},
// Save current zoom to detect if library changed it
let zoom_before = self.state.pan_zoom.zoom;
let pan_before = self.state.pan_zoom.pan;
// Draw dot grid background with pan/zoom
let pan_zoom = &self.state.pan_zoom;
Self::draw_dot_grid_background(ui, rect, bg_color, grid_color, pan_zoom);
// Draw the graph editor (library will process scroll as zoom by default)
let _graph_response = self.state.draw_graph_editor(
ui,
AllNodeTemplates,
&mut self.user_state,
Vec::default(),
);
// Right panel: Graph area (fill remaining space)
ui.allocate_ui_with_layout(
ui.available_size(),
egui::Layout::top_down(egui::Align::Min),
// Override library's default scroll behavior:
// - Library uses scroll for zoom
// - We want: scroll = pan, ctrl+scroll = zoom
if has_scroll && ui.rect_contains_pointer(rect) {
if !has_ctrl {
// Scroll without ctrl: library zoomed, but we want pan instead
// Undo the zoom and apply pan
if self.state.pan_zoom.zoom != zoom_before {
// Library changed zoom - revert it
let undo_zoom = zoom_before / self.state.pan_zoom.zoom;
self.state.zoom(ui, undo_zoom);
}
// Apply pan
self.state.pan_zoom.pan = pan_before + scroll_delta;
}
// If ctrl is held, library already zoomed correctly, so do nothing
}
// Draw menu button in top-left corner
let button_pos = rect.min + egui::vec2(8.0, 8.0);
ui.allocate_ui_at_rect(
egui::Rect::from_min_size(button_pos, egui::vec2(100.0, 24.0)),
|ui| {
let mut style = SnarlStyle::new();
style.bg_pattern = Some(BackgroundPattern::Grid(Grid::default()));
// Get the graph rect before showing the widget
let graph_rect = ui.available_rect_before_wrap();
let response = SnarlWidget::new()
.style(style)
.show(&mut self.snarl, &mut self.viewer, ui);
// Handle drop first - check for released payload
let mut handled_drop = false;
if let Some(payload) = response.dnd_release_payload::<String>() {
// Try using hover_pos from response, which should be in the right coordinate space
if let Some(pos) = response.hover_pos() {
println!("Drop detected! Node type: {} at hover_pos {:?}", payload, pos);
self.add_node_at_position(&payload, pos);
handled_drop = true;
}
}
// Add clicked node at center only if we didn't handle a drop
if !handled_drop {
if let Some(ref node_type) = clicked_node {
// Place at a fixed graph-space position (origin) with small offset to avoid stacking
// This ensures nodes appear at a predictable location regardless of pan/zoom
let pos = egui::pos2(self.click_node_offset, self.click_node_offset);
self.click_node_offset += 30.0;
if self.click_node_offset > 300.0 {
self.click_node_offset = 0.0;
}
println!("Click detected! Adding {} at graph origin with offset {:?}", node_type, pos);
self.add_node_at_position(node_type, pos);
}
if ui.button(" Add Node").clicked() {
// Open node finder at button's top-left position
self.state.node_finder = Some(egui_node_graph2::NodeFinder::new_at(button_pos));
}
},
);
});
});
// TODO: Handle node responses and sync with backend
}
fn name(&self) -> &str {
@ -180,50 +212,6 @@ impl crate::panes::PaneRenderer for NodeGraphPane {
}
}
impl NodeGraphPane {
/// Add a node at a specific position
fn add_node_at_position(&mut self, node_type: &str, pos: egui::Pos2) {
println!("add_node_at_position called with: {} at {:?}", node_type, pos);
// Map node type string to AudioNode enum
let node = match node_type {
"Oscillator" => AudioNode::Oscillator {
frequency: 440.0,
waveform: "sine".to_string(),
},
"Noise" => AudioNode::Noise {
color: "white".to_string(),
},
"Filter" => AudioNode::Filter {
cutoff: 1000.0,
resonance: 0.5,
},
"Gain" => AudioNode::Gain { gain: 1.0 },
"ADSR" => AudioNode::Adsr {
attack: 0.01,
decay: 0.1,
sustain: 0.7,
release: 0.3,
},
"LFO" => AudioNode::Lfo {
frequency: 1.0,
waveform: "sine".to_string(),
},
"AudioOutput" => AudioNode::AudioOutput,
"AudioInput" => AudioNode::AudioOutput, // Map to output for now
"MidiInput" => AudioNode::MidiInput,
_ => {
eprintln!("Unknown node type: {}", node_type);
return;
}
};
let node_id = self.snarl.insert_node(pos, node);
println!("Added node: {} (ID: {:?}) at position {:?}", node_type, node_id, pos);
}
}
impl Default for NodeGraphPane {
fn default() -> Self {
Self::new()