LCOV - code coverage report
Current view: top level - src/gui/pedalboard - pedal_board_chain.cpp (source / functions) Coverage Total Hit
Test: merged.info Lines: 65.0 % 586 381
Test Date: 2026-06-01 11:15:25 Functions: 100.0 % 2 2

            Line data    Source code
       1              : #include "gui/commands/command.h"
       2              : #include "gui/pedalboard/pedal_board.h"
       3              : #include "gui/pedalboard/pedal_widget.h"
       4              : #include "gui/state/gui_graph_state.h"
       5              : #include "gui/theme/theme.h"
       6              : #include "gui/views/gui_midi.h"
       7              : #include <cmath>
       8              : #include <imgui.h>
       9              : #include <unordered_map>
      10              : namespace Amplitron {
      11          150 : void PedalBoard::render_signal_chain() {
      12          150 :   auto &ui_state = GuiGraphState::get_instance();
      13              :   // canvas_hovered is updated after the InvisibleButton is drawn (see below)
      14          150 :   float dt = ImGui::GetIO().DeltaTime;
      15          150 :   float lerp_factor = 1.0f - std::exp(-30.0f * dt);
      16          150 :   if (lerp_factor < 0.0f)
      17            0 :     lerp_factor = 0.0f;
      18          100 :   if (lerp_factor > 1.0f)
      19            0 :     lerp_factor = 1.0f;
      20          150 :   float old_zoom = ui_state.zoom;
      21          150 :   if (std::abs(ui_state.target_zoom - ui_state.zoom) > 0.0001f) {
      22           39 :     ui_state.zoom += (ui_state.target_zoom - ui_state.zoom) * lerp_factor;
      23           13 :   } else {
      24          111 :     ui_state.zoom = ui_state.target_zoom;
      25              :   }
      26          150 :   if (old_zoom != ui_state.zoom) {
      27              :     // Use canvas-local mouse coords to avoid drift when canvas is not at screen
      28              :     // origin
      29           39 :     float actual_factor = ui_state.zoom / old_zoom;
      30           39 :     ImVec2 mouse_pos = ImGui::GetMousePos();
      31           39 :     ImVec2 canvas_pos_now = ui_state.last_canvas_pos;
      32           39 :     float local_x = mouse_pos.x - canvas_pos_now.x;
      33           39 :     float local_y = mouse_pos.y - canvas_pos_now.y;
      34           39 :     ui_state.scrolling.x =
      35           39 :         local_x - (local_x - ui_state.scrolling.x) * actual_factor;
      36           39 :     ui_state.scrolling.y =
      37           39 :         local_y - (local_y - ui_state.scrolling.y) * actual_factor;
      38           39 :     ui_state.target_scrolling = ui_state.scrolling;
      39           13 :   }
      40          200 :   if (std::abs(ui_state.target_scrolling.x - ui_state.scrolling.x) > 0.01f ||
      41          150 :       std::abs(ui_state.target_scrolling.y - ui_state.scrolling.y) > 0.01f) {
      42            0 :     ui_state.scrolling.x +=
      43            0 :         (ui_state.target_scrolling.x - ui_state.scrolling.x) * lerp_factor;
      44            0 :     ui_state.scrolling.y +=
      45            0 :         (ui_state.target_scrolling.y - ui_state.scrolling.y) * lerp_factor;
      46            0 :   } else {
      47          150 :     ui_state.scrolling = ui_state.target_scrolling;
      48              :   }
      49          150 :   auto &audio_graph = engine_.graph();
      50          150 :   ImDrawList *draw_list = ImGui::GetWindowDrawList();
      51          150 :   ImVec2 canvas_pos = ImGui::GetCursorScreenPos();
      52          150 :   ImVec2 canvas_size = ImGui::GetContentRegionAvail();
      53           50 :   ImVec2 canvas_end =
      54          150 :       ImVec2(canvas_pos.x + canvas_size.x, canvas_pos.y + canvas_size.y);
      55          150 :   ui_state.last_canvas_pos = canvas_pos;
      56          150 :   ImGui::SetCursorScreenPos(canvas_pos);
      57          150 :   ImGuiButtonFlags btn_flags = ImGuiButtonFlags_MouseButtonRight | ImGuiButtonFlags_MouseButtonMiddle | ImGuiButtonFlags_MouseButtonLeft;
      58              : 
      59          150 :   ImGui::SetNextItemAllowOverlap();
      60          150 :   if (canvas_size.x <= 0.0f) canvas_size.x = 1.0f;
      61          150 :   if (canvas_size.y <= 0.0f) canvas_size.y = 1.0f;
      62          150 :   ImGui::InvisibleButton("canvas_panning_hotspot", canvas_size, btn_flags);
      63              :   // Update canvas_hovered here — after InvisibleButton — so it reflects the
      64              :   // actual canvas item
      65          150 :   ui_state.canvas_hovered = ImGui::IsItemHovered();
      66              : 
      67          150 :   if (ImGui::IsItemHovered() && !ImGui::IsAnyItemHovered()) {
      68            0 :     ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
      69            0 :   }
      70              : 
      71          176 :   if (ImGui::IsItemActive() && (ImGui::IsMouseDragging(ImGuiMouseButton_Right) || 
      72           35 :                                 ImGui::IsMouseDragging(ImGuiMouseButton_Middle) || 
      73           24 :                                 ImGui::IsMouseDragging(ImGuiMouseButton_Left))) {
      74           12 :     ui_state.scrolling.x += ImGui::GetIO().MouseDelta.x;
      75           12 :     ui_state.scrolling.y += ImGui::GetIO().MouseDelta.y;
      76           12 :     ui_state.target_scrolling = ui_state.scrolling;
      77            4 :   }
      78              :   // Zooming is now allowed in both fullscreen and normal modes
      79          150 :   if (ImGui::IsItemHovered()) {
      80           87 :     float scroll_x = ImGui::GetIO().MouseWheelH;
      81           87 :     float scroll_y = ImGui::GetIO().MouseWheel;
      82           87 :     if (scroll_x != 0.0f || scroll_y != 0.0f) {
      83            9 :       if (ImGui::GetIO().KeyCtrl) {
      84            6 :         float zoom_factor = std::pow(1.1f, scroll_y);
      85            6 :         ImVec2 mouse_pos = ImGui::GetMousePos();
      86            2 :         ImVec2 mouse_in_canvas =
      87            6 :             ImVec2(mouse_pos.x - canvas_pos.x, mouse_pos.y - canvas_pos.y);
      88            6 :         float old_zoom = ui_state.target_zoom;
      89            6 :         float new_zoom = old_zoom * zoom_factor;
      90            6 :         if (new_zoom < 0.2f)
      91            0 :           new_zoom = 0.2f;
      92            6 :         if (new_zoom > 5.0f)
      93            0 :           new_zoom = 5.0f;
      94            6 :         float actual_factor = new_zoom / old_zoom;
      95            6 :         ui_state.target_scrolling.x =
      96            8 :             mouse_in_canvas.x -
      97            6 :             (mouse_in_canvas.x - ui_state.target_scrolling.x) * actual_factor;
      98            6 :         ui_state.target_scrolling.y =
      99            8 :             mouse_in_canvas.y -
     100            6 :             (mouse_in_canvas.y - ui_state.target_scrolling.y) * actual_factor;
     101            6 :         ui_state.target_zoom = new_zoom;
     102            2 :       } else {
     103            3 :         ui_state.target_scrolling.x += scroll_x * 20.0f;
     104            3 :         ui_state.target_scrolling.y += scroll_y * 20.0f;
     105              :       }
     106            3 :     }
     107           29 :   }
     108              :   // Draw fullscreen button at top right
     109          150 :   ImGui::SetCursorScreenPos(
     110          150 :       ImVec2(canvas_pos.x + canvas_size.x - 70, canvas_pos.y + 10));
     111          150 :   ImGui::SetNextItemAllowOverlap();
     112          200 :   if (ImGui::Button(ui_state.is_fullscreen ? "Exit FS" : "Full Screen")) {
     113            0 :     ui_state.is_fullscreen = !ui_state.is_fullscreen;
     114            0 :     if (!ui_state.is_fullscreen) {
     115            0 :       ui_state.zoom = 1.0f;
     116            0 :       ui_state.target_zoom = 1.0f;
     117            0 :     }
     118            0 :   }
     119          150 :   if (ui_state.show_grid) {
     120          150 :     float GRID_SZ = 32.0f * ui_state.zoom;
     121          150 :     ImU32 GRID_COLOR = IM_COL32(36, 34, 30, 255);
     122         4393 :     for (float x = std::fmod(ui_state.scrolling.x, GRID_SZ); x < canvas_size.x;
     123         4243 :          x += GRID_SZ) {
     124         7075 :       draw_list->AddLine(ImVec2(canvas_pos.x + x, canvas_pos.y),
     125         5654 :                          ImVec2(canvas_pos.x + x, canvas_end.y), GRID_COLOR);
     126         1411 :     }
     127         3192 :     for (float y = std::fmod(ui_state.scrolling.y, GRID_SZ); y < canvas_size.y;
     128         3042 :          y += GRID_SZ) {
     129         5071 :       draw_list->AddLine(ImVec2(canvas_pos.x, canvas_pos.y + y),
     130         4055 :                          ImVec2(canvas_end.x, canvas_pos.y + y), GRID_COLOR);
     131         1013 :     }
     132           50 :   }
     133          150 :   draw_list->PushClipRect(canvas_pos, canvas_end, true);
     134          200 :   ImVec2 offset = ImVec2(canvas_pos.x + ui_state.scrolling.x,
     135          150 :                          canvas_pos.y + ui_state.scrolling.y);
     136          150 :   std::unordered_map<int, ImVec2> pin_positions_cache;
     137          150 :   int node_to_delete = -1; // Safely track deletions outside the render loop
     138              :   // Prune stale nodes from the UI state if the backend graph was reset or
     139              :   // rebuilt
     140          150 :   std::vector<int> stale_ids;
     141          503 :   for (auto &pair : ui_state.node_positions) {
     142          353 :     if (!audio_graph.find_node(pair.first)) {
     143           52 :       stale_ids.push_back(pair.first);
     144           21 :     }
     145              :   }
     146          202 :   for (int id : stale_ids) {
     147           52 :     ui_state.node_positions.erase(id);
     148              :   }
     149              :   // Give all new nodes a default position at the end of the chain without
     150              :   // shifting existing nodes
     151          453 :   for (const auto &node : audio_graph.get_nodes()) {
     152          303 :     if (ui_state.node_positions.find(node.id) ==
     153          404 :         ui_state.node_positions.end()) {
     154            2 :       if (node.x != 0.0f || node.y != 0.0f) {
     155            2 :         ui_state.node_positions[node.id] = {ImVec2(node.x, node.y), false, ImVec2(0, 0)};
     156            0 :       } else {
     157            0 :         float max_right = 40.0f;
     158            0 :         for (const auto &existing_node : audio_graph.get_nodes()) {
     159            0 :           auto pos_it = ui_state.node_positions.find(existing_node.id);
     160            0 :           if (pos_it != ui_state.node_positions.end()) {
     161            0 :             float width =
     162            0 :                 (existing_node.routing_type == NodeRoutingType::StandardEffect)
     163            0 :                     ? 190.0f
     164              :                     : 110.0f;
     165            0 :             float right_edge = pos_it->second.position.x + width;
     166            0 :             if (right_edge > max_right) {
     167            0 :               max_right = right_edge;
     168            0 :             }
     169            0 :           }
     170              :         }
     171            0 :         float insert_x =
     172            0 :             ui_state.node_positions.empty() ? 40.0f : max_right + 80.0f;
     173            0 :         ui_state.node_positions[node.id] = {ImVec2(insert_x, 60.0f), false, ImVec2(0, 0)};
     174              :       }
     175            0 :     }
     176              :   }
     177              :   // Animation pulse based on time and audio level
     178          150 :   float level = engine_.get_output_level();
     179          150 :   float time = (float)ImGui::GetTime();
     180          150 :   bool is_running = engine_.is_running();
     181          453 :   for (const auto &node : audio_graph.get_nodes()) {
     182          303 :     auto &node_layout = ui_state.node_positions[node.id];
     183          101 :     ImVec2 node_screen_pos =
     184          404 :         ImVec2(offset.x + node_layout.position.x * ui_state.zoom,
     185          303 :                offset.y + node_layout.position.y * ui_state.zoom);
     186          303 :     PedalWidget *target_widget = nullptr;
     187          303 :     if (node.routing_type == NodeRoutingType::StandardEffect) {
     188          459 :       for (auto &w : widgets_) {
     189          412 :         if (w->get_effect() == node.pedal) {
     190          153 :           target_widget = w.get();
     191          153 :           break;
     192              :         }
     193              :       }
     194          101 :     }
     195          404 :     bool is_mb_comp = false;
     196          405 :     if (target_widget && std::strcmp(target_widget->get_effect()->name(),
     197           51 :                                      "MultiBand Compressor") == 0) {
     198            0 :       is_mb_comp = true;
     199            0 :     }
     200          404 :     float node_width =
     201          303 :         (target_widget ? (is_mb_comp ? 190.0f * 2.2f : 190.0f) : 110.0f) *
     202          303 :         ui_state.zoom;
     203          303 :     float node_height = (target_widget ? 360.0f : 70.0f) * ui_state.zoom;
     204          303 :     if (!target_widget && (node.routing_type == NodeRoutingType::Mixer ||
     205          100 :                            node.routing_type == NodeRoutingType::MergeSum)) {
     206            0 :       node_width = 130.0f * ui_state.zoom;
     207            0 :       node_height =
     208            0 :           std::max(70.0f, (float)node.input_pin_ids.size() * 32.0f + 55.0f) *
     209            0 :           ui_state.zoom;
     210          252 :     } else if (!target_widget && node.routing_type == NodeRoutingType::Splitter) {
     211            0 :       node_width = 130.0f * ui_state.zoom;
     212            0 :       node_height =
     213            0 :           std::max(70.0f, (float)node.output_pin_ids.size() * 32.0f + 55.0f) *
     214            0 :           ui_state.zoom;
     215            0 :     }
     216          303 :     ImGui::PushID(node.id);
     217          303 :     if (target_widget) {
     218          153 :       ImGui::SetCursorScreenPos(node_screen_pos);
     219          153 :       ImGui::BeginGroup();
     220          153 :       ImGui::SetWindowFontScale(ui_state.zoom);
     221          153 :       target_widget->render(ui_state.zoom);
     222          153 :       ImGui::SetWindowFontScale(1.0f);
     223          153 :       ImGui::EndGroup();
     224          153 :       ImGui::SetCursorScreenPos(node_screen_pos);
     225          153 :       ImGui::SetNextItemAllowOverlap();
     226          153 :       ImGui::InvisibleButton(
     227              :           "native_drag_handle",
     228          153 :           ImVec2(node_width - 25.0f * ui_state.zoom, 30.0f * ui_state.zoom));
     229          153 :       if (ImGui::IsItemActive() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
     230            0 :         if (!node_layout.is_dragging) {
     231            0 :           node_layout.is_dragging = true;
     232            0 :           node_layout.drag_start_pos = node_layout.position;
     233            0 :         }
     234            0 :         node_layout.position.x += ImGui::GetIO().MouseDelta.x / ui_state.zoom;
     235            0 :         node_layout.position.y += ImGui::GetIO().MouseDelta.y / ui_state.zoom;
     236          153 :       } else if (node_layout.is_dragging && ImGui::IsItemDeactivated()) {
     237            0 :         node_layout.is_dragging = false;
     238            0 :         if (node_layout.position.x != node_layout.drag_start_pos.x ||
     239            0 :             node_layout.position.y != node_layout.drag_start_pos.y) {
     240            0 :           history_.push_executed(std::make_unique<MoveGraphNodeCommand>(
     241            0 :               node.id, node_layout.drag_start_pos, node_layout.position));
     242            0 :         }
     243            0 :       }
     244           51 :     } else {
     245          200 :       ImVec2 node_end = ImVec2(node_screen_pos.x + node_width,
     246          150 :                                node_screen_pos.y + node_height);
     247          150 :       ImU32 bg_color = IM_COL32(50, 35, 60, 255);
     248          200 :       draw_list->AddRectFilled(node_screen_pos, node_end, bg_color,
     249          150 :                                Theme::ROUNDING_MD * ui_state.zoom);
     250          200 :       draw_list->AddRect(node_screen_pos, node_end, IM_COL32(180, 140, 80, 180),
     251          100 :                          Theme::ROUNDING_MD * ui_state.zoom, 0,
     252          150 :                          1.5f * ui_state.zoom);
     253          150 :       ImGui::SetCursorScreenPos(node_screen_pos);
     254          150 :       ImGui::SetNextItemAllowOverlap();
     255          150 :       ImGui::InvisibleButton(
     256              :           "util_drag_handle",
     257          150 :           ImVec2(node_width - 25.0f * ui_state.zoom, node_height));
     258          150 :       if (ImGui::IsItemActive() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
     259            0 :         if (!node_layout.is_dragging) {
     260            0 :           node_layout.is_dragging = true;
     261            0 :           node_layout.drag_start_pos = node_layout.position;
     262            0 :         }
     263            0 :         node_layout.position.x += ImGui::GetIO().MouseDelta.x / ui_state.zoom;
     264            0 :         node_layout.position.y += ImGui::GetIO().MouseDelta.y / ui_state.zoom;
     265          150 :       } else if (node_layout.is_dragging && ImGui::IsItemDeactivated()) {
     266            0 :         node_layout.is_dragging = false;
     267            0 :         if (node_layout.position.x != node_layout.drag_start_pos.x ||
     268            0 :             node_layout.position.y != node_layout.drag_start_pos.y) {
     269            0 :           history_.push_executed(std::make_unique<MoveGraphNodeCommand>(
     270            0 :               node.id, node_layout.drag_start_pos, node_layout.position));
     271            0 :         }
     272            0 :       }
     273          150 :       ImVec2 text_pos = ImVec2(node_screen_pos.x + 12.0f * ui_state.zoom, node_screen_pos.y + 25.0f * ui_state.zoom);
     274          150 :       ImGui::SetWindowFontScale(ui_state.zoom);
     275          150 :       draw_list->AddText(text_pos, IM_COL32(255, 255, 255, 255), node.name.c_str());
     276          150 :       ImGui::SetWindowFontScale(1.0f);
     277              :     }
     278              : 
     279          404 :       bool is_mixer = (node.routing_type == NodeRoutingType::Mixer ||
     280          202 :                        node.routing_type == NodeRoutingType::MergeSum);
     281              : 
     282          303 :       if (is_mixer && ImGui::IsItemHovered() &&
     283            0 :           ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
     284            0 :         ImGui::OpenPopup("MixerContextMenu");
     285            0 :       }
     286          303 :       if (ImGui::BeginPopup("MixerContextMenu")) {
     287            0 :         if (ImGui::MenuItem("Add Input", nullptr, false,
     288            0 :                             node.input_pin_ids.size() < 8)) {
     289            0 :           audio_graph.add_input_pin(node.id);
     290            0 :           engine_.commit_graph_changes();
     291            0 :         }
     292            0 :         if (ImGui::MenuItem("Remove Last Input", nullptr, false,
     293            0 :                             node.input_pin_ids.size() > 2)) {
     294            0 :           if (audio_graph.remove_input_pin(node.id)) {
     295            0 :             engine_.commit_graph_changes();
     296            0 :           }
     297            0 :         }
     298            0 :         ImGui::EndPopup();
     299            0 :       }
     300              : 
     301          303 :       std::string display_name = node.name;
     302          303 :       if (is_mixer) {
     303            0 :         display_name = std::to_string(node.input_pin_ids.size()) + "-in Mixer";
     304            0 :       }
     305          404 :       ImVec2 text_pos = ImVec2(node_screen_pos.x + 12.0f * ui_state.zoom,
     306          303 :                                node_screen_pos.y + 10.0f * ui_state.zoom);
     307          303 :       ImGui::SetWindowFontScale(ui_state.zoom);
     308          404 :       draw_list->AddText(text_pos, IM_COL32(255, 255, 255, 255),
     309          101 :                          display_name.c_str());
     310          303 :       ImGui::SetWindowFontScale(1.0f);
     311              : 
     312          303 :       if (is_mixer) {
     313            0 :         ImGui::SetWindowFontScale(ui_state.zoom * 0.8f);
     314            0 :         for (size_t idx = 0; idx < node.input_pin_ids.size(); ++idx) {
     315            0 :           float pin_y =
     316            0 :               node_screen_pos.y +
     317            0 :               (node_height * (idx + 1.0f) / (node.input_pin_ids.size() + 1.0f));
     318            0 :           ImGui::SetCursorScreenPos(
     319            0 :               ImVec2(node_screen_pos.x + 8.0f * ui_state.zoom,
     320            0 :                      pin_y - 8.0f * ui_state.zoom));
     321            0 :           ImGui::PushID(static_cast<int>(idx));
     322              :           
     323            0 :           ImGui::Text("In %d", (int)(idx + 1));
     324            0 :           ImGui::SameLine();
     325            0 :           ImGui::PushItemWidth(65.0f * ui_state.zoom);
     326              : 
     327            0 :           float gain = 1.0f;
     328            0 :           if (idx < node.input_gains.size())
     329            0 :             gain = node.input_gains[idx];
     330              : 
     331            0 :           if (ImGui::SliderFloat("##gain", &gain, 0.0f, 2.0f, "%.2f")) {
     332            0 :             audio_graph.set_mixer_input_gain(node.id, idx, gain);
     333            0 :             engine_.push_mixer_gain_change(node.id, idx, gain);
     334            0 :           }
     335              : 
     336            0 :           if (ImGui::IsItemHovered() &&
     337            0 :               ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
     338            0 :             ImGui::OpenPopup("GainMidiMenu");
     339            0 :           }
     340              : 
     341            0 :           if (ImGui::BeginPopup("GainMidiMenu")) {
     342            0 :             std::string effect_name = "Mixer_" + std::to_string(node.id);
     343            0 :             std::string param_name = "Gain " + std::to_string(idx);
     344            0 :             if (gui_midi_) {
     345            0 :               gui_midi_->render_remove_mapping_item(effect_name, param_name);
     346            0 :               gui_midi_->render_learn_menu_item(effect_name, param_name);
     347            0 :             } else {
     348            0 :               ImGui::TextDisabled("MIDI manager not available");
     349              :             }
     350            0 :             ImGui::EndPopup();
     351            0 :           }
     352              : 
     353            0 :           ImGui::PopItemWidth();
     354            0 :           ImGui::PopID();
     355            0 :         }
     356              :         
     357            0 :         ImGui::SetWindowFontScale(ui_state.zoom * 0.7f);
     358            0 :         ImGui::SetCursorScreenPos(
     359            0 :             ImVec2(node_screen_pos.x + 35.0f * ui_state.zoom,
     360            0 :                    node_screen_pos.y + node_height - 16.0f * ui_state.zoom));
     361            0 :         ImGui::TextDisabled("(Gain Fader)");
     362            0 :         ImGui::SetWindowFontScale(1.0f);
     363            0 :       }
     364          303 :     if (!node.is_reachable) {
     365            4 :       ImVec2 node_end = ImVec2(node_screen_pos.x + node_width,
     366            3 :                                node_screen_pos.y + node_height);
     367            4 :       draw_list->AddRectFilled(node_screen_pos, node_end,
     368              :                                IM_COL32(0, 0, 0, 180),
     369            3 :                                Theme::ROUNDING_MD * ui_state.zoom);
     370              : 
     371            1 :       ImVec2 text_pos =
     372            4 :           ImVec2(node_screen_pos.x + 10.0f * ui_state.zoom,
     373            3 :                  node_screen_pos.y + node_height - 25.0f * ui_state.zoom);
     374            3 :       ImGui::SetWindowFontScale(ui_state.zoom * 0.9f);
     375            3 :       draw_list->AddText(text_pos, IM_COL32(255, 60, 60, 255), "DISCONNECTED");
     376            3 :       ImGui::SetWindowFontScale(1.0f);
     377            1 :     }
     378              :     // --- INTERNAL SIGNAL FLOW / ELECTRICITY EFFECT ---
     379          403 :     if (node.is_reachable &&
     380          300 :         (!node.input_pin_ids.empty() || !node.output_pin_ids.empty())) {
     381          300 :       bool enabled = true;
     382          300 :       if (target_widget) {
     383          200 :         enabled = target_widget->get_effect()->is_enabled();
     384           50 :       }
     385              :       // Align with pins
     386          300 :       float in_y = node_screen_pos.y + node_height * 0.5f;
     387          300 :       if (!node.input_pin_ids.empty()) {
     388          600 :         in_y = node_screen_pos.y +
     389          300 :                (node_height * (0 + 1.0f) / (node.input_pin_ids.size() + 1.0f));
     390          100 :       }
     391          300 :       float out_y = node_screen_pos.y + node_height * 0.5f;
     392          300 :       if (!node.output_pin_ids.empty()) {
     393          600 :         out_y = node_screen_pos.y + (node_height * (0 + 1.0f) /
     394          300 :                                      (node.output_pin_ids.size() + 1.0f));
     395          100 :       }
     396              : 
     397          300 :       ImVec2 flow_p1(node_screen_pos.x, in_y);
     398          300 :       ImVec2 flow_p2(node_screen_pos.x + node_width, out_y);
     399              :       // --- COLOR & PULSE CALCULATIONS (SHARED) ---
     400          300 :       ImU32 flow_col = IM_COL32(200, 230, 255, 255);
     401          300 :       if (target_widget) {
     402          100 :         const auto *colors =
     403          150 :             get_effect_color(target_widget->get_effect()->name());
     404          150 :         flow_col = ImGui::ColorConvertFloat4ToU32(colors->led_color);
     405           50 :       }
     406              : 
     407          300 :       ImU32 r = (flow_col >> 0) & 0xFF;
     408          300 :       ImU32 g = (flow_col >> 8) & 0xFF;
     409          300 :       ImU32 b = (flow_col >> 16) & 0xFF;
     410          300 :       float pulse = 0.6f + 0.4f * std::sin(time * 8.0f) * (0.5f + level * 2.0f);
     411          300 :       float jitter = std::sin(time * 40.0f) * 1.5f * ui_state.zoom;
     412          300 :       if (enabled) {
     413              :         // --- ELECTRICITY PASSING THROUGH (ACTIVE) ---
     414              :         // No internal line, only glowing edges
     415          392 :         ImVec2 p_min(node_screen_pos.x - (2.0f + jitter),
     416          294 :                      node_screen_pos.y - (2.0f + jitter));
     417          392 :         ImVec2 p_max(node_screen_pos.x + node_width + (2.0f + jitter),
     418          294 :                      node_screen_pos.y + node_height + (2.0f + jitter));
     419              : 
     420          392 :         draw_list->AddRect(p_min, p_max, IM_COL32(r, g, b, (int)(180 * pulse)),
     421          196 :                            Theme::ROUNDING_MD * ui_state.zoom, 0,
     422          196 :                            3.0f * ui_state.zoom);
     423          392 :         draw_list->AddRect(
     424          294 :             p_min, p_max, IM_COL32(255, 255, 255, (int)(220 * pulse)),
     425          294 :             Theme::ROUNDING_MD * ui_state.zoom, 0, 1.0f * ui_state.zoom);
     426           98 :       } else {
     427              :         // --- ELECTRIC HIGH-RAIL BYPASS PATH ---
     428              :         // Rectangular "bridge" with CONSISTENT glow style
     429            6 :         float rail_height = 65.0f * ui_state.zoom;
     430            6 :         float rail_y = node_screen_pos.y - rail_height;
     431              : 
     432            6 :         ImVec2 p_rail_in(flow_p1.x, rail_y);
     433            6 :         ImVec2 p_rail_out(flow_p2.x, rail_y);
     434           22 :         auto draw_consistent_glow_line = [&](ImVec2 p1, ImVec2 p2) {
     435           18 :           ImVec2 p1j(p1.x, p1.y + jitter);
     436           18 :           ImVec2 p2j(p2.x, p2.y + jitter);
     437           24 :           draw_list->AddLine(p1j, p2j, IM_COL32(r, g, b, (int)(180 * pulse)),
     438           18 :                              4.0f * ui_state.zoom);
     439           24 :           draw_list->AddLine(p1j, p2j,
     440           18 :                              IM_COL32(255, 255, 255, (int)(220 * pulse)),
     441           18 :                              1.5f * ui_state.zoom);
     442           20 :         };
     443              :         // Bridge segments
     444            6 :         draw_consistent_glow_line(flow_p1, p_rail_in);
     445            6 :         draw_consistent_glow_line(p_rail_in, p_rail_out);
     446            6 :         draw_consistent_glow_line(p_rail_out, flow_p2);
     447              :         // --- MOVING ELECTRONS (PULSES) ---
     448              :         // Electrons travel along the bridge path: Up -> Across -> Down
     449            6 :         float electron_time = std::fmod(time * 2.5f, 1.0f);
     450           18 :         for (int e = 0; e < 2; ++e) {
     451           12 :           float t = std::fmod(electron_time + e * 0.5f, 1.0f);
     452           12 :           ImVec2 electron_pos;
     453              : 
     454           12 :           if (t < 0.2f) { // Segment 1: Up
     455            3 :             float st = t / 0.2f;
     456            3 :             electron_pos = ImVec2(
     457            3 :                 flow_p1.x, flow_p1.y + (rail_y - flow_p1.y) * st + jitter);
     458           10 :           } else if (t < 0.8f) { // Segment 2: Across
     459            6 :             float st = (t - 0.2f) / 0.6f;
     460            6 :             electron_pos =
     461            8 :                 ImVec2(p_rail_in.x + (p_rail_out.x - p_rail_in.x) * st,
     462            4 :                        rail_y + jitter);
     463            2 :           } else { // Segment 3: Down
     464            3 :             float st = (t - 0.8f) / 0.2f;
     465            4 :             electron_pos = ImVec2(p_rail_out.x,
     466            3 :                                   rail_y + (flow_p2.y - rail_y) * st + jitter);
     467              :           }
     468              : 
     469           16 :           draw_list->AddCircleFilled(
     470           12 :               electron_pos, 3.5f * ui_state.zoom,
     471           12 :               IM_COL32(255, 255, 255, (int)(220 * pulse)));
     472           16 :           draw_list->AddCircle(electron_pos, 7.0f * ui_state.zoom,
     473           12 :                                IM_COL32(r, g, b, (int)(180 * pulse)), 0,
     474           12 :                                2.0f * ui_state.zoom);
     475            4 :         }
     476            6 :         if (ImGui::IsMouseHoveringRect(ImVec2(node_screen_pos.x, rail_y - 10),
     477              :                                        flow_p2)) {
     478            0 :           ImGui::SetTooltip("%s (Bypassed - High Rail Signal Path)",
     479            0 :                             target_widget->get_effect()->name());
     480            0 :         }
     481              :       }
     482          300 :       if (ImGui::IsMouseHoveringRect(flow_p1, flow_p2) &&
     483            0 :           (target_widget || !node.name.empty())) {
     484            0 :         const char *name = target_widget ? target_widget->get_effect()->name()
     485            0 :                                          : node.name.c_str();
     486            0 :         ImGui::SetTooltip("%s (%s)", name, enabled ? "Active" : "Bypassed");
     487            0 :       }
     488          100 :     }
     489              :     // ====================================================================
     490              :     // THE DELETION [X] BUTTON
     491              :     // ====================================================================
     492          303 :     bool is_amp = (node.name == "Amp Sim");
     493          303 :     bool is_input_node = (node.name == "Input");
     494          303 :     if (!is_amp && !is_input_node) {
     495           48 :       ImVec2 cross_pos =
     496          192 :           ImVec2(node_screen_pos.x + node_width - 24.0f * ui_state.zoom,
     497          144 :                  node_screen_pos.y + 4.0f * ui_state.zoom);
     498          144 :       ImGui::SetCursorScreenPos(cross_pos);
     499              : 
     500              :       // Your exact color styling
     501          144 :       ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
     502          144 :       ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
     503           96 :                             ImVec4(0.6f, 0.1f, 0.1f, 0.8f));
     504              : 
     505              :       // Use SmallButton and exact string formatting
     506          144 :       std::string remove_label = "X##rm" + std::to_string(node.id);
     507          144 :       ImGui::SetNextItemAllowOverlap();
     508          144 :       if (ImGui::SmallButton(remove_label.c_str())) {
     509            0 :         node_to_delete = node.id;
     510            0 :       }
     511          144 :       if (ImGui::IsItemHovered()) {
     512            0 :         ImGui::SetTooltip("Remove %s from chain", node.name.c_str());
     513            0 :       }
     514              : 
     515          144 :       ImGui::PopStyleColor(2);
     516          144 :     }
     517              :     // ====================================================================
     518              :     // FIX: THE WIRE DROP ZONE (Input Pins)
     519              :     // ====================================================================
     520          303 :     if (!is_input_node) {
     521          306 :       for (size_t idx = 0; idx < node.input_pin_ids.size(); ++idx) {
     522          153 :         int pin_id = node.input_pin_ids[idx];
     523          255 :         float pin_y = node_screen_pos.y + (node_height * (idx + 1.0f) /
     524          153 :                                            (node.input_pin_ids.size() + 1.0f));
     525          153 :         ImVec2 pin_pos(node_screen_pos.x - 2.0f * ui_state.zoom, pin_y);
     526          153 :         pin_positions_cache[pin_id] = pin_pos;
     527          153 :         draw_list->AddCircleFilled(pin_pos, 5.0f * ui_state.zoom,
     528              :                                    IM_COL32(46, 204, 113, 255));
     529          153 :         draw_list->AddCircle(pin_pos, 6.5f * ui_state.zoom,
     530              :                              IM_COL32(255, 255, 255, 200));
     531          255 :         ImGui::SetCursorScreenPos(ImVec2(pin_pos.x - 10.0f * ui_state.zoom,
     532          153 :                                          pin_pos.y - 10.0f * ui_state.zoom));
     533          153 :         ImGui::PushID(pin_id);
     534          153 :         ImGui::SetNextItemAllowOverlap();
     535          153 :         ImGui::InvisibleButton(
     536          153 :             "in_pin", ImVec2(20.0f * ui_state.zoom, 20.0f * ui_state.zoom));
     537              : 
     538              :         // Check if hovered while releasing a dragged wire
     539          153 :         ImVec2 mouse_pos = ImGui::GetMousePos();
     540          153 :         float dist_sq =
     541          153 :             pow(mouse_pos.x - pin_pos.x, 2) + pow(mouse_pos.y - pin_pos.y, 2);
     542          153 :         if (dist_sq < pow(15.0f * ui_state.zoom, 2) &&
     543            0 :             ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
     544            0 :           if (ui_state.active_src_pin_id != -1) {
     545            0 :             history_.execute(std::make_unique<AddGraphLinkCommand>(
     546            0 :                 engine_, ui_state.active_src_pin_id, pin_id));
     547            0 :             ui_state.active_src_pin_id = -1;
     548            0 :           }
     549            0 :         }
     550          153 :         ImGui::PopID();
     551           51 :       }
     552           51 :     }
     553              :     // ====================================================================
     554              :     // FIX: THE WIRE DRAG START (Output Pins)
     555              :     // ====================================================================
     556          303 :     if (!is_amp) {
     557          588 :       for (size_t idx = 0; idx < node.output_pin_ids.size(); ++idx) {
     558          294 :         int pin_id = node.output_pin_ids[idx];
     559          490 :         float pin_y = node_screen_pos.y + (node_height * (idx + 1.0f) /
     560          294 :                                            (node.output_pin_ids.size() + 1.0f));
     561          392 :         ImVec2 pin_pos(node_screen_pos.x + node_width + 2.0f * ui_state.zoom,
     562          294 :                        pin_y);
     563          294 :         pin_positions_cache[pin_id] = pin_pos;
     564              :         // Track active wire position to snap to the pin perfectly
     565          294 :         if (ui_state.active_src_pin_id == pin_id)
     566            0 :           ui_state.active_src_pin_pos = pin_pos;
     567          294 :         draw_list->AddCircleFilled(pin_pos, 5.0f * ui_state.zoom,
     568              :                                    IM_COL32(231, 76, 60, 255));
     569          294 :         draw_list->AddCircle(pin_pos, 6.5f * ui_state.zoom,
     570              :                              IM_COL32(255, 255, 255, 200));
     571          490 :         ImGui::SetCursorScreenPos(ImVec2(pin_pos.x - 10.0f * ui_state.zoom,
     572          294 :                                          pin_pos.y - 10.0f * ui_state.zoom));
     573          294 :         ImGui::PushID(pin_id);
     574          294 :         ImGui::SetNextItemAllowOverlap();
     575          294 :         ImGui::InvisibleButton(
     576          294 :             "out_pin", ImVec2(20.0f * ui_state.zoom, 20.0f * ui_state.zoom));
     577              : 
     578              :         // Start drafting wire instantly on Mouse DOWN or delete on right click
     579          294 :         if (ImGui::IsItemHovered()) {
     580            0 :           if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
     581            0 :             ui_state.active_src_pin_id = pin_id;
     582            0 :             ui_state.active_src_pin_pos = pin_pos;
     583            0 :           } else if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
     584            0 :             auto links = audio_graph.get_links(); // copy
     585            0 :             for (const auto &l : links) {
     586            0 :               if (l.source_pin_id == pin_id) {
     587            0 :                 history_.execute(
     588            0 :                     std::make_unique<RemoveGraphLinkCommand>(engine_, l));
     589            0 :               }
     590              :             }
     591            0 :           }
     592            0 :         }
     593          294 :         ImGui::PopID();
     594           98 :       }
     595           98 :     }
     596          303 :     ImGui::PopID();
     597          303 :   }
     598              :   // Process Deletions safely after iterating
     599          150 :   if (node_to_delete != -1) {
     600            0 :     auto *node_ptr = audio_graph.find_node(node_to_delete);
     601            0 :     if (node_ptr && node_ptr->routing_type == NodeRoutingType::StandardEffect) {
     602            0 :       auto &effects = engine_.effects();
     603            0 :       for (size_t i = 0; i < effects.size(); ++i) {
     604            0 :         if (effects[i] == node_ptr->pedal) {
     605            0 :           history_.execute(std::make_unique<RemoveEffectCommand>(
     606            0 :               engine_, static_cast<int>(i)));
     607            0 :           rebuild_widgets();
     608            0 :           break;
     609              :         }
     610            0 :       }
     611            0 :     } else if (node_ptr) {
     612            0 :       history_.execute(std::make_unique<RemoveGraphNodeCommand>(
     613            0 :           engine_, node_to_delete, node_ptr->routing_type,
     614            0 :           ui_state.node_positions[node_to_delete].position));
     615            0 :     }
     616            0 :     ui_state.node_positions.erase(node_to_delete);
     617            0 :     ui_state.active_src_pin_id =
     618              :         -1; // avoid stale pin state after topology change
     619            0 :   }
     620              :   // Draw Patch Cables
     621          150 :   int link_to_delete = -1;
     622          300 :   for (const auto &link : audio_graph.get_links()) {
     623          250 :     if (pin_positions_cache.count(link.source_pin_id) &&
     624          150 :         pin_positions_cache.count(link.dest_pin_id)) {
     625          150 :       ImVec2 p1 = pin_positions_cache[link.source_pin_id];
     626          150 :       ImVec2 p2 = pin_positions_cache[link.dest_pin_id];
     627              : 
     628          150 :       ImVec2 cp1 = ImVec2(p1.x + 45.0f * ui_state.zoom, p1.y);
     629          150 :       ImVec2 cp2 = ImVec2(p2.x - 45.0f * ui_state.zoom, p2.y);
     630              :       // Distance detection for hovering/clicking
     631          150 :       bool hovered = false;
     632          150 :       ImVec2 mouse_pos = ImGui::GetMousePos();
     633         1650 :       for (float t = 0.0f; t <= 1.0f; t += 0.1f) {
     634         1500 :         float u = 1.0f - t;
     635         2500 :         float px = (u * u * u) * p1.x + (3 * u * u * t) * cp1.x +
     636         2000 :                    (3 * u * t * t) * cp2.x + (t * t * t) * p2.x;
     637         2500 :         float py = (u * u * u) * p1.y + (3 * u * u * t) * cp1.y +
     638         2000 :                    (3 * u * t * t) * cp2.y + (t * t * t) * p2.y;
     639         1500 :         float dx = px - mouse_pos.x;
     640         1500 :         float dy = py - mouse_pos.y;
     641         1500 :         if (dx * dx + dy * dy < 100.0f * ui_state.zoom * ui_state.zoom) {
     642            0 :           hovered = true;
     643            0 :           break;
     644              :         }
     645          500 :       }
     646          150 :       ImU32 color =
     647          150 :           hovered ? IM_COL32(255, 100, 100, 255) : IM_COL32(212, 175, 55, 255);
     648          250 :       draw_list->AddBezierCubic(p1, cp1, cp2, p2, color,
     649           50 :                                 hovered ? 5.0f * ui_state.zoom
     650          150 :                                         : 3.0f * ui_state.zoom);
     651              :       // --- Signal Pulse Animation on Cables ---
     652          150 :       if (is_running) {
     653            0 :         float t_off = std::fmod(time * 2.0f, 1.0f); // Slightly faster
     654            0 :         for (int step = 0; step < 4; ++step) {      // More pulses
     655            0 :           float t = std::fmod(t_off + step * 0.25f, 1.0f);
     656            0 :           float u = 1.0f - t;
     657            0 :           float hx = (u * u * u) * p1.x + (3 * u * u * t) * cp1.x +
     658            0 :                      (3 * u * t * t) * cp2.x + (t * t * t) * p2.x;
     659            0 :           float hy = (u * u * u) * p1.y + (3 * u * u * t) * cp1.y +
     660            0 :                      (3 * u * t * t) * cp2.y + (t * t * t) * p2.y;
     661              : 
     662              :           // Larger, more vibrant pulse
     663            0 :           float pulse_size = (3.0f + 2.0f * level) * ui_state.zoom;
     664            0 :           draw_list->AddCircleFilled(ImVec2(hx, hy), pulse_size,
     665              :                                      Theme::ACCENT_GOLD_HOT);
     666            0 :           draw_list->AddCircle(ImVec2(hx, hy),
     667            0 :                                pulse_size + 1.0f * ui_state.zoom,
     668              :                                IM_COL32(255, 255, 255, 150));
     669            0 :         }
     670            0 :       }
     671          150 :       if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
     672            0 :         link_to_delete = link.id;
     673            0 :       }
     674           50 :     }
     675              :   }
     676              : 
     677          150 :   if (link_to_delete != -1) {
     678            0 :     for (const auto &l : audio_graph.get_links()) {
     679            0 :       if (l.id == link_to_delete) {
     680            0 :         history_.execute(std::make_unique<RemoveGraphLinkCommand>(engine_, l));
     681            0 :         break;
     682              :       }
     683              :     }
     684            0 :   }
     685              :   // Draw Wire Spline Drafting
     686          150 :   if (ui_state.active_src_pin_id != -1) {
     687           12 :     ImVec2 mouse_pos = ImGui::GetMousePos();
     688           12 :     ImVec2 p1 = ui_state.active_src_pin_pos;
     689           12 :     ImVec2 cp1 = ImVec2(p1.x + 45.0f * ui_state.zoom, p1.y);
     690           12 :     ImVec2 cp2 = ImVec2(mouse_pos.x - 45.0f * ui_state.zoom, mouse_pos.y);
     691           12 :     draw_list->AddBezierCubic(p1, cp1, cp2, mouse_pos,
     692              :                               IM_COL32(255, 255, 255, 160), 2.0f, 0);
     693           12 :     if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
     694            6 :       ui_state.active_src_pin_id =
     695              :           -1; // Snap cable back if dropped in empty space
     696            2 :     }
     697            4 :   }
     698              :   // Fix ImGui cursor bounds warnings after free panning
     699          150 :   ImGui::SetCursorPos(ImVec2(0, 0));
     700          150 :   ImGui::Dummy(canvas_size);
     701          150 :   draw_list->PopClipRect();
     702          150 : }
     703              : } // namespace Amplitron
        

Generated by: LCOV version 2.0-1