Use egui_node_graph2 for node graph
This commit is contained in:
parent
fa7bae12a6
commit
c58192a7da
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in New Issue