LCOV - code coverage report
Current view: top level - src/gui/commands - command_graph.h (source / functions) Coverage Total Hit
Test: merged.info Lines: 64.7 % 283 183
Test Date: 2026-06-07 15:51:50 Functions: 79.2 % 24 19

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

Generated by: LCOV version 2.0-1