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.6 % 547 359
Test Date: 2026-06-07 15:51:50 Functions: 100.0 % 2 2

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

Generated by: LCOV version 2.0-1