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
|