LCOV - code coverage report
Current view: top level - src/gui/commands - command_graph.h (source / functions) Coverage Total Hit
Test: merged.info Lines: 68.0 % 259 176
Test Date: 2026-06-03 09:13:19 Functions: 79.2 % 24 19

            Line data    Source code
       1              : #pragma once
       2              : 
       3              : #include "gui/commands/command_base.h"
       4              : #include "audio/engine/audio_engine.h"
       5              : #include "audio/engine/audio_graph.h"
       6              : #include "gui/state/gui_graph_state.h"
       7              : 
       8              : namespace Amplitron {
       9              : 
      10              : using NodeId = int;
      11              : using EffectType = NodeRoutingType;
      12              : 
      13              : struct AddGraphNodeCommand : public Command {
      14              :     AudioEngine& engine_;
      15            2 :     NodeId node_id = -1; // Assigned on first execute
      16              :     std::string name;
      17              :     EffectType type;
      18              :     std::shared_ptr<Effect> pedal;
      19              :     ImVec2 position;
      20              :     DSPNode cached_node; // To remember exactly what was added for redo
      21              : 
      22              :     int num_inputs = 0;
      23              : 
      24            8 :     AddGraphNodeCommand(AudioEngine& engine, const std::string& name, EffectType type, std::shared_ptr<Effect> pedal, ImVec2 pos, int num_inputs = 0)
      25           12 :         : engine_(engine), name(name), type(type), pedal(pedal), position(pos), num_inputs(num_inputs) {}
      26              : 
      27            9 :     bool execute() override {
      28            9 :         if (node_id == -1) {
      29            6 :             node_id = engine_.graph().add_node(name, type, pedal, num_inputs);
      30            6 :             auto* added_node = engine_.graph().find_node(node_id);
      31            6 :             if (added_node) cached_node = *added_node;
      32            2 :         } else {
      33              :             // Re-adding the previously deleted/undone node
      34            3 :             engine_.graph().restore_node(cached_node);
      35              :         }
      36              :         // Only write a fixed position when one was explicitly requested;
      37              :         // if position is (0,0) the auto-placement logic in render_signal_chain
      38              :         // will assign the correct cascading position on the next frame.
      39            9 :         if (position.x != 0.0f || position.y != 0.0f) {
      40            6 :             GuiGraphState::get_instance().node_positions[node_id] = { position, false, ImVec2(0, 0) };
      41            2 :         }
      42            9 :         engine_.commit_graph_changes();
      43            9 :         return true;
      44            0 :     }
      45              : 
      46            3 :     void undo() override {
      47            3 :         engine_.graph().remove_node(node_id);
      48            3 :         GuiGraphState::get_instance().node_positions.erase(node_id);
      49            3 :         engine_.commit_graph_changes();
      50            3 :     }
      51              : 
      52            0 :     const char* description() const override { return "Add Node"; }
      53              : };
      54              : 
      55              : struct RemovedPinInfo {
      56              :     int node_id;
      57              :     int pin_id;
      58              :     int index;
      59              :     float gain;
      60              : };
      61              : 
      62              : struct RemoveGraphNodeCommand : public Command {
      63              :     AudioEngine& engine_;
      64              :     NodeId node_id;
      65              :     EffectType type;
      66              :     ImVec2 position;
      67              :     std::vector<GraphLink> severed_links; // cache for undo
      68              :     DSPNode cached_node;                  // full node data for exact restoration
      69              :     std::vector<RemovedPinInfo> auto_removed_pins; // cache for dynamically removed pins on other nodes
      70              :     std::vector<RemovedPinInfo> auto_removed_out_pins;
      71              : 
      72           10 :     RemoveGraphNodeCommand(AudioEngine& engine, NodeId id, EffectType t, ImVec2 pos)
      73           10 :         : engine_(engine), node_id(id), type(t), position(pos) {}
      74              : 
      75            9 :     bool execute() override {
      76            9 :         auto* node_to_remove = engine_.graph().find_node(node_id);
      77            9 :         if (!node_to_remove) return false;
      78              :         
      79            6 :         cached_node = *node_to_remove;
      80              : 
      81              :         // Cache severed links before removal
      82            6 :         severed_links.clear();
      83           12 :         for (const auto& link : engine_.graph().get_links()) {
      84           10 :             if (std::find(cached_node.input_pin_ids.begin(), cached_node.input_pin_ids.end(), link.dest_pin_id) != cached_node.input_pin_ids.end() ||
      85           10 :                 std::find(cached_node.output_pin_ids.begin(), cached_node.output_pin_ids.end(), link.source_pin_id) != cached_node.output_pin_ids.end()) {
      86            6 :                 severed_links.push_back(link);
      87            2 :             }
      88              :         }
      89              : 
      90            6 :         engine_.graph().remove_node(node_id);
      91            6 :         GuiGraphState::get_instance().node_positions.erase(node_id);
      92              :         
      93              :         // Clean up empty pins on mixers that were affected by severed links
      94           12 :         for (const auto& link : severed_links) {
      95            6 :             int dest_node_id = engine_.graph().get_node_from_pin(link.dest_pin_id);
      96            6 :             if (dest_node_id != -1) {
      97            6 :                 const DSPNode* n = engine_.graph().find_node(dest_node_id);
      98            6 :                 if (n && (n->routing_type == NodeRoutingType::Mixer || n->routing_type == NodeRoutingType::MergeSum)) {
      99            6 :                     if (n->input_pin_ids.size() > 2) {
     100            0 :                         int idx = -1; float gain = 1.0f;
     101            0 :                         for (size_t i = 0; i < n->input_pin_ids.size(); ++i) {
     102            0 :                             if (n->input_pin_ids[i] == link.dest_pin_id) {
     103            0 :                                 idx = i; gain = (i < n->input_gains.size()) ? n->input_gains[i] : 1.0f; break;
     104              :                             }
     105            0 :                         }
     106            0 :                         if (engine_.graph().remove_input_pin(dest_node_id, link.dest_pin_id)) {
     107            0 :                             auto_removed_pins.push_back({dest_node_id, link.dest_pin_id, idx, gain});
     108            0 :                         }
     109            0 :                     }
     110            2 :                 }
     111            2 :             }
     112              : 
     113            6 :             int source_node_id = engine_.graph().get_node_from_pin(link.source_pin_id);
     114            6 :             if (source_node_id != -1) {
     115            0 :                 const DSPNode* n = engine_.graph().find_node(source_node_id);
     116            0 :                 if (n && n->routing_type == NodeRoutingType::Splitter) {
     117            0 :                     if (n->output_pin_ids.size() > 2) {
     118            0 :                         int idx = -1;
     119            0 :                         for (size_t i = 0; i < n->output_pin_ids.size(); ++i) {
     120            0 :                             if (n->output_pin_ids[i] == link.source_pin_id) {
     121            0 :                                 idx = i; break;
     122              :                             }
     123            0 :                         }
     124            0 :                         if (engine_.graph().remove_output_pin(source_node_id, link.source_pin_id)) {
     125            0 :                             auto_removed_out_pins.push_back({source_node_id, link.source_pin_id, idx, 1.0f});
     126            0 :                         }
     127            0 :                     }
     128            0 :                 }
     129            0 :             }
     130              :         }
     131              : 
     132            6 :         engine_.commit_graph_changes();
     133            6 :         return true;
     134            3 :     }
     135              : 
     136            3 :     void undo() override {
     137            3 :         engine_.graph().restore_node(cached_node);
     138            3 :         GuiGraphState::get_instance().node_positions[node_id] = { position, false, ImVec2(0, 0) };
     139              :         
     140            3 :         for (auto it = auto_removed_pins.rbegin(); it != auto_removed_pins.rend(); ++it) {
     141            0 :             engine_.graph().restore_input_pin(it->node_id, it->pin_id, it->index, it->gain);
     142            0 :         }
     143            3 :         for (auto it = auto_removed_out_pins.rbegin(); it != auto_removed_out_pins.rend(); ++it) {
     144            0 :             engine_.graph().restore_output_pin(it->node_id, it->pin_id, it->index);
     145            0 :         }
     146            6 :         for (const auto& link : severed_links) {
     147            3 :             engine_.graph().restore_link(link);
     148              :         }
     149            3 :         engine_.commit_graph_changes();
     150            3 :     }
     151              : 
     152            0 :     const char* description() const override { return "Remove Node"; }
     153              : };
     154              : 
     155              : struct AddGraphLinkCommand : public Command {
     156              :     AudioEngine& engine_;
     157              :     GraphLink link;
     158            3 :     bool was_successful = false;
     159              :     
     160            3 :     int auto_added_pin_node_id = -1;
     161            3 :     int auto_added_pin_id = -1;
     162            3 :     int auto_added_pin_index = -1;
     163            3 :     float auto_added_pin_gain = 1.0f;
     164              :     
     165            3 :     int auto_added_out_pin_node_id = -1;
     166            3 :     int auto_added_out_pin_id = -1;
     167            3 :     int auto_added_out_pin_index = -1;
     168              : 
     169           15 :     AddGraphLinkCommand(AudioEngine& engine, int src_pin, int dst_pin)
     170           12 :         : engine_(engine) {
     171            9 :             link.source_pin_id = src_pin;
     172            9 :             link.dest_pin_id = dst_pin;
     173            9 :             link.id = -1; // Unknown until execute
     174           12 :         }
     175              : 
     176           12 :     bool execute() override {
     177           12 :         if (link.id == -1) {
     178            9 :             bool already_exists = false;
     179            9 :             for (const auto& l : engine_.graph().get_links()) {
     180            3 :                 if (l.source_pin_id == link.source_pin_id && l.dest_pin_id == link.dest_pin_id) {
     181            2 :                     already_exists = true;
     182            2 :                     break;
     183              :                 }
     184              :             }
     185            9 :             if (already_exists) return false;
     186              : 
     187            6 :             link.id = engine_.graph().add_link(link.source_pin_id, link.dest_pin_id);
     188            6 :             was_successful = (link.id != -1);
     189              :             
     190            6 :             if (was_successful) {
     191            6 :                 int dest_node_id = engine_.graph().get_node_from_pin(link.dest_pin_id);
     192            6 :                 if (dest_node_id != -1) {
     193            6 :                     const DSPNode* node = engine_.graph().find_node(dest_node_id);
     194            6 :                     if (node && (node->routing_type == NodeRoutingType::Mixer || node->routing_type == NodeRoutingType::MergeSum)) {
     195            6 :                         size_t occupied_count = 0;
     196           18 :                         for (int p : node->input_pin_ids) {
     197           18 :                             for (const auto& l : engine_.graph().get_links()) {
     198           12 :                                 if (l.dest_pin_id == p) {
     199            6 :                                     occupied_count++; break;
     200              :                                 }
     201              :                             }
     202              :                         }
     203            6 :                         if (occupied_count == node->input_pin_ids.size()) {
     204            0 :                             if (engine_.graph().add_input_pin(dest_node_id)) {
     205            0 :                                 const DSPNode* updated_node = engine_.graph().find_node(dest_node_id);
     206            0 :                                 int new_pin = updated_node->input_pin_ids.back();
     207              :                                 
     208            0 :                                 auto_added_pin_node_id = dest_node_id;
     209            0 :                                 auto_added_pin_id = new_pin;
     210            0 :                                 auto_added_pin_index = updated_node->input_pin_ids.size() - 1;
     211            0 :                                 auto_added_pin_gain = 1.0f;
     212            0 :                             }
     213            0 :                         }
     214            2 :                     }
     215            2 :                 }
     216              : 
     217            6 :                 int source_node_id = engine_.graph().get_node_from_pin(link.source_pin_id);
     218            6 :                 if (source_node_id != -1) {
     219            6 :                     const DSPNode* node = engine_.graph().find_node(source_node_id);
     220            6 :                     if (node && node->routing_type == NodeRoutingType::Splitter) {
     221            6 :                         size_t occupied_count = 0;
     222           18 :                         for (int p : node->output_pin_ids) {
     223           18 :                             for (const auto& l : engine_.graph().get_links()) {
     224           12 :                                 if (l.source_pin_id == p) {
     225            6 :                                     occupied_count++; break;
     226              :                                 }
     227              :                             }
     228              :                         }
     229            6 :                         if (occupied_count == node->output_pin_ids.size()) {
     230            0 :                             if (engine_.graph().add_output_pin(source_node_id)) {
     231            0 :                                 const DSPNode* updated_node = engine_.graph().find_node(source_node_id);
     232            0 :                                 int new_pin = updated_node->output_pin_ids.back();
     233              :                                 
     234            0 :                                 auto_added_out_pin_node_id = source_node_id;
     235            0 :                                 auto_added_out_pin_id = new_pin;
     236            0 :                                 auto_added_out_pin_index = updated_node->output_pin_ids.size() - 1;
     237            0 :                             }
     238            0 :                         }
     239            2 :                     }
     240            2 :                 }
     241            2 :             }
     242            5 :         } else if (was_successful) {
     243            3 :             if (auto_added_pin_node_id != -1) {
     244            0 :                 engine_.graph().restore_input_pin(auto_added_pin_node_id, auto_added_pin_id, auto_added_pin_index, auto_added_pin_gain);
     245            0 :             }
     246            3 :             if (auto_added_out_pin_node_id != -1) {
     247            0 :                 engine_.graph().restore_output_pin(auto_added_out_pin_node_id, auto_added_out_pin_id, auto_added_out_pin_index);
     248            0 :             }
     249            3 :             engine_.graph().restore_link(link);
     250            1 :         }
     251            9 :         if (was_successful) {
     252            9 :             engine_.commit_graph_changes();
     253            3 :         }
     254            9 :         return was_successful;
     255            4 :     }
     256              : 
     257            3 :     void undo() override {
     258            3 :         if (was_successful) {
     259            3 :             engine_.graph().remove_link(link.id);
     260            3 :             if (auto_added_pin_node_id != -1) {
     261            0 :                 engine_.graph().remove_input_pin(auto_added_pin_node_id, auto_added_pin_id);
     262            0 :             }
     263            3 :             if (auto_added_out_pin_node_id != -1) {
     264            0 :                 engine_.graph().remove_output_pin(auto_added_out_pin_node_id, auto_added_out_pin_id);
     265            0 :             }
     266            3 :             engine_.commit_graph_changes();
     267            1 :         }
     268            3 :     }
     269              : 
     270            0 :     const char* description() const override { return "Add Link"; }
     271              : };
     272              : 
     273              : struct RemoveGraphLinkCommand : public Command {
     274              :     AudioEngine& engine_;
     275              :     GraphLink link;
     276              :     
     277            2 :     int auto_removed_pin_node_id = -1;
     278            2 :     int auto_removed_pin_id = -1;
     279            2 :     int auto_removed_pin_index = -1;
     280            2 :     float auto_removed_pin_gain = 1.0f;
     281              :     
     282            2 :     int auto_removed_out_pin_node_id = -1;
     283            2 :     int auto_removed_out_pin_id = -1;
     284            2 :     int auto_removed_out_pin_index = -1;
     285              : 
     286            8 :     RemoveGraphLinkCommand(AudioEngine& engine, const GraphLink& l)
     287           10 :         : engine_(engine), link(l) {}
     288              : 
     289            9 :     bool execute() override {
     290            9 :         bool success = engine_.graph().remove_link(link.id);
     291            9 :         if (success) {
     292            6 :             int dest_node_id = engine_.graph().get_node_from_pin(link.dest_pin_id);
     293            6 :             if (dest_node_id != -1) {
     294            6 :                 const DSPNode* node = engine_.graph().find_node(dest_node_id);
     295            6 :                 if (node && (node->routing_type == NodeRoutingType::Mixer || node->routing_type == NodeRoutingType::MergeSum)) {
     296            6 :                     if (node->input_pin_ids.size() > 2) {
     297            0 :                         for (size_t i = 0; i < node->input_pin_ids.size(); ++i) {
     298            0 :                             if (node->input_pin_ids[i] == link.dest_pin_id) {
     299            0 :                                 auto_removed_pin_index = i;
     300            0 :                                 auto_removed_pin_gain = (i < node->input_gains.size()) ? node->input_gains[i] : 1.0f;
     301            0 :                                 break;
     302              :                             }
     303            0 :                         }
     304            0 :                         if (engine_.graph().remove_input_pin(dest_node_id, link.dest_pin_id)) {
     305            0 :                             auto_removed_pin_node_id = dest_node_id;
     306            0 :                             auto_removed_pin_id = link.dest_pin_id;
     307            0 :                         }
     308            0 :                     }
     309            2 :                 }
     310            2 :             }
     311              : 
     312            6 :             int source_node_id = engine_.graph().get_node_from_pin(link.source_pin_id);
     313            6 :             if (source_node_id != -1) {
     314            6 :                 const DSPNode* node = engine_.graph().find_node(source_node_id);
     315            6 :                 if (node && node->routing_type == NodeRoutingType::Splitter) {
     316            6 :                     if (node->output_pin_ids.size() > 2) {
     317            0 :                         for (size_t i = 0; i < node->output_pin_ids.size(); ++i) {
     318            0 :                             if (node->output_pin_ids[i] == link.source_pin_id) {
     319            0 :                                 auto_removed_out_pin_index = i;
     320            0 :                                 break;
     321              :                             }
     322            0 :                         }
     323            0 :                         if (engine_.graph().remove_output_pin(source_node_id, link.source_pin_id)) {
     324            0 :                             auto_removed_out_pin_node_id = source_node_id;
     325            0 :                             auto_removed_out_pin_id = link.source_pin_id;
     326            0 :                         }
     327            0 :                     }
     328            2 :                 }
     329            2 :             }
     330            6 :             engine_.commit_graph_changes();
     331            2 :         }
     332            9 :         return success;
     333              :     }
     334              : 
     335            3 :     void undo() override {
     336            3 :         if (auto_removed_pin_node_id != -1) {
     337            0 :             engine_.graph().restore_input_pin(auto_removed_pin_node_id, auto_removed_pin_id, auto_removed_pin_index, auto_removed_pin_gain);
     338            0 :         }
     339            3 :         if (auto_removed_out_pin_node_id != -1) {
     340            0 :             engine_.graph().restore_output_pin(auto_removed_out_pin_node_id, auto_removed_out_pin_id, auto_removed_out_pin_index);
     341            0 :         }
     342            3 :         engine_.graph().restore_link(link);
     343            3 :         engine_.commit_graph_changes();
     344            3 :     }
     345              : 
     346            0 :     const char* description() const override { return "Remove Link"; }
     347              : };
     348              : 
     349              : struct MoveGraphNodeCommand : public Command {
     350              :     NodeId node_id;
     351              :     ImVec2 old_pos;
     352              :     ImVec2 new_pos;
     353              : 
     354           12 :     MoveGraphNodeCommand(NodeId id, ImVec2 old_pos, ImVec2 new_pos)
     355           12 :         : node_id(id), old_pos(old_pos), new_pos(new_pos) {}
     356              : 
     357           12 :     bool execute() override {
     358           12 :         auto& positions = GuiGraphState::get_instance().node_positions;
     359           12 :         auto it = positions.find(node_id);
     360           12 :         if (it == positions.end()) return false;
     361              :         
     362            9 :         if (it->second.position.x == new_pos.x && it->second.position.y == new_pos.y) {
     363            2 :             return false;
     364              :         }
     365              :         
     366            6 :         it->second.position = new_pos;
     367            6 :         return true;
     368            4 :     }
     369              : 
     370            3 :     void undo() override {
     371            3 :         auto& positions = GuiGraphState::get_instance().node_positions;
     372            3 :         if (positions.count(node_id)) {
     373            3 :             positions[node_id].position = old_pos;
     374            1 :         }
     375            3 :     }
     376              : 
     377            0 :     const char* description() const override { return "Move Node"; }
     378              : };
     379              : 
     380              : } // namespace Amplitron
        

Generated by: LCOV version 2.0-1