Node graph initial work
This commit is contained in:
parent
dda1319c42
commit
798d8420af
|
|
@ -15,6 +15,7 @@ eframe = { workspace = true }
|
|||
egui_extras = { workspace = true }
|
||||
egui-wgpu = { workspace = true }
|
||||
egui_code_editor = { workspace = true }
|
||||
egui-snarl = "0.9"
|
||||
|
||||
# GPU
|
||||
wgpu = { workspace = true }
|
||||
|
|
@ -42,6 +43,7 @@ pollster = { workspace = true }
|
|||
lightningcss = "1.0.0-alpha.68"
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
uuid = { version = "1.0", features = ["v4", "serde"] }
|
||||
petgraph = "0.6"
|
||||
|
||||
# Native file dialogs
|
||||
rfd = "0.15"
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ pub mod outliner;
|
|||
pub mod piano_roll;
|
||||
pub mod virtual_piano;
|
||||
pub mod node_editor;
|
||||
pub mod node_graph;
|
||||
pub mod preset_browser;
|
||||
pub mod asset_library;
|
||||
pub mod shader_editor;
|
||||
|
|
|
|||
|
|
@ -1,45 +1,5 @@
|
|||
/// Node Editor pane - node-based visual programming
|
||||
///
|
||||
/// This will eventually render a node graph with Vello.
|
||||
/// For now, it's a placeholder.
|
||||
//! Node Editor pane - node-based audio/MIDI synthesis
|
||||
//!
|
||||
//! Re-exports the node graph implementation
|
||||
|
||||
use eframe::egui;
|
||||
use super::{NodePath, PaneRenderer, SharedPaneState};
|
||||
|
||||
pub struct NodeEditorPane {}
|
||||
|
||||
impl NodeEditorPane {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl PaneRenderer for NodeEditorPane {
|
||||
fn render_content(
|
||||
&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
rect: egui::Rect,
|
||||
_path: &NodePath,
|
||||
_shared: &mut SharedPaneState,
|
||||
) {
|
||||
// Placeholder rendering
|
||||
ui.painter().rect_filled(
|
||||
rect,
|
||||
0.0,
|
||||
egui::Color32::from_rgb(30, 45, 50),
|
||||
);
|
||||
|
||||
let text = "Node Editor\n(TODO: Implement node graph)";
|
||||
ui.painter().text(
|
||||
rect.center(),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
text,
|
||||
egui::FontId::proportional(16.0),
|
||||
egui::Color32::from_gray(150),
|
||||
);
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
"Node Editor"
|
||||
}
|
||||
}
|
||||
pub use super::node_graph::NodeGraphPane as NodeEditorPane;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,532 @@
|
|||
//! Node Graph Actions
|
||||
//!
|
||||
//! Implements the Action trait for node graph operations, ensuring undo/redo support
|
||||
|
||||
use super::backend::BackendNodeId;
|
||||
use lightningbeam_core::action::{Action, BackendContext};
|
||||
use lightningbeam_core::document::Document;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Node graph action variants
|
||||
///
|
||||
/// Note: Node graph state is managed by the audio backend, not the document.
|
||||
/// Therefore, execute() and rollback() are no-ops, and all work happens in
|
||||
/// execute_backend() and rollback_backend().
|
||||
pub enum NodeGraphAction {
|
||||
AddNode(AddNodeAction),
|
||||
RemoveNode(RemoveNodeAction),
|
||||
MoveNode(MoveNodeAction),
|
||||
Connect(ConnectAction),
|
||||
Disconnect(DisconnectAction),
|
||||
SetParameter(SetParameterAction),
|
||||
}
|
||||
|
||||
impl Action for NodeGraphAction {
|
||||
fn execute(&mut self, _document: &mut Document) -> Result<(), String> {
|
||||
// Node graph state is in the audio backend, not document
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn rollback(&mut self, _document: &mut Document) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
match self {
|
||||
NodeGraphAction::AddNode(a) => a.description(),
|
||||
NodeGraphAction::RemoveNode(a) => a.description(),
|
||||
NodeGraphAction::MoveNode(a) => a.description(),
|
||||
NodeGraphAction::Connect(a) => a.description(),
|
||||
NodeGraphAction::Disconnect(a) => a.description(),
|
||||
NodeGraphAction::SetParameter(a) => a.description(),
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_backend(
|
||||
&mut self,
|
||||
backend: &mut BackendContext,
|
||||
document: &Document,
|
||||
) -> Result<(), String> {
|
||||
match self {
|
||||
NodeGraphAction::AddNode(a) => a.execute_backend(backend, document),
|
||||
NodeGraphAction::RemoveNode(a) => a.execute_backend(backend, document),
|
||||
NodeGraphAction::MoveNode(a) => a.execute_backend(backend, document),
|
||||
NodeGraphAction::Connect(a) => a.execute_backend(backend, document),
|
||||
NodeGraphAction::Disconnect(a) => a.execute_backend(backend, document),
|
||||
NodeGraphAction::SetParameter(a) => a.execute_backend(backend, document),
|
||||
}
|
||||
}
|
||||
|
||||
fn rollback_backend(
|
||||
&mut self,
|
||||
backend: &mut BackendContext,
|
||||
document: &Document,
|
||||
) -> Result<(), String> {
|
||||
match self {
|
||||
NodeGraphAction::AddNode(a) => a.rollback_backend(backend, document),
|
||||
NodeGraphAction::RemoveNode(a) => a.rollback_backend(backend, document),
|
||||
NodeGraphAction::MoveNode(a) => a.rollback_backend(backend, document),
|
||||
NodeGraphAction::Connect(a) => a.rollback_backend(backend, document),
|
||||
NodeGraphAction::Disconnect(a) => a.rollback_backend(backend, document),
|
||||
NodeGraphAction::SetParameter(a) => a.rollback_backend(backend, document),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// AddNodeAction
|
||||
// ============================================================================
|
||||
|
||||
pub struct AddNodeAction {
|
||||
/// Layer ID (maps to backend track ID)
|
||||
layer_id: Uuid,
|
||||
|
||||
/// Node type to add
|
||||
node_type: String,
|
||||
|
||||
/// Position in canvas coordinates
|
||||
position: (f32, f32),
|
||||
|
||||
/// Backend node ID (stored after execute for rollback)
|
||||
backend_node_id: Option<BackendNodeId>,
|
||||
}
|
||||
|
||||
impl AddNodeAction {
|
||||
pub fn new(layer_id: Uuid, node_type: String, position: (f32, f32)) -> Self {
|
||||
Self {
|
||||
layer_id,
|
||||
node_type,
|
||||
position,
|
||||
backend_node_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
format!("Add {} node", self.node_type)
|
||||
}
|
||||
|
||||
fn execute_backend(
|
||||
&mut self,
|
||||
backend: &mut BackendContext,
|
||||
_document: &Document,
|
||||
) -> Result<(), String> {
|
||||
let controller = backend
|
||||
.audio_controller
|
||||
.as_mut()
|
||||
.ok_or("Audio controller not available")?;
|
||||
|
||||
let track_id = backend
|
||||
.layer_to_track_map
|
||||
.get(&self.layer_id)
|
||||
.ok_or("Track not found")?;
|
||||
|
||||
// Add node to backend (using async API for now - TODO: use sync query)
|
||||
controller.graph_add_node(*track_id, self.node_type.clone(), self.position.0, self.position.1);
|
||||
|
||||
// TODO: Get actual node ID from synchronous query
|
||||
// For now, we can't track the backend ID properly with async API
|
||||
// This will be fixed when we add synchronous query methods
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn rollback_backend(
|
||||
&mut self,
|
||||
backend: &mut BackendContext,
|
||||
_document: &Document,
|
||||
) -> Result<(), String> {
|
||||
if let Some(backend_id) = self.backend_node_id {
|
||||
let controller = backend
|
||||
.audio_controller
|
||||
.as_mut()
|
||||
.ok_or("Audio controller not available")?;
|
||||
|
||||
let track_id = backend
|
||||
.layer_to_track_map
|
||||
.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);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RemoveNodeAction
|
||||
// ============================================================================
|
||||
|
||||
pub struct RemoveNodeAction {
|
||||
layer_id: Uuid,
|
||||
backend_node_id: BackendNodeId,
|
||||
|
||||
// Store node state for undo (TODO: implement when we have graph state query)
|
||||
#[allow(dead_code)]
|
||||
node_type: Option<String>,
|
||||
#[allow(dead_code)]
|
||||
position: Option<(f32, f32)>,
|
||||
}
|
||||
|
||||
impl RemoveNodeAction {
|
||||
pub fn new(layer_id: Uuid, backend_node_id: BackendNodeId) -> Self {
|
||||
Self {
|
||||
layer_id,
|
||||
backend_node_id,
|
||||
node_type: None,
|
||||
position: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Remove node".to_string()
|
||||
}
|
||||
|
||||
fn execute_backend(
|
||||
&mut self,
|
||||
backend: &mut BackendContext,
|
||||
_document: &Document,
|
||||
) -> Result<(), String> {
|
||||
// TODO: Query and store node state before removing for undo
|
||||
|
||||
let controller = backend
|
||||
.audio_controller
|
||||
.as_mut()
|
||||
.ok_or("Audio controller not available")?;
|
||||
|
||||
let track_id = backend
|
||||
.layer_to_track_map
|
||||
.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);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn rollback_backend(
|
||||
&mut self,
|
||||
_backend: &mut BackendContext,
|
||||
_document: &Document,
|
||||
) -> Result<(), String> {
|
||||
// TODO: Re-add node using stored state
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MoveNodeAction
|
||||
// ============================================================================
|
||||
|
||||
pub struct MoveNodeAction {
|
||||
layer_id: Uuid,
|
||||
backend_node_id: BackendNodeId,
|
||||
new_position: (f32, f32),
|
||||
old_position: Option<(f32, f32)>,
|
||||
}
|
||||
|
||||
impl MoveNodeAction {
|
||||
pub fn new(layer_id: Uuid, backend_node_id: BackendNodeId, new_position: (f32, f32)) -> Self {
|
||||
Self {
|
||||
layer_id,
|
||||
backend_node_id,
|
||||
new_position,
|
||||
old_position: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Move node".to_string()
|
||||
}
|
||||
|
||||
fn execute_backend(
|
||||
&mut self,
|
||||
_backend: &mut BackendContext,
|
||||
_document: &Document,
|
||||
) -> Result<(), String> {
|
||||
// TODO: Query old position and call graph_move_node() when available
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn rollback_backend(
|
||||
&mut self,
|
||||
_backend: &mut BackendContext,
|
||||
_document: &Document,
|
||||
) -> Result<(), String> {
|
||||
// TODO: Restore old position
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ConnectAction
|
||||
// ============================================================================
|
||||
|
||||
pub struct ConnectAction {
|
||||
layer_id: Uuid,
|
||||
from_node: BackendNodeId,
|
||||
from_port: usize,
|
||||
to_node: BackendNodeId,
|
||||
to_port: usize,
|
||||
}
|
||||
|
||||
impl ConnectAction {
|
||||
pub fn new(
|
||||
layer_id: Uuid,
|
||||
from_node: BackendNodeId,
|
||||
from_port: usize,
|
||||
to_node: BackendNodeId,
|
||||
to_port: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
layer_id,
|
||||
from_node,
|
||||
from_port,
|
||||
to_node,
|
||||
to_port,
|
||||
}
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Connect nodes".to_string()
|
||||
}
|
||||
|
||||
fn execute_backend(
|
||||
&mut self,
|
||||
backend: &mut BackendContext,
|
||||
_document: &Document,
|
||||
) -> Result<(), String> {
|
||||
let controller = backend
|
||||
.audio_controller
|
||||
.as_mut()
|
||||
.ok_or("Audio controller not available")?;
|
||||
|
||||
let track_id = backend
|
||||
.layer_to_track_map
|
||||
.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_port,
|
||||
to_idx.index() as u32,
|
||||
self.to_port,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn rollback_backend(
|
||||
&mut self,
|
||||
backend: &mut BackendContext,
|
||||
_document: &Document,
|
||||
) -> Result<(), String> {
|
||||
let controller = backend
|
||||
.audio_controller
|
||||
.as_mut()
|
||||
.ok_or("Audio controller not available")?;
|
||||
|
||||
let track_id = backend
|
||||
.layer_to_track_map
|
||||
.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_port,
|
||||
to_idx.index() as u32,
|
||||
self.to_port,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DisconnectAction
|
||||
// ============================================================================
|
||||
|
||||
pub struct DisconnectAction {
|
||||
layer_id: Uuid,
|
||||
from_node: BackendNodeId,
|
||||
from_port: usize,
|
||||
to_node: BackendNodeId,
|
||||
to_port: usize,
|
||||
}
|
||||
|
||||
impl DisconnectAction {
|
||||
pub fn new(
|
||||
layer_id: Uuid,
|
||||
from_node: BackendNodeId,
|
||||
from_port: usize,
|
||||
to_node: BackendNodeId,
|
||||
to_port: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
layer_id,
|
||||
from_node,
|
||||
from_port,
|
||||
to_node,
|
||||
to_port,
|
||||
}
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Disconnect nodes".to_string()
|
||||
}
|
||||
|
||||
fn execute_backend(
|
||||
&mut self,
|
||||
backend: &mut BackendContext,
|
||||
_document: &Document,
|
||||
) -> Result<(), String> {
|
||||
let controller = backend
|
||||
.audio_controller
|
||||
.as_mut()
|
||||
.ok_or("Audio controller not available")?;
|
||||
|
||||
let track_id = backend
|
||||
.layer_to_track_map
|
||||
.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_port,
|
||||
to_idx.index() as u32,
|
||||
self.to_port,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn rollback_backend(
|
||||
&mut self,
|
||||
backend: &mut BackendContext,
|
||||
_document: &Document,
|
||||
) -> Result<(), String> {
|
||||
// Undo disconnect by reconnecting
|
||||
let controller = backend
|
||||
.audio_controller
|
||||
.as_mut()
|
||||
.ok_or("Audio controller not available")?;
|
||||
|
||||
let track_id = backend
|
||||
.layer_to_track_map
|
||||
.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_port,
|
||||
to_idx.index() as u32,
|
||||
self.to_port,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SetParameterAction
|
||||
// ============================================================================
|
||||
|
||||
pub struct SetParameterAction {
|
||||
layer_id: Uuid,
|
||||
backend_node_id: BackendNodeId,
|
||||
param_id: u32,
|
||||
new_value: f64,
|
||||
old_value: Option<f64>,
|
||||
}
|
||||
|
||||
impl SetParameterAction {
|
||||
pub fn new(layer_id: Uuid, backend_node_id: BackendNodeId, param_id: u32, new_value: f64) -> Self {
|
||||
Self {
|
||||
layer_id,
|
||||
backend_node_id,
|
||||
param_id,
|
||||
new_value,
|
||||
old_value: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Set parameter".to_string()
|
||||
}
|
||||
|
||||
fn execute_backend(
|
||||
&mut self,
|
||||
backend: &mut BackendContext,
|
||||
_document: &Document,
|
||||
) -> Result<(), String> {
|
||||
// TODO: Query and store old value before changing
|
||||
|
||||
let controller = backend
|
||||
.audio_controller
|
||||
.as_mut()
|
||||
.ok_or("Audio controller not available")?;
|
||||
|
||||
let track_id = backend
|
||||
.layer_to_track_map
|
||||
.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.param_id,
|
||||
self.new_value as f32,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn rollback_backend(
|
||||
&mut self,
|
||||
backend: &mut BackendContext,
|
||||
_document: &Document,
|
||||
) -> Result<(), String> {
|
||||
if let Some(old_value) = self.old_value {
|
||||
let controller = backend
|
||||
.audio_controller
|
||||
.as_mut()
|
||||
.ok_or("Audio controller not available")?;
|
||||
|
||||
let track_id = backend
|
||||
.layer_to_track_map
|
||||
.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.param_id,
|
||||
old_value as f32,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,212 @@
|
|||
//! Audio Graph Backend Implementation
|
||||
//!
|
||||
//! Wraps daw_backend's EngineController for audio node graph operations
|
||||
|
||||
use super::backend::{BackendNodeId, GraphBackend, GraphState};
|
||||
use daw_backend::EngineController;
|
||||
use petgraph::stable_graph::NodeIndex;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// Audio graph backend wrapping daw_backend
|
||||
pub struct AudioGraphBackend {
|
||||
/// Track ID this graph belongs to
|
||||
track_id: u32,
|
||||
|
||||
/// Shared audio controller (thread-safe)
|
||||
audio_controller: Arc<Mutex<EngineController>>,
|
||||
|
||||
/// Maps backend NodeIndex to stable IDs for round-trip serialization
|
||||
node_index_to_stable: HashMap<NodeIndex, u32>,
|
||||
next_stable_id: u32,
|
||||
}
|
||||
|
||||
impl AudioGraphBackend {
|
||||
pub fn new(track_id: u32, audio_controller: Arc<Mutex<EngineController>>) -> Self {
|
||||
Self {
|
||||
track_id,
|
||||
audio_controller,
|
||||
node_index_to_stable: HashMap::new(),
|
||||
next_stable_id: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GraphBackend for AudioGraphBackend {
|
||||
fn add_node(&mut self, node_type: &str, x: f32, y: f32) -> Result<BackendNodeId, String> {
|
||||
// TODO: Call EngineController.graph_add_node_sync() once implemented
|
||||
// For now, return placeholder
|
||||
let mut controller = self.audio_controller.lock().unwrap();
|
||||
controller.graph_add_node(self.track_id, node_type.to_string(), x, y);
|
||||
|
||||
// Generate placeholder node ID
|
||||
// This will be replaced with actual backend NodeIndex from sync query
|
||||
let stable_id = self.next_stable_id;
|
||||
self.next_stable_id += 1;
|
||||
|
||||
// Placeholder: use stable_id as backend index (will be wrong, but compiles)
|
||||
let node_idx = NodeIndex::new(stable_id as usize);
|
||||
self.node_index_to_stable.insert(node_idx, stable_id);
|
||||
|
||||
Ok(BackendNodeId::Audio(node_idx))
|
||||
}
|
||||
|
||||
fn remove_node(&mut self, backend_id: BackendNodeId) -> Result<(), String> {
|
||||
let BackendNodeId::Audio(node_idx) = backend_id else {
|
||||
return Err("Invalid backend node type".to_string());
|
||||
};
|
||||
|
||||
let mut controller = self.audio_controller.lock().unwrap();
|
||||
controller.graph_remove_node(self.track_id, node_idx.index() as u32);
|
||||
|
||||
self.node_index_to_stable.remove(&node_idx);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&mut self,
|
||||
output_node: BackendNodeId,
|
||||
output_port: usize,
|
||||
input_node: BackendNodeId,
|
||||
input_port: usize,
|
||||
) -> Result<(), String> {
|
||||
let BackendNodeId::Audio(from_idx) = output_node else {
|
||||
return Err("Invalid output node type".to_string());
|
||||
};
|
||||
let BackendNodeId::Audio(to_idx) = input_node else {
|
||||
return Err("Invalid input node type".to_string());
|
||||
};
|
||||
|
||||
let mut controller = self.audio_controller.lock().unwrap();
|
||||
controller.graph_connect(
|
||||
self.track_id,
|
||||
from_idx.index() as u32,
|
||||
output_port,
|
||||
to_idx.index() as u32,
|
||||
input_port,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn disconnect(
|
||||
&mut self,
|
||||
output_node: BackendNodeId,
|
||||
output_port: usize,
|
||||
input_node: BackendNodeId,
|
||||
input_port: usize,
|
||||
) -> Result<(), String> {
|
||||
let BackendNodeId::Audio(from_idx) = output_node else {
|
||||
return Err("Invalid output node type".to_string());
|
||||
};
|
||||
let BackendNodeId::Audio(to_idx) = input_node else {
|
||||
return Err("Invalid input node type".to_string());
|
||||
};
|
||||
|
||||
let mut controller = self.audio_controller.lock().unwrap();
|
||||
controller.graph_disconnect(
|
||||
self.track_id,
|
||||
from_idx.index() as u32,
|
||||
output_port,
|
||||
to_idx.index() as u32,
|
||||
input_port,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_parameter(
|
||||
&mut self,
|
||||
backend_id: BackendNodeId,
|
||||
param_id: u32,
|
||||
value: f64,
|
||||
) -> Result<(), String> {
|
||||
let BackendNodeId::Audio(node_idx) = backend_id else {
|
||||
return Err("Invalid backend node type".to_string());
|
||||
};
|
||||
|
||||
let mut controller = self.audio_controller.lock().unwrap();
|
||||
controller.graph_set_parameter(
|
||||
self.track_id,
|
||||
node_idx.index() as u32,
|
||||
param_id,
|
||||
value as f32,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_state(&self) -> Result<GraphState, String> {
|
||||
// TODO: Implement graph state query
|
||||
// For now, return empty state
|
||||
Ok(GraphState {
|
||||
nodes: vec![],
|
||||
connections: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
fn load_state(&mut self, _state: &GraphState) -> Result<(), String> {
|
||||
// TODO: Implement graph state loading
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_node_to_template(
|
||||
&mut self,
|
||||
voice_allocator_id: BackendNodeId,
|
||||
node_type: &str,
|
||||
x: f32,
|
||||
y: f32,
|
||||
) -> Result<BackendNodeId, String> {
|
||||
let BackendNodeId::Audio(allocator_idx) = voice_allocator_id else {
|
||||
return Err("Invalid voice allocator node type".to_string());
|
||||
};
|
||||
|
||||
let mut controller = self.audio_controller.lock().unwrap();
|
||||
controller.graph_add_node_to_template(
|
||||
self.track_id,
|
||||
allocator_idx.index() as u32,
|
||||
node_type.to_string(),
|
||||
x,
|
||||
y,
|
||||
);
|
||||
|
||||
// Placeholder return
|
||||
let stable_id = self.next_stable_id;
|
||||
self.next_stable_id += 1;
|
||||
let node_idx = NodeIndex::new(stable_id as usize);
|
||||
|
||||
Ok(BackendNodeId::Audio(node_idx))
|
||||
}
|
||||
|
||||
fn connect_in_template(
|
||||
&mut self,
|
||||
voice_allocator_id: BackendNodeId,
|
||||
output_node: BackendNodeId,
|
||||
output_port: usize,
|
||||
input_node: BackendNodeId,
|
||||
input_port: usize,
|
||||
) -> Result<(), String> {
|
||||
let BackendNodeId::Audio(allocator_idx) = voice_allocator_id else {
|
||||
return Err("Invalid voice allocator node type".to_string());
|
||||
};
|
||||
let BackendNodeId::Audio(from_idx) = output_node else {
|
||||
return Err("Invalid output node type".to_string());
|
||||
};
|
||||
let BackendNodeId::Audio(to_idx) = input_node else {
|
||||
return Err("Invalid input node type".to_string());
|
||||
};
|
||||
|
||||
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,
|
||||
output_port,
|
||||
to_idx.index() as u32,
|
||||
input_port,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
//! Graph Backend Trait
|
||||
//!
|
||||
//! Provides an abstraction layer for different graph backends (audio, VFX shaders, etc.)
|
||||
|
||||
use petgraph::stable_graph::NodeIndex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Backend node identifier (abstraction over different backend types)
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum BackendNodeId {
|
||||
Audio(NodeIndex),
|
||||
// Future: Vfx(u32),
|
||||
}
|
||||
|
||||
/// Abstract backend for node graph operations
|
||||
///
|
||||
/// Implementations:
|
||||
/// - AudioGraphBackend: Wraps daw_backend::AudioGraph via EngineController
|
||||
/// - VfxGraphBackend (future): GPU-based shader graph
|
||||
pub trait GraphBackend: Send {
|
||||
/// Add a node to the backend graph
|
||||
fn add_node(&mut self, node_type: &str, x: f32, y: f32) -> Result<BackendNodeId, String>;
|
||||
|
||||
/// Remove a node from the backend
|
||||
fn remove_node(&mut self, backend_id: BackendNodeId) -> Result<(), String>;
|
||||
|
||||
/// Connect two nodes
|
||||
fn connect(
|
||||
&mut self,
|
||||
output_node: BackendNodeId,
|
||||
output_port: usize,
|
||||
input_node: BackendNodeId,
|
||||
input_port: usize,
|
||||
) -> Result<(), String>;
|
||||
|
||||
/// Disconnect two nodes
|
||||
fn disconnect(
|
||||
&mut self,
|
||||
output_node: BackendNodeId,
|
||||
output_port: usize,
|
||||
input_node: BackendNodeId,
|
||||
input_port: usize,
|
||||
) -> Result<(), String>;
|
||||
|
||||
/// Set a node parameter
|
||||
fn set_parameter(
|
||||
&mut self,
|
||||
backend_id: BackendNodeId,
|
||||
param_id: u32,
|
||||
value: f64,
|
||||
) -> Result<(), String>;
|
||||
|
||||
/// Get current graph state (for serialization)
|
||||
fn get_state(&self) -> Result<GraphState, String>;
|
||||
|
||||
/// Load graph state (for presets)
|
||||
fn load_state(&mut self, state: &GraphState) -> Result<(), String>;
|
||||
|
||||
/// Add node to VoiceAllocator template (Phase 2)
|
||||
fn add_node_to_template(
|
||||
&mut self,
|
||||
voice_allocator_id: BackendNodeId,
|
||||
node_type: &str,
|
||||
x: f32,
|
||||
y: f32,
|
||||
) -> Result<BackendNodeId, String>;
|
||||
|
||||
/// Connect nodes inside VoiceAllocator template (Phase 2)
|
||||
fn connect_in_template(
|
||||
&mut self,
|
||||
voice_allocator_id: BackendNodeId,
|
||||
output_node: BackendNodeId,
|
||||
output_port: usize,
|
||||
input_node: BackendNodeId,
|
||||
input_port: usize,
|
||||
) -> Result<(), String>;
|
||||
}
|
||||
|
||||
/// Serializable graph state (for presets and save/load)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GraphState {
|
||||
pub nodes: Vec<SerializedNode>,
|
||||
pub connections: Vec<SerializedConnection>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SerializedNode {
|
||||
pub id: u32, // Frontend node ID (stable)
|
||||
pub node_type: String,
|
||||
pub position: (f32, f32),
|
||||
pub parameters: HashMap<u32, f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SerializedConnection {
|
||||
pub from_node: u32,
|
||||
pub from_port: usize,
|
||||
pub to_node: u32,
|
||||
pub to_port: usize,
|
||||
}
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
//! Graph Data Types for egui-snarl
|
||||
//!
|
||||
//! Node definitions and viewer implementation 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};
|
||||
|
||||
/// 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
|
||||
MidiInput,
|
||||
}
|
||||
|
||||
impl AudioNode {
|
||||
/// Get the display name for this node type
|
||||
pub fn type_name(&self) -> &'static str {
|
||||
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",
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the signal type for an output pin
|
||||
fn output_type(&self, _pin: usize) -> SignalType {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Viewer implementation for audio node graph
|
||||
pub struct AudioNodeViewer;
|
||||
|
||||
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 outputs(&mut self, node: &AudioNode) -> usize {
|
||||
match node {
|
||||
AudioNode::AudioOutput => 0,
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
|
||||
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 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 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>,
|
||||
) {
|
||||
if ui.button("Remove").clicked() {
|
||||
snarl.remove_node(node);
|
||||
ui.close_menu();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
//! Node Graph Pane
|
||||
//!
|
||||
//! Audio/MIDI node graph editor for modular synthesis and effects processing
|
||||
|
||||
pub mod actions;
|
||||
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 super::NodePath;
|
||||
use eframe::egui;
|
||||
use egui_snarl::ui::{SnarlWidget, SnarlStyle, BackgroundPattern, Grid};
|
||||
use egui_snarl::Snarl;
|
||||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Node graph pane with egui-snarl integration
|
||||
pub struct NodeGraphPane {
|
||||
/// The graph structure
|
||||
snarl: Snarl<AudioNode>,
|
||||
|
||||
/// Node viewer for rendering
|
||||
viewer: AudioNodeViewer,
|
||||
|
||||
/// Node palette (left sidebar)
|
||||
palette: NodePalette,
|
||||
|
||||
/// Node type registry
|
||||
node_registry: NodeTypeRegistry,
|
||||
|
||||
/// Backend integration
|
||||
#[allow(dead_code)]
|
||||
backend: Option<Box<dyn GraphBackend>>,
|
||||
|
||||
/// Maps frontend node IDs to backend node IDs
|
||||
#[allow(dead_code)]
|
||||
node_id_map: HashMap<egui_snarl::NodeId, BackendNodeId>,
|
||||
|
||||
/// Track ID this graph belongs to
|
||||
#[allow(dead_code)]
|
||||
track_id: Option<Uuid>,
|
||||
|
||||
/// Pending action to execute
|
||||
#[allow(dead_code)]
|
||||
pending_action: Option<Box<dyn lightningbeam_core::action::Action>>,
|
||||
}
|
||||
|
||||
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(),
|
||||
},
|
||||
);
|
||||
|
||||
Self {
|
||||
snarl,
|
||||
viewer: AudioNodeViewer,
|
||||
palette: NodePalette::new(),
|
||||
node_registry: NodeTypeRegistry::new(),
|
||||
backend: None,
|
||||
node_id_map: HashMap::new(),
|
||||
track_id: None,
|
||||
pending_action: None,
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
let backend = Box::new(audio_backend::AudioGraphBackend::new(
|
||||
backend_track_id,
|
||||
audio_controller,
|
||||
));
|
||||
|
||||
Self {
|
||||
snarl: Snarl::new(),
|
||||
viewer: AudioNodeViewer,
|
||||
palette: NodePalette::new(),
|
||||
node_registry: NodeTypeRegistry::new(),
|
||||
backend: Some(backend),
|
||||
node_id_map: HashMap::new(),
|
||||
track_id: Some(track_id),
|
||||
pending_action: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::panes::PaneRenderer for NodeGraphPane {
|
||||
fn render_content(
|
||||
&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
rect: egui::Rect,
|
||||
_path: &NodePath,
|
||||
_shared: &mut crate::panes::SharedPaneState,
|
||||
) {
|
||||
// Use a horizontal layout for palette + graph
|
||||
ui.allocate_ui_at_rect(rect, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
// Track clicked node from palette
|
||||
let mut clicked_node: Option<String> = None;
|
||||
|
||||
// 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());
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Right panel: Graph area (fill remaining space)
|
||||
ui.allocate_ui_with_layout(
|
||||
ui.available_size(),
|
||||
egui::Layout::top_down(egui::Align::Min),
|
||||
|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 {
|
||||
// Use center of the graph rect directly
|
||||
let center_pos = graph_rect.center();
|
||||
println!("Click detected! Adding {} at center {:?}", node_type, center_pos);
|
||||
self.add_node_at_position(node_type, center_pos);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
"Node Graph"
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,689 @@
|
|||
//! 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(
|
||||
"Delay".to_string(),
|
||||
NodeTypeInfo {
|
||||
id: "Delay".to_string(),
|
||||
display_name: "Delay".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: "Delayed 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: "Delay 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: "Time-based delay effect".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()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
//! 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.allocate_ui_at_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