From d7176a13b72ad668a73ff8b46edbeb245f03e9fb Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Tue, 16 Dec 2025 11:37:07 -0500 Subject: [PATCH] Node graph improvements --- .../src/panes/node_graph/graph_data.rs | 197 +++++++++++++++--- .../src/panes/node_graph/mod.rs | 40 +++- 2 files changed, 204 insertions(+), 33 deletions(-) diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/graph_data.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/graph_data.rs index 1d5fcb5..20aa427 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/graph_data.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/graph_data.rs @@ -28,10 +28,14 @@ pub enum NodeTemplate { // Effects Filter, Gain, + Delay, // Utilities Adsr, Lfo, + Mixer, + Splitter, + Constant, // Outputs AudioOutput, @@ -101,8 +105,12 @@ impl NodeTemplateTrait for NodeTemplate { NodeTemplate::Noise => "Noise".into(), NodeTemplate::Filter => "Filter".into(), NodeTemplate::Gain => "Gain".into(), + NodeTemplate::Delay => "Delay".into(), NodeTemplate::Adsr => "ADSR".into(), NodeTemplate::Lfo => "LFO".into(), + NodeTemplate::Mixer => "Mixer".into(), + NodeTemplate::Splitter => "Splitter".into(), + NodeTemplate::Constant => "Constant".into(), NodeTemplate::AudioOutput => "Audio Output".into(), } } @@ -111,8 +119,9 @@ impl NodeTemplateTrait for NodeTemplate { match self { NodeTemplate::MidiInput | NodeTemplate::AudioInput => vec!["Inputs"], NodeTemplate::Oscillator | NodeTemplate::Noise => vec!["Generators"], - NodeTemplate::Filter | NodeTemplate::Gain => vec!["Effects"], - NodeTemplate::Adsr | NodeTemplate::Lfo => vec!["Utilities"], + NodeTemplate::Filter | NodeTemplate::Gain | NodeTemplate::Delay => vec!["Effects"], + NodeTemplate::Adsr | NodeTemplate::Lfo | NodeTemplate::Mixer + | NodeTemplate::Splitter | NodeTemplate::Constant => vec!["Utilities"], NodeTemplate::AudioOutput => vec!["Outputs"], } } @@ -133,34 +142,34 @@ impl NodeTemplateTrait for NodeTemplate { ) { match self { NodeTemplate::Oscillator => { - // FM input + // V/Oct input (pitch control voltage) graph.add_input_param( node_id, - "FM".into(), - DataType::Audio, + "V/Oct".into(), + DataType::CV, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOnly, true, ); - // Frequency parameter + // FM input (frequency modulation) graph.add_input_param( node_id, - "Freq".into(), + "FM".into(), DataType::CV, - ValueType::Float { value: 440.0 }, - InputParamKind::ConstantOnly, + ValueType::Float { value: 0.0 }, + InputParamKind::ConnectionOnly, true, ); // Audio output - graph.add_output_param(node_id, "Out".into(), DataType::Audio); + graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio); } NodeTemplate::Noise => { - graph.add_output_param(node_id, "Out".into(), DataType::Audio); + graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio); } NodeTemplate::Filter => { graph.add_input_param( node_id, - "In".into(), + "Audio In".into(), DataType::Audio, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOnly, @@ -168,18 +177,18 @@ impl NodeTemplateTrait for NodeTemplate { ); graph.add_input_param( node_id, - "Cutoff".into(), + "Cutoff CV".into(), DataType::CV, - ValueType::Float { value: 1000.0 }, - InputParamKind::ConnectionOrConstant, + ValueType::Float { value: 0.0 }, + InputParamKind::ConnectionOnly, true, ); - graph.add_output_param(node_id, "Out".into(), DataType::Audio); + graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio); } NodeTemplate::Gain => { graph.add_input_param( node_id, - "In".into(), + "Audio In".into(), DataType::Audio, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOnly, @@ -187,32 +196,65 @@ impl NodeTemplateTrait for NodeTemplate { ); graph.add_input_param( node_id, - "Gain".into(), + "Gain CV".into(), DataType::CV, - ValueType::Float { value: 1.0 }, - InputParamKind::ConnectionOrConstant, + ValueType::Float { value: 0.0 }, + InputParamKind::ConnectionOnly, true, ); - graph.add_output_param(node_id, "Out".into(), DataType::Audio); + graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio); } NodeTemplate::Adsr => { graph.add_input_param( node_id, "Gate".into(), - DataType::Midi, + DataType::CV, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOnly, true, ); - graph.add_output_param(node_id, "Out".into(), DataType::CV); + // Parameters + graph.add_input_param( + node_id, + "Attack".into(), + DataType::CV, + ValueType::Float { value: 0.01 }, + InputParamKind::ConstantOnly, + true, + ); + graph.add_input_param( + node_id, + "Decay".into(), + DataType::CV, + ValueType::Float { value: 0.1 }, + InputParamKind::ConstantOnly, + true, + ); + graph.add_input_param( + node_id, + "Sustain".into(), + DataType::CV, + ValueType::Float { value: 0.7 }, + InputParamKind::ConstantOnly, + true, + ); + graph.add_input_param( + node_id, + "Release".into(), + DataType::CV, + ValueType::Float { value: 0.2 }, + InputParamKind::ConstantOnly, + true, + ); + graph.add_output_param(node_id, "Envelope Out".into(), DataType::CV); } NodeTemplate::Lfo => { - graph.add_output_param(node_id, "Out".into(), DataType::CV); + graph.add_output_param(node_id, "CV Out".into(), DataType::CV); } NodeTemplate::AudioOutput => { graph.add_input_param( node_id, - "In".into(), + "Audio In".into(), DataType::Audio, ValueType::Float { value: 0.0 }, InputParamKind::ConnectionOnly, @@ -220,10 +262,107 @@ impl NodeTemplateTrait for NodeTemplate { ); } NodeTemplate::AudioInput => { - graph.add_output_param(node_id, "Out".into(), DataType::Audio); + graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio); } NodeTemplate::MidiInput => { - graph.add_output_param(node_id, "Out".into(), DataType::Midi); + graph.add_output_param(node_id, "MIDI Out".into(), DataType::Midi); + } + NodeTemplate::Delay => { + graph.add_input_param( + node_id, + "Audio In".into(), + DataType::Audio, + ValueType::Float { value: 0.0 }, + InputParamKind::ConnectionOnly, + true, + ); + // Parameters + graph.add_input_param( + node_id, + "Delay Time".into(), + DataType::CV, + ValueType::Float { value: 0.5 }, + InputParamKind::ConstantOnly, + true, + ); + graph.add_input_param( + node_id, + "Feedback".into(), + DataType::CV, + ValueType::Float { value: 0.5 }, + InputParamKind::ConstantOnly, + true, + ); + graph.add_input_param( + node_id, + "Wet/Dry".into(), + DataType::CV, + ValueType::Float { value: 0.5 }, + InputParamKind::ConstantOnly, + true, + ); + graph.add_output_param(node_id, "Audio Out".into(), DataType::Audio); + } + NodeTemplate::Mixer => { + graph.add_input_param( + node_id, + "Input 1".into(), + DataType::Audio, + ValueType::Float { value: 0.0 }, + InputParamKind::ConnectionOnly, + true, + ); + graph.add_input_param( + node_id, + "Input 2".into(), + DataType::Audio, + ValueType::Float { value: 0.0 }, + InputParamKind::ConnectionOnly, + true, + ); + graph.add_input_param( + node_id, + "Input 3".into(), + DataType::Audio, + ValueType::Float { value: 0.0 }, + InputParamKind::ConnectionOnly, + true, + ); + graph.add_input_param( + node_id, + "Input 4".into(), + DataType::Audio, + ValueType::Float { value: 0.0 }, + InputParamKind::ConnectionOnly, + true, + ); + graph.add_output_param(node_id, "Mixed Out".into(), DataType::Audio); + } + NodeTemplate::Splitter => { + graph.add_input_param( + node_id, + "Audio In".into(), + DataType::Audio, + ValueType::Float { value: 0.0 }, + InputParamKind::ConnectionOnly, + true, + ); + graph.add_output_param(node_id, "Out 1".into(), DataType::Audio); + graph.add_output_param(node_id, "Out 2".into(), DataType::Audio); + graph.add_output_param(node_id, "Out 3".into(), DataType::Audio); + graph.add_output_param(node_id, "Out 4".into(), DataType::Audio); + } + NodeTemplate::Constant => { + // No inputs - value is set via parameter + graph.add_input_param( + node_id, + "Value".into(), + DataType::CV, + ValueType::Float { value: 0.0 }, + InputParamKind::ConstantOnly, + true, + ); + graph.add_output_param(node_id, "CV Out".into(), DataType::CV); } } } @@ -298,8 +437,12 @@ impl NodeTemplateIter for AllNodeTemplates { NodeTemplate::Noise, NodeTemplate::Filter, NodeTemplate::Gain, + NodeTemplate::Delay, NodeTemplate::Adsr, NodeTemplate::Lfo, + NodeTemplate::Mixer, + NodeTemplate::Splitter, + NodeTemplate::Constant, NodeTemplate::AudioOutput, ] } diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mod.rs index afc27ed..a186b42 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/node_graph/mod.rs @@ -99,7 +99,7 @@ impl NodeGraphPane { // Calculate zoom center (same as nodes - they zoom relative to viewport center) let half_size = rect.size() / 2.0; - let zoom_center = rect.min.to_vec2() + half_size - pan; + let zoom_center = rect.min.to_vec2() + half_size + pan; // Calculate grid bounds in graph space // Screen to graph: (screen_pos - zoom_center) / zoom @@ -151,12 +151,31 @@ impl crate::panes::PaneRenderer for NodeGraphPane { // Allocate the rect and render the graph editor within it ui.allocate_ui_at_rect(rect, |ui| { + // Disable debug warning for unaligned widgets (happens when zoomed) + ui.style_mut().debug.show_unaligned = false; + // Check for scroll input to override library's default zoom behavior - let scroll_delta = ui.input(|i| i.smooth_scroll_delta); let modifiers = ui.input(|i| i.modifiers); - let has_scroll = scroll_delta != egui::Vec2::ZERO; let has_ctrl = modifiers.ctrl || modifiers.command; + // When ctrl is held, check for raw scroll events in the events list + let scroll_delta = if has_ctrl { + // Sum up scroll events from the raw event list + ui.input(|i| { + let mut total_scroll = egui::Vec2::ZERO; + for event in &i.events { + if let egui::Event::MouseWheel { delta, .. } = event { + total_scroll += *delta; + } + } + total_scroll + }) + } else { + ui.input(|i| i.smooth_scroll_delta) + }; + let has_scroll = scroll_delta != egui::Vec2::ZERO; + + // Save current zoom to detect if library changed it let zoom_before = self.state.pan_zoom.zoom; let pan_before = self.state.pan_zoom.pan; @@ -176,8 +195,18 @@ impl crate::panes::PaneRenderer for NodeGraphPane { // Override library's default scroll behavior: // - Library uses scroll for zoom // - We want: scroll = pan, ctrl+scroll = zoom - if has_scroll && ui.rect_contains_pointer(rect) { - if !has_ctrl { + if has_scroll { + if has_ctrl { + // Ctrl+scroll: zoom (explicitly handle it instead of relying on library) + // First undo any zoom the library applied + if self.state.pan_zoom.zoom != zoom_before { + let undo_zoom = zoom_before / self.state.pan_zoom.zoom; + self.state.zoom(ui, undo_zoom); + } + // Now apply zoom based on scroll + let zoom_delta = (scroll_delta.y * 0.002).exp(); + self.state.zoom(ui, zoom_delta); + } else { // Scroll without ctrl: library zoomed, but we want pan instead // Undo the zoom and apply pan if self.state.pan_zoom.zoom != zoom_before { @@ -188,7 +217,6 @@ impl crate::panes::PaneRenderer for NodeGraphPane { // Apply pan self.state.pan_zoom.pan = pan_before + scroll_delta; } - // If ctrl is held, library already zoomed correctly, so do nothing } // Draw menu button in top-left corner