diff --git a/src/connection.h b/src/connection.h index d5c29ad..1c123f0 100644 --- a/src/connection.h +++ b/src/connection.h @@ -2,10 +2,11 @@ #include "button.h" #include "datatypes.h" +#include "graph-item.h" #include "node.h" #include "raylib.h" -class Connection{ +class Connection : public GraphItem{ public: Connection(std::map *nodes, int v1, int v2) : v1(v1), v2(v2), nodes(nodes) {} diff --git a/src/datatypes.h b/src/datatypes.h index 6b33460..fabbf59 100644 --- a/src/datatypes.h +++ b/src/datatypes.h @@ -166,6 +166,11 @@ struct AppSettings{ int gui_selected_midi_out; }; + +// Graph + +inline enum GraphItemType {NODE, EDGE} type; + // MIDI struct MidiOutPort{ @@ -180,6 +185,7 @@ struct MidiOut{ }; namespace MIDI{ + inline std::vector Fractions = {"1/32","1/16","3/32","1/8","5/32","3/16","7/32","1/4","9/32","5/16","11/32","3/8","13/32","7/16","15/32","1/2","17/32","9/16","19/32","5/8","21/32","11/16","23/32","3/4","25/32","13/16","27/32","7/8","29/32","15/16","31/32","1/1"}; inline std::vector Notes = { "C-1", "C#-1", "D-1", "D#-1", "E-1", "F-1", "F#-1", "G-1", "G#-1", "A-1", "A#-1", "B-1", "C0", "C#0", "D0", "D#0", diff --git a/src/graph-item.h b/src/graph-item.h new file mode 100644 index 0000000..f38b092 --- /dev/null +++ b/src/graph-item.h @@ -0,0 +1,13 @@ +#pragma once + +#include "datatypes.h" +#include + +class GraphItem{ + + public: + int id; + GraphItemType type; + std::vector connections; + +}; diff --git a/src/graph-manager.h b/src/graph-manager.h new file mode 100644 index 0000000..41e45d3 --- /dev/null +++ b/src/graph-manager.h @@ -0,0 +1,169 @@ +#pragma once + +#include "datatypes.h" +#include "node.h" +#include "connection.h" +#include +#include +#include + +class GraphManager{ + + public: + + GraphManager() {} + + std::map nodes; + std::vector connections; + + std::vector keys; + int iterator = 0; + + std::map counter; + + int getRandomNode(){ + std::vector o; + std::ranges::sample(keys, std::back_inserter(o), 1, std::mt19937{std::random_device{}()}); + return o.front(); + } + + void copyNodes(Vec2D position){ + for(auto n: nodes | std::ranges::views::filter([](std::pair n){ return n.second.copied;})){ + n.second.position = position + n.second.drag_offset; + n.second.id = iterator; + n.second.copied = false; + n.second.selected = true; + nodes.insert(std::make_pair(iterator, n.second)); + keys.push_back(iterator); + iterator++; + } + } + + int copyNode(int id, Vec2D position){ + Node n = nodes.at(id); + n.position = position; + n.id = iterator; + nodes.insert(std::make_pair(iterator, n)); + keys.push_back(iterator); + iterator++; + return iterator - 1; + } + + void setNodeMode(int mode){ + for(auto &n : nodes | std::ranges::views::values){ + n.mode = mode; + } + } + + int addNode(float x, float y){ + return addNode(Vec2D(x,y)); + } + + int addNode(Vec2D position){ + nodes.emplace(std::make_pair(iterator, Node(iterator, position, 15.0f))); + keys.push_back(iterator); + iterator++; + return iterator - 1; + } + + void triggerNode(int node){ + nodes.at(node).trigger(); + } + + void reset(){ + counter.clear(); + } + + int next(int current, int last){ + std::vector possible_connections; + std::vector next_node_ids; + + for(auto& c : connections){ + if((c.v1 == current && c.v1_v2) || (c.v2 == current && c.v2_v1)) { + c.active = false; + possible_connections.push_back(&c); + next_node_ids.push_back( c.v1 == current ? c.v2 : c.v1 ); + } + } + + if(possible_connections.empty()) return -1; + + int return_path = 0; + float angle = -1; + if(nodes.at(current).distributionMode == Distribution::GEOMETRIC){ + Vec2D coming_from = (nodes.at(current).position - nodes.at(last).position).norm(); + int i = 0; + for(auto n : possible_connections){ + int next_position = n->v1 == current ? n->v2 : n->v1; + float angle_v1_v2 = coming_from.norm().dot((nodes.at(next_position).position - nodes.at(current).position).norm()); + if(angle_v1_v2 > angle) { + return_path = i; + angle = angle_v1_v2; + } + ++i; + } + } + + if(nodes.at(current).distributionMode == Distribution::ROUNDROBIN){ + if(counter.find(current) == counter.end()) { + counter[current] = 0; + } else { + counter[current] = (counter[current] + 1) % possible_connections.size(); + return_path = counter[current]; + } + } + + if(nodes.at(current).distributionMode == Distribution::RANDOM){ + return_path = rand() % possible_connections.size(); + } + + possible_connections.at(return_path)->active = true; + return possible_connections.at(return_path)->v1 == current ? possible_connections.at(return_path)->v2 : possible_connections.at(return_path)->v1; + } + + void addConnection(int from, int to){ + connections.emplace_back(&nodes, from, to); + } + + void drawConnections(Vec2D *mouse_pos, Camera2D *cam){ + std::erase_if(connections, [](Connection c){ return c.remove == true;}); + for(auto& c : connections){ + c.draw(mouse_pos, cam); + } + } + void drawNodes(Vec2D *mouse_pos, Camera2D *camera){ + auto neighbors = nodes | std::ranges::views::values | std::ranges::views::filter([](Node n){ return !n.dragging && !n.selected;}) | std::ranges::to>(); + float min_d = 480; + for(auto& n : nodes | std::ranges::views::values ){ + if(n.dragging){ + std::sort(neighbors.begin(), neighbors.end(), [n](Node na, Node nb){ return na.position.dist(n.position) < nb.position.dist(n.position);}); + if(neighbors.size() > 1){ + min_d = neighbors.front().position.dist(n.position); + n.neighbor = &nodes.at(neighbors.at(1).id); + int i = 0; + for(auto it = (neighbors.begin()); it != neighbors.end(); it++, i++){ + float d = it->position.dist(n.position); + if(d > 0 && d < min_d*1.5 && d < 960) { + std::string frac = MIDI::Fractions.at((int)Clamp(round(d/60) - 1, 0, 31)); + Vector2 text_length = MeasureTextEx(GetFontDefault(), std::format("{:.1f}",d).c_str(), 10.0f, 2.0f); + Vector2 text_length_frac = MeasureTextEx(GetFontDefault(), frac.c_str(), 10.0f, 2.0f); + + Vec2D middle_handle = it->position + (n.position - it->position) * ((d - text_length.x)*0.5f)/d; + Vec2D middle_handle_frac = it->position + (n.position - it->position) * ((d - text_length_frac.x)*0.5f)/d; + Vec2D text_position = middle_handle + (it->position - n.position).norm().orth() * 12.0f; + Vec2D text_position_frac = middle_handle_frac - (it->position - n.position).norm().orth() * 4.0f; + + float rotation = Vector2Angle((Vector2){1,0}, (n.position - it->position)) * RAD2DEG; + DrawTextPro(GetFontDefault(), std::format("{:.1f}",d).c_str(), text_position, (Vector2){0,0}, rotation, 10.0f, 2.0f, VS_COLOR_PINK); + if(i == 0 && IsKeyDown(KEY_LEFT_SHIFT)) DrawTextPro(GetFontDefault(), frac.c_str(), text_position_frac, (Vector2){0,0}, rotation, 10.0f, 2.0f, VS_COLOR_PINK); + DrawLineV(n.position, it->position, VS_COLOR_PINK); + } + } + } + } + n.draw(mouse_pos, camera); + } + } + +}; + diff --git a/src/node.h b/src/node.h index 7fdff5f..25a5af2 100644 --- a/src/node.h +++ b/src/node.h @@ -4,6 +4,7 @@ #include #include "datatypes.h" +#include "graph-item.h" #include "moveable.h" #include "raymath.h" @@ -15,19 +16,17 @@ struct NodeMidiOut{ bool enabled; }; -class Node : public Movable{ +class Node : public Movable, public GraphItem{ public: Node() {} - Node(int id, float x, float y, float r) : Movable(Vec2D(x, y), 0.0f), radius(r), id(id) {} - Node(int id, Vec2D p, float r) : Movable(p, 0.0f), radius(r), id(id) {} + Node(int id, float x, float y, float r) : Movable(Vec2D(x, y), 0.0f), GraphItem(id), radius(r) {} + Node(int id, Vec2D p, float r) : Movable(p, 0.0f), GraphItem(id), radius(r) {} Node *neighbor = nullptr; float radius; - int id; - bool picked_up = false; bool dragging = false; bool in_rect = false; diff --git a/src/signal-manager.h b/src/signal-manager.h index 0b15dc9..7c0f4ad 100644 --- a/src/signal-manager.h +++ b/src/signal-manager.h @@ -1,10 +1,8 @@ #pragma once -#include "connection-manager.h" #include "datatypes.h" -#include "node-manager.h" +#include "graph-manager.h" #include "signal.h" -#include "node.h" #include "midi.h" #include @@ -14,13 +12,12 @@ class SignalManager{ public: SignalManager() {} - SignalManager(NodeManager *nm, ConnectionManager *cm, std::mutex *mutex) : mutex(mutex), cm(cm), nm(nm) {} + SignalManager(GraphManager *gm, std::mutex *mutex) : mutex(mutex), gm(gm), nm(nm) {} Midi midi; std::mutex *mutex; - ConnectionManager *cm; - NodeManager *nm; + GraphManager *gm; std::vector signals; @@ -42,11 +39,11 @@ class SignalManager{ void reset(){ for(auto &s : signals){ - s.position = nm->nodes[s.start].position + Vec2D{0.1, 0.1}; + s.position = gm->nodes[s.start].position + Vec2D{0.1, 0.1}; s.next = s.start; s.last = s.start; } - cm->reset(); + gm->reset(); } void startSignalThread(){ @@ -60,7 +57,7 @@ class SignalManager{ void addSignalAtNode(int node_id){ Color c = {(unsigned char)(20 + (rand() % 200)), 100, (unsigned char)(20 + (int)(rand() % 200)), 200}; - signals.emplace(signals.end(), Signal(nm->nodes[node_id].position.x+1, nm->nodes[node_id].position.y+1, 8.0f, 0, c)); // position + 1 or else it does not render. why? no idea! + signals.emplace(signals.end(), Signal(gm->nodes[node_id].position.x+1, gm->nodes[node_id].position.y+1, 8.0f, 0, c)); // position + 1 or else it does not render. why? no idea! signals.back().next = node_id; signals.back().last = node_id; signals.back().start = node_id; @@ -78,18 +75,18 @@ class SignalManager{ mutex->lock(); if(play){ for(auto& s : signals){ - s.direction = (nm->nodes[s.next].position - s.position).norm(); + s.direction = (gm->node(s.next).position - s.position).norm(); s.update(diff.count()); - if(s.position.dist(&nm->nodes[s.next].position) < 0.6f) { - int n = cm->next(s.next, s.last); - nm->nodes[s.next].trigger(); + if(s.position.dist(&gm->nodes[s.next].position) < 0.6f) { + int n = gm->next(s.next, s.last); + gm->triggerNode(s.next); if(n != s.next && n != -1) { - for(auto &m : nm->nodes[s.next].midi_outs){ + for(auto &m : gm->nodes[s.next].midi_outs){ if(m.second.enabled) { - if(nm->nodes[s.next].send_note) midi.note(m.second.i, nm->nodes[s.next].midi_note_channel, nm->nodes[s.next].midi_note, nm->nodes[s.next].midi_velocity, nm->nodes[s.next].midi_gate); - if(nm->nodes[s.next].send_cc) midi.cc(m.second.i, nm->nodes[s.next].midi_cc_channel, nm->nodes[s.next].midi_cc, nm->nodes[s.next].midi_cc_value); - if(nm->nodes[s.next].send_pitch_bend) midi.pitch_bend(m.second.i, nm->nodes[s.next].midi_pitch_bend_channel, nm->nodes[s.next].midi_pitch_bend); + if(gm->nodes[s.next].send_note) midi.note(m.second.i, gm->nodes[s.next].midi_note_channel, gm->nodes[s.next].midi_note, gm->nodes[s.next].midi_velocity, gm->nodes[s.next].midi_gate); + if(gm->nodes[s.next].send_cc) midi.cc(m.second.i, gm->nodes[s.next].midi_cc_channel, gm->nodes[s.next].midi_cc, gm->nodes[s.next].midi_cc_value); + if(gm->nodes[s.next].send_pitch_bend) midi.pitch_bend(m.second.i, gm->nodes[s.next].midi_pitch_bend_channel, gm->nodes[s.next].midi_pitch_bend); } } s.last = s.next; diff --git a/src/state-manager.h b/src/state-manager.h index 419af13..c4c51a2 100644 --- a/src/state-manager.h +++ b/src/state-manager.h @@ -2,8 +2,7 @@ #include "connection.h" #include "datatypes.h" -#include "node-manager.h" -#include "connection-manager.h" +#include "graph-manager.h" #include "signal-manager.h" #include "ui.h" @@ -15,7 +14,7 @@ class AppState{ public: - AppState() : cm(&nm), sm(&nm, &cm, &mutex), ui(&settings, &nm, &cm, &sm) {} + AppState() : sm(&gm, &mutex), ui(&settings, &sm, &gm) {} enum States {IDLE, CONFIG}; @@ -25,14 +24,13 @@ class AppState{ std::mutex mutex; - ConnectionManager cm; + GraphManager gm; SignalManager sm; - NodeManager nm; AppUI ui; void updateMidiPorts(){ - for(auto&n : nm.nodes | std::views::values){ + for(auto&n : gm.nodes | std::views::values){ for(auto& mo : sm.midi.midi_out_ports){ if(!n.midi_outs.contains(mo.number) && mo.enabled){ n.midi_outs[mo.number] = {mo.number, mo.port.port_name, false}; @@ -45,16 +43,16 @@ class AppState{ } void copyNodesAndConnections(Vec2D position){ - auto copy_nodes_ids = nm.nodes | std::ranges::views::values | std::ranges::views::filter([](Node n){return n.copied;}) | std::views::transform([](Node n){return n.id;}) | std::ranges::to>(); + auto copy_nodes_ids = gm.nodes | std::ranges::views::values | std::ranges::views::filter([](Node n){return n.copied;}) | std::views::transform([](Node n){return n.id;}) | std::ranges::to>(); std::vector copy_connections; - std::ranges::copy_if(cm.connections, std::back_inserter(copy_connections), [copy_nodes_ids](Connection c){return std::ranges::contains(copy_nodes_ids, c.v1) && std::ranges::contains(copy_nodes_ids, c.v2);}); + std::ranges::copy_if(gm.connections, std::back_inserter(copy_connections), [copy_nodes_ids](Connection c){return std::ranges::contains(copy_nodes_ids, c.v1) && std::ranges::contains(copy_nodes_ids, c.v2);}); for(auto &n : copy_nodes_ids){ int old_id = n; - int new_id = nm.copyNode(n, position + nm.nodes[n].drag_offset); - nm.nodes[new_id].copied = false; - nm.nodes[new_id].selected = true; + int new_id = gm.copyNode(n, position + gm.nodes[n].drag_offset); + gm.nodes[new_id].copied = false; + gm.nodes[new_id].selected = true; for(auto &c : copy_connections){ if(c.v1 == old_id) c.v1 = new_id; @@ -62,29 +60,29 @@ class AppState{ } } - cm.connections.insert(cm.connections.end(), copy_connections.cbegin(), copy_connections.cend()); + gm.connections.insert(gm.connections.end(), copy_connections.cbegin(), copy_connections.cend()); } void removeNodes(){ - for(auto &n : nm.nodes){ + for(auto &n : gm.nodes){ n.second.highlighted = false; } - auto erase_nodes = nm.nodes | std::ranges::views::values | std::ranges::views::filter([](Node n){return n.erase;}); + auto erase_nodes = gm.nodes | std::ranges::views::values | std::ranges::views::filter([](Node n){return n.erase;}); std::vector erase_nodes_ids; for(auto &e : erase_nodes){ erase_nodes_ids.push_back(e.id); } - auto erase_connections = cm.connections | std::ranges::views::filter([erase_nodes_ids](Connection c){ return std::ranges::contains(erase_nodes_ids, c.v1) || std::ranges::contains(erase_nodes_ids, c.v2);}); + auto erase_connections = gm.connections | std::ranges::views::filter([erase_nodes_ids](Connection c){ return std::ranges::contains(erase_nodes_ids, c.v1) || std::ranges::contains(erase_nodes_ids, c.v2);}); std::vector adjacent_nodes_ids; for(auto &c : erase_connections){ - if(!nm.nodes.at(c.v1).selected) adjacent_nodes_ids.push_back(c.v1); - if(!nm.nodes.at(c.v2).selected) adjacent_nodes_ids.push_back(c.v2); + if(!gm.nodes.at(c.v1).selected) adjacent_nodes_ids.push_back(c.v1); + if(!gm.nodes.at(c.v2).selected) adjacent_nodes_ids.push_back(c.v2); } auto reroute_signals = sm.signals | std::ranges::views::filter([erase_nodes_ids](Signal s){ @@ -95,8 +93,8 @@ class AppState{ float dist = 10000000; int next_id = -1; for(int i : adjacent_nodes_ids){ - if(nm.nodes.at(i).position.dist(&s.position) < dist) { - dist = nm.nodes.at(i).position.dist(&s.position); + if(gm.nodes.at(i).position.dist(&s.position) < dist) { + dist = gm.nodes.at(i).position.dist(&s.position); next_id = i; } } @@ -113,19 +111,19 @@ class AppState{ } for(auto &n : erase_nodes){ - std::erase_if(cm.connections, [n](Connection c){ return c.v1 == n.id || c.v2 == n.id;}); + std::erase_if(gm.connections, [n](Connection c){ return c.v1 == n.id || c.v2 == n.id;}); } - std::erase_if(nm.nodes, [](std::pair n){ return n.second.erase;}); - nm.keys.clear(); - std::ranges::copy(nm.nodes | std::ranges::views::keys, std::back_inserter(nm.keys)); + std::erase_if(gm.nodes, [](std::pair n){ return n.second.erase;}); + gm.keys.clear(); + std::ranges::copy(gm.nodes | std::ranges::views::keys, std::back_inserter(gm.keys)); } void draw(Vec2D *mouse_pos, Camera2D *camera){ mutex.lock(); updateMidiPorts(); mutex.unlock(); - nm.drawNodes(mouse_pos, camera); - cm.drawConnections(mouse_pos, camera); + gm.drawNodes(mouse_pos, camera); + gm.drawConnections(mouse_pos, camera); sm.drawSignals(mouse_pos, camera); } diff --git a/src/ui.h b/src/ui.h index 58aca8e..78cb75f 100644 --- a/src/ui.h +++ b/src/ui.h @@ -1,25 +1,23 @@ #pragma once +#include "raylib.h" #include "raygui/raygui.h" #include "raygui/dark/style_dark.h" #include "datatypes.h" - -#include "connection-manager.h" -#include "node-manager.h" -#include "raylib.h" +#include "graph-manager.h" #include "signal-manager.h" + #include class AppUI{ public: - AppUI(AppSettings *settings, NodeManager *nm, ConnectionManager *cm, SignalManager *sm) : settings(settings), nm(nm), sm(sm), cm(cm) {} + AppUI(AppSettings *settings, SignalManager *sm, GraphManager *gm) : settings(settings), gm(gm), sm(sm) {} AppSettings *settings; - NodeManager *nm; + GraphManager *gm; SignalManager *sm; - ConnectionManager *cm; void init(){ GuiLoadStyleDark(); @@ -54,7 +52,7 @@ class AppUI{ if(GuiButton((Rectangle){20, (float)s_midi + 45 + i*20.0f, 240, 20}, "ENABLE FOR ALL")) { for(auto &midis : sm->midi.midi_out_ports){ if(midis.enabled){ - for(auto &n : nm->nodes){ + for(auto &n : gm->nodes){ n.second.setMidiPort(midis.number, true); } } @@ -64,7 +62,7 @@ class AppUI{ int s_signal = s_midi + 100 + i*20; GuiLabel((Rectangle){20, (float)s_signal, 240, 20}, "SIGNALS"); if(GuiButton((Rectangle){20, (float)s_signal + 35, 240, 20}, "ADD RANDOM SIGNAL")){ - int ni = nm->getRandomNode(); + int ni = gm->getRandomNode(); sm->addSignalAtNode(ni); } @@ -83,14 +81,14 @@ class AppUI{ int s_nodes = s_signal + 70 + signal_configs*20; GuiLabel((Rectangle){20, (float)s_nodes + 30, 240, 20}, "NODES"); if(GuiButton((Rectangle){20, (float)s_nodes + 60, 240, 20}, "ADD RANDOM NODE")){ - nm->addNode(200.0f, 200.0f); + gm->addNode(200.0f, 200.0f); } - DrawText(std::format("{}", nm->nodes.size()).c_str(), 20, settings->height - 70, 15, VS_COLOR_WHITE); + DrawText(std::format("{}", gm->nodes.size()).c_str(), 20, settings->height - 70, 15, VS_COLOR_WHITE); DrawFPS(20, settings->height - 30); } void drawPerNodeConfig(Camera2D *camera){ - for(auto& n : nm->nodes | std::ranges::views::values | std::views::filter([](Node n){ return n.config; })){ + for(auto& n : gm->nodes | std::ranges::views::values | std::views::filter([](Node n){ return n.config; })){ Vector2 np = GetWorldToScreen2D((Vector2){n.position.x, n.position.y}, *camera); n.config = !GuiWindowBox((Rectangle){np.x + 10, np.y + 10, 240, 440}, "config");