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
|