diff --git a/.gitignore b/.gitignore index 3643a01..ade31b3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ *.out /build /src/test.cpp +compile_flags.txt +.cache/ + +external/raylib-build/ +external/raylib-subbuild/ +external/raylib-src/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..dda6bd7 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "external/raylib"] + path = external/raylib + url = https://github.com/raysan5/raylib.git diff --git a/CMakeLists.txt b/CMakeLists.txt index d62a1ac..8255365 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,18 +4,30 @@ project(graph CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -find_package(raylib REQUIRED) +if(WIN32) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) +endif() + +add_subdirectory(external) + set(SOURCES - src/test.cpp + src/main.cpp src/board.cpp src/node.cpp src/sidebar.cpp - src/algorithms/bfs.cpp - src/algorithms/dfs.cpp - src/algorithms/dijkstra.cpp + + src/algorithms/BFS/bfs.cpp + src/algorithms/DFS/dfs.cpp + src/algorithms/Dijkstra/dijkstra.cpp + src/algorithms/Bellman-Ford/bellmanFord.cpp ) add_executable(${PROJECT_NAME} ${SOURCES}) -target_include_directories(${PROJECT_NAME} PRIVATE src) -target_link_libraries(${PROJECT_NAME} raylib) +target_link_libraries(${PROJECT_NAME} PRIVATE raylib) +file(COPY ${CMAKE_SOURCE_DIR}/assets DESTINATION ${CMAKE_BINARY_DIR}) +target_include_directories(${PROJECT_NAME} PRIVATE + src + src/algorithms + src/data +) diff --git a/README.md b/README.md index 8df8901..c1d9cbe 100644 --- a/README.md +++ b/README.md @@ -1 +1,82 @@ -# BFS/DFS VISUALIZATOR +# Graph Algorithms Visualizer + +## Introduction +Graph Algorithms Visualizer is an advanced tool developed entirely in C++ that demonstrates a variety of graph algorithms through visual representation. This tool is mainly designed as a learning resource. + +--- + +## Algorithms +Visualizer supports the following algorithms: + +- **Depth First Search** +- **Breadth First Search** +- **Dijkstra** +- **Bellman-Ford** + +TODO - Add such Algorithms as A* and Floyd-Warshall. + +--- + +## Features + +### Step-by-Step Algorithm Execution +**Purpose**: The user controls the algorithm one atomic step at a time. Each step performs exactly one logical action of the chosen algorithm. The user can move forward or backward through those atomic actions. + +**Controls**: +- Right Arrow Key: advance one step. +- Left Arrow Key: go back one step. +- R key (Reset): Reset clears all visitation/highlighting/algorithm data but keeps the current graph topology and chosen default weights/radius settings. + +**Behavior**: +On each step highlight the current node/edge that is being processed. +Colorscheme: + +- Blue Node: Unvisited Node +- Orange Node: Node from which traversal starts +- Red Node: Visited Node + +- Green Edge: Univisited Path +- Blue Edge: Visited Path + +### Simple UI +**Interaction Rules**: +- Add node: Left click once on an empty area of the canvas -> new node placed at the pointer with current node radius. +- Delete node: Left click on an existing node -> node is removed (and its incident edges). +- Choose starting node: Right-click twice on a node (double-right-click) -> set that node as the start node. The start node is highlighted (and used by algorithms that require a source). +- Add edge: Right-click on node A, then right-click on node B -> create an edge between A and B. If graph is weighted (you can set that option via sidebar), user has to input weight. +- Reset algorithm: Press R (or Reset button) -> clear all algorithm state/highlights and clear active algorithm selection so the user can pick another algorithm or edit the graph. The underlying nodes/edges and GUI defaults (weights, radius) remain. +- Step controls: Use Left/Right arrow keys to step through algorithm actions. + +--- + +## Requirements + +- C++17(20, 24) compiler +- CMake 3.10 +- Raylib + +--- + +## How to Compile and Build + +1. **Clone the Repository** + ```bash + git clone https://github.com/Stone-r1/graph-algorithms-visualizer.git + cd graph-algorithms-visualizer + ``` + +2. **Run Python Script** + ```bash + python build.py + ``` + +3. **Run** + - Windows: + ```bash + ./bin/graph.exe + ``` + - Linux: + ```bash + ./graph + ``` +--- diff --git a/assets/RobotoRegular.ttf b/assets/RobotoRegular.ttf new file mode 100644 index 0000000..7d9a6c4 Binary files /dev/null and b/assets/RobotoRegular.ttf differ diff --git a/build.py b/build.py new file mode 100644 index 0000000..12e8bfd --- /dev/null +++ b/build.py @@ -0,0 +1,55 @@ +import os +import platform +import subprocess +import sys +import shutil + +def run(cmd, shell=False): + print(f"> {' '.join(cmd) if isinstance(cmd, list) else cmd}") + result = subprocess.run(cmd, shell=shell) + if result.returncode != 0: + sys.exit(result.returncode) + +def get_generator(): + if shutil.which("g++"): + return ("MinGW Makefiles", []) + elif shutil.which("cl.exe"): + # Force 64-bit Visual Studio build if available + return ("Visual Studio 17 2022", ["-A", "x64"]) + else: + return None + +def main(): + # Git submodules + # run(["git", "submodule", "update", "--init", "--recursive"]) + + # Build dir + if not os.path.exists("build"): + os.mkdir("build") + os.chdir("build") + + system = platform.system() + + if system == "Windows": + generator_info = get_generator() + if not generator_info: + print("No compiler was found! Install MinGW or Visual Studio.") + return + + generator, extra_args = generator_info + + if not os.path.exists("CMakeCache.txt"): + run(["cmake", "..", "-G", generator] + extra_args) + + run(["cmake", "--build", "."]) + os.chdir("build") + + # Linux + else: + if not os.path.exists("CMakeCache.txt"): + run(["cmake", ".."]) + + run(["cmake", "--build", "."]) + +if __name__ == "__main__": + main() diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt new file mode 100644 index 0000000..c3c54d8 --- /dev/null +++ b/external/CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 3.12) +project(ExternalLibraries) + +include(FetchContent) + +set(FETCHCONTENT_BASE_DIR ${CMAKE_SOURCE_DIR}/external) + +FetchContent_Declare( + raylib + GIT_REPOSITORY https://github.com/raysan5/raylib.git + GIT_TAG 5.5 + GIT_SHALLOW TRUE # only latest commit, speeds up a lot +) + +FetchContent_MakeAvailable(raylib) diff --git a/src/algorithms/bfs.cpp b/src/algorithms/BFS/bfs.cpp similarity index 86% rename from src/algorithms/bfs.cpp rename to src/algorithms/BFS/bfs.cpp index e3cd53c..762bfe3 100644 --- a/src/algorithms/bfs.cpp +++ b/src/algorithms/BFS/bfs.cpp @@ -1,6 +1,6 @@ #include "bfs.h" -BFS::BFS(const vector>>& adj, int startNode) : +BFS::BFS(const std::vector>>& adj, int startNode) : graph(adj), visited(adj.size(), false), finished(false), @@ -11,17 +11,17 @@ BFS::BFS(const vector>>& adj, int startNode) : } Step BFS::stepForward() { - if (finished) { - return {-1, -1, -1}; - } - if (currentStepIndex + 1 < history.size()) { return history[++currentStepIndex]; } + if (finished) { + return Step::invalidStep(); + } + if (q.empty()) { finished = true; - return {-1, -1, -1}; + return Step::invalidStep(); } auto [parent, node] = q.front(); @@ -42,7 +42,7 @@ Step BFS::stepForward() { Step BFS::stepBackward() { if (currentStepIndex < 0) { - return {-1, -1, -1}; + return Step::invalidStep(); } return history[currentStepIndex--]; diff --git a/src/algorithms/bfs.h b/src/algorithms/BFS/bfs.h similarity index 64% rename from src/algorithms/bfs.h rename to src/algorithms/BFS/bfs.h index e3d7e70..d07f67a 100644 --- a/src/algorithms/bfs.h +++ b/src/algorithms/BFS/bfs.h @@ -5,21 +5,18 @@ #include #include -using std::vector; -using std::pair; - class BFS : public TraversalAlgorithm { private: - const vector>>& graph; - std::queue> q; - vector visited; + const std::vector>>& graph; + std::queue> q; + std::vector visited; bool finished; int currentStepIndex = -1; - vector history; + std::vector history; int start; public: - BFS(const vector>>& adj, int startNode); + BFS(const std::vector>>& adj, int startNode); Step stepForward(); Step stepBackward(); diff --git a/src/algorithms/Bellman-Ford/bellmanFord.cpp b/src/algorithms/Bellman-Ford/bellmanFord.cpp new file mode 100644 index 0000000..3aeda9c --- /dev/null +++ b/src/algorithms/Bellman-Ford/bellmanFord.cpp @@ -0,0 +1,93 @@ +#include "bellmanFord.h" +const long long MAX_VALUE = 1e18; + +BellmanFord::BellmanFord(const std::vector>>& adj, int startNode) : + graph(adj), + distances(adj.size(), MAX_VALUE), + finished(false), + start(startNode) +{ + vertices = graph.size(); + distances[startNode] = 0; + history.push_back({startNode, startNode, 0}); + + // flatten edges + for (int node = 0; node < vertices; node++) { + for (const auto& [neighbor, weight] : graph[node]) { + edges.emplace_back(node, neighbor, weight); + } + } +} + +Step BellmanFord::stepForward() { + if (currentStepIndex + 1 < history.size()) { + return history[++currentStepIndex]; + } + + if (currentPhase >= vertices - 1) { + finished = true; + return Step::invalidStep(); + } + + if (finished) { + return Step::invalidStep(); + } + + while (currentEdgeIndex < edges.size()) { + auto [node, neighbor, weight] = edges[currentEdgeIndex]; + currentEdgeIndex++; + + if (distances[node] != MAX_VALUE && distances[node] + weight < distances[neighbor]) { + distances[neighbor] = distances[node] + weight; + + Step step = Step({node, neighbor, distances[neighbor]}); + history.push_back(step); + currentStepIndex++; + + if (currentEdgeIndex >= edges.size()) { + currentEdgeIndex = 0; + currentPhase++; + } + return step; + } + } + + currentEdgeIndex = 0; + if (++currentPhase >= vertices - 1) { + finished = true; + } + + return Step::invalidStep(); +} + +Step BellmanFord::stepBackward() { + if (currentStepIndex <= 0) { + return Step::invalidStep(); + } + + return history[--currentStepIndex]; +} + +bool BellmanFord::isFinished() const { + return finished && currentStepIndex + 1 >= history.size(); +} + +int BellmanFord::getCurrentStepIndex() const { + return currentStepIndex; +} + +int BellmanFord::getTotalSteps() const { + return history.size(); +} + +Step BellmanFord::getHistory(int index) const { + return history[index]; +} + +int BellmanFord::getStartNode() const { + return start; +} + +int BellmanFord::getCumulativeDistance(int nodeIndex) const { + return distances[nodeIndex]; +} diff --git a/src/algorithms/Bellman-Ford/bellmanFord.h b/src/algorithms/Bellman-Ford/bellmanFord.h new file mode 100644 index 0000000..ad8f6d6 --- /dev/null +++ b/src/algorithms/Bellman-Ford/bellmanFord.h @@ -0,0 +1,36 @@ +#ifndef BELLMANFORD_H +#define BELLMANFORD_H + +#include "traversalAlgorithm.h" +#include +#include + +class BellmanFord : public TraversalAlgorithm { +private: + const std::vector>>& graph; + std::vector> edges; + std::vector distances; + bool finished; + std::vector history; + + int currentStepIndex = -1; + int start; + int vertices; + int currentPhase; + int currentEdgeIndex; + +public: + BellmanFord(const std::vector>>& adj, int startNode); + + Step stepForward(); + Step stepBackward(); + + bool isFinished() const; + int getCurrentStepIndex() const; + int getTotalSteps() const; + Step getHistory(int index) const; + int getStartNode() const; + int getCumulativeDistance(int nodeIndex) const; +}; + +#endif diff --git a/src/algorithms/dfs.cpp b/src/algorithms/DFS/dfs.cpp similarity index 84% rename from src/algorithms/dfs.cpp rename to src/algorithms/DFS/dfs.cpp index 881f27c..b4dc253 100644 --- a/src/algorithms/dfs.cpp +++ b/src/algorithms/DFS/dfs.cpp @@ -1,6 +1,6 @@ #include "dfs.h" -DFS::DFS(const vector>>& adj, int startNode) : +DFS::DFS(const std::vector>>& adj, int startNode) : graph(adj), visited(adj.size(), false), finished(false), @@ -10,18 +10,18 @@ DFS::DFS(const vector>>& adj, int startNode) : visited[startNode] = true; } -Step DFS::stepForward() { - if (finished) { - return {-1, -1, -1}; - } - +Step DFS::stepForward() { if (currentStepIndex + 1 < history.size()) { return history[++currentStepIndex]; } + if (finished) { + return Step::invalidStep(); + } + if (st.empty()) { finished = true; - return {-1, -1, -1}; + return Step::invalidStep(); } auto [parent, node] = st.top(); @@ -42,7 +42,7 @@ Step DFS::stepForward() { Step DFS::stepBackward() { if (currentStepIndex < 0) { - return {-1, -1, -1}; + return Step::invalidStep(); } return history[currentStepIndex--]; diff --git a/src/algorithms/dfs.h b/src/algorithms/DFS/dfs.h similarity index 64% rename from src/algorithms/dfs.h rename to src/algorithms/DFS/dfs.h index 52acaa2..47919f1 100644 --- a/src/algorithms/dfs.h +++ b/src/algorithms/DFS/dfs.h @@ -5,21 +5,18 @@ #include #include -using std::vector; -using std::pair; - class DFS : public TraversalAlgorithm { private: - const vector>>& graph; - std::stack> st; - vector visited; + const std::vector>>& graph; + std::stack> st; + std::vector visited; bool finished; int currentStepIndex = -1; - vector history; + std::vector history; int start; public: - DFS(const vector>>& adj, int startNode); + DFS(const std::vector>>& adj, int startNode); Step stepForward(); Step stepBackward(); diff --git a/src/algorithms/dijkstra.cpp b/src/algorithms/Dijkstra/dijkstra.cpp similarity index 89% rename from src/algorithms/dijkstra.cpp rename to src/algorithms/Dijkstra/dijkstra.cpp index b849b68..7264ba8 100644 --- a/src/algorithms/dijkstra.cpp +++ b/src/algorithms/Dijkstra/dijkstra.cpp @@ -1,7 +1,7 @@ #include "dijkstra.h" const long long MAX_VALUE = 1e18; -Dijkstra::Dijkstra(const vector>>& adj, int startNode) : +Dijkstra::Dijkstra(const std::vector>>& adj, int startNode) : graph(adj), distances(adj.size(), MAX_VALUE), visited(adj.size(), false), @@ -19,7 +19,7 @@ Step Dijkstra::stepForward() { } if (finished) { - return {-1, -1, -1}; + return Step::invalidStep(); } while (!pq.empty()) { @@ -49,12 +49,12 @@ Step Dijkstra::stepForward() { } finished = true; - return {-1, -1, -1}; + return Step::invalidStep(); } Step Dijkstra::stepBackward() { if (currentStepIndex <= 0) { - return {-1, -1, -1}; + return Step::invalidStep(); } return history[--currentStepIndex]; diff --git a/src/algorithms/dijkstra.h b/src/algorithms/Dijkstra/dijkstra.h similarity index 65% rename from src/algorithms/dijkstra.h rename to src/algorithms/Dijkstra/dijkstra.h index 895a780..6564191 100644 --- a/src/algorithms/dijkstra.h +++ b/src/algorithms/Dijkstra/dijkstra.h @@ -4,25 +4,21 @@ #include "traversalAlgorithm.h" #include #include -#include - -using std::vector; -using std::pair; class Dijkstra : public TraversalAlgorithm { private: - const vector>>& graph; - std::priority_queue, vector>, std::greater<>> pq; + const std::vector>>& graph; + std::priority_queue, std::vector>, std::greater<>> pq; std::vector distances; - vector visited; + std::vector visited; bool finished; bool precompiled = false; int currentStepIndex = -1; - vector history; + std::vector history; int start; public: - Dijkstra(const vector>>& adj, int startNode); + Dijkstra(const std::vector>>& adj, int startNode); Step stepForward(); Step stepBackward(); diff --git a/src/algorithms/traversalAlgorithm.h b/src/algorithms/traversalAlgorithm.h index a3a62a2..250f577 100644 --- a/src/algorithms/traversalAlgorithm.h +++ b/src/algorithms/traversalAlgorithm.h @@ -1,13 +1,18 @@ #ifndef TRAVERSAL_H #define TRAVERSAL_H -#include - - struct Step { int from; int to; long long distance; + + static Step invalidStep() { + return Step{-1, -1, -1}; + } + + bool isValid() const { + return from != -1 && to != -1 && distance != -1; + } }; class TraversalAlgorithm { diff --git a/src/board.cpp b/src/board.cpp index 1346c0e..6c269a1 100644 --- a/src/board.cpp +++ b/src/board.cpp @@ -1,16 +1,16 @@ -#include -#include #include -#include -#include #include -#include "raylib.h" #include "board.h" #include "node.h" +#include "data/constants.h" -#define MAX_NODES 1000 +#include "algorithms/BFS/bfs.h" +#include "algorithms/DFS/dfs.h" +#include "algorithms/Dijkstra/dijkstra.h" +#include "algorithms/Bellman-Ford/bellmanFord.h" +// TODO: Make general pattern for running algorithms Board::Board() : edges(0), lastNodeIndex(0), @@ -18,12 +18,29 @@ Board::Board() : isDirected(false), isWeighted(false), isRunning(false), - lastClickedNode(-1, {0, 0}, 0), - graph(MAX_NODES), - nodes(MAX_NODES, Node(-1, {0, 0}, 0)) -{} + lastClickedNode(Node::makeInvalidNode()), + graph(NodeConstants::MAX_NODES), + nodes(NodeConstants::MAX_NODES, Node::makeInvalidNode()) +{ + graph.reserve(NodeConstants::MAX_NODES); + nodes.reserve(NodeConstants::MAX_NODES); +} + +void Board::clear() { + resetRunning(); + graph.clear(); + nodes.clear(); + + graph.resize(NodeConstants::MAX_NODES); + nodes.assign(NodeConstants::MAX_NODES, Node::makeInvalidNode()); + edges = 0; -Node* Board::findNodeFromPosition(Vector2 firstNodePosition) { + if (isGraphWeighted()) { + flipGraphWeight(); + } +} + +Node* Board::findNodeFromPosition(const Vector2& firstNodePosition) { for (Node& node : nodes) { if (node.isNodeValid() && node.isInRadiusDomain(firstNodePosition)) { return &node; @@ -32,19 +49,45 @@ Node* Board::findNodeFromPosition(Vector2 firstNodePosition) { return nullptr; } -Vector2 Board::isInNodeDomain(Vector2 mousePosition) { +// general pattern for running algorithm +bool Board::runAlgorithm(const Vector2& mousePosition, bool weightRequired, std::function(int)> algorithmConstructor) { + if (weightRequired != isGraphWeighted()) { + // graph must be weighted/not weighted to run | later for UI + return false; + } + + Node* startNode = findNodeFromPosition(mousePosition); + if (!startNode) { + return false; + } + + resetRunning(); + currentAlgo = algorithmConstructor(startNode -> getNodeIndex()); + isRunning = true; + return true; +} + +std::optional Board::isInNodeDomain(const Vector2& mousePosition) { for (const Node& node : nodes) { if (node.isNodeValid() && node.isInRadiusDomain(mousePosition)) { return node.getNodePosition(); } } - return {0.0f, 0.0f}; + return std::nullopt; } -void Board::addNode(Vector2 mousePosition, float currentRadius) { +bool Board::isInBoardBorderDomain(const Vector2& mousePosition, float currentRadius, int screenWidth, int screenHeight) { + const float allowedMargin = UIConstants::MARGIN_FROM_BORDER * currentRadius; + return (mousePosition.y <= allowedMargin || + mousePosition.y >= screenHeight - allowedMargin || + mousePosition.x <= allowedMargin || + mousePosition.x >= screenWidth - allowedMargin); +} + +void Board::addNode(const Vector2& mousePosition, float currentRadius) { for (const Node& node : nodes) { - float minDistance = node.getNodeRadius() * 3; + float minDistance = node.getNodeRadius() * UIConstants::MARGIN_FROM_NODE; if (node.isNodeValid()) { Vector2 nodePosition = node.getNodePosition(); @@ -62,7 +105,7 @@ void Board::addNode(Vector2 mousePosition, float currentRadius) { nodes[lastNodeIndex++] = currentNode; } -void Board::addEdge(Vector2 firstNodePosition, Vector2 secondNodePosition, int weight) { +void Board::addEdge(const Vector2& firstNodePosition, const Vector2& secondNodePosition, int weight) { Node* firstNode = findNodeFromPosition(firstNodePosition); Node* secondNode = findNodeFromPosition(secondNodePosition); @@ -77,14 +120,13 @@ void Board::addEdge(Vector2 firstNodePosition, Vector2 secondNodePosition, int w graph[firstNodeIndex].push_back({secondNodeIndex, weight}); firstNode->addNeighbor(secondNodeIndex); - if (!isDirected) { - graph[secondNodeIndex].push_back({firstNodeIndex, weight}); - secondNode->addNeighbor(firstNodeIndex); - edges++; - } + graph[secondNodeIndex].push_back({firstNodeIndex, weight}); + secondNode->addNeighbor(firstNodeIndex); + + printf("Current amount of edges is: %d\n", edges); } -void Board::removeEdge(Vector2 firstNodePosition, Vector2 secondNodePosition) { +void Board::removeEdge(const Vector2& firstNodePosition, const Vector2& secondNodePosition) { Node* firstNode = findNodeFromPosition(firstNodePosition); Node* secondNode = findNodeFromPosition(secondNodePosition); @@ -98,24 +140,24 @@ void Board::removeEdge(Vector2 firstNodePosition, Vector2 secondNodePosition) { auto& neighbors1 = graph[firstNodeIndex]; neighbors1.erase(std::remove_if(neighbors1.begin(), neighbors1.end(), - [secondNodeIndex](const pair& p) {return p.first == secondNodeIndex; + [secondNodeIndex](const std::pair& p) {return p.first == secondNodeIndex; }), neighbors1.end()); firstNode->removeNeighbor(secondNodeIndex); - if (!isDirected) { - auto& neighbors2 = graph[secondNodeIndex]; - neighbors2.erase(std::remove_if(neighbors2.begin(), neighbors2.end(), - [firstNodeIndex](const pair& p) {return p.first == firstNodeIndex; - }), neighbors2.end()); - secondNode->removeNeighbor(firstNodeIndex); - edges--; - } + auto& neighbors2 = graph[secondNodeIndex]; + neighbors2.erase(std::remove_if(neighbors2.begin(), neighbors2.end(), + [firstNodeIndex](const std::pair& p) {return p.first == firstNodeIndex; + }), neighbors2.end()); + secondNode->removeNeighbor(firstNodeIndex); + + printf("Current amount of edges is: %d\n", edges); } -void Board::removeNode(Vector2 mousePosition) { +void Board::removeNode(const Vector2& mousePosition) { Node* nodeToRemove = findNodeFromPosition(mousePosition); if (!nodeToRemove) return; + edges -= graph[nodeToRemove->getNodeIndex()].size(); graph[nodeToRemove->getNodeIndex()].clear(); nodeToRemove->removeNeighbors(); @@ -124,30 +166,33 @@ void Board::removeNode(Vector2 mousePosition) { auto& adj = graph[i]; adj.erase(std::remove_if(adj.begin(), adj.end(), - [nodeToRemove](const pair& p) {return p.first == nodeToRemove->getNodeIndex(); + [nodeToRemove](const std::pair& p) {return p.first == nodeToRemove->getNodeIndex(); }), adj.end()); nodes[i].removeNeighbor(nodeToRemove->getNodeIndex()); } // mark node as invalid - *nodeToRemove = Node(-1, {0.0f, 0.0f}, 0.0f); + *nodeToRemove = Node::makeInvalidNode(); + printf("Current amount of edges is: %d\n", edges); } void Board::drawNodes() { + using namespace ColorConstants; int startIndex = currentAlgo ? currentAlgo->getStartNode() : startNodeIndex; for (const Node& node : nodes) { if (!node.isNodeValid()) continue; Vector2 nodePosition = node.getNodePosition(); - Color color = BLUE; // standard + Color color = NODE_DEFAULT; if (node.getNodeIndex() == startNodeIndex) { - color = ORANGE; + color = NODE_START; } else if (node.highlighted()) { - color = currentStep == -1 ? ORANGE : RED; + color = currentStep == -1 ? NODE_START : NODE_VISITED; } DrawCircle(nodePosition.x, nodePosition.y, node.getNodeRadius(), color); + DrawRing(nodePosition, node.getNodeRadius() - node.getNodeRadius() / 8, node.getNodeRadius(), 0.0f, 360.0f, 100, BLACK); } } @@ -155,31 +200,37 @@ void Board::drawEdges() { for (const Node& node : nodes) { if (!node.isNodeValid()) continue; - std::set neighbors = node.getNodeNeighbors(); + std::unordered_set neighbors = node.getNodeNeighbors(); for (auto it = neighbors.begin(); it != neighbors.end(); it++) { Node* neighbor = &nodes[*it]; if (!neighbor->isNodeValid()) continue; - Color color = highlightedEdges.count({node.getNodeIndex(), neighbor->getNodeIndex()}) ? DARKBLUE : GREEN; - DrawLineEx(node.getNodePosition(), neighbor->getNodePosition(), 5.0f, color); + Color color = highlightedEdges.count({node.getNodeIndex(), neighbor->getNodeIndex()}) ? ColorConstants::EDGE_HIGHLIGHT : ColorConstants::EDGE_DEFAULT; + DrawLineEx(node.getNodePosition(), neighbor->getNodePosition(), 6.0f, color); } } } -void Board::drawWeights() { +void Board::drawWeights(const Font& font) { for (auto& [nodeIndex, weight] : highlightedWeights) { char buffer[10]; snprintf(buffer, sizeof(buffer), "%d", weight); Vector2 positions = nodes[nodeIndex].getNodePosition(); - DrawText(buffer, positions.x - 10, positions.y - 10, 30, BLACK); + Vector2 textSize = MeasureTextEx(font, buffer, UIConstants::FONT_SIZE_NODE, 1); + Vector2 drawPos = { + positions.x - textSize.x / 2, + positions.y - textSize.y / 2 + }; + + DrawTextEx(font, buffer, drawPos, UIConstants::FONT_SIZE_NODE, 1, ColorConstants::TEXT); } for (Node& node : nodes) { if (!node.isNodeValid()) continue; - std::set neighbors = node.getNodeNeighbors(); + std::unordered_set neighbors = node.getNodeNeighbors(); for (auto it = neighbors.begin(); it != neighbors.end(); ++it) { if (!nodes[*it].isNodeValid()) continue; if (!isDirected && node.getNodeIndex() > *it) continue; @@ -216,12 +267,10 @@ void Board::drawWeights() { float nx = -dy / length; float ny = dx / length; - float offset = 20.0f; - - midX += nx * offset; - midY += ny * offset; + midX += nx * UIConstants::WEIGHT_OFFSET; + midY += ny * UIConstants::WEIGHT_OFFSET; - DrawText(buffer, midX - 10, midY - 10, 30, BLACK); + DrawTextEx(font, buffer, {midX - 10, midY - 10}, UIConstants::FONT_SIZE_EDGE, 1, ColorConstants::TEXT); // ========================== } } @@ -236,42 +285,28 @@ void Board::resetRunning() { isRunning = false; weightReady = false; isEnteringWeight = false; + startNodeIndex = -1; std::fill(std::begin(weightInput), std::end(weightInput), '\0'); } -void Board::runBFS(Vector2 startNodePosition) { - Node* startNode = findNodeFromPosition(startNodePosition); - if (startNode) { - resetRunning(); - startNodeIndex = -1; - currentAlgo = std::make_unique(graph, startNode->getNodeIndex()); - isRunning = true; - } +bool Board::isAlgorithmRunning() const { + return isRunning; } -void Board::runDFS(Vector2 startNodePosition) { - Node* startNode = findNodeFromPosition(startNodePosition); - if (startNode) { - resetRunning(); - startNodeIndex = -1; - currentAlgo = std::make_unique(graph, startNode->getNodeIndex()); - isRunning = true; - } +bool Board::runBFS(const Vector2& startNodePosition) { + return runAlgorithm(startNodePosition, false, [this](int startNodeIndex){return std::make_unique(graph, startNodeIndex);}); } -void Board::runDijkstra(Vector2 startNodePosition) { - if (!isGraphWeighted()) { - std::cout << "Graph must be weighted to run dijkstra\n"; - return; - } +bool Board::runDFS(const Vector2& startNodePosition) { + return runAlgorithm(startNodePosition, false, [this](int startNodeIndex){return std::make_unique(graph, startNodeIndex);}); +} - Node* startNode = findNodeFromPosition(startNodePosition); - if (startNode) { - resetRunning(); - startNodeIndex = -1; - currentAlgo = std::make_unique(graph, startNode->getNodeIndex()); - isRunning = true; - } +bool Board::runDijkstra(const Vector2& startNodePosition) { + return runAlgorithm(startNodePosition, true, [this](int startNodeIndex){return std::make_unique(graph, startNodeIndex);}); +} + +bool Board::runBellmanFord(const Vector2& startNodePosition) { + return runAlgorithm(startNodePosition, true, [this](int startNodeIndex){return std::make_unique(graph, startNodeIndex);}); } void Board::highlightNode(int index) { @@ -291,7 +326,7 @@ void Board::highlightWeight(int to, int weight) { highlightedWeights[to] = weight; } -void Board::highlightStartingNode(Vector2 mousePosition) { +void Board::highlightStartingNode(const Vector2& mousePosition) { Node* startNode = findNodeFromPosition(mousePosition); if (!startNode) { return; @@ -317,8 +352,10 @@ void Board::resetHighlights() { void Board::stepForward() { if (currentAlgo && !currentAlgo->isFinished()) { - auto [from, to, weight] = currentAlgo->stepForward(); - if (from != -1) { + auto step = currentAlgo->stepForward(); + if (step.isValid()) { + auto [from, to, weight] = step; + if (isGraphWeighted()) { highlightWeight(to, weight); } @@ -361,16 +398,18 @@ void Board::flipGraphWeight() { void Board::askForWeight() { Rectangle box = {300, 200, 200, 50}; - Color color = {254, 248, 221, 255}; - DrawRectangleRec(box, color); - DrawRectangleLinesEx(box, 2, BLACK); - DrawText(weightInput, box.x + 10, box.y + 15, 30, DARKGRAY); - int key = GetCharPressed(); + DrawRectangleRec(box, ColorConstants::WEIGHT_BOX_BACKGROUND); + DrawRectangleLinesEx(box, 2, ColorConstants::WEIGHT_BOX_BORDER); + DrawText(weightInput, box.x + 10, box.y + 15, 30, ColorConstants::TEXT); + int key = GetCharPressed(); while (key > 0) { if (isdigit(key) && weightDigitCount < 9) { weightInput[weightDigitCount++] = (char)key; weightInput[weightDigitCount] = '\0'; + } else if (key == '-' && !weightDigitCount) { + weightInput[weightDigitCount++] = '-'; + weightInput[weightDigitCount] = '\0'; } key = GetCharPressed(); diff --git a/src/board.h b/src/board.h index f77fd5b..75c99b8 100644 --- a/src/board.h +++ b/src/board.h @@ -1,16 +1,13 @@ #include -#include #include #include #include +#include +#include #include "raylib.h" #include "node.h" -#include "algorithms/bfs.h" -#include "algorithms/dfs.h" -#include "algorithms/dijkstra.h" -using std::vector; - +#include "traversalAlgorithm.h" class Board { private: @@ -28,30 +25,46 @@ class Board { int weightDigitCount = 0; bool weightReady = false; - vector>> graph; - vector nodes; - std::set> highlightedEdges; + std::vector>> graph; + std::vector nodes; + std::set> highlightedEdges; std::unordered_map highlightedWeights; std::unique_ptr currentAlgo; - Node* findNodeFromPosition(Vector2 firstNodePosition); + Node* findNodeFromPosition(const Vector2& firstNodePosition); + bool runAlgorithm(const Vector2& mousePosition, bool weightRequired, std::function(int)> algorithmConstructor); public: Board(); + ~Board() = default; + + // ================= + // Disable copying + Board(const Board&) = delete; + Board& operator=(const Board&) = delete; + + // Enable moving + Board(Board&&) noexcept = default; + Board& operator=(Board&&) noexcept = default; + // Callers can transfer ownership but can't copy. + // ================= - Vector2 isInNodeDomain(Vector2 mousePosition); + void clear(); - void addNode(Vector2 mousePosition, float currentRadius); - void addEdge(Vector2 firstNodePosition, Vector2 secondNodePosition, int weight = 1); - void removeEdge(Vector2 firstNodePosition, Vector2 secondNodePosition); - void removeNode(Vector2 mousePosition); + std::optional isInNodeDomain(const Vector2& mousePosition); + bool isInBoardBorderDomain(const Vector2& mousePosition, float currentRadius, int screenWidth, int screenHeight); + void addNode(const Vector2& mousePosition, float currentRadius); + void addEdge(const Vector2& firstNodePosition, const Vector2& secondNodePosition, int weight = 1); + void removeEdge(const Vector2& firstNodePosition, const Vector2& secondNodePosition); + void removeNode(const Vector2& mousePosition); void clearGraph(); // ==== algorithms ==== - void runBFS(Vector2 startNodePosition); - void runDFS(Vector2 startNodePosition); - void runDijkstra(Vector2 startNodePosition); + bool runBFS(const Vector2& startNodePosition); + bool runDFS(const Vector2& startNodePosition); + bool runDijkstra(const Vector2& startNodePosition); + bool runBellmanFord(const Vector2& startNodePosition); void stepForward(); void stepBackward(); @@ -61,16 +74,17 @@ class Board { void highlightNode(int index); void highlightEdge(int from, int to); void highlightWeight(int to, int weight); - void highlightStartingNode(Vector2 mousePosition); + void highlightStartingNode(const Vector2& mousePosition); void resetHighlights(); // ==================== void stopRunning(); void resetRunning(); + bool isAlgorithmRunning() const; void drawNodes(); void drawEdges(); - void drawWeights(); + void drawWeights(const Font& font); bool isGraphEmpty() const; bool isGraphWeighted() const; diff --git a/src/data/constants.h b/src/data/constants.h new file mode 100644 index 0000000..5300394 --- /dev/null +++ b/src/data/constants.h @@ -0,0 +1,65 @@ +#ifndef CONSTANTS_H +#define CONSTANTS_H + +#include +#include "raylib.h" + +namespace UIConstants { + inline constexpr float MARGIN = 10.0f; + inline constexpr int FONT_SIZE_EDGE = 30; + inline constexpr int FONT_SIZE_SIDEBAR = 30; + inline constexpr int FONT_SIZE_NODE = 40; + inline constexpr float SIDEBAR_WIDTH = 250.0f; + inline constexpr float SIDEBAR_HEIGHT = 800.0f; + inline constexpr float BUTTON_HEIGHT = 60.0f; + inline constexpr float WEIGHT_OFFSET = 20.0f; + + inline constexpr float MARGIN_FROM_NODE = 3.0f; + inline constexpr float MARGIN_FROM_SIDEBAR = 2.0f; + inline constexpr float MARGIN_FROM_BORDER = 1.5f; +} + +namespace ColorConstants { + // Graph + inline constexpr Color NODE_DEFAULT = BLUE; + inline constexpr Color NODE_START = ORANGE; + inline constexpr Color NODE_VISITED = RED; + inline constexpr Color EDGE_DEFAULT = GREEN; + inline constexpr Color EDGE_HIGHLIGHT = DARKBLUE; + + // Sidebar + inline constexpr Color SIDEBAR_BUTTON_ACTIVE = GREEN; + inline constexpr Color SIDEBAR_BUTTON_INACTIVE = LIGHTGRAY; + inline constexpr Color SIDEBAR_BORDER = DARKGRAY; + inline constexpr Color SIDEBAR_BACKGROUND = GRAY; + + inline constexpr Color TEXT = BLACK; + inline constexpr Color WEIGHT_BOX_BORDER = DARKGRAY; + inline constexpr Color WEIGHT_BOX_BACKGROUND = {254, 248, 221, 255}; + inline constexpr Color BOARD_BACKGROUND = WHITE; +} + +namespace NodeConstants { + inline constexpr int MAX_NODES = 1000; + inline constexpr float INVALID_RADIUS = 0.0f; + inline constexpr float SMALL_RADIUS = 30.0f; + inline constexpr float MEDIUM_RADIUS = 50.0f; + inline constexpr float LARGE_RADIUS = 70.0f; +} + +namespace LabelNames { + // Algorithm Labels + inline const std::string CLEAR = "Clear"; + inline const std::string WEIGHTED = "Weighted"; + inline const std::string DFS = "DFS"; + inline const std::string BFS = "BFS"; + inline const std::string DIJKSTRA = "Dijkstra"; + inline const std::string BELLMAN_FORD = "Bellman-Ford"; + + // Size Labels + inline const std::string SMALL = "S"; + inline const std::string MEDIUM = "M"; + inline const std::string LARGE = "L"; +} + +#endif diff --git a/src/main.cpp b/src/main.cpp index 4d06bd4..c780257 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,14 +1,12 @@ #include "board.h" #include -#include "raylib.h" #include "sidebar.h" +#include "data/constants.h" -#define SMALL 30.0f -#define MEDIUM 50.0f -#define LARGE 70.0f - -float radius = MEDIUM; -static Vector2 firstNode = {-1, -1}, lastNode = {-1, -1}; +// TODO: Make general pattern for clicking +float radius; +static std::optional firstNode = std::nullopt; +static std::optional lastNode = std::nullopt; static Vector2 startNode; static bool weightBox = false; @@ -17,54 +15,74 @@ void handleLeftClick(Board& board, Sidebar& sidebar, Vector2 mouse) { return; } - if (weightBox) { + if (weightBox || board.isAlgorithmRunning()) { + return; + } + + std::optional nodePosition = board.isInNodeDomain(mouse); + if (nodePosition) { + std::cout << "deleted a node\n"; + board.removeNode(*nodePosition); return; } - if (!sidebar.isInSidebarDomain(mouse, radius)) { + if (!sidebar.isInSidebarDomain(mouse, radius) && + !board.isInBoardBorderDomain(mouse, radius, GetScreenWidth(), GetScreenHeight())) { board.addNode(mouse, radius); } else { sidebar.handleMouse(mouse); - if (sidebar.isButtonClicked("Small")) { - radius = SMALL; + if (sidebar.isButtonClicked(LabelNames::CLEAR)) { + board.clear(); sidebar.resetClicks(); return; } + + if (sidebar.isButtonClicked(LabelNames::BFS)) { + if (!board.runBFS(startNode)) { + sidebar.resetClick(LabelNames::BFS); + } - if (sidebar.isButtonClicked("Medium")) { - radius = MEDIUM; - sidebar.resetClicks(); return; } - if (sidebar.isButtonClicked("Large")) { - radius = LARGE; - sidebar.resetClicks(); + if (sidebar.isButtonClicked(LabelNames::DFS)) { + if (!board.runDFS(startNode)) { + sidebar.resetClick(LabelNames::DFS); + } + return; } - - if (sidebar.isButtonClicked("BFS")) { - board.runBFS(startNode); - sidebar.resetClicks(); + + if (sidebar.isButtonClicked(LabelNames::DIJKSTRA)) { + if (!board.runDijkstra(startNode)) { + sidebar.resetClick(LabelNames::DIJKSTRA); + } + return; } - if (sidebar.isButtonClicked("DFS")) { - board.runDFS(startNode); - sidebar.resetClicks(); + if (sidebar.isButtonClicked(LabelNames::BELLMAN_FORD)) { + if (!board.runBellmanFord(startNode)) { + sidebar.resetClick(LabelNames::BELLMAN_FORD); + } + return; } - if (sidebar.isButtonClicked("Weighted")) { + if (sidebar.isButtonClicked(LabelNames::WEIGHTED)) { + sidebar.weightButtonAvailable(board.isGraphEmpty()); if (!board.isGraphEmpty()) { return; // can't flip the graph weight if edges were added. } else { board.flipGraphWeight(); + if (!board.isGraphWeighted()) { + sidebar.flipGraphWeight(); + } + std::cout << "karoche graph weight is now set to " << (board.isGraphWeighted() ? "true\n" : "false\n"); } - sidebar.resetClicks(); return; } } @@ -75,52 +93,61 @@ void handleRightClick(Board& board, Sidebar& sidebar, Vector2 mouse) { return; } - if (weightBox) { + if (weightBox || board.isAlgorithmRunning()) { return; } - Vector2 nodePosition = board.isInNodeDomain(mouse); - if (nodePosition.x == 0.0f && nodePosition.y == 0.0f) { + std::optional nodePosition = board.isInNodeDomain(mouse); + if (!nodePosition) { return; - } - - if (firstNode.x == -1) { + } + + if (!firstNode) { firstNode = nodePosition; std::cout << "Node 1 was chosen successfully\n"; - } else if (firstNode.x == nodePosition.x && firstNode.y == nodePosition.y) { + } else if (firstNode->x == nodePosition->x && firstNode->y == nodePosition->y) { // so basically If you clicked on the same node twice std::cout << "Selected the starting node.\n"; - startNode = firstNode; + startNode = *firstNode; + firstNode = std::nullopt; board.highlightStartingNode(startNode); - firstNode = {-1, -1}; } else { lastNode = nodePosition; if (board.isGraphWeighted()) { weightBox = true; } else { std::cout << "Added the edge between Node 1 and Node2\n"; - board.addEdge(firstNode, lastNode, 1); - firstNode = {-1, -1}; - lastNode = firstNode; + board.addEdge(*firstNode, *lastNode, 1); + firstNode = std::nullopt; + lastNode = std::nullopt; } } } int main() { int monitor = GetCurrentMonitor(); + SetConfigFlags(FLAG_WINDOW_RESIZABLE); InitWindow(1200, 800, "Graph Visualizer"); - SetWindowMinSize(1200, 800); SetTargetFPS(60); Board board; Sidebar sidebar(GetScreenHeight()); Vector2 selectedNode = {-1, -1}; + Font myFont = LoadFont("assets/RobotoRegular.ttf"); + + if (!myFont.texture.id) { + std::cerr << "Failed to load font!\n"; + } while (!WindowShouldClose()) { Vector2 mouse = GetMousePosition(); handleLeftClick(board, sidebar, mouse); handleRightClick(board, sidebar, mouse); + // Sync radius with currently selected sidebar option + radius = sidebar.getSelectedRadius(); + + if (IsKeyPressed(KEY_RIGHT)) { board.stepForward(); } @@ -131,29 +158,35 @@ int main() { if (IsKeyPressed(KEY_R)) { board.resetRunning(); + sidebar.resetClicks(); + board.highlightStartingNode(startNode); } BeginDrawing(); - ClearBackground(WHITE); - sidebar.draw(); + ClearBackground(ColorConstants::BOARD_BACKGROUND); + sidebar.draw(myFont); board.drawEdges(); board.drawNodes(); + if (board.isGraphWeighted()) { + board.drawWeights(myFont); + } if (weightBox) { board.askForWeight(); if (board.isWeightReady()) { int weight = board.getCurrentWeight(); - board.addEdge(firstNode, lastNode, weight); + board.addEdge(*firstNode, *lastNode, weight); weightBox = false; - firstNode = {-1, -1}; - lastNode = firstNode; + firstNode = std::nullopt; + lastNode = std::nullopt; board.setWeightReady(); } } EndDrawing(); } - + + UnloadFont(myFont); CloseWindow(); } diff --git a/src/node.cpp b/src/node.cpp index 2229a45..95ae817 100644 --- a/src/node.cpp +++ b/src/node.cpp @@ -1,8 +1,8 @@ -#include "raylib.h" #include "node.h" +#include "data/constants.h" -Node::Node(int index, Vector2 position, float radius = 50.0f) : +Node::Node(int index, Vector2 position, float radius = NodeConstants::MEDIUM_RADIUS) : nodeIndex(index), nodePosition(position), nodeRadius(radius), @@ -28,7 +28,7 @@ float Node::getNodeRadius() const { return nodeRadius; } -std::set Node::getNodeNeighbors() const { +const std::unordered_set& Node::getNodeNeighbors() const { return neighbors; } @@ -67,7 +67,7 @@ void Node::changeNodeStatus() { } void Node::drawNode() const { - DrawCircle(nodePosition.x, nodePosition.y, nodeRadius, RED); + DrawCircle(nodePosition.x, nodePosition.y, nodeRadius, ColorConstants::NODE_VISITED); } void Node::setHighlight() { @@ -81,3 +81,7 @@ void Node::resetHighlight() { bool Node::highlighted() const { return isHighlighted; } + +Node Node::makeInvalidNode() { + return Node(-1, {0, 0}, 0); +} diff --git a/src/node.h b/src/node.h index 93de6dc..bfb0e25 100644 --- a/src/node.h +++ b/src/node.h @@ -2,7 +2,7 @@ #define NODE_H #include "raylib.h" -#include +#include class Node { private: @@ -10,7 +10,7 @@ class Node { Vector2 nodePosition; float nodeRadius; bool isActive; - std::set neighbors; + std::unordered_set neighbors; bool isHighlighted = false; @@ -21,7 +21,7 @@ class Node { int getNodeIndex() const; Vector2 getNodePosition() const; float getNodeRadius() const; - std::set getNodeNeighbors() const; + const std::unordered_set& getNodeNeighbors() const; // no copies bool isNodeActive() const; bool isNodeValid() const; @@ -37,6 +37,8 @@ class Node { void setHighlight(); void resetHighlight(); bool highlighted() const; + + static Node makeInvalidNode(); }; #endif diff --git a/src/sidebar.cpp b/src/sidebar.cpp index f0c8ca8..ad62f09 100644 --- a/src/sidebar.cpp +++ b/src/sidebar.cpp @@ -1,53 +1,105 @@ #include "raylib.h" #include "sidebar.h" -#include +#include "data/constants.h" #include -#include -const float margin = 10.0f; -float topButtons; +// local helper +namespace { + static float getRadiusValue(const std::string& label) { + if (label == LabelNames::SMALL) return NodeConstants::SMALL_RADIUS; + if (label == LabelNames::MEDIUM) return NodeConstants::MEDIUM_RADIUS; + if (label == LabelNames::LARGE) return NodeConstants::LARGE_RADIUS; + return NodeConstants::INVALID_RADIUS; + } +} Sidebar::Sidebar(int screenHeight) : x(0), - width(250), - height(screenHeight) + width(UIConstants::SIDEBAR_WIDTH), + height(UIConstants::SIDEBAR_HEIGHT), + ystart(screenHeight - 6.0f * screenHeight / 7) { - float buttonHeight = 60.0f; - float yOffset = margin; + using namespace UIConstants; + + float buttonWidth = width - 2 * MARGIN; + float buttonX = x + (width - buttonWidth) / 2; + float yOffset = ystart + 3 * MARGIN; - std::vector labels = {"Run", "Pause", "Reset", "Weighted", "DFS", "BFS", "Dijkstra", "Bellman-Ford"}; + std::vector labels = { + LabelNames::CLEAR, + LabelNames::WEIGHTED, + LabelNames::DFS, + LabelNames::BFS, + LabelNames::DIJKSTRA, + LabelNames::BELLMAN_FORD + }; for (const auto& label : labels) { - buttons.emplace_back(Rectangle{(float)x + margin, yOffset, (float)width - 2 * margin, (float)buttonHeight}, label); - yOffset += (buttonHeight + margin); + buttons.emplace_back(Rectangle{buttonX + MARGIN, yOffset, buttonWidth, BUTTON_HEIGHT}, label); + yOffset += (BUTTON_HEIGHT + MARGIN); } topButtons = yOffset; - yOffset += 2.5 * margin; - std::vector radiusLabels = {"Small", "Medium", "Large"}; - - for (const auto& label : radiusLabels) { - radiuses.emplace_back(Rectangle{(float)x + margin, yOffset, (float)width - 2 * margin, (float)buttonHeight}, label); - yOffset += (buttonHeight + margin); + yOffset += 2.5 * MARGIN; + std::vector radiusLabels = { + LabelNames::SMALL, + LabelNames::MEDIUM, + LabelNames::LARGE + }; + + buttonWidth = (width - 4 * MARGIN) / 3.0f; // 3 buttons + 2 gaps = 4 margins + float totalWidth = 3 * buttonWidth + 2 * MARGIN; + float baseX = x + (width - totalWidth) / 2 + MARGIN; + float radiusY = yOffset; + + for (int i = 0; i < radiusLabels.size(); ++i) { + float buttonX = baseX + i * (buttonWidth + MARGIN); + radiuses.emplace_back(Rectangle{buttonX, radiusY, buttonWidth, BUTTON_HEIGHT}, radiusLabels[i]); + if (radiusLabels[i] == LabelNames::MEDIUM) { + // default value + radiuses[i].isClicked = true; + } } + + yOffset += BUTTON_HEIGHT + MARGIN; } -void Sidebar::draw() { - DrawRectangle(x, 0, width, height, GRAY); +void Sidebar::draw(const Font& font) { + using namespace UIConstants; + using namespace ColorConstants; + + float sidebarHeight = height - 2 * ystart; + Rectangle rect = {x + MARGIN, ystart, width, sidebarHeight}; + + DrawRectangleRounded(rect, 0.2f, 1, SIDEBAR_BACKGROUND); + Rectangle temp = {x + MARGIN + 1, ystart, width - 2, sidebarHeight - 2}; // 1 pixel was playing with my nerves... + DrawRectangleRoundedLinesEx(temp, 0.2f, 1, 5, SIDEBAR_BORDER); for (const auto& button : buttons) { - DrawRectangleRec(button.domain, LIGHTGRAY); - DrawRectangleLinesEx(button.domain, 2, DARKGRAY); - DrawText(button.label.c_str(), button.domain.x + margin, button.domain.y + margin, 18, BLACK); + DrawRectangleRec(button.domain, isButtonClicked(button.getButtonLabel()) ? SIDEBAR_BUTTON_ACTIVE : SIDEBAR_BUTTON_INACTIVE); + DrawRectangleLinesEx(button.domain, 2, SIDEBAR_BORDER); + + Vector2 textSize = MeasureTextEx(font, button.label.c_str(), FONT_SIZE_SIDEBAR, 1); + Vector2 pos = { + button.domain.x + (button.domain.width - textSize.x) / 2, + button.domain.y + (button.domain.height - textSize.y) / 2 + }; + DrawTextEx(font, button.label.c_str(), pos, FONT_SIZE_SIDEBAR, 1, TEXT); } - DrawLineEx({margin, topButtons + margin}, {(float)width - margin, topButtons + margin}, 5.0f, DARKGRAY); + DrawLineEx({x + 3 * MARGIN, topButtons + MARGIN}, {(float)width - MARGIN - MARGIN / 2, topButtons + MARGIN}, 5.0f, SIDEBAR_BORDER); for (const auto& button : radiuses) { - DrawRectangleRec(button.domain, LIGHTGRAY); - DrawRectangleLinesEx(button.domain, 2, DARKGRAY); - DrawText(button.label.c_str(), button.domain.x + margin, button.domain.y + margin, 18, BLACK); - } + DrawRectangleRec(button.domain, isButtonClicked(button.getButtonLabel()) ? SIDEBAR_BUTTON_ACTIVE : SIDEBAR_BUTTON_INACTIVE); + DrawRectangleLinesEx(button.domain, 2, SIDEBAR_BORDER); + + Vector2 textSize = MeasureTextEx(font, button.label.c_str(), FONT_SIZE_SIDEBAR, 1); + Vector2 pos = { + button.domain.x + (button.domain.width - textSize.x) / 2, + button.domain.y + (button.domain.height - textSize.y) / 2 + }; + DrawTextEx(font, button.label.c_str(), pos, FONT_SIZE_SIDEBAR, 1, TEXT); + } } void Sidebar::handleMouse(Vector2 mousePosition) { @@ -55,32 +107,46 @@ void Sidebar::handleMouse(Vector2 mousePosition) { return; } - for (auto& button : buttons) { - button.isClicked = false; - } - for (auto& button : buttons) { const Rectangle& rect = button.domain; - button.isClicked = false; - if (mousePosition.x > rect.x && mousePosition.x < rect.x + rect.width && - mousePosition.y > rect.y && mousePosition.y < rect.y + rect.height) { + + if (button.getButtonLabel() != LabelNames::WEIGHTED) { + button.isClicked = false; + } + + if (CheckCollisionPointRec(mousePosition, rect)) { std::cout << "Is In Domain of " << button.label << '\n'; button.isClicked = true; + return; } - } + } for (auto& button : radiuses) { const Rectangle& rect = button.domain; - if (mousePosition.x > rect.x && mousePosition.x < rect.x + rect.width && - mousePosition.y > rect.y && mousePosition.y < rect.y + rect.height) { + button.isClicked = false; + if (CheckCollisionPointRec(mousePosition, rect)) { std::cout << "Is In Domain of " << button.label << '\n'; + + for (auto& b : radiuses) { + b.isClicked = false; + } + button.isClicked = true; + return; + } + } +} + +void Sidebar::weightButtonAvailable(const bool& status) { + for (auto& button : buttons) { + if (button.getButtonLabel() == LabelNames::WEIGHTED) { + button.isClicked = status; } } } bool Sidebar::isInSidebarDomain(Vector2 mousePosition, float currentRadius) { - if (mousePosition.x - currentRadius < width && mousePosition.y - currentRadius < height) { + if (mousePosition.x - 2 * currentRadius < width && mousePosition.y - 2 * currentRadius < height) { return true; } return false; @@ -102,8 +168,33 @@ bool Sidebar::isButtonClicked(const std::string& label) { return false; } +void Sidebar::flipGraphWeight() { + for (auto& button : buttons) { + if (button.getButtonLabel() == LabelNames::WEIGHTED) { + button.isClicked = !button.isClicked; + return; + } + } +} + +float Sidebar::getSelectedRadius() const { + for (const auto& button : radiuses) { + if (button.isClicked) { + return getRadiusValue(button.getButtonLabel()); + } + } + + // default + return NodeConstants::MEDIUM_RADIUS; +} + void Sidebar::resetClicks() { for (auto& button : radiuses) { + if (button.getButtonLabel() == LabelNames::MEDIUM) { + button.isClicked = true; + continue; + } + button.isClicked = false; } @@ -111,3 +202,12 @@ void Sidebar::resetClicks() { button.isClicked = false; } } + +void Sidebar::resetClick(const std::string& label) { + for (auto& button : buttons) { + if (button.getButtonLabel() == label) { + button.isClicked = false; + break; + } + } +} diff --git a/src/sidebar.h b/src/sidebar.h index 99944e2..d635b65 100644 --- a/src/sidebar.h +++ b/src/sidebar.h @@ -13,14 +13,19 @@ struct Button { Button(Rectangle rect, const std::string& str) : domain(rect), label(str), isClicked(false) {} -}; + const std::string& getButtonLabel() const { + return label; + } +}; class Sidebar { private: - int x; - int width; - int height; + float x; + float width; + float height; + float ystart; + float topButtons; std::vector