add popups to certain user actions

This commit is contained in:
2026-02-28 21:29:57 +01:00
parent bc8dd423be
commit 809fbf1b93
20 changed files with 513 additions and 251 deletions

View File

@ -12,7 +12,7 @@ AllowShortIfStatementsOnASingleLine: true
AllowShortLoopsOnASingleLine: true
AlwaysBreakAfterDefinitionReturnType: false
AlwaysBreakTemplateDeclarations: Yes
BraceWrapping:
BraceWrapping:
AfterCaseLabel: true
AfterClass: true
AfterControlStatement: false
@ -30,8 +30,8 @@ BraceWrapping:
SplitEmptyRecord: false
SplitEmptyNamespace: false
BreakBeforeBraces: Custom
ColumnLimit: 120
IncludeCategories:
ColumnLimit: 100
IncludeCategories:
- Regex: '^<.*'
Priority: 1
- Regex: '^".*'

View File

@ -24,7 +24,8 @@ public:
auto pan(Vector2 last_mouse, Vector2 mouse) -> void;
auto update(const Vector3& current_target, const Vector3& mass_center, bool lock, bool mass_center_lock) -> void;
auto update(const Vector3& current_target, const Vector3& mass_center, bool lock,
bool mass_center_lock) -> void;
};
#endif

View File

@ -5,9 +5,28 @@
#include "state_manager.hpp"
#include <functional>
#include <queue>
#include <raylib.h>
#include <raymath.h>
struct show_ok_message
{
std::string title;
std::string message;
};
struct show_yes_no_message
{
std::string title;
std::string message;
std::function<void(void)> on_yes;
};
struct show_save_preset_window
{};
using ui_command = std::variant<show_ok_message, show_yes_no_message, show_save_preset_window>;
class input_handler
{
struct generic_handler
@ -36,6 +55,8 @@ public:
state_manager& state;
orbit_camera& camera;
std::queue<ui_command> ui_commands;
bool disable = false;
// Block selection
@ -115,7 +136,7 @@ public:
auto load_next_preset() -> void;
auto goto_starting_state() -> void;
auto populate_graph() const -> void;
auto clear_graph() const -> void;
auto clear_graph() -> void;
auto toggle_mark_solutions() -> void;
auto toggle_connect_solutions() -> void;
auto toggle_mark_path() -> void;
@ -132,20 +153,25 @@ public:
auto add_board_row() const -> void;
auto toggle_editing() -> void;
auto clear_goal() const -> void;
auto save_preset() -> void;
// General
auto register_generic_handler(const std::function<void(input_handler&)>& handler) -> void;
auto register_mouse_pressed_handler(MouseButton button, const std::function<void(input_handler&)>& handler) -> void;
auto register_mouse_pressed_handler(MouseButton button,
const std::function<void(input_handler&)>& handler) -> void;
auto register_mouse_released_handler(MouseButton button, const std::function<void(input_handler&)>& handler)
auto register_mouse_released_handler(MouseButton button,
const std::function<void(input_handler&)>& handler)
-> void;
auto register_key_pressed_handler(KeyboardKey key, const std::function<void(input_handler&)>& handler) -> void;
auto register_key_pressed_handler(KeyboardKey key,
const std::function<void(input_handler&)>& handler) -> void;
auto register_key_released_handler(KeyboardKey key, const std::function<void(input_handler&)>& handler) -> void;
auto register_key_released_handler(KeyboardKey key,
const std::function<void(input_handler&)>& handler) -> void;
auto handle_input() -> void;
};
#endif
#endif

View File

@ -41,7 +41,8 @@ public:
[[nodiscard]] auto get_octant(int node_idx, const Vector3& pos) const -> int;
[[nodiscard]] auto get_child_bounds(int node_idx, int octant) const -> std::pair<Vector3, Vector3>;
[[nodiscard]] auto get_child_bounds(int node_idx, int octant) const
-> std::pair<Vector3, Vector3>;
auto insert(int node_idx, int mass_id, const Vector3& pos, float mass, int depth) -> void;

View File

@ -198,14 +198,8 @@ public:
auto clear_cmd() -> void;
auto add_mass_springs_cmd(size_t num_masses, const std::vector<std::pair<size_t, size_t>>& springs) -> void;
};
// https://en.cppreference.com/w/cpp/utility/variant/visit
template <class... Ts>
struct overloads : Ts...
{
using Ts::operator()...;
auto add_mass_springs_cmd(size_t num_masses,
const std::vector<std::pair<size_t, size_t>>& springs) -> void;
};
#endif

View File

@ -48,8 +48,8 @@ public:
bool immovable = false;
public:
block(const int _x, const int _y, const int _width, const int _height, const bool _target = false,
const bool _immovable = false)
block(const int _x, const int _y, const int _width, const int _height,
const bool _target = false, const bool _immovable = false)
: x(_x), y(_y), width(_width), height(_height), target(_target), immovable(_immovable)
{
if (_x < 0 || _x + _width > 9 || _y < 0 || _y + _height > 9) {
@ -84,7 +84,8 @@ public:
}
immovable = false;
constexpr std::array<char, 9> immovable_chars{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'};
constexpr std::array<char, 9> immovable_chars{'A', 'B', 'C', 'D', 'E',
'F', 'G', 'H', 'I'};
for (const char c : immovable_chars) {
if (b.contains(c)) {
immovable = true;
@ -151,7 +152,8 @@ private:
explicit block_iterator(const puzzle& _state) : state(_state), current_pos(0)
{}
block_iterator(const puzzle& _state, const int _current_pos) : state(_state), current_pos(_current_pos)
block_iterator(const puzzle& _state, const int _current_pos)
: state(_state), current_pos(_current_pos)
{}
auto operator*() const -> block
@ -199,7 +201,8 @@ public:
puzzle(const int w, const int h, const int tx, const int ty, const bool r)
: width(w), height(h), target_x(tx), target_y(ty), restricted(r),
state(std::format("{}{}{}{}{}{}", r ? "R" : "F", w, h, tx, ty, std::string(w * h * 2, '.')))
state(
std::format("{}{}{}{}{}{}", r ? "R" : "F", w, h, tx, ty, std::string(w * h * 2, '.')))
{
if (w < 1 || w > 9 || h < 1 || h > 9) {
errln("State width/height must be in [1, 9]!");
@ -217,8 +220,9 @@ public:
{}
explicit puzzle(const std::string& s)
: width(std::stoi(s.substr(1, 1))), height(std::stoi(s.substr(2, 1))), target_x(std::stoi(s.substr(3, 1))),
target_y(std::stoi(s.substr(4, 1))), restricted(s.substr(0, 1) == "R"), state(s)
: width(std::stoi(s.substr(1, 1))), height(std::stoi(s.substr(2, 1))),
target_x(std::stoi(s.substr(3, 1))), target_y(std::stoi(s.substr(4, 1))),
restricted(s.substr(0, 1) == "R"), state(s)
{
if (width < 1 || width > 9 || height < 1 || height > 9) {
errln("State width/height must be in [1, 9]!");
@ -271,7 +275,7 @@ public:
[[nodiscard]] auto has_win_condition() const -> bool;
[[nodiscard]] auto won() const -> bool;
[[nodiscard]] auto valid() const -> bool;
[[nodiscard]] auto valid_thorough() const -> bool;
[[nodiscard]] auto try_get_invalid_reason() const -> std::optional<std::string>;
// Repr helpers
@ -297,7 +301,8 @@ public:
// Playing
[[nodiscard]] auto try_move_block_at(int x, int y, direction dir) const -> std::optional<puzzle>;
[[nodiscard]] auto try_move_block_at(int x, int y, direction dir) const
-> std::optional<puzzle>;
// Statespace
@ -333,9 +338,11 @@ struct std::hash<std::pair<puzzle, puzzle>>
template <>
struct std::equal_to<std::pair<puzzle, puzzle>>
{
auto operator()(const std::pair<puzzle, puzzle>& a, const std::pair<puzzle, puzzle>& b) const noexcept -> bool
auto operator()(const std::pair<puzzle, puzzle>& a,
const std::pair<puzzle, puzzle>& b) const noexcept -> bool
{
return (a.first == b.first && a.second == b.second) || (a.first == b.second && a.second == b.first);
return (a.first == b.first && a.second == b.second) ||
(a.first == b.second && a.second == b.first);
}
};

View File

@ -18,8 +18,12 @@ private:
user_interface& gui;
const orbit_camera& camera;
RenderTexture render_target = LoadRenderTexture(GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT);
RenderTexture klotski_target = LoadRenderTexture(GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT);
RenderTexture render_target =
LoadRenderTexture(GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT);
// TODO: Those should be moved to the user_interface.h
RenderTexture klotski_target =
LoadRenderTexture(GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT);
RenderTexture menu_target = LoadRenderTexture(GetScreenWidth(), MENU_HEIGHT);
// Batching
@ -31,7 +35,8 @@ private:
std::vector<Color> colors;
Material vertex_mat = LoadMaterialDefault();
Mesh cube_instance = GenMeshCube(VERTEX_SIZE, VERTEX_SIZE, VERTEX_SIZE);
Shader instancing_shader = LoadShader("shader/instancing_vertex.glsl", "shader/instancing_fragment.glsl");
Shader instancing_shader =
LoadShader("shader/instancing_vertex.glsl", "shader/instancing_fragment.glsl");
unsigned int color_vbo_id = 0;
@ -43,10 +48,13 @@ public:
instancing_shader.locs[SHADER_LOC_MATRIX_MVP] = GetShaderLocation(instancing_shader, "mvp");
instancing_shader.locs[SHADER_LOC_MATRIX_MODEL] =
GetShaderLocationAttrib(instancing_shader, "instanceTransform");
instancing_shader.locs[SHADER_LOC_VECTOR_VIEW] = GetShaderLocation(instancing_shader, "viewPos");
instancing_shader.locs[SHADER_LOC_VECTOR_VIEW] =
GetShaderLocation(instancing_shader, "viewPos");
infoln("LOC vertexPosition: {}", rlGetLocationAttrib(instancing_shader.id, "vertexPosition"));
infoln("LOC instanceTransform: {}", rlGetLocationAttrib(instancing_shader.id, "instanceTransform"));
infoln("LOC vertexPosition: {}",
rlGetLocationAttrib(instancing_shader.id, "vertexPosition"));
infoln("LOC instanceTransform: {}",
rlGetLocationAttrib(instancing_shader.id, "instanceTransform"));
infoln("LOC instanceColor: {}", rlGetLocationAttrib(instancing_shader.id, "instanceColor"));
// vertex_mat.maps[MATERIAL_MAP_DIFFUSE].color = VERTEX_COLOR;
@ -94,7 +102,8 @@ private:
auto draw_textures(int fps, int ups, size_t mass_count, size_t spring_count) const -> void;
public:
auto render(const std::vector<Vector3>& masses, int fps, int ups, size_t mass_count, size_t spring_count) -> void;
auto render(const std::vector<Vector3>& masses, int fps, int ups, size_t mass_count,
size_t spring_count) -> void;
};
#endif

View File

@ -22,16 +22,19 @@ private:
// State storage (store states twice for bidirectional lookup).
// Everything else should only store indices to state_pool.
std::vector<puzzle> state_pool; // Indices are equal to mass_springs mass indices
std::vector<puzzle> state_pool; // Indices are equal to mass_springs mass indices
std::unordered_map<puzzle, size_t> state_indices; // Maps states to indices
std::vector<std::pair<size_t, size_t>> links; // Indices are equal to mass_springs springs indices
std::vector<std::pair<size_t, size_t>>
links; // Indices are equal to mass_springs springs indices
graph_distances node_target_distances; // Buffered and reused if the graph doesn't change
std::unordered_set<size_t> winning_indices; // Indices of all states where the board is solved
std::vector<size_t> winning_path; // Ordered list of node indices leading to the nearest solved state
std::unordered_set<size_t> path_indices; // For faster lookup if a vertex is part of the path in renderer
std::vector<size_t>
winning_path; // Ordered list of node indices leading to the nearest solved state
std::unordered_set<size_t>
path_indices; // For faster lookup if a vertex is part of the path in renderer
std::stack<size_t> move_history; // Moves between the starting state and the current state
std::stack<size_t> move_history; // Moves between the starting state and the current state
std::unordered_map<size_t, int> visit_counts; // How often each state was visited
size_t starting_state_index = 0;

View File

@ -22,13 +22,15 @@ class user_interface
const int padding;
public:
grid(const int _x, const int _y, const int _width, const int _height, const int _columns, const int _rows,
const int _padding)
: x(_x), y(_y), width(_width), height(_height), columns(_columns), rows(_rows), padding(_padding)
grid(const int _x, const int _y, const int _width, const int _height, const int _columns,
const int _rows, const int _padding)
: x(_x), y(_y), width(_width), height(_height), columns(_columns), rows(_rows),
padding(_padding)
{}
public:
auto update_bounds(int _x, int _y, int _width, int _height, int _columns, int _rows) -> void;
auto update_bounds(int _x, int _y, int _width, int _height, int _columns, int _rows)
-> void;
auto update_bounds(int _x, int _y, int _width, int _height) -> void;
auto update_bounds(int _x, int _y) -> void;
@ -36,7 +38,8 @@ class user_interface
[[nodiscard]] auto bounds(int _x, int _y, int _width, int _height) const -> Rectangle;
[[nodiscard]] auto square_bounds() const -> Rectangle;
[[nodiscard]] auto square_bounds(int _x, int _y, int _width, int _height) const -> Rectangle;
[[nodiscard]] auto square_bounds(int _x, int _y, int _width, int _height) const
-> Rectangle;
};
struct style
@ -84,13 +87,22 @@ private:
grid menu_grid = grid(0, 0, GetScreenWidth(), MENU_HEIGHT, MENU_COLS, MENU_ROWS, MENU_PAD);
grid board_grid = grid(0, MENU_HEIGHT, GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT,
state.get_current_state().width, state.get_current_state().height, BOARD_PADDING);
grid board_grid =
grid(0, MENU_HEIGHT, GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT,
state.get_current_state().width, state.get_current_state().height, BOARD_PADDING);
grid graph_overlay_grid = grid(GetScreenWidth() / 2, MENU_HEIGHT, 200, 100, 1, 4, MENU_PAD);
grid debug_overlay_grid = grid(GetScreenWidth() / 2, GetScreenHeight() - 75, 200, 75, 1, 3, MENU_PAD);
grid debug_overlay_grid =
grid(GetScreenWidth() / 2, GetScreenHeight() - 75, 200, 75, 1, 3, MENU_PAD);
// Windows
std::string message_title;
std::string message_message;
std::function<void(void)> yes_no_handler;
bool ok_message = false;
bool yes_no_message = false;
bool save_window = false;
std::array<char, 256> preset_name = {};
bool help_window = false;
@ -119,29 +131,34 @@ private:
static auto get_component_style(int component) -> component_style;
static auto set_component_style(int component, const component_style& style) -> void;
[[nodiscard]] static auto popup_bounds() -> Rectangle;
auto draw_button(Rectangle bounds, const std::string& label, Color color, bool enabled = true,
int font_size = FONT_SIZE) const -> int;
auto draw_menu_button(int x, int y, int width, int height, const std::string& label, Color color,
bool enabled = true, int font_size = FONT_SIZE) const -> int;
auto draw_menu_button(int x, int y, int width, int height, const std::string& label,
Color color, bool enabled = true, int font_size = FONT_SIZE) const -> int;
auto draw_toggle_slider(Rectangle bounds, const std::string& off_label, const std::string& on_label, int* active,
Color color, bool enabled = true, int font_size = FONT_SIZE) const -> int;
auto draw_toggle_slider(Rectangle bounds, const std::string& off_label,
const std::string& on_label, int* active, Color color,
bool enabled = true, int font_size = FONT_SIZE) const -> int;
auto draw_menu_toggle_slider(int x, int y, int width, int height, const std::string& off_label,
const std::string& on_label, int* active, Color color, bool enabled = true,
int font_size = FONT_SIZE) const -> int;
const std::string& on_label, int* active, Color color,
bool enabled = true, int font_size = FONT_SIZE) const -> int;
auto draw_spinner(Rectangle bounds, const std::string& label, int* value, int min, int max, Color color,
bool enabled = true, int font_size = FONT_SIZE) const -> int;
auto draw_spinner(Rectangle bounds, const std::string& label, int* value, int min, int max,
Color color, bool enabled = true, int font_size = FONT_SIZE) const -> int;
auto draw_menu_spinner(int x, int y, int width, int height, const std::string& label, int* value, int min, int max,
Color color, bool enabled = true, int font_size = FONT_SIZE) const -> int;
auto draw_menu_spinner(int x, int y, int width, int height, const std::string& label,
int* value, int min, int max, Color color, bool enabled = true,
int font_size = FONT_SIZE) const -> int;
auto draw_label(Rectangle bounds, const std::string& text, Color color, bool enabled = true,
int font_size = FONT_SIZE) const -> int;
auto draw_board_block(int x, int y, int width, int height, Color color, bool enabled = true) const -> bool;
auto draw_board_block(int x, int y, int width, int height, Color color,
bool enabled = true) const -> bool;
[[nodiscard]] auto window_open() const -> bool;
@ -158,10 +175,12 @@ public:
static auto get_background_color() -> Color;
auto help_popup() -> void;
auto draw_save_preset_popup() -> void;
auto draw_ok_message_box() -> void;
auto draw_yes_no_message_box() -> void;
auto draw_main_menu() -> void;
auto draw_puzzle_board() -> void;
auto draw_graph_overlay(int fps, int ups, size_t mass_count, size_t spring_count) -> void;
auto update() const -> void;
auto draw(int fps, int ups, size_t mass_count, size_t spring_count) -> void;
};
#endif

View File

@ -17,6 +17,13 @@ inline auto operator<<(std::ostream& os, const Vector3& v) -> std::ostream&
return os;
}
// https://en.cppreference.com/w/cpp/utility/variant/visit
template <class... Ts>
struct overloads : Ts...
{
using Ts::operator()...;
};
enum ctrl
{
reset = 0,

View File

@ -35,13 +35,14 @@ auto orbit_camera::pan(const Vector2 last_mouse, const Vector2 mouse) -> void
const Vector3 right = Vector3Normalize(Vector3CrossProduct(forward, camera.up));
const Vector3 up = Vector3Normalize(Vector3CrossProduct(right, forward));
const Vector3 offset = Vector3Add(Vector3Scale(right, -dx * speed), Vector3Scale(up, dy * speed));
const Vector3 offset =
Vector3Add(Vector3Scale(right, -dx * speed), Vector3Scale(up, dy * speed));
target = Vector3Add(target, offset);
}
auto orbit_camera::update(const Vector3& current_target, const Vector3& mass_center, const bool lock,
const bool mass_center_lock) -> void
auto orbit_camera::update(const Vector3& current_target, const Vector3& mass_center,
const bool lock, const bool mass_center_lock) -> void
{
if (lock) {
if (mass_center_lock) {

View File

@ -18,7 +18,8 @@ auto graph_distances::empty() const -> bool
return distances.empty() || parents.empty() || nearest_targets.empty();
}
auto graph_distances::calculate_distances(const size_t node_count, const std::vector<std::pair<size_t, size_t>>& edges,
auto graph_distances::calculate_distances(const size_t node_count,
const std::vector<std::pair<size_t, size_t>>& edges,
const std::vector<size_t>& targets) -> void
{
// Build a list of adjacent nodes to speed up BFS

View File

@ -53,6 +53,7 @@ auto input_handler::init_handlers() -> void
register_key_pressed_handler(KEY_LEFT, &input_handler::remove_board_column);
register_key_pressed_handler(KEY_X, &input_handler::clear_goal);
register_key_pressed_handler(KEY_P, &input_handler::print_state);
register_key_pressed_handler(KEY_S, &input_handler::save_preset); // + CTRL
register_key_pressed_handler(KEY_L, &input_handler::toggle_camera_lock);
register_key_pressed_handler(KEY_LEFT_ALT, &input_handler::toggle_camera_projection);
@ -87,8 +88,8 @@ auto input_handler::camera_start_pan() -> void
}
camera_panning = true;
// Enable this if the camera should be pannable even when locked (releasing the lock in the process):
// camera_lock = false;
// Enable this if the camera should be pannable even when locked (releasing the lock in the
// process): camera_lock = false;
}
auto input_handler::camera_pan() const -> void
@ -197,8 +198,8 @@ auto input_handler::add_block() -> void
block_add_y = -1;
has_block_add_xy = false;
} else if (current.covers(block_add_x, block_add_y, block_add_width, block_add_height)) {
const std::optional<puzzle>& next =
current.try_add_block(puzzle::block(block_add_x, block_add_y, block_add_width, block_add_height, false));
const std::optional<puzzle>& next = current.try_add_block(
puzzle::block(block_add_x, block_add_y, block_add_width, block_add_height, false));
if (next) {
sel_x = block_add_x;
@ -331,10 +332,19 @@ auto input_handler::load_previous_preset() -> void
return;
}
block_add_x = -1;
block_add_y = -1;
has_block_add_xy = false;
state.load_previous_preset();
const auto handler = [&]()
{
block_add_x = -1;
block_add_y = -1;
has_block_add_xy = false;
state.load_previous_preset();
};
if (state.was_edited()) {
ui_commands.emplace(show_yes_no_message{"Switch Preset?", "Edits Will Be Lost.", handler});
} else {
handler();
}
}
auto input_handler::load_next_preset() -> void
@ -343,17 +353,31 @@ auto input_handler::load_next_preset() -> void
return;
}
block_add_x = -1;
block_add_y = -1;
has_block_add_xy = false;
state.load_next_preset();
const auto handler = [&]()
{
block_add_x = -1;
block_add_y = -1;
has_block_add_xy = false;
state.load_next_preset();
};
if (state.was_edited()) {
ui_commands.emplace(show_yes_no_message{"Switch Preset?", "Edits Will Be Lost.", handler});
} else {
handler();
}
}
auto input_handler::goto_starting_state() -> void
{
state.goto_starting_state();
sel_x = 0;
sel_y = 0;
const auto handler = [&]()
{
state.goto_starting_state();
sel_x = 0;
sel_y = 0;
};
ui_commands.emplace(show_yes_no_message{"Reset Board?", "This Clears the Move History.", handler});
}
auto input_handler::populate_graph() const -> void
@ -361,9 +385,14 @@ auto input_handler::populate_graph() const -> void
state.populate_graph();
}
auto input_handler::clear_graph() const -> void
auto input_handler::clear_graph() -> void
{
state.clear_graph_and_add_current();
const auto handler = [&]()
{
state.clear_graph_and_add_current();
};
ui_commands.emplace(show_yes_no_message{"Clear Graph?", "This Clears the Move History.", handler});
}
auto input_handler::toggle_mark_solutions() -> void
@ -508,6 +537,19 @@ auto input_handler::clear_goal() const -> void
state.edit_starting_state(*next);
}
auto input_handler::save_preset() -> void
{
if (!IsKeyDown(KEY_LEFT_CONTROL)) {
return;
}
if (const std::optional<std::string>& reason = state.get_current_state().try_get_invalid_reason()) {
ui_commands.emplace(show_ok_message{"Can't Save Preset", std::format("Invalid Board: {}.", *reason)});
} else {
ui_commands.emplace(show_save_preset_window{});
}
}
auto input_handler::register_generic_handler(const std::function<void(input_handler&)>& handler) -> void
{
generic_handlers.push_back({handler});
@ -570,4 +612,4 @@ auto input_handler::handle_input() -> void
handler.handler(*this);
}
}
}
}

View File

@ -13,17 +13,10 @@
#include <tracy/Tracy.hpp>
#endif
// TODO: Click states in the graph to display them in the board
// TODO: Move selection accordingly when undoing moves (need to diff two states
// and get the moved blocks)
// TODO: Add some popups (my split between input.cpp/gui.cpp makes this ugly)
// - Next move, goto target, goto worst: Notify that the graph needs to be
// populated
// - Clear graph: Notify that this will clear the visited states and move
// history
// - Reset state: Notify that this will reset the move count
// - Next/Previous preset: Notify that this will clear all edits
// TODO: Reduce memory usage
// - The memory model of the puzzle board is terrible (bitboards?)
@ -38,6 +31,11 @@
// given the initial state
// - Would allow to generate random puzzles with a certain move count
// TODO: Move selection accordingly when undoing moves (need to diff two states
// and get the moved blocks)
// TODO: Click states in the graph to display them in the board
// NOTE: Tracy uses a huge amount of memory. For longer testing disable Tracy.
auto main(int argc, char* argv[]) -> int
@ -139,7 +137,8 @@ auto main(int argc, char* argv[]) -> int
size_t current_index = state.get_current_index();
if (masses.size() > current_index) {
const mass& current_mass = mass(masses.at(current_index));
camera.update(current_mass.position, mass_center, input.camera_lock, input.camera_mass_center_lock);
camera.update(current_mass.position, mass_center, input.camera_lock,
input.camera_mass_center_lock);
}
// Rendering
@ -162,4 +161,4 @@ auto main(int argc, char* argv[]) -> int
CloseWindow();
return 0;
}
}

View File

@ -32,8 +32,9 @@ auto octree::create_empty_leaf(const Vector3& box_min, const Vector3& box_max) -
auto octree::get_octant(const int node_idx, const Vector3& pos) const -> int
{
const node& n = nodes[node_idx];
auto [cx, cy, cz] = Vector3((n.box_min.x + n.box_max.x) / 2.0f, (n.box_min.y + n.box_max.y) / 2.0f,
(n.box_min.z + n.box_max.z) / 2.0f);
auto [cx, cy, cz] =
Vector3((n.box_min.x + n.box_max.x) / 2.0f, (n.box_min.y + n.box_max.y) / 2.0f,
(n.box_min.z + n.box_max.z) / 2.0f);
// The octant is encoded as a 3-bit integer "zyx". The node area is split
// along all 3 axes, if a position is right of an axis, this bit is set to 1.
@ -53,11 +54,13 @@ auto octree::get_octant(const int node_idx, const Vector3& pos) const -> int
return octant;
}
auto octree::get_child_bounds(const int node_idx, const int octant) const -> std::pair<Vector3, Vector3>
auto octree::get_child_bounds(const int node_idx, const int octant) const
-> std::pair<Vector3, Vector3>
{
const node& n = nodes[node_idx];
auto [cx, cy, cz] = Vector3((n.box_min.x + n.box_max.x) / 2.0f, (n.box_min.y + n.box_max.y) / 2.0f,
(n.box_min.z + n.box_max.z) / 2.0f);
auto [cx, cy, cz] =
Vector3((n.box_min.x + n.box_max.x) / 2.0f, (n.box_min.y + n.box_max.y) / 2.0f,
(n.box_min.z + n.box_max.z) / 2.0f);
Vector3 min = Vector3Zero();
Vector3 max = Vector3Zero();
@ -75,21 +78,23 @@ auto octree::get_child_bounds(const int node_idx, const int octant) const -> std
return std::make_pair(min, max);
}
auto octree::insert(const int node_idx, const int mass_id, const Vector3& pos, const float mass, const int depth)
-> void
auto octree::insert(const int node_idx, const int mass_id, const Vector3& pos, const float mass,
const int depth) -> void
{
// infoln("Inserting position ({}, {}, {}) into octree at node {} (depth {})", pos.x, pos.y, pos.z, node_idx,
// depth);
// infoln("Inserting position ({}, {}, {}) into octree at node {} (depth {})", pos.x, pos.y,
// pos.z, node_idx, depth);
if (depth > MAX_DEPTH) {
errln("MAX_DEPTH! node={} box_min=({},{},{}) box_max=({},{},{}) pos=({},{},{})", node_idx,
nodes[node_idx].box_min.x, nodes[node_idx].box_min.y, nodes[node_idx].box_min.z,
nodes[node_idx].box_max.x, nodes[node_idx].box_max.y, nodes[node_idx].box_max.z, pos.x, pos.y, pos.z);
nodes[node_idx].box_max.x, nodes[node_idx].box_max.y, nodes[node_idx].box_max.z,
pos.x, pos.y, pos.z);
// This runs from inside the physics thread so it won't exit cleanly
exit(1);
}
// NOTE: Do not store a nodes[node_idx] reference as the nodes vector might reallocate during this function
// NOTE: Do not store a nodes[node_idx] reference as the nodes vector might reallocate during
// this function
// We can place the particle in the empty leaf
if (nodes[node_idx].leaf && nodes[node_idx].mass_id == -1) {
@ -116,9 +121,8 @@ auto octree::insert(const int node_idx, const int mass_id, const Vector3& pos, c
insert(node_idx, mass_id, jittered, mass, depth);
return;
// Could also merge them, but that leads to the octree having less leafs than we have masses
// nodes[node_idx].mass_total += mass;
// return;
// Could also merge them, but that leads to the octree having less leafs than we have
// masses nodes[node_idx].mass_total += mass; return;
}
// Convert the leaf to an internal node
@ -148,9 +152,12 @@ auto octree::insert(const int node_idx, const int mass_id, const Vector3& pos, c
// Update the center of mass
const float new_mass = nodes[node_idx].mass_total + mass;
nodes[node_idx].mass_center.x = (nodes[node_idx].mass_center.x * nodes[node_idx].mass_total + pos.x) / new_mass;
nodes[node_idx].mass_center.y = (nodes[node_idx].mass_center.y * nodes[node_idx].mass_total + pos.y) / new_mass;
nodes[node_idx].mass_center.z = (nodes[node_idx].mass_center.z * nodes[node_idx].mass_total + pos.z) / new_mass;
nodes[node_idx].mass_center.x =
(nodes[node_idx].mass_center.x * nodes[node_idx].mass_total + pos.x) / new_mass;
nodes[node_idx].mass_center.y =
(nodes[node_idx].mass_center.y * nodes[node_idx].mass_total + pos.y) / new_mass;
nodes[node_idx].mass_center.z =
(nodes[node_idx].mass_center.z * nodes[node_idx].mass_total + pos.z) / new_mass;
nodes[node_idx].mass_total = new_mass;
}

View File

@ -57,7 +57,8 @@ auto spring::calculate_spring_force(mass& _a, mass& _b) -> void
const Vector3 delta_velocity = Vector3Subtract(_a.velocity, _b.velocity);
const float hooke = SPRING_CONSTANT * (current_length - REST_LENGTH);
const float dampening = DAMPENING_CONSTANT * Vector3DotProduct(delta_velocity, delta_position) * inv_current_length;
const float dampening =
DAMPENING_CONSTANT * Vector3DotProduct(delta_velocity, delta_position) * inv_current_length;
const Vector3 force_a = Vector3Scale(delta_position, -(hooke + dampening) * inv_current_length);
const Vector3 force_b = Vector3Scale(force_a, -1.0);
@ -72,8 +73,8 @@ auto mass_spring_system::add_mass() -> void
// Done when adding springs
// Vector3 position{
// static_cast<float>(GetRandomValue(-100, 100)), static_cast<float>(GetRandomValue(-100, 100)),
// static_cast<float>(GetRandomValue(-100, 100))
// static_cast<float>(GetRandomValue(-100, 100)), static_cast<float>(GetRandomValue(-100,
// 100)), static_cast<float>(GetRandomValue(-100, 100))
// };
// position = Vector3Scale(Vector3Normalize(position), REST_LENGTH * 2.0);
@ -86,13 +87,15 @@ auto mass_spring_system::add_spring(size_t a, size_t b) -> void
const mass& mass_a = masses.at(a);
mass& mass_b = masses.at(b);
Vector3 offset{static_cast<float>(GetRandomValue(-100, 100)), static_cast<float>(GetRandomValue(-100, 100)),
Vector3 offset{static_cast<float>(GetRandomValue(-100, 100)),
static_cast<float>(GetRandomValue(-100, 100)),
static_cast<float>(GetRandomValue(-100, 100))};
offset = Vector3Normalize(offset) * REST_LENGTH;
// If the offset moves the mass closer to the current center of mass, flip it
if (!tree.nodes.empty()) {
const Vector3 mass_center_direction = Vector3Subtract(mass_a.position, tree.nodes.at(0).mass_center);
const Vector3 mass_center_direction =
Vector3Subtract(mass_a.position, tree.nodes.at(0).mass_center);
const float mass_center_distance = Vector3Length(mass_center_direction);
if (mass_center_distance > 0 && Vector3DotProduct(offset, mass_center_direction) < 0.0f) {
@ -103,7 +106,8 @@ auto mass_spring_system::add_spring(size_t a, size_t b) -> void
mass_b.position = mass_a.position + offset;
mass_b.previous_position = mass_b.position;
// infoln("Adding spring: ({}, {}, {})->({}, {}, {})", mass_a.position.x, mass_a.position.y, mass_a.position.z,
// infoln("Adding spring: ({}, {}, {})->({}, {}, {})", mass_a.position.x, mass_a.position.y,
// mass_a.position.z,
// mass_b.position.x, mass_b.position.y, mass_b.position.z);
springs.emplace_back(a, b);
@ -201,7 +205,8 @@ auto mass_spring_system::calculate_repulsion_forces() -> void
// Calculate forces using Barnes-Hut
#ifdef THREADPOOL
const BS::multi_future<void> loop_future = threads.submit_loop(0, masses.size(), solve_octree, 256);
const BS::multi_future<void> loop_future =
threads.submit_loop(0, masses.size(), solve_octree, 256);
loop_future.wait();
#else
for (size_t i = 0; i < masses.size(); ++i) {
@ -291,9 +296,9 @@ auto threaded_physics::physics_thread(physics_state& state) -> void
mass_springs.calculate_repulsion_forces();
mass_springs.verlet_update(TIMESTEP * SIM_SPEED);
// This is only helpful if we're drawing a grid at (0, 0, 0). Otherwise, it's just expensive
// and yields no benefit since we can lock the camera to the center of mass cheaply.
// mass_springs.center_masses();
// This is only helpful if we're drawing a grid at (0, 0, 0). Otherwise, it's just
// expensive and yields no benefit since we can lock the camera to the center of mass
// cheaply. mass_springs.center_masses();
++loop_iterations;
physics_accumulator -= std::chrono::duration<double>(TIMESTEP);
@ -309,7 +314,8 @@ auto threaded_physics::physics_thread(physics_state& state) -> void
#else
std::unique_lock<std::mutex> lock(state.data_mtx);
#endif
state.data_consumed_cnd.wait(lock, [&] { return state.data_consumed || !state.running.load(); });
state.data_consumed_cnd.wait(lock, [&]
{ return state.data_consumed || !state.running.load(); });
if (!state.running.load()) {
// Running turned false while we were waiting for the condition
break;
@ -386,7 +392,8 @@ auto threaded_physics::clear_cmd() -> void
}
auto threaded_physics::add_mass_springs_cmd(const size_t num_masses,
const std::vector<std::pair<size_t, size_t>>& springs) -> void
const std::vector<std::pair<size_t, size_t>>& springs)
-> void
{
{
#ifdef TRACY

View File

@ -1,5 +1,6 @@
#include "puzzle.hpp"
#include <optional>
#include <unordered_set>
#ifdef TRACY
@ -85,45 +86,64 @@ auto puzzle::valid() const -> bool
return width >= MIN_WIDTH && width <= MAX_WIDTH && height >= MIN_HEIGHT && height <= MAX_HEIGHT;
}
auto puzzle::valid_thorough() const -> bool
auto puzzle::try_get_invalid_reason() const -> std::optional<std::string>
{
if (has_win_condition() && !try_get_target_block()) {
return false;
const std::optional<block>& b = try_get_target_block();
if (has_win_condition() && !b) {
return "Goal Without Target";
}
if (!has_win_condition() && b) {
return "Target Without Goal";
}
if (has_win_condition() && b && restricted) {
const int dirs = b->principal_dirs();
if ((dirs & nor && b->x != target_x) || (dirs & eas && b->y != target_y)) {
return "Goal Unreachable";
}
}
if (target_x > 0 && target_x + b->width < width && target_y > 0 && target_y + b->height < height) {
return "Goal Inside";
}
infoln("Validating puzzle {}", state);
if (static_cast<int>(state.length()) != width * height * 2 + PREFIX) {
infoln("Puzzle invalid: Representation length {} doesn't match {}x{} board", state.length(), width, height);
return false;
infoln("Puzzle invalid: Representation length {} doesn't match {}x{} board", state.length(),
width, height);
return "Invalid Repr Length";
}
// Check prefix
if (!std::string("FR").contains(state[0])) {
infoln("Puzzle invalid: Representation[0] {} doesn't match [FR]", state[0]);
return false;
return "Invalid Restricted Repr";
}
if (restricted && state[0] != 'R') {
infoln("Puzzle invalid: Representation[0] {} doesn't match restricted={}", state[0], restricted);
return false;
infoln("Puzzle invalid: Representation[0] {} doesn't match restricted={}", state[0],
restricted);
return "Restricted != Restricted Repr";
}
if (!std::string("3456789").contains(state[1]) || !std::string("3456789").contains(state[2])) {
if (!std::string("0123456789").contains(state[1]) ||
!std::string("0123456789").contains(state[2])) {
infoln("Puzzle invalid: Representation[1/2] {}/{} doesn't match [3-9]", state[1], state[2]);
return false;
return "Invalid Dims Repr";
}
if (std::stoi(state.substr(1, 1)) != width || std::stoi(state.substr(2, 1)) != height) {
infoln("Puzzle invalid: Representation[1/2] {}/{} doesn't match {}x{} board", state[1], state[2], width,
height);
return false;
infoln("Puzzle invalid: Representation[1/2] {}/{} doesn't match {}x{} board", state[1],
state[2], width, height);
return "Dims != Dims Repr";
}
if (!std::string("012345678").contains(state[3]) || !std::string("012345678").contains(state[4])) {
if (!std::string("0123456789").contains(state[3]) ||
!std::string("0123456789").contains(state[4])) {
infoln("Puzzle invalid: Representation[3/4] {}/{} doesn't match [1-9]", state[3], state[4]);
return false;
return "Invalid Goal Repr";
}
if (std::stoi(state.substr(3, 1)) != target_x || std::stoi(state.substr(4, 1)) != target_y) {
infoln("Puzzle invalid: Representation[3/4] {}/{} doesn't match target ({}, {})", state[3], state[4], target_x,
target_y);
return false;
infoln("Puzzle invalid: Representation[3/4] {}/{} doesn't match target ({}, {})", state[3],
state[4], target_x, target_y);
return "Goal != Goal Repr";
}
// Check blocks
@ -131,17 +151,19 @@ auto puzzle::valid_thorough() const -> bool
for (const char c : state.substr(PREFIX, state.length() - PREFIX)) {
if (!allowed_chars.contains(c)) {
infoln("Puzzle invalid: Block {} has invalid character", c);
return false;
return "Invalid Block Repr";
}
}
const bool success = width >= MIN_WIDTH && width <= MAX_WIDTH && height >= MIN_HEIGHT && height <= MAX_HEIGHT;
const bool success =
width >= MIN_WIDTH && width <= MAX_WIDTH && height >= MIN_HEIGHT && height <= MAX_HEIGHT;
if (!success) {
infoln("Puzzle invalid: Board size {}x{} not in range [3-9]", width, height);
return "Invalid Dims";
} else {
return std::nullopt;
}
return success;
}
auto puzzle::try_get_block(const int x, const int y) const -> std::optional<block>
@ -365,7 +387,8 @@ auto puzzle::try_toggle_wall(const int x, const int y) const -> std::optional<pu
return p;
}
auto puzzle::try_move_block_at(const int x, const int y, const direction dir) const -> std::optional<puzzle>
auto puzzle::try_move_block_at(const int x, const int y, const direction dir) const
-> std::optional<puzzle>
{
const std::optional<block>& b = try_get_block(x, y);
if (!b || b->immovable) {
@ -468,7 +491,8 @@ auto puzzle::find_adjacent_puzzles() const -> std::vector<puzzle>
return puzzles;
}
auto puzzle::explore_state_space() const -> std::pair<std::vector<puzzle>, std::vector<std::pair<size_t, size_t>>>
auto puzzle::explore_state_space() const
-> std::pair<std::vector<puzzle>, std::vector<std::pair<size_t, size_t>>>
{
#ifdef TRACY
ZoneScoped;
@ -506,4 +530,4 @@ auto puzzle::explore_state_space() const -> std::pair<std::vector<puzzle>, std::
infoln("State space has size {} with {} transitions.", state_pool.size(), links.size());
return std::make_pair(state_pool, links);
}
}

View File

@ -73,10 +73,12 @@ auto renderer::draw_mass_springs(const std::vector<Vector3>& masses) -> void
// Normal vertex
Color c = VERTEX_COLOR;
if ((input.mark_solutions || input.mark_path) && state.get_winning_indices().contains(mass)) {
if ((input.mark_solutions || input.mark_path) &&
state.get_winning_indices().contains(mass)) {
// Winning vertex
c = VERTEX_TARGET_COLOR;
} else if ((input.mark_solutions || input.mark_path) && state.get_path_indices().contains(mass)) {
} else if ((input.mark_solutions || input.mark_path) &&
state.get_path_indices().contains(mass)) {
// Path vertex
c = VERTEX_PATH_COLOR;
} else if (mass == state.get_starting_index()) {
@ -151,7 +153,8 @@ auto renderer::draw_mass_springs(const std::vector<Vector3>& masses) -> void
const size_t current_index = state.get_current_index();
if (masses.size() > current_index) {
const Vector3& current_mass = masses.at(current_index);
DrawCube(current_mass, VERTEX_SIZE * 2, VERTEX_SIZE * 2, VERTEX_SIZE * 2, VERTEX_CURRENT_COLOR);
DrawCube(current_mass, VERTEX_SIZE * 2, VERTEX_SIZE * 2, VERTEX_SIZE * 2,
VERTEX_CURRENT_COLOR);
}
EndMode3D();
@ -186,36 +189,37 @@ auto renderer::draw_menu() const -> void
EndTextureMode();
}
auto renderer::draw_textures(const int fps, const int ups, const size_t mass_count, const size_t spring_count) const
-> void
auto renderer::draw_textures(const int fps, const int ups, const size_t mass_count,
const size_t spring_count) const -> void
{
BeginDrawing();
DrawTextureRec(menu_target.texture, Rectangle(0, 0, menu_target.texture.width, -menu_target.texture.height),
DrawTextureRec(menu_target.texture,
Rectangle(0, 0, menu_target.texture.width, -menu_target.texture.height),
Vector2(0, 0), WHITE);
DrawTextureRec(klotski_target.texture,
Rectangle(0, 0, klotski_target.texture.width, -klotski_target.texture.height),
Vector2(0, MENU_HEIGHT), WHITE);
DrawTextureRec(render_target.texture, Rectangle(0, 0, render_target.texture.width, -render_target.texture.height),
DrawTextureRec(render_target.texture,
Rectangle(0, 0, render_target.texture.width, -render_target.texture.height),
Vector2(GetScreenWidth() / 2.0f, MENU_HEIGHT), WHITE);
// Draw borders
DrawRectangleLinesEx(Rectangle(0, 0, GetScreenWidth(), MENU_HEIGHT), 1.0f, BLACK);
DrawRectangleLinesEx(Rectangle(0, MENU_HEIGHT, GetScreenWidth() / 2.0f, GetScreenHeight() - MENU_HEIGHT), 1.0f,
BLACK);
DrawRectangleLinesEx(
Rectangle(GetScreenWidth() / 2.0f, MENU_HEIGHT, GetScreenWidth() / 2.0f, GetScreenHeight() - MENU_HEIGHT), 1.0f,
Rectangle(0, MENU_HEIGHT, GetScreenWidth() / 2.0f, GetScreenHeight() - MENU_HEIGHT), 1.0f,
BLACK);
DrawRectangleLinesEx(Rectangle(GetScreenWidth() / 2.0f, MENU_HEIGHT, GetScreenWidth() / 2.0f,
GetScreenHeight() - MENU_HEIGHT),
1.0f, BLACK);
gui.draw_graph_overlay(fps, ups, mass_count, spring_count);
gui.draw_save_preset_popup();
gui.update();
gui.draw(fps, ups, mass_count, spring_count);
EndDrawing();
}
auto renderer::render(const std::vector<Vector3>& masses, const int fps, const int ups, const size_t mass_count,
const size_t spring_count) -> void
auto renderer::render(const std::vector<Vector3>& masses, const int fps, const int ups,
const size_t mass_count, const size_t spring_count) -> void
{
update_texture_sizes();

View File

@ -35,7 +35,8 @@ auto state_manager::synced_insert_link(size_t first_index, size_t second_index)
}
auto state_manager::synced_insert_statespace(const std::vector<puzzle>& states,
const std::vector<std::pair<size_t, size_t>>& _links) -> void
const std::vector<std::pair<size_t, size_t>>& _links)
-> void
{
if (!state_pool.empty() || !state_indices.empty() || !links.empty()) {
warnln("Inserting statespace but collections haven't been cleared");
@ -70,7 +71,7 @@ auto state_manager::synced_clear_statespace() -> void
winning_path.clear();
path_indices.clear();
// move_history does not get cleared here, but when resetting the board
move_history = std::stack<size_t>();
visit_counts.clear();
// Queue an update to the physics engine state to keep in sync
@ -107,9 +108,9 @@ auto state_manager::parse_preset_file(const std::string& _preset_file) -> bool
for (const auto& preset : preset_lines) {
const puzzle& p = puzzle(preset);
if (!p.valid_thorough()) {
if (const std::optional<std::string>& reason = p.try_get_invalid_reason()) {
preset_states = {puzzle(4, 5, 9, 9, false)};
infoln("Preset file \"{}\" contained invalid presets.", preset_file);
infoln("Preset file \"{}\" contained invalid presets: {}", preset_file, *reason);
return false;
}
preset_states.emplace_back(p);
@ -125,7 +126,7 @@ auto state_manager::append_preset_file(const std::string& preset_name) -> bool
{
infoln(R"(Saving preset "{}" to "{}")", preset_name, preset_file);
if (!get_current_state().valid_thorough()) {
if (get_current_state().try_get_invalid_reason()) {
return false;
}
@ -176,7 +177,8 @@ auto state_manager::update_current_state(const puzzle& p) -> void
// Because synced_insert_link does not check for duplicates we do it here,
// if the size grows, it was not a duplicate, and we can add the spring
if (state_pool.size() > size_before) {
// The order is important, as the position of the second mass will be updated depending on the first
// The order is important, as the position of the second mass will be updated depending on
// the first
synced_insert_link(current_state_index, index);
}
@ -312,12 +314,12 @@ auto state_manager::populate_graph() -> void
auto state_manager::clear_graph_and_add_current(const puzzle& p) -> void
{
// Do we need to make a copy before clearing the state_pool?
// const puzzle _p = p;
const puzzle _p = p; // NOLINT(*-unnecessary-copy-initialization)
synced_clear_statespace();
// Re-add the current state
current_state_index = synced_try_insert_state(p);
current_state_index = synced_try_insert_state(_p);
// These states are no longer in the graph
previous_state_index = current_state_index;
@ -482,4 +484,4 @@ auto state_manager::get_total_moves() const -> size_t
auto state_manager::was_edited() const -> bool
{
return preset_states.at(current_preset) != get_state(starting_state_index);
}
}

View File

@ -1,5 +1,6 @@
#include "user_interface.hpp"
#include "config.hpp"
#include "input.hpp"
#include <raylib.h>
@ -10,8 +11,9 @@
#include <tracy/Tracy.hpp>
#endif
auto user_interface::grid::update_bounds(const int _x, const int _y, const int _width, const int _height,
const int _columns, const int _rows) -> void
auto user_interface::grid::update_bounds(const int _x, const int _y, const int _width,
const int _height, const int _columns, const int _rows)
-> void
{
x = _x;
y = _y;
@ -21,7 +23,8 @@ auto user_interface::grid::update_bounds(const int _x, const int _y, const int _
rows = _rows;
}
auto user_interface::grid::update_bounds(const int _x, const int _y, const int _width, const int _height) -> void
auto user_interface::grid::update_bounds(const int _x, const int _y, const int _width,
const int _height) -> void
{
x = _x;
y = _y;
@ -45,7 +48,8 @@ auto user_interface::grid::bounds() const -> Rectangle
return bounds;
}
auto user_interface::grid::bounds(const int _x, const int _y, const int _width, const int _height) const -> Rectangle
auto user_interface::grid::bounds(const int _x, const int _y, const int _width,
const int _height) const -> Rectangle
{
if (_x < 0 || _x + _width > columns || _y < 0 || _y + _height > rows) {
errln("Grid bounds are outside range.");
@ -55,8 +59,8 @@ auto user_interface::grid::bounds(const int _x, const int _y, const int _width,
const int cell_width = (width - padding) / columns;
const int cell_height = (height - padding) / rows;
return Rectangle(x + _x * cell_width + padding, y + _y * cell_height + padding, _width * cell_width - padding,
_height * cell_height - padding);
return Rectangle(x + _x * cell_width + padding, y + _y * cell_height + padding,
_width * cell_width - padding, _height * cell_height - padding);
}
auto user_interface::grid::square_bounds() const -> Rectangle
@ -69,8 +73,8 @@ auto user_interface::grid::square_bounds() const -> Rectangle
return bounds;
}
auto user_interface::grid::square_bounds(const int _x, const int _y, const int _width, const int _height) const
-> Rectangle
auto user_interface::grid::square_bounds(const int _x, const int _y, const int _width,
const int _height) const -> Rectangle
{
// Assumes each cell is square, so either width or height are not completely
// filled
@ -89,8 +93,10 @@ auto user_interface::grid::square_bounds(const int _x, const int _y, const int _
const int x_offset = (width - grid_width) / 2;
const int y_offset = (height - grid_height) / 2;
return Rectangle(x_offset + _x * (cell_size + padding) + padding, y_offset + _y * (cell_size + padding) + padding,
_width * cell_size + padding * (_width - 1), _height * cell_size + padding * (_height - 1));
return Rectangle(x_offset + _x * (cell_size + padding) + padding,
y_offset + _y * (cell_size + padding) + padding,
_width * cell_size + padding * (_width - 1),
_height * cell_size + padding * (_height - 1));
}
auto user_interface::init() -> void
@ -194,15 +200,16 @@ auto user_interface::set_default_style(const default_style& style) -> void
auto user_interface::get_component_style(const int component) -> component_style
{
return {{GuiGetStyle(component, BORDER_COLOR_NORMAL), GuiGetStyle(component, BASE_COLOR_NORMAL),
GuiGetStyle(component, TEXT_COLOR_NORMAL), GuiGetStyle(component, BORDER_COLOR_FOCUSED),
GuiGetStyle(component, BASE_COLOR_FOCUSED), GuiGetStyle(component, TEXT_COLOR_FOCUSED),
GuiGetStyle(component, BORDER_COLOR_PRESSED), GuiGetStyle(component, BASE_COLOR_PRESSED),
GuiGetStyle(component, TEXT_COLOR_PRESSED), GuiGetStyle(component, BORDER_COLOR_DISABLED),
GuiGetStyle(component, BASE_COLOR_DISABLED), GuiGetStyle(component, TEXT_COLOR_DISABLED)},
GuiGetStyle(component, BORDER_WIDTH),
GuiGetStyle(component, TEXT_PADDING),
GuiGetStyle(component, TEXT_ALIGNMENT)};
return {
{GuiGetStyle(component, BORDER_COLOR_NORMAL), GuiGetStyle(component, BASE_COLOR_NORMAL),
GuiGetStyle(component, TEXT_COLOR_NORMAL), GuiGetStyle(component, BORDER_COLOR_FOCUSED),
GuiGetStyle(component, BASE_COLOR_FOCUSED), GuiGetStyle(component, TEXT_COLOR_FOCUSED),
GuiGetStyle(component, BORDER_COLOR_PRESSED), GuiGetStyle(component, BASE_COLOR_PRESSED),
GuiGetStyle(component, TEXT_COLOR_PRESSED), GuiGetStyle(component, BORDER_COLOR_DISABLED),
GuiGetStyle(component, BASE_COLOR_DISABLED), GuiGetStyle(component, TEXT_COLOR_DISABLED)},
GuiGetStyle(component, BORDER_WIDTH),
GuiGetStyle(component, TEXT_PADDING),
GuiGetStyle(component, TEXT_ALIGNMENT)};
}
auto user_interface::set_component_style(const int component, const component_style& style) -> void
@ -228,8 +235,16 @@ auto user_interface::set_component_style(const int component, const component_st
GuiSetStyle(component, TEXT_ALIGNMENT, style.text_alignment);
}
auto user_interface::draw_button(const Rectangle bounds, const std::string& label, const Color color,
const bool enabled, const int font_size) const -> int
auto user_interface::popup_bounds() -> Rectangle
{
return Rectangle(static_cast<float>(GetScreenWidth()) / 2.0f - POPUP_WIDTH / 2.0f,
static_cast<float>(GetScreenHeight()) / 2.0f - POPUP_HEIGHT / 2.0f,
POPUP_WIDTH, POPUP_HEIGHT);
}
auto user_interface::draw_button(const Rectangle bounds, const std::string& label,
const Color color, const bool enabled, const int font_size) const
-> int
{
// Save original styling
const default_style original_default = get_default_style();
@ -260,16 +275,16 @@ auto user_interface::draw_button(const Rectangle bounds, const std::string& labe
}
auto user_interface::draw_menu_button(const int x, const int y, const int width, const int height,
const std::string& label, const Color color, const bool enabled,
const int font_size) const -> int
const std::string& label, const Color color,
const bool enabled, const int font_size) const -> int
{
const Rectangle bounds = menu_grid.bounds(x, y, width, height);
return draw_button(bounds, label, color, enabled, font_size);
}
auto user_interface::draw_toggle_slider(const Rectangle bounds, const std::string& off_label,
const std::string& on_label, int* active, Color color, bool enabled,
int font_size) const -> int
const std::string& on_label, int* active, Color color,
bool enabled, int font_size) const -> int
{
// Save original styling
const default_style original_default = get_default_style();
@ -304,16 +319,18 @@ auto user_interface::draw_toggle_slider(const Rectangle bounds, const std::strin
return pressed;
}
auto user_interface::draw_menu_toggle_slider(const int x, const int y, const int width, const int height,
const std::string& off_label, const std::string& on_label, int* active,
const Color color, const bool enabled, const int font_size) const -> int
auto user_interface::draw_menu_toggle_slider(const int x, const int y, const int width,
const int height, const std::string& off_label,
const std::string& on_label, int* active,
const Color color, const bool enabled,
const int font_size) const -> int
{
const Rectangle bounds = menu_grid.bounds(x, y, width, height);
return draw_toggle_slider(bounds, off_label, on_label, active, color, enabled, font_size);
}
auto user_interface::draw_spinner(Rectangle bounds, const std::string& label, int* value, int min, int max, Color color,
bool enabled, int font_size) const -> int
auto user_interface::draw_spinner(Rectangle bounds, const std::string& label, int* value, int min,
int max, Color color, bool enabled, int font_size) const -> int
{
// Save original styling
const default_style original_default = get_default_style();
@ -349,15 +366,16 @@ auto user_interface::draw_spinner(Rectangle bounds, const std::string& label, in
}
auto user_interface::draw_menu_spinner(const int x, const int y, const int width, const int height,
const std::string& label, int* value, const int min, const int max,
const Color color, const bool enabled, const int font_size) const -> int
const std::string& label, int* value, const int min,
const int max, const Color color, const bool enabled,
const int font_size) const -> int
{
const Rectangle bounds = menu_grid.bounds(x, y, width, height);
return draw_spinner(bounds, label, value, min, max, color, enabled, font_size);
}
auto user_interface::draw_label(const Rectangle bounds, const std::string& text, const Color color, const bool enabled,
const int font_size) const -> int
auto user_interface::draw_label(const Rectangle bounds, const std::string& text, const Color color,
const bool enabled, const int font_size) const -> int
{
// Save original styling
const default_style original_default = get_default_style();
@ -387,8 +405,8 @@ auto user_interface::draw_label(const Rectangle bounds, const std::string& text,
return pressed;
}
auto user_interface::draw_board_block(const int x, const int y, const int width, const int height, const Color color,
const bool enabled) const -> bool
auto user_interface::draw_board_block(const int x, const int y, const int width, const int height,
const Color color, const bool enabled) const -> bool
{
component_style s = get_component_style(BUTTON);
apply_block_color(s, color);
@ -428,13 +446,14 @@ auto user_interface::draw_board_block(const int x, const int y, const int width,
auto user_interface::window_open() const -> bool
{
return save_window || help_window;
return save_window || help_window || ok_message || yes_no_message;
}
auto user_interface::draw_menu_header(const Color color) const -> void
{
int preset = state.get_current_preset();
draw_menu_spinner(0, 0, 1, 1, "Preset: ", &preset, -1, state.get_preset_count(), color, !input.editing);
int preset = static_cast<int>(state.get_current_preset());
draw_menu_spinner(0, 0, 1, 1, "Preset: ", &preset, -1,
static_cast<int>(state.get_preset_count()), color, !input.editing);
if (preset > static_cast<int>(state.get_current_preset())) {
input.load_next_preset();
} else if (preset < static_cast<int>(state.get_current_preset())) {
@ -456,15 +475,17 @@ auto user_interface::draw_menu_header(const Color color) const -> void
auto user_interface::draw_graph_info(const Color color) const -> void
{
draw_menu_button(0, 1, 1, 1,
std::format("Found {} States ({} Winning)", state.get_state_count(), state.get_target_count()),
std::format("Found {} States ({} Winning)", state.get_state_count(),
state.get_target_count()),
color);
draw_menu_button(1, 1, 1, 1, std::format("Found {} Transitions", state.get_link_count()), color);
draw_menu_button(1, 1, 1, 1, std::format("Found {} Transitions", state.get_link_count()),
color);
draw_menu_button(
2, 1, 1, 1,
std::format("{} Moves to Nearest Solution", state.get_path_length() > 0 ? state.get_path_length() - 1 : 0),
color);
draw_menu_button(2, 1, 1, 1,
std::format("{} Moves to Nearest Solution",
state.get_path_length() > 0 ? state.get_path_length() - 1 : 0),
color);
}
auto user_interface::draw_graph_controls(const Color color) const -> void
@ -485,7 +506,8 @@ auto user_interface::draw_graph_controls(const Color color) const -> void
}
int mark_solutions = input.mark_solutions;
draw_menu_toggle_slider(2, 2, 1, 1, "Solution Hidden (I)", "Solution Shown (I)", &mark_solutions, color);
draw_menu_toggle_slider(2, 2, 1, 1, "Solution Hidden (I)", "Solution Shown (I)",
&mark_solutions, color);
if (mark_solutions != input.mark_solutions) {
input.toggle_mark_solutions();
}
@ -495,20 +517,22 @@ auto user_interface::draw_graph_controls(const Color color) const -> void
auto user_interface::draw_camera_controls(const Color color) const -> void
{
int lock_camera = input.camera_lock;
draw_menu_toggle_slider(0, 3, 1, 1, "Free Camera (L)", "Locked Camera (L)", &lock_camera, color);
draw_menu_toggle_slider(0, 3, 1, 1, "Free Camera (L)", "Locked Camera (L)", &lock_camera,
color);
if (lock_camera != input.camera_lock) {
input.toggle_camera_lock();
}
int lock_camera_mass_center = input.camera_mass_center_lock;
draw_menu_toggle_slider(1, 3, 1, 1, "Current Block (U)", "Graph Center (U)", &lock_camera_mass_center, color,
input.camera_lock);
draw_menu_toggle_slider(1, 3, 1, 1, "Current Block (U)", "Graph Center (U)",
&lock_camera_mass_center, color, input.camera_lock);
if (lock_camera_mass_center != input.camera_mass_center_lock) {
input.toggle_camera_mass_center_lock();
}
int projection = camera.projection == CAMERA_ORTHOGRAPHIC;
draw_menu_toggle_slider(2, 3, 1, 1, "Perspective (Alt)", "Orthographic (Alt)", &projection, color);
draw_menu_toggle_slider(2, 3, 1, 1, "Perspective (Alt)", "Orthographic (Alt)", &projection,
color);
if (projection != (camera.projection == CAMERA_ORTHOGRAPHIC)) {
input.toggle_camera_projection();
}
@ -535,7 +559,8 @@ auto user_interface::draw_puzzle_controls(const Color color) const -> void
const int visits = state.get_current_visits();
draw_menu_button(0, 4, 1, 1,
std::format("{} Moves ({}{} Time at this State)", state.get_total_moves(), visits, nth(visits)),
std::format("{} Moves ({}{} Time at this State)", state.get_total_moves(),
visits, nth(visits)),
color);
if (draw_menu_button(1, 4, 1, 1, "Make Optimal Move (Space)", color, state.has_distances())) {
@ -606,10 +631,18 @@ auto user_interface::draw_edit_controls(const Color color) const -> void
auto user_interface::draw_menu_footer(const Color color) -> void
{
draw_menu_button(0, 6, 2, 1, std::format("State: \"{}\"", state.get_current_state().state), color);
draw_menu_button(0, 6, 2, 1, std::format("State: \"{}\"", state.get_current_state().state),
color);
if (draw_menu_button(2, 6, 1, 1, "Save as Preset", color)) {
save_window = true;
if (const std::optional<std::string>& reason =
state.get_current_state().try_get_invalid_reason()) {
message_title = "Can't Save Preset";
message_message = std::format("Invalid Board: {}.", *reason);
ok_message = true;
} else {
save_window = true;
}
}
}
@ -628,10 +661,8 @@ auto user_interface::draw_save_preset_popup() -> void
}
// Returns the pressed button index
const int button =
GuiTextInputBox(Rectangle((GetScreenWidth() - POPUP_WIDTH) / 2.0f, (GetScreenHeight() - POPUP_HEIGHT) / 2.0f,
POPUP_WIDTH, POPUP_HEIGHT),
"Save as Preset", "Enter Preset Name", "Ok;Cancel", preset_name.data(), 255, nullptr);
const int button = GuiTextInputBox(popup_bounds(), "Save as Preset", "Enter Preset Name",
"Ok;Cancel", preset_name.data(), 255, nullptr);
if (button == 1) {
state.append_preset_file(preset_name.data());
}
@ -641,6 +672,39 @@ auto user_interface::draw_save_preset_popup() -> void
}
}
auto user_interface::draw_ok_message_box() -> void
{
if (!ok_message) {
return;
}
const int button =
GuiMessageBox(popup_bounds(), message_title.data(), message_message.data(), "Ok");
if (button == 0 || button == 1) {
message_title = "";
message_message = "";
ok_message = false;
}
}
auto user_interface::draw_yes_no_message_box() -> void
{
if (!yes_no_message) {
return;
}
const int button =
GuiMessageBox(popup_bounds(), message_title.data(), message_message.data(), "Yes;No");
if (button == 1) {
yes_no_handler();
}
if (button == 0 || button == 1 || button == 2) {
message_title = "";
message_message = "";
yes_no_message = false;
}
}
auto user_interface::draw_main_menu() -> void
{
menu_grid.update_bounds(0, 0, GetScreenWidth(), MENU_HEIGHT);
@ -663,27 +727,30 @@ auto user_interface::draw_puzzle_board() -> void
{
const puzzle& current = state.get_current_state();
board_grid.update_bounds(0, MENU_HEIGHT, GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT, current.width,
current.height);
board_grid.update_bounds(0, MENU_HEIGHT, GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT,
current.width, current.height);
// Draw outer border
const Rectangle bounds = board_grid.square_bounds();
DrawRectangleRec(bounds, current.won() ? BOARD_COLOR_WON : BOARD_COLOR_RESTRICTED);
// Draw inner borders
DrawRectangle(bounds.x + BOARD_PADDING, bounds.y + BOARD_PADDING, bounds.width - 2 * BOARD_PADDING,
bounds.height - 2 * BOARD_PADDING, current.restricted ? BOARD_COLOR_RESTRICTED : BOARD_COLOR_FREE);
DrawRectangle(bounds.x + BOARD_PADDING, bounds.y + BOARD_PADDING,
bounds.width - 2 * BOARD_PADDING, bounds.height - 2 * BOARD_PADDING,
current.restricted ? BOARD_COLOR_RESTRICTED : BOARD_COLOR_FREE);
// Draw target opening
// TODO: Only draw single direction (in corner) if restricted (use target block principal direction)
// TODO: Only draw single direction (in corner) if restricted (use target block principal
// direction)
const std::optional<puzzle::block> target_block = current.try_get_target_block();
if (current.has_win_condition() && target_block.has_value()) {
const int target_x = current.target_x;
const int target_y = current.target_y;
auto [x, y, width, height] =
board_grid.square_bounds(target_x, target_y, target_block.value().width, target_block.value().height);
const int target_x = current.target_x;
const int target_y = current.target_y;
if (current.has_win_condition() && target_block) {
auto [x, y, width, height] = board_grid.square_bounds(
target_x, target_y, target_block.value().width, target_block.value().height);
const Color opening_color = Fade(current.won() ? BOARD_COLOR_WON : BOARD_COLOR_RESTRICTED, 0.3);
const Color opening_color =
Fade(current.won() ? BOARD_COLOR_WON : BOARD_COLOR_RESTRICTED, 0.3);
if (target_x == 0) {
// Left opening
@ -745,41 +812,82 @@ auto user_interface::draw_puzzle_board() -> void
// Draw block placing
if (input.editing && input.has_block_add_xy) {
if (current.covers(input.block_add_x, input.block_add_y) && input.hov_x >= input.block_add_x &&
input.hov_y >= input.block_add_y) {
if (current.covers(input.block_add_x, input.block_add_y) &&
input.hov_x >= input.block_add_x && input.hov_y >= input.block_add_y) {
bool collides = false;
for (const puzzle::block& b : current) {
if (b.collides(puzzle::block(input.block_add_x, input.block_add_y, input.hov_x - input.block_add_x + 1,
if (b.collides(puzzle::block(input.block_add_x, input.block_add_y,
input.hov_x - input.block_add_x + 1,
input.hov_y - input.block_add_y + 1, false))) {
collides = true;
break;
}
}
if (!collides) {
draw_board_block(input.block_add_x, input.block_add_y, input.hov_x - input.block_add_x + 1,
draw_board_block(input.block_add_x, input.block_add_y,
input.hov_x - input.block_add_x + 1,
input.hov_y - input.block_add_y + 1, PURPLE);
}
}
}
// Draw goal boundaries when editing
if (input.editing) {
DrawRectangleLinesEx(board_grid.square_bounds(target_x, target_y, target_block->width, target_block->height), 2.0, TARGET_BLOCK_COLOR);
}
}
auto user_interface::draw_graph_overlay(int fps, int ups, size_t mass_count, size_t spring_count) -> void
auto user_interface::draw_graph_overlay(int fps, int ups, size_t mass_count, size_t spring_count)
-> void
{
graph_overlay_grid.update_bounds(GetScreenWidth() / 2, MENU_HEIGHT);
debug_overlay_grid.update_bounds(GetScreenWidth() / 2, GetScreenHeight() - 75);
draw_label(graph_overlay_grid.bounds(0, 0, 1, 1), std::format("Dist: {:0>7.2f}", camera.distance), BLACK);
draw_label(graph_overlay_grid.bounds(0, 1, 1, 1), std::format("FoV: {:0>6.2f}", camera.fov), BLACK);
draw_label(graph_overlay_grid.bounds(0, 0, 1, 1),
std::format("Dist: {:0>7.2f}", camera.distance), BLACK);
draw_label(graph_overlay_grid.bounds(0, 1, 1, 1), std::format("FoV: {:0>6.2f}", camera.fov),
BLACK);
draw_label(graph_overlay_grid.bounds(0, 2, 1, 1), std::format("FPS: {:0>3}", fps), LIME);
draw_label(graph_overlay_grid.bounds(0, 3, 1, 1), std::format("UPS: {:0>3}", ups), ORANGE);
// Debug
draw_label(debug_overlay_grid.bounds(0, 0, 1, 1), std::format("Debug:"), BLACK);
draw_label(debug_overlay_grid.bounds(0, 1, 1, 1), std::format("Masses: {}", mass_count), BLACK);
draw_label(debug_overlay_grid.bounds(0, 2, 1, 1), std::format("Springs: {}", spring_count), BLACK);
draw_label(debug_overlay_grid.bounds(0, 0, 1, 1), std::format("Physics Debug:"), BLACK);
draw_label(debug_overlay_grid.bounds(0, 1, 1, 1), std::format("Masses: {}", mass_count),
BLACK);
draw_label(debug_overlay_grid.bounds(0, 2, 1, 1), std::format("Springs: {}", spring_count),
BLACK);
}
auto user_interface::update() const -> void
auto user_interface::draw(const int fps, const int ups, const size_t mass_count,
const size_t spring_count) -> void
{
const auto visitor = overloads{[&](const show_ok_message& msg)
{
message_title = msg.title;
message_message = msg.message;
ok_message = true;
},
[&](const show_yes_no_message& msg)
{
message_title = msg.title;
message_message = msg.message;
yes_no_handler = msg.on_yes;
yes_no_message = true;
},
[&](const show_save_preset_window& msg) { save_window = true; }};
while (!input.ui_commands.empty()) {
const ui_command& cmd = input.ui_commands.front();
cmd.visit(visitor);
input.ui_commands.pop();
}
input.disable = window_open();
}
draw_graph_overlay(fps, ups, mass_count, spring_count);
draw_save_preset_popup();
draw_ok_message_box();
draw_yes_no_message_box();
}