diff --git a/CMakeLists.txt b/CMakeLists.txt index 4e5d209..dc067b5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.25) -project(graphs VERSION 0.0.18) +project(graphs VERSION 0.0.19) option(ENABLE_ADDRSAN "Enable the address sanitizer" OFF) option(ENABLE_UBSAN "Enable the ub sanitizer" OFF) diff --git a/src/main.cpp b/src/main.cpp index 44f30fe..4a01f59 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -104,6 +104,141 @@ struct edge_hash } }; +struct equation_variables +{ + float repulsive_constant = 12.0; + float spring_constant = 24.0; + float ideal_spring_length = 175.0; + float initial_temperature = 69.5; + float cooling_rate = 0.999; +}; + +class force_equation +{ + public: + using node_pair = const std::pair&; + protected: + const equation_variables variables; + + struct equation_data + { + blt::vec2 unit; + float mag, mag_sq; + + equation_data(blt::vec2 unit, float mag, float mag_sq): unit(unit), mag(mag), mag_sq(mag_sq) + {} + }; + + inline static blt::vec2 dir_v(node_pair v1, node_pair v2) + { + return v2.second.getPosition() - v1.second.getPosition(); + } + + inline static equation_data calc_data(node_pair v1, node_pair v2) + { + auto dir = dir_v(v1, v2); + auto mag = dir.magnitude(); + auto unit = mag == 0 ? blt::vec2() : dir / mag; + auto mag_sq = mag * mag; + return {unit, mag, mag_sq}; + } + + public: + + explicit force_equation(const equation_variables& variables): variables(variables) + {} + + [[nodiscard]] virtual blt::vec2 attr(node_pair v1, node_pair v2) const = 0; + + [[nodiscard]] virtual blt::vec2 rep(node_pair v1, node_pair v2) const = 0; + + [[nodiscard]] virtual std::string name() const = 0; + + [[nodiscard]] virtual float cooling_factor(int t) const + { + return static_cast(variables.initial_temperature * std::pow(variables.cooling_rate, t)); + } + + virtual ~force_equation() = default; +}; + +class Eades_equation : public force_equation +{ + public: + explicit Eades_equation(const equation_variables& variables): force_equation(variables) + {} + + [[nodiscard]] blt::vec2 attr(node_pair v1, node_pair v2) const final + { + auto data = calc_data(v1, v2); + + auto ideal = std::log(data.mag / variables.ideal_spring_length); + + return variables.spring_constant * ideal * data.unit; + } + + [[nodiscard]] blt::vec2 rep(node_pair v1, node_pair v2) const final + { + auto data = calc_data(v1, v2); + + auto scale = variables.repulsive_constant / data.mag_sq; + return scale * -data.unit; + } + + [[nodiscard]] std::string name() const final + { + return "Eades"; + } +}; + +class Fruchterman_Reingold_equation : public force_equation +{ + public: + explicit Fruchterman_Reingold_equation(const equation_variables& variables): force_equation(variables) + {} + + [[nodiscard]] blt::vec2 attr(node_pair v1, node_pair v2) const final + { + auto data = calc_data(v1, v2); + + float scale = data.mag_sq / variables.ideal_spring_length; + + return scale * data.unit; + } + + [[nodiscard]] blt::vec2 rep(node_pair v1, node_pair v2) const final + { + auto data = calc_data(v1, v2); + + float scale = (variables.ideal_spring_length * variables.ideal_spring_length) / data.mag; + + return scale * -data.unit; + } + + [[nodiscard]] float cooling_factor(int t) const override + { + return force_equation::cooling_factor(t) * 0.025f; + } + + [[nodiscard]] std::string name() const final + { + return "Fruchterman & Reingold"; + } +}; + +struct bounding_box +{ + int min_x = 0; + int min_y = 0; + int max_x = 0; + int max_y = 0; + + bounding_box(int min_x, int min_y, int max_x, int max_y): min_x(min_x), min_y(min_y), max_x(max_x), max_y(max_y) + {} + + bool is_screen = true; +}; + class graph { private: @@ -114,52 +249,27 @@ class graph float sim_speed = 1; float threshold = 0.01; float max_force_last = 1; - float repulsive_constant = 12.0; - float spring_constant = 24.0; - float ideal_spring_length = 175.0; - float initial_temperature = 69.5; - float cooling_rate = 0.999; int current_iterations = 0; int max_iterations = 5000; + equation_variables variables; + std::unique_ptr equation; static constexpr float POINT_SIZE = 35; - [[nodiscard]] blt::vec2 attr(const std::pair& v1, const std::pair& v2) const - { - auto dir = v2.second.getPosition() - v1.second.getPosition(); - auto mag = dir.magnitude(); - auto unit = mag == 0 ? blt::vec2() : dir / mag; - - auto ideal = std::log(mag / ideal_spring_length); - - return spring_constant * ideal * unit; - } - - [[nodiscard]] blt::vec2 rep(const std::pair& v1, const std::pair& v2) const - { - auto dir = v2.second.getPosition() - v1.second.getPosition(); - auto mag = dir.magnitude(); - auto unit = mag == 0 ? blt::vec2() : dir / mag; - auto mag_sq = mag * mag; - - auto scale = repulsive_constant / mag_sq; - return scale * unit; - } - - [[nodiscard]] float cooling_factor() const - { - return static_cast(initial_temperature * std::pow(cooling_rate, current_iterations)); - } - - void create_random_graph(blt::i32 width, blt::i32 height, blt::size_t min_nodes, blt::size_t max_nodes, blt::f64 connectivity) + void create_random_graph(bounding_box bb, blt::size_t min_nodes, blt::size_t max_nodes, blt::f64 connectivity) { // don't allow points too close to the edges of the window. - width -= POINT_SIZE; - height -= POINT_SIZE; + if (bb.is_screen) + { + bb.max_x -= POINT_SIZE; + bb.max_y -= POINT_SIZE; + bb.min_x += POINT_SIZE; + bb.min_y += POINT_SIZE; + } static std::random_device dev; static std::uniform_real_distribution chance(0.0, 1.0); std::uniform_int_distribution node_count_dist(min_nodes, max_nodes); - std::uniform_real_distribution pos_x_dist(POINT_SIZE, static_cast(width)); - std::uniform_real_distribution pos_y_dist(POINT_SIZE, static_cast(height)); + std::uniform_real_distribution pos_x_dist(static_cast(bb.min_x), static_cast(bb.max_x)); + std::uniform_real_distribution pos_y_dist(static_cast(bb.min_y), static_cast(bb.max_y)); auto node_count = node_count_dist(dev); @@ -224,12 +334,13 @@ class graph public: graph() = default; - graph(blt::i32 width, blt::i32 height, blt::size_t min_nodes, blt::size_t max_nodes, blt::f64 connectivity) + graph(const bounding_box& bb, blt::size_t min_nodes, blt::size_t max_nodes, blt::f64 connectivity) { - create_random_graph(width, height, min_nodes, max_nodes, connectivity); + create_random_graph(bb, min_nodes, max_nodes, connectivity); + use_Eades(); } - void reset(blt::i32 width, blt::i32 height, blt::size_t min_nodes, blt::size_t max_nodes, blt::f64 connectivity) + void reset(const bounding_box& bb, blt::size_t min_nodes, blt::size_t max_nodes, blt::f64 connectivity) { sim = false; current_iterations = 0; @@ -237,7 +348,7 @@ class graph nodes.clear(); edges.clear(); connected_nodes.clear(); - create_random_graph(width, height, min_nodes, max_nodes, connectivity); + create_random_graph(bb, min_nodes, max_nodes, connectivity); } void connect(blt::u64 n1, blt::u64 n2) @@ -267,8 +378,8 @@ class graph { if (v1.first == v2.first) continue; - attractive += attr(v1, v2); - repulsive += rep(v1, v2); + attractive += equation->attr(v1, v2); + repulsive += equation->rep(v1, v2); } v1.second.getVelocityRef() = attractive + repulsive; } @@ -276,7 +387,8 @@ class graph // update positions for (auto& v : nodes) { - v.getPositionRef() += v.getVelocityRef() * cooling_factor() * static_cast(frame_time * sim_speed) * 0.05f; + float sim_factor = static_cast(frame_time * sim_speed) * 0.05f; + v.getPositionRef() += v.getVelocityRef() * equation->cooling_factor(current_iterations) * sim_factor; max_force_last = std::max(max_force_last, v.getVelocityRef().magnitude()); } current_iterations++; @@ -299,6 +411,16 @@ class graph } } + void use_Eades() + { + equation = std::make_unique(variables); + } + + void use_Fruchterman_Reingold() + { + equation = std::make_unique(variables); + } + void start_sim() { sim = true; @@ -309,6 +431,11 @@ class graph sim = false; } + std::string getSimulatorName() + { + return equation->name(); + } + float& getSimSpeed() { return sim_speed; @@ -321,27 +448,27 @@ class graph float& getSpringConstant() { - return spring_constant; + return variables.spring_constant; } float& getInitialTemperature() { - return initial_temperature; + return variables.initial_temperature; } float& getCoolingRate() { - return cooling_rate; + return variables.cooling_rate; } float& getIdealSpringLength() { - return ideal_spring_length; + return variables.ideal_spring_length; } float& getRepulsionConstant() { - return repulsive_constant; + return variables.repulsive_constant; } int& getMaxIterations() @@ -376,7 +503,8 @@ void init(const blt::gfx::window_data& data) resources.load_resources(); renderer_2d.create(); - main_graph = graph(data.width, data.height, 5, 25, 0.2); + bounding_box bb(0, 0, data.width, data.height); + main_graph = graph(bb, 5, 25, 0.2); lastTime = blt::system::nanoTime(); //render_texture = fbo_t::make_multisample_render_texture(1440, 720, 4); @@ -409,19 +537,48 @@ void update(const blt::gfx::window_data& data) { static int min_nodes = 5; static int max_nodes = 25; + + static bounding_box bb {0, 0, data.width, data.height}; + static float connectivity = 0.12; //im::SetNextItemOpen(true, ImGuiCond_Once); im::Text("FPS: %lf Frame-time (ms): %lf Frame-time (S): %lf", fps, ft * 1000.0, ft); im::Text("Number of Nodes: %d", main_graph.numberOfNodes()); + im::SetNextItemOpen(true, ImGuiCond_Once); + if (im::CollapsingHeader("Help")) + { + + } if (im::CollapsingHeader("Graph Generation Settings")) { + im::Checkbox("Screen Auto-Scale", &bb.is_screen); + if (im::CollapsingHeader("Spawning Area")) + { + bool result = false; + result |= im::InputInt("Min X", &bb.min_x, 5, 100); + result |= im::InputInt("Max X", &bb.max_x, 5, 100); + result |= im::InputInt("Min Y", &bb.min_y, 5, 100); + result |= im::InputInt("Max Y", &bb.max_y, 5, 100); + if (result) + { + bb.is_screen = false; + } + } + if (bb.is_screen) + { + bb.max_x = data.width; + bb.max_y = data.height; + bb.min_x = 0; + bb.min_y = 0; + } + im::SeparatorText("Node Settings"); im::InputInt("Min Nodes", &min_nodes); im::InputInt("Max Nodes", &max_nodes); im::SliderFloat("Connectivity", &connectivity, 0, 1); if (im::Button("Reset Graph")) { - main_graph.reset(data.width, data.height, min_nodes, max_nodes, connectivity); + main_graph.reset(bb, min_nodes, max_nodes, connectivity); } } im::SetNextItemOpen(true, ImGuiCond_Once); @@ -442,6 +599,34 @@ void update(const blt::gfx::window_data& data) if (im::Button("Stop")) main_graph.stop_sim(); } + im::SetNextItemOpen(true, ImGuiCond_Once); + if (im::CollapsingHeader("System Controls")) + { + //im::Text("Current System: %s", main_graph.getSimulatorName().c_str()); + //im:: + auto current_sim = main_graph.getSimulatorName(); + const char* items[] = {"Eades", "Fruchterman & Reingold"}; + static int item_current = 0; + ImGui::ListBox("", &item_current, items, IM_ARRAYSIZE(items), 4); + + if (strcmp(items[item_current], current_sim.c_str()) != 0) + { + switch (item_current) + { + case 0: + main_graph.use_Eades(); + BLT_INFO("Using Eades"); + break; + case 1: + main_graph.use_Fruchterman_Reingold(); + BLT_INFO("Using Fruchterman & Reingold"); + break; + default: + BLT_WARN("This is not a valid selection! How did we get here?"); + break; + } + } + } im::End(); }