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_extras = { workspace = true }
|
||||||
egui-wgpu = { workspace = true }
|
egui-wgpu = { workspace = true }
|
||||||
egui_code_editor = { workspace = true }
|
egui_code_editor = { workspace = true }
|
||||||
|
egui-snarl = "0.9"
|
||||||
|
|
||||||
# GPU
|
# GPU
|
||||||
wgpu = { workspace = true }
|
wgpu = { workspace = true }
|
||||||
|
|
@ -42,6 +43,7 @@ pollster = { workspace = true }
|
||||||
lightningcss = "1.0.0-alpha.68"
|
lightningcss = "1.0.0-alpha.68"
|
||||||
clap = { version = "4.5", features = ["derive"] }
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
uuid = { version = "1.0", features = ["v4", "serde"] }
|
uuid = { version = "1.0", features = ["v4", "serde"] }
|
||||||
|
petgraph = "0.6"
|
||||||
|
|
||||||
# Native file dialogs
|
# Native file dialogs
|
||||||
rfd = "0.15"
|
rfd = "0.15"
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ pub mod outliner;
|
||||||
pub mod piano_roll;
|
pub mod piano_roll;
|
||||||
pub mod virtual_piano;
|
pub mod virtual_piano;
|
||||||
pub mod node_editor;
|
pub mod node_editor;
|
||||||
|
pub mod node_graph;
|
||||||
pub mod preset_browser;
|
pub mod preset_browser;
|
||||||
pub mod asset_library;
|
pub mod asset_library;
|
||||||
pub mod shader_editor;
|
pub mod shader_editor;
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,5 @@
|
||||||
/// Node Editor pane - node-based visual programming
|
//! Node Editor pane - node-based audio/MIDI synthesis
|
||||||
///
|
//!
|
||||||
/// This will eventually render a node graph with Vello.
|
//! Re-exports the node graph implementation
|
||||||
/// For now, it's a placeholder.
|
|
||||||
|
|
||||||
use eframe::egui;
|
pub use super::node_graph::NodeGraphPane as NodeEditorPane;
|
||||||
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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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