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