Node graph improvements

This commit is contained in:
Skyler Lehmkuhl 2025-12-16 11:37:07 -05:00
parent c58192a7da
commit d7176a13b7
2 changed files with 204 additions and 33 deletions

View File

@ -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,
]
}

View File

@ -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